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{""}, "ordered": []string{"
    ", "
"}, "descriptive": []string{"
", "
"}, } func NewHTMLWriter() *HTMLWriter { return &HTMLWriter{ HighlightCodeBlock: func(source, lang string) string { return fmt.Sprintf("%s\n
\n%s\n
\n", `
`, 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") w.WriteString(fmt.Sprintf(`%s`, n, n, n) + "\n") w.WriteString(`
` + "\n") w.writeNodes(f.Children...) w.WriteString("
\n
\n") } func (w *HTMLWriter) writeFootnotes(d *Document) { fs := d.Footnotes if len(fs.Definitions) == 0 { return } w.WriteString(`
` + "\n") w.WriteString(`

` + fs.Title + `

` + "\n") w.WriteString(`
` + "\n") for _, definition := range d.Footnotes.Ordered() { w.writeNodes(definition) } w.WriteString("
\n
\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(`%s`, 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(`%s`, url, description, description)) case "video": w.WriteString(fmt.Sprintf(``, 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("\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}) }