package org
import (
"fmt"
"html"
"strings"
h "golang.org/x/net/html"
"golang.org/x/net/html/atom"
)
type HTMLWriter struct {
stringBuilder
HighlightCodeBlock func(source, lang string) string
}
var emphasisTags = map[string][]string{
"/": []string{"", " "},
"*": []string{"", " "},
"+": []string{"", ""},
"~": []string{"", "
"},
"=": []string{``, "
"},
"_": []string{``, " "},
"_{}": []string{"", " "},
"^{}": []string{"", " "},
}
var listTags = map[string][]string{
"unordered": []string{"
`, html.EscapeString(source))
},
}
}
func (w *HTMLWriter) emptyClone() *HTMLWriter {
wcopy := *w
wcopy.stringBuilder = stringBuilder{}
return &wcopy
}
func (w *HTMLWriter) nodesAsString(nodes ...Node) string {
tmp := w.emptyClone()
tmp.writeNodes(nodes...)
return tmp.String()
}
func (w *HTMLWriter) before(d *Document) {}
func (w *HTMLWriter) after(d *Document) {
w.writeFootnotes(d)
}
func (w *HTMLWriter) writeNodes(ns ...Node) {
for _, n := range ns {
switch n := n.(type) {
case Keyword:
w.writeKeyword(n)
case Include:
w.writeInclude(n)
case Comment:
continue
case NodeWithMeta:
w.writeNodeWithMeta(n)
case Headline:
w.writeHeadline(n)
case Block:
w.writeBlock(n)
case Drawer:
w.writeDrawer(n)
case FootnoteDefinition:
w.writeFootnoteDefinition(n)
case List:
w.writeList(n)
case ListItem:
w.writeListItem(n)
case DescriptiveListItem:
w.writeDescriptiveListItem(n)
case Table:
w.writeTable(n)
case Paragraph:
w.writeParagraph(n)
case Example:
w.writeExample(n)
case HorizontalRule:
w.writeHorizontalRule(n)
case Text:
w.writeText(n)
case Emphasis:
w.writeEmphasis(n)
case ExplicitLineBreak:
w.writeExplicitLineBreak(n)
case LineBreak:
w.writeLineBreak(n)
case RegularLink:
w.writeRegularLink(n)
case FootnoteLink:
w.writeFootnoteLink(n)
default:
if n != nil {
panic(fmt.Sprintf("bad node %#v", n))
}
}
}
}
func (w *HTMLWriter) writeBlock(b Block) {
switch name := b.Name; {
case name == "SRC":
source, lang := b.Children[0].(Text).Content, "text"
if len(b.Parameters) >= 1 {
lang = strings.ToLower(b.Parameters[0])
}
w.WriteString(w.HighlightCodeBlock(source, lang) + "\n")
case name == "EXAMPLE":
w.WriteString(`
` + "\n")
w.writeNodes(b.Children...)
w.WriteString("\n \n")
case name == "EXPORT" && len(b.Parameters) >= 1 && strings.ToLower(b.Parameters[0]) == "html":
w.WriteString(b.Children[0].(Text).Content + "\n")
case name == "QUOTE":
w.WriteString("
\n")
w.writeNodes(b.Children...)
w.WriteString(" \n")
case name == "CENTER":
w.WriteString(`
` + "\n")
w.writeNodes(b.Children...)
w.WriteString("
\n")
default:
w.WriteString(fmt.Sprintf(`
`, strings.ToLower(b.Name)) + "\n")
w.writeNodes(b.Children...)
w.WriteString("
\n")
}
}
func (w *HTMLWriter) writeDrawer(d Drawer) {
w.writeNodes(d.Children...)
}
func (w *HTMLWriter) writeKeyword(k Keyword) {
if k.Key == "HTML" {
w.WriteString(k.Value + "\n")
}
}
func (w *HTMLWriter) writeInclude(i Include) {
w.writeNodes(i.Resolve())
}
func (w *HTMLWriter) writeFootnoteDefinition(f FootnoteDefinition) {
n := f.Name
w.WriteString(`\n")
}
func (w *HTMLWriter) writeFootnotes(d *Document) {
fs := d.Footnotes
if len(fs.Definitions) == 0 {
return
}
w.WriteString(`\n")
}
func (w *HTMLWriter) writeHeadline(h Headline) {
w.WriteString(fmt.Sprintf("
\n", h.Lvl))
if h.Status != "" {
w.WriteString(fmt.Sprintf(`%s `, h.Status) + "\n")
}
w.writeNodes(h.Title...)
if len(h.Tags) != 0 {
tags := make([]string, len(h.Tags))
for i, tag := range h.Tags {
tags[i] = fmt.Sprintf(`%s `, tag)
}
w.WriteString(" ")
w.WriteString(fmt.Sprintf(`%s `, strings.Join(tags, " ")))
}
w.WriteString(fmt.Sprintf("\n \n", h.Lvl))
w.writeNodes(h.Children...)
}
func (w *HTMLWriter) writeText(t Text) {
w.WriteString(html.EscapeString(htmlEntityReplacer.Replace(t.Content)))
}
func (w *HTMLWriter) writeEmphasis(e Emphasis) {
tags, ok := emphasisTags[e.Kind]
if !ok {
panic(fmt.Sprintf("bad emphasis %#v", e))
}
w.WriteString(tags[0])
w.writeNodes(e.Content...)
w.WriteString(tags[1])
}
func (w *HTMLWriter) writeLineBreak(l LineBreak) {
w.WriteString("\n")
}
func (w *HTMLWriter) writeExplicitLineBreak(l ExplicitLineBreak) {
w.WriteString("
\n")
}
func (w *HTMLWriter) writeFootnoteLink(l FootnoteLink) {
n := html.EscapeString(l.Name)
w.WriteString(fmt.Sprintf(``, n, n, n))
}
func (w *HTMLWriter) writeRegularLink(l RegularLink) {
url := html.EscapeString(l.URL)
if l.Protocol == "file" {
url = url[len("file:"):]
}
description := url
if l.Description != nil {
description = w.nodesAsString(l.Description...)
}
switch l.Kind() {
case "image":
w.WriteString(fmt.Sprintf(`
`, url, description, description))
case "video":
w.WriteString(fmt.Sprintf(`
%s `, url, description, description))
default:
w.WriteString(fmt.Sprintf(`
%s `, url, description))
}
}
func (w *HTMLWriter) writeList(l List) {
tags, ok := listTags[l.Kind]
if !ok {
panic(fmt.Sprintf("bad list kind %#v", l))
}
w.WriteString(tags[0] + "\n")
w.writeNodes(l.Items...)
w.WriteString(tags[1] + "\n")
}
func (w *HTMLWriter) writeListItem(li ListItem) {
w.WriteString("
\n")
w.writeNodes(li.Children...)
w.WriteString(" \n")
}
func (w *HTMLWriter) writeDescriptiveListItem(di DescriptiveListItem) {
w.WriteString("
\n")
if len(di.Term) != 0 {
w.writeNodes(di.Term...)
} else {
w.WriteString("?")
}
w.WriteString(" \n")
w.writeNodes(di.Details...)
w.WriteString("\n")
}
func (w *HTMLWriter) writeParagraph(p Paragraph) {
if isEmptyLineParagraph(p) {
return
}
w.WriteString("")
if _, ok := p.Children[0].(LineBreak); !ok {
w.WriteString("\n")
}
w.writeNodes(p.Children...)
w.WriteString("\n
\n")
}
func (w *HTMLWriter) writeExample(e Example) {
w.WriteString(`` + "\n")
if len(e.Children) != 0 {
for _, n := range e.Children {
w.writeNodes(n)
w.WriteString("\n")
}
}
w.WriteString("\n \n")
}
func (w *HTMLWriter) writeHorizontalRule(h HorizontalRule) {
w.WriteString(" \n")
}
func (w *HTMLWriter) writeNodeWithMeta(n NodeWithMeta) {
out := w.nodesAsString(n.Node)
if p, ok := n.Node.(Paragraph); ok {
if len(p.Children) == 1 && isImageOrVideoLink(p.Children[0]) {
out = w.nodesAsString(p.Children[0])
}
}
for _, attributes := range n.Meta.HTMLAttributes {
out = withHTMLAttributes(out, attributes...) + "\n"
}
if len(n.Meta.Caption) != 0 {
caption := ""
for i, ns := range n.Meta.Caption {
if i != 0 {
caption += " "
}
caption += w.nodesAsString(ns...)
}
out = fmt.Sprintf("\n%s\n%s\n \n \n", out, caption)
}
w.WriteString(out)
}
func (w *HTMLWriter) writeTable(t Table) {
w.WriteString("\n")
beforeFirstContentRow := true
for i, row := range t.Rows {
if row.IsSpecial || len(row.Columns) == 0 {
continue
}
if beforeFirstContentRow {
beforeFirstContentRow = false
if i+1 < len(t.Rows) && len(t.Rows[i+1].Columns) == 0 {
w.WriteString("\n")
w.writeTableColumns(row.Columns, "th")
w.WriteString(" \n\n")
continue
} else {
w.WriteString(" \n")
}
}
w.writeTableColumns(row.Columns, "td")
}
w.WriteString(" \n
\n")
}
func (w *HTMLWriter) writeTableColumns(columns []Column, tag string) {
w.WriteString("\n")
for _, column := range columns {
if column.Align == "" {
w.WriteString(fmt.Sprintf("<%s>", tag))
} else {
w.WriteString(fmt.Sprintf(`<%s class="align-%s">`, tag, column.Align))
}
w.writeNodes(column.Children...)
w.WriteString(fmt.Sprintf("%s>\n", tag))
}
w.WriteString(" \n")
}
func withHTMLAttributes(input string, kvs ...string) string {
if len(kvs)%2 != 0 {
panic(fmt.Sprintf("len of kvs must be even: %#v", kvs))
}
context := &h.Node{Type: h.ElementNode, Data: "body", DataAtom: atom.Body}
nodes, err := h.ParseFragment(strings.NewReader(strings.TrimSpace(input)), context)
if err != nil || len(nodes) != 1 {
panic(fmt.Sprintf("could not extend html attributes of %s: %v (%s)", input, len(nodes), err))
}
out, node := strings.Builder{}, nodes[0]
for i := 0; i < len(kvs)-1; i += 2 {
node.Attr = setHTMLAttribute(node.Attr, strings.TrimPrefix(kvs[i], ":"), kvs[i+1])
}
err = h.Render(&out, nodes[0])
if err != nil {
panic(fmt.Sprintf("could not extend html attributes of %s: %#v (%s)", input, nodes, err))
}
return out.String()
}
func setHTMLAttribute(attributes []h.Attribute, k, v string) []h.Attribute {
for i, a := range attributes {
if strings.ToLower(a.Key) == strings.ToLower(k) {
switch strings.ToLower(k) {
case "class", "style":
attributes[i].Val += " " + v
default:
attributes[i].Val = v
}
return attributes
}
}
return append(attributes, h.Attribute{Namespace: "", Key: k, Val: v})
}