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 }