skipper/dom/document.go

548 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 ++
for
x = element.parent.metrics.margin;
x < element.parent.metrics.maxWidth;
x ++ {
target.SetRune(x, y + 1, 0)
target.SetStyle(x, y + 1, stone.StyleUnderline)
target.SetColor(x, y + 1, stone.ColorDim)
}
}
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 + 2
y := offset
target.SetColor(element.parent.metrics.margin, offset, stone.ColorGreen)
for _, character := range element.text {
if x >= element.parent.metrics.maxWidth || character == '\n' {
height ++
x = element.parent.metrics.margin + 2
y ++
}
if unicode.IsPrint(character) {
target.SetRune(x, y, character)
target.SetColor(x, y, stone.ColorGreen)
target.SetStyle(x, y, stone.StyleItalic)
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
}