Initial commit
This commit is contained in:
548
dom/document.go
Normal file
548
dom/document.go
Normal file
@@ -0,0 +1,548 @@
|
||||
package dom
|
||||
|
||||
import "io"
|
||||
import "fmt"
|
||||
import "net"
|
||||
import "bufio"
|
||||
import "net/url"
|
||||
import "strings"
|
||||
import "unicode"
|
||||
import "git.tebibyte.media/sashakoshka/stone"
|
||||
|
||||
type Document struct {
|
||||
title string
|
||||
location *url.URL
|
||||
elements []Element
|
||||
|
||||
metrics struct {
|
||||
width int
|
||||
maxWidth int
|
||||
margin int
|
||||
}
|
||||
}
|
||||
|
||||
type Element interface {
|
||||
Render (target stone.Buffer, offset int) ()
|
||||
Height () (height int)
|
||||
}
|
||||
|
||||
type ElementPreformatted struct {
|
||||
parent *Document
|
||||
text string
|
||||
altText string
|
||||
height int
|
||||
}
|
||||
|
||||
func (element *ElementPreformatted) Render (
|
||||
target stone.Buffer,
|
||||
offset int,
|
||||
) {
|
||||
height := 1
|
||||
x := element.parent.metrics.margin
|
||||
y := offset
|
||||
for _, character := range element.text {
|
||||
if character == '\n' {
|
||||
height ++
|
||||
x = element.parent.metrics.margin
|
||||
y ++
|
||||
} else if unicode.IsPrint(character) {
|
||||
target.SetRune(x, y, character)
|
||||
target.SetColor(x, y, stone.ColorYellow)
|
||||
x ++
|
||||
}
|
||||
}
|
||||
|
||||
element.height = height
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (element *ElementPreformatted) Height () (height int) {
|
||||
height = element.height
|
||||
return
|
||||
}
|
||||
|
||||
type ElementText struct {
|
||||
parent *Document
|
||||
text string
|
||||
height int
|
||||
}
|
||||
|
||||
func (element *ElementText) Render (
|
||||
target stone.Buffer,
|
||||
offset int,
|
||||
) {
|
||||
height := 1
|
||||
x := element.parent.metrics.margin
|
||||
y := offset
|
||||
for _, character := range element.text {
|
||||
if x >= element.parent.metrics.maxWidth {
|
||||
height ++
|
||||
x = element.parent.metrics.margin
|
||||
y ++
|
||||
}
|
||||
|
||||
if unicode.IsPrint(character) {
|
||||
target.SetRune(x, y, character)
|
||||
x ++
|
||||
}
|
||||
}
|
||||
|
||||
element.height = height
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (element *ElementText) Height () (height int) {
|
||||
height = element.height
|
||||
return
|
||||
}
|
||||
|
||||
type HeadingLevel int
|
||||
|
||||
const (
|
||||
HeadingLevel1 HeadingLevel = iota
|
||||
HeadingLevel2
|
||||
HeadingLevel3
|
||||
)
|
||||
|
||||
type ElementHeading struct {
|
||||
parent *Document
|
||||
text string
|
||||
level HeadingLevel
|
||||
height int
|
||||
}
|
||||
|
||||
func (element *ElementHeading) Render (
|
||||
target stone.Buffer,
|
||||
offset int,
|
||||
) {
|
||||
height := 1
|
||||
x := element.parent.metrics.margin
|
||||
y := offset
|
||||
|
||||
text := ""
|
||||
switch element.level {
|
||||
case HeadingLevel2:
|
||||
text = "## "
|
||||
case HeadingLevel3:
|
||||
text = "### "
|
||||
}
|
||||
|
||||
text += element.text
|
||||
|
||||
for _, character := range text {
|
||||
if x >= element.parent.metrics.maxWidth {
|
||||
height ++
|
||||
x = element.parent.metrics.margin
|
||||
y ++
|
||||
}
|
||||
|
||||
if unicode.IsPrint(character) {
|
||||
target.SetRune(x, y, character)
|
||||
target.SetColor(x, y, stone.ColorBlue)
|
||||
x ++
|
||||
}
|
||||
}
|
||||
|
||||
if element.level == HeadingLevel1 {
|
||||
height ++
|
||||
y ++
|
||||
for
|
||||
x = element.parent.metrics.margin;
|
||||
x < element.parent.metrics.maxWidth;
|
||||
x ++ {
|
||||
|
||||
target.SetRune(x, y, '-')
|
||||
target.SetColor(x, y, stone.ColorBlue)
|
||||
}
|
||||
}
|
||||
|
||||
element.height = height
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (element *ElementHeading) Height () (height int) {
|
||||
height = element.height
|
||||
return
|
||||
}
|
||||
|
||||
type ElementHyperlink struct {
|
||||
parent *Document
|
||||
text string
|
||||
number int
|
||||
url *url.URL
|
||||
height int
|
||||
}
|
||||
|
||||
func (element *ElementHyperlink) Render (
|
||||
target stone.Buffer,
|
||||
offset int,
|
||||
) {
|
||||
height := 1
|
||||
x := element.parent.metrics.margin
|
||||
y := offset
|
||||
|
||||
text := "=> "
|
||||
text += fmt.Sprint("[", element.number, "] ")
|
||||
prefixLen := len(text)
|
||||
|
||||
if element.text == "" {
|
||||
text += element.url.String()
|
||||
} else {
|
||||
text += element.text
|
||||
}
|
||||
|
||||
for
|
||||
x := element.parent.metrics.margin;
|
||||
x < prefixLen + element.parent.metrics.margin;
|
||||
x ++ {
|
||||
|
||||
target.SetColor(x, offset, stone.ColorPurple)
|
||||
|
||||
}
|
||||
|
||||
for _, character := range text {
|
||||
if x >= element.parent.metrics.maxWidth {
|
||||
height ++
|
||||
x = element.parent.metrics.margin
|
||||
y ++
|
||||
}
|
||||
|
||||
if unicode.IsPrint(character) {
|
||||
target.SetRune(x, y, character)
|
||||
x ++
|
||||
}
|
||||
}
|
||||
|
||||
element.height = height
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (element *ElementHyperlink) Height () (height int) {
|
||||
height = element.height
|
||||
return
|
||||
}
|
||||
|
||||
func (element *ElementHyperlink) Location () (location *url.URL) {
|
||||
location = element.url
|
||||
return
|
||||
}
|
||||
|
||||
type ElementListItem struct {
|
||||
parent *Document
|
||||
text string
|
||||
height int
|
||||
}
|
||||
|
||||
func (element *ElementListItem) Render (
|
||||
target stone.Buffer,
|
||||
offset int,
|
||||
) {
|
||||
height := 1
|
||||
x := element.parent.metrics.margin
|
||||
y := offset
|
||||
|
||||
text := "* " + element.text
|
||||
|
||||
target.SetColor(element.parent.metrics.margin, offset, stone.ColorGreen)
|
||||
|
||||
for _, character := range text {
|
||||
if x >= element.parent.metrics. maxWidth {
|
||||
height ++
|
||||
x = element.parent.metrics.margin + 2
|
||||
y ++
|
||||
}
|
||||
|
||||
if unicode.IsPrint(character) {
|
||||
target.SetRune(x, y, character)
|
||||
x ++
|
||||
}
|
||||
}
|
||||
|
||||
element.height = height
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (element *ElementListItem) Height () (height int) {
|
||||
height = element.height
|
||||
return
|
||||
}
|
||||
|
||||
type ElementQuote struct {
|
||||
parent *Document
|
||||
text string
|
||||
height int
|
||||
}
|
||||
|
||||
func (element *ElementQuote) Render (
|
||||
target stone.Buffer,
|
||||
offset int,
|
||||
) {
|
||||
height := 1
|
||||
x := element.parent.metrics.margin
|
||||
y := offset
|
||||
|
||||
text := " ''" + element.text + ",,"
|
||||
|
||||
target.SetColor(element.parent.metrics.margin, offset, stone.ColorGreen)
|
||||
|
||||
for _, character := range text {
|
||||
if x >= element.parent.metrics.maxWidth || character == '\n' {
|
||||
height ++
|
||||
x = element.parent.metrics.margin + 3
|
||||
y ++
|
||||
}
|
||||
|
||||
if unicode.IsPrint(character) {
|
||||
target.SetRune(x, y, character)
|
||||
target.SetColor(x, y, stone.ColorGreen)
|
||||
x ++
|
||||
}
|
||||
}
|
||||
|
||||
element.height = height
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (element *ElementQuote) Height () (height int) {
|
||||
height = element.height
|
||||
return
|
||||
}
|
||||
|
||||
func (document *Document) Title () (title string) {
|
||||
title = document.title
|
||||
return
|
||||
}
|
||||
|
||||
func (document *Document) Render (target stone.Buffer, scroll int) (height int) {
|
||||
width, _ := target.Size()
|
||||
document.metrics.width = width
|
||||
if document.metrics.width > 90 { document.metrics.width = 90 }
|
||||
|
||||
document.metrics.margin = (width - document.metrics.width) / 2
|
||||
document.metrics.maxWidth = document.metrics.width + document.metrics.margin
|
||||
|
||||
y := scroll
|
||||
for _, element := range document.elements {
|
||||
element.Render(target, y)
|
||||
y += element.Height()
|
||||
}
|
||||
|
||||
height = y - scroll
|
||||
return
|
||||
}
|
||||
|
||||
func (document *Document) ElementAtY (y int) (element Element) {
|
||||
currentY := 0
|
||||
for _, currentElement := range document.elements {
|
||||
height := currentElement.Height()
|
||||
if y >= currentY && y < currentY + height {
|
||||
element = currentElement
|
||||
return
|
||||
}
|
||||
currentY += height
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (document *Document) UrlAtId (id int) (location *url.URL) {
|
||||
for _, element := range document.elements {
|
||||
hyperlink, ok := element.(*ElementHyperlink)
|
||||
if !ok { continue }
|
||||
|
||||
if hyperlink.number == id {
|
||||
location = hyperlink.url
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func ParseDocument (reader io.Reader, location *url.URL) (document *Document) {
|
||||
document = &Document { location: location }
|
||||
|
||||
scanner := bufio.NewScanner(reader)
|
||||
gotTitle := false
|
||||
amountOfHyperlinks := 0
|
||||
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
|
||||
// preformatted
|
||||
if strings.HasPrefix(line, "```") {
|
||||
altText := strings.TrimSpace(line[3:])
|
||||
|
||||
text := ""
|
||||
for scanner.Scan() {
|
||||
currentLine := scanner.Text()
|
||||
|
||||
if strings.HasPrefix(currentLine, "```") {
|
||||
break
|
||||
} else {
|
||||
if len(text) > 0 {
|
||||
text += "\n"
|
||||
}
|
||||
text += currentLine
|
||||
}
|
||||
}
|
||||
|
||||
document.elements = append (
|
||||
document.elements,
|
||||
&ElementPreformatted {
|
||||
parent: document,
|
||||
text: text,
|
||||
altText: altText,
|
||||
})
|
||||
continue
|
||||
|
||||
// heading
|
||||
} else if len(line) >= 1 && line[0] == '#' {
|
||||
level := HeadingLevel1
|
||||
if len(line) >= 2 && line[1] == '#' {
|
||||
level = HeadingLevel2
|
||||
if len(line) >= 3 && line[2] == '#' {
|
||||
level = HeadingLevel3
|
||||
}
|
||||
}
|
||||
|
||||
line = strings.TrimSpace(strings.TrimLeft(line, "# "))
|
||||
|
||||
if level == HeadingLevel1 && !gotTitle {
|
||||
gotTitle = true
|
||||
document.title = line
|
||||
}
|
||||
|
||||
document.elements = append (
|
||||
document.elements,
|
||||
&ElementHeading {
|
||||
parent: document,
|
||||
text: line,
|
||||
level: level,
|
||||
})
|
||||
|
||||
// hyperlink
|
||||
} else if strings.HasPrefix(line, "=>") {
|
||||
line = line [2:]
|
||||
fields := strings.Fields(line)
|
||||
|
||||
if len(fields) >= 1 {
|
||||
urlString := fields[0]
|
||||
text := ""
|
||||
fields = fields[1:]
|
||||
if len(fields) >= 1 {
|
||||
text = strings.Join(fields, " ")
|
||||
}
|
||||
|
||||
location, _ :=
|
||||
url.Parse (strings.TrimSpace(urlString))
|
||||
|
||||
document.elements = append (
|
||||
document.elements,
|
||||
&ElementHyperlink {
|
||||
parent: document,
|
||||
url: location,
|
||||
text: text,
|
||||
number: amountOfHyperlinks,
|
||||
})
|
||||
amountOfHyperlinks ++
|
||||
}
|
||||
|
||||
// list item
|
||||
} else if strings.HasPrefix(line, "*") {
|
||||
document.elements = append (
|
||||
document.elements,
|
||||
&ElementListItem {
|
||||
parent: document,
|
||||
text: strings.TrimSpace(line[1:]),
|
||||
})
|
||||
|
||||
// block quote
|
||||
} else if strings.HasPrefix(line, ">") {
|
||||
line = strings.TrimSpace(line[1:])
|
||||
|
||||
if len (document.elements) > 1 {
|
||||
quote, alreadyInQuote :=
|
||||
document.elements [
|
||||
len(document.elements) - 1].
|
||||
(*ElementQuote)
|
||||
|
||||
if alreadyInQuote {
|
||||
quote.text += "\n" + line
|
||||
} else {
|
||||
document.elements = append (
|
||||
document.elements,
|
||||
&ElementQuote {
|
||||
parent: document,
|
||||
text: line,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
document.elements = append (
|
||||
document.elements,
|
||||
&ElementQuote {
|
||||
parent: document,
|
||||
text: line,
|
||||
})
|
||||
}
|
||||
|
||||
// normal text
|
||||
} else {
|
||||
document.elements = append (
|
||||
document.elements,
|
||||
&ElementText {
|
||||
text: line,
|
||||
parent: document,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
for {
|
||||
if len(document.elements) < 1 { break }
|
||||
element, ok :=
|
||||
document.elements[len(document.elements) - 1].
|
||||
(*ElementText)
|
||||
if !ok { break }
|
||||
if element.text != "" { break }
|
||||
document.elements =
|
||||
document.elements[:len(document.elements) - 1]
|
||||
}
|
||||
|
||||
if !gotTitle {
|
||||
host, _, err := net.SplitHostPort(document.location.Host)
|
||||
if err != nil {
|
||||
document.title = document.location.String()
|
||||
} else {
|
||||
document.title = host
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func ParseSource (reader io.Reader, location *url.URL) (document *Document) {
|
||||
document = &Document { location: location }
|
||||
|
||||
scanner := bufio.NewScanner(reader)
|
||||
text := ""
|
||||
number := 0
|
||||
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
text += fmt.Sprintf("%4d %s\n", number, line)
|
||||
number ++
|
||||
}
|
||||
|
||||
document.elements = append (
|
||||
document.elements,
|
||||
&ElementPreformatted {
|
||||
parent: document,
|
||||
text: text,
|
||||
})
|
||||
return
|
||||
}
|
||||
Reference in New Issue
Block a user