amu/formatter/html/html.go

268 lines
7.7 KiB
Go

/*
AMU: Custom simple markup language
Copyright (C) 2021 Arsen Musayelyan
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package html
import (
"bytes"
"fmt"
"strings"
"github.com/alecthomas/chroma"
"github.com/alecthomas/chroma/formatters/html"
"github.com/alecthomas/chroma/lexers"
"github.com/alecthomas/chroma/styles"
"go.arsenm.dev/amu/ast"
)
// HTMLFormatter formats an AMU AST into HTML source
type HTMLFormatter struct {
Funcs ast.Funcs
ast *ast.AST
out *bytes.Buffer
}
// NewFormatter creates a new HTML formatter using the proided AST and functions
func NewFormatter(ast *ast.AST, funcs ast.Funcs) *HTMLFormatter {
return &HTMLFormatter{ast: ast, Funcs: funcs, out: &bytes.Buffer{}}
}
// formatPara formats a paragraph from the AST into HTML
func (h *HTMLFormatter) formatPara(para *ast.Para, includeTags bool) {
// If tags are to be included
if includeTags {
// Write opening p tag to buffer
h.out.WriteString("<p>")
}
// For every fragment in paragraph
for _, fragment := range para.Fragments {
if fragment.Word != nil {
// Write word to buffer
h.out.WriteString(*fragment.Word)
} else if fragment.Whitespace != nil {
// Write whitespace to buffer
h.out.WriteString(*fragment.Whitespace)
} else if fragment.Punct != nil {
// Write punctuatuon to buffer
h.out.WriteString(*fragment.Punct)
} else if fragment.Format != nil {
// For every format type, write appropriate opening tag to buffer
for _, ftype := range fragment.Format.Types {
if ftype == ast.FormatTypeBold {
h.out.WriteString("<strong>")
}
if ftype == ast.FormatTypeItalic {
h.out.WriteString("<em>")
}
if ftype == ast.FormatTypeCode {
h.out.WriteString("<code>")
}
if ftype == ast.FormatTypeStrike {
h.out.WriteString(`<del>`)
}
if ftype == ast.FormatTypeMath {
h.out.WriteString(`\(`)
}
}
// Write format text to buffer
h.out.WriteString(fragment.Format.Text)
// For every format type, write appropriate closing tag to buffer in reverse
for i := len(fragment.Format.Types) - 1; i >= 0; i-- {
ftype := fragment.Format.Types[i]
if ftype == ast.FormatTypeBold {
h.out.WriteString("</strong>")
}
if ftype == ast.FormatTypeItalic {
h.out.WriteString("</em>")
}
if ftype == ast.FormatTypeCode {
h.out.WriteString("</code>")
}
if ftype == ast.FormatTypeStrike {
h.out.WriteString(`</del>`)
}
if ftype == ast.FormatTypeMath {
h.out.WriteString(`\)`)
}
}
} else if fragment.Func != nil {
// Attempt to get requested function
fn, err := h.Funcs.Get(fragment.Func.Name)
if err != nil {
// Write error text to function
h.out.WriteString("Error running function: " + err.Error())
// Continue to next AST entry
continue
}
// Write output of function to buffer
h.out.WriteString(fn(fragment.Func.Args))
} else if fragment.Link != nil {
// Write link to buffer
fmt.Fprintf(h.out, `<a href="%s">%s</a>`, fragment.Link.Link, fragment.Link.Text)
}
}
// If tags are to be included
if includeTags {
// Write closing p tag to buffer
h.out.WriteString("</p>")
}
}
// Format formats the provided AST into an HTML string
func (h *HTMLFormatter) Format() string {
// For every entry in AST
for _, entry := range h.ast.Entries {
if entry.Heading != nil {
// Write opening heading tag
fmt.Fprintf(h.out, "<h%d>", entry.Heading.Level)
// Format paragraph with heading content without tags
h.formatPara(entry.Heading.Content, false)
// Write closing heading tag
fmt.Fprintf(h.out, "</h%d>", entry.Heading.Level)
} else if entry.Para != nil {
// Format paragraph with tags
h.formatPara(entry.Para, true)
} else if entry.Image != nil {
// If image link exists
if entry.Image.Link != "" {
// Write opening link tag
fmt.Fprintf(h.out, `<a href="%s">`, entry.Image.Link)
}
// Weite image tag
fmt.Fprintf(h.out, `<img src="%s" alt="%s">`, entry.Image.Source, entry.Image.Alternate)
// If image link exists
if entry.Image.Link != "" {
// Write closing link tag
h.out.WriteString("</a>")
}
} else if entry.List != nil {
var openTag, closeTag string
// Set opening and closing tags depending on list type
switch entry.List.Type {
case "unordered":
openTag = "<ul>"
closeTag = "</ul>"
case "ordered":
openTag = "<ol>"
closeTag = "</ol>"
default:
// Write unknown list type
h.out.WriteString("unknown list type " + entry.List.Type)
// Continue to next entry
continue
}
// Create variables for keeping track of list state
lastLevel := 0
openLists := 0
for _, item := range entry.List.Items {
if item.Level > lastLevel {
// Get amount of lists to open
amtOpen := item.Level - lastLevel
// Increment openLists by amount
openLists += amtOpen
// Write opening tag the amount of times required
h.out.WriteString(strings.Repeat(openTag, amtOpen))
// Set lastLevel
lastLevel = item.Level
} else if item.Level < lastLevel {
// Get amount of lists to close
amtClose := lastLevel - item.Level
// DEcrement openLists by amount
openLists -= amtClose
// Write closing tag the amount of times required
h.out.WriteString(strings.Repeat(closeTag, amtClose))
// Set lastLevel
lastLevel = item.Level
}
// If content exists
if len(item.Content) > 0 {
// Write opening list item tag to output
h.out.WriteString("<li>")
// Format content as paragraph
h.formatPara(item.Content[0], true)
// Write closing list item tag to output
h.out.WriteString("</li>")
// For every line other than the first
for _, line := range item.Content[1:] {
// Format content as paragraph
h.formatPara(line, true)
}
}
}
// Close all open lists
h.out.WriteString(strings.Repeat(closeTag, openLists))
} else if entry.Code != nil {
// Get lexer for provided language
lexer := lexers.Get(entry.Code.Language)
if lexer == nil {
lexer = lexers.Fallback
}
// Coalesce lexer tokens
lexer = chroma.Coalesce(lexer)
// Tokenise provided source using lexer
iterator, err := lexer.Tokenise(nil, entry.Code.Text)
if err != nil {
h.out.WriteString("Error tokenising code: " + err.Error())
continue
}
// Create new HTML formatter
formatter := html.New(
html.Standalone(false),
html.WithLineNumbers(true),
html.LineNumbersInTable(true),
)
// If no style
if entry.Code.Style == "" {
// Set style to monokai
entry.Code.Style = "monokai"
}
// Get provided style
chromaStyle := styles.Get(entry.Code.Style)
// Create buffer
buf := &bytes.Buffer{}
// Format source code, writing output to buffer
err = formatter.Format(buf, chromaStyle, iterator)
if err != nil {
h.out.WriteString("Error formatting code: " + err.Error())
continue
}
// Write buffer contents to output
buf.WriteTo(h.out)
} else if entry.Break != nil {
// Write break tag to output
h.out.WriteString("<br>")
} else if entry.Hline != nil {
// Write horizonhal line tag to output
h.out.WriteString("<hr>")
}
}
// Return contents of output buffer
return h.out.String()
}