268 lines
7.7 KiB
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()
|
|
}
|