549 lines
9.9 KiB
Go
549 lines
9.9 KiB
Go
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
|
|
}
|