/* 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 . */ 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("

") } // 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("") } if ftype == ast.FormatTypeItalic { h.out.WriteString("") } if ftype == ast.FormatTypeCode { h.out.WriteString("") } if ftype == ast.FormatTypeStrike { h.out.WriteString(``) } 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("") } if ftype == ast.FormatTypeItalic { h.out.WriteString("") } if ftype == ast.FormatTypeCode { h.out.WriteString("") } if ftype == ast.FormatTypeStrike { h.out.WriteString(``) } 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, `%s`, fragment.Link.Link, fragment.Link.Text) } } // If tags are to be included if includeTags { // Write closing p tag to buffer h.out.WriteString("

") } } // 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, "", entry.Heading.Level) // Format paragraph with heading content without tags h.formatPara(entry.Heading.Content, false) // Write closing heading tag fmt.Fprintf(h.out, "", 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, ``, entry.Image.Link) } // Weite image tag fmt.Fprintf(h.out, `%s`, entry.Image.Source, entry.Image.Alternate) // If image link exists if entry.Image.Link != "" { // Write closing link tag h.out.WriteString("") } } 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 = "" case "ordered": openTag = "
    " closeTag = "
" 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("
  • ") // Format content as paragraph h.formatPara(item.Content[0], true) // Write closing list item tag to output h.out.WriteString("
  • ") // 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("
    ") } else if entry.Hline != nil { // Write horizonhal line tag to output h.out.WriteString("
    ") } } // Return contents of output buffer return h.out.String() }