package org
import (
"fmt"
"html"
"log"
"regexp"
"strconv"
"strings"
"unicode"
h "golang.org/x/net/html"
"golang.org/x/net/html/atom"
)
// HTMLWriter exports an org document into a html document.
type HTMLWriter struct {
ExtendingWriter Writer
HighlightCodeBlock func(source, lang string, inline bool) string
PrettyRelativeLinks bool
strings.Builder
document *Document
htmlEscape bool
log *log.Logger
footnotes *footnotes
}
type footnotes struct {
mapping map[string]int
list []*FootnoteDefinition
}
var emphasisTags = map[string][]string{
"/": []string{"", " "},
"*": []string{"", " "},
"+": []string{"", ""},
"~": []string{"", "
"},
"=": []string{``, "
"},
"_": []string{``, " "},
"_{}": []string{"", " "},
"^{}": []string{"", " "},
}
var listTags = map[string][]string{
"unordered": []string{"
"},
"ordered": []string{"", " "},
"descriptive": []string{"", " "},
}
var listItemStatuses = map[string]string{
" ": "unchecked",
"-": "indeterminate",
"X": "checked",
}
var cleanHeadlineTitleForHTMLAnchorRegexp = regexp.MustCompile(`?a[^>]*>`) // nested a tags are not valid HTML
func NewHTMLWriter() *HTMLWriter {
defaultConfig := New()
return &HTMLWriter{
document: &Document{Configuration: defaultConfig},
log: defaultConfig.Log,
htmlEscape: true,
HighlightCodeBlock: func(source, lang string, inline bool) string {
if inline {
return fmt.Sprintf("", html.EscapeString(source))
}
return fmt.Sprintf("", html.EscapeString(source))
},
footnotes: &footnotes{
mapping: map[string]int{},
},
}
}
func (w *HTMLWriter) WriteNodesAsString(nodes ...Node) string {
original := w.Builder
w.Builder = strings.Builder{}
WriteNodes(w, nodes...)
out := w.String()
w.Builder = original
return out
}
func (w *HTMLWriter) WriterWithExtensions() Writer {
if w.ExtendingWriter != nil {
return w.ExtendingWriter
}
return w
}
func (w *HTMLWriter) Before(d *Document) {
w.document = d
w.log = d.Log
if title := d.Get("TITLE"); title != "" && w.document.GetOption("title") != "nil" {
titleDocument := d.Parse(strings.NewReader(title), d.Path)
if titleDocument.Error == nil {
title = w.WriteNodesAsString(titleDocument.Nodes...)
}
w.WriteString(fmt.Sprintf(`%s `+"\n", title))
}
w.WriteOutline(d)
}
func (w *HTMLWriter) After(d *Document) {
w.WriteFootnotes(d)
}
func (w *HTMLWriter) WriteComment(Comment) {}
func (w *HTMLWriter) WritePropertyDrawer(PropertyDrawer) {}
func (w *HTMLWriter) WriteBlock(b Block) {
content, params := w.blockContent(b.Name, b.Children), b.ParameterMap()
switch b.Name {
case "SRC":
if params[":exports"] == "results" || params[":exports"] == "none" {
break
}
lang := "text"
if len(b.Parameters) >= 1 {
lang = strings.ToLower(b.Parameters[0])
}
content = w.HighlightCodeBlock(content, lang, false)
w.WriteString(fmt.Sprintf("\n%s\n
\n", lang, content))
case "EXAMPLE":
w.WriteString(`` + "\n" + html.EscapeString(content) + "\n \n")
case "EXPORT":
if len(b.Parameters) >= 1 && strings.ToLower(b.Parameters[0]) == "html" {
w.WriteString(content + "\n")
}
case "QUOTE":
w.WriteString("\n" + content + " \n")
case "CENTER":
w.WriteString(`` + "\n")
w.WriteString(content + "
\n")
default:
w.WriteString(fmt.Sprintf(``, strings.ToLower(b.Name)) + "\n")
w.WriteString(content + "
\n")
}
if b.Result != nil && params[":exports"] != "code" && params[":exports"] != "none" {
WriteNodes(w, b.Result)
}
}
func (w *HTMLWriter) WriteResult(r Result) { WriteNodes(w, r.Node) }
func (w *HTMLWriter) WriteInlineBlock(b InlineBlock) {
content := w.blockContent(strings.ToUpper(b.Name), b.Children)
switch b.Name {
case "src":
lang := strings.ToLower(b.Parameters[0])
content = w.HighlightCodeBlock(content, lang, true)
w.WriteString(fmt.Sprintf("\n%s\n
", lang, content))
case "export":
if strings.ToLower(b.Parameters[0]) == "html" {
w.WriteString(content)
}
}
}
func (w *HTMLWriter) WriteDrawer(d Drawer) {
WriteNodes(w, d.Children...)
}
func (w *HTMLWriter) WriteKeyword(k Keyword) {
if k.Key == "HTML" {
w.WriteString(k.Value + "\n")
}
}
func (w *HTMLWriter) WriteInclude(i Include) {
WriteNodes(w, i.Resolve())
}
func (w *HTMLWriter) WriteFootnoteDefinition(f FootnoteDefinition) {
w.footnotes.updateDefinition(f)
}
func (w *HTMLWriter) WriteFootnotes(d *Document) {
if w.document.GetOption("f") == "nil" || len(w.footnotes.list) == 0 {
return
}
w.WriteString(`\n")
}
func (w *HTMLWriter) WriteOutline(d *Document) {
if w.document.GetOption("toc") != "nil" && len(d.Outline.Children) != 0 {
maxLvl, _ := strconv.Atoi(w.document.GetOption("toc"))
w.WriteString("\n\n")
for _, section := range d.Outline.Children {
w.writeSection(section, maxLvl)
}
w.WriteString(" \n \n")
}
}
func (w *HTMLWriter) writeSection(section *Section, maxLvl int) {
if maxLvl != 0 && section.Headline.Lvl > maxLvl {
return
}
// NOTE: To satisfy hugo ExtractTOC() check we cannot use `\n` here. Doesn't really matter, just a note.
w.WriteString(" ")
h := section.Headline
title := cleanHeadlineTitleForHTMLAnchorRegexp.ReplaceAllString(w.WriteNodesAsString(h.Title...), "")
w.WriteString(fmt.Sprintf("%s \n", h.ID(), title))
hasChildren := false
for _, section := range section.Children {
hasChildren = hasChildren || maxLvl == 0 || section.Headline.Lvl <= maxLvl
}
if hasChildren {
w.WriteString("\n")
for _, section := range section.Children {
w.writeSection(section, maxLvl)
}
w.WriteString(" \n")
}
w.WriteString(" \n")
}
func (w *HTMLWriter) WriteHeadline(h Headline) {
for _, excludeTag := range strings.Fields(w.document.Get("EXCLUDE_TAGS")) {
for _, tag := range h.Tags {
if excludeTag == tag {
return
}
}
}
w.WriteString(fmt.Sprintf(``, h.ID(), h.Lvl+1) + "\n")
w.WriteString(fmt.Sprintf(`
`, h.Lvl+1, h.ID()) + "\n")
if w.document.GetOption("todo") != "nil" && h.Status != "" {
w.WriteString(fmt.Sprintf(`%s `, h.Status) + "\n")
}
if w.document.GetOption("pri") != "nil" && h.Priority != "" {
w.WriteString(fmt.Sprintf(`[%s] `, h.Priority) + "\n")
}
WriteNodes(w, h.Title...)
if w.document.GetOption("tags") != "nil" && 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+1))
if content := w.WriteNodesAsString(h.Children...); content != "" {
w.WriteString(fmt.Sprintf(`
`, h.ID(), h.Lvl+1) + "\n" + content + "
\n")
}
w.WriteString("
\n")
}
func (w *HTMLWriter) WriteText(t Text) {
if !w.htmlEscape {
w.WriteString(t.Content)
} else if w.document.GetOption("e") == "nil" || t.IsRaw {
w.WriteString(html.EscapeString(t.Content))
} else {
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])
WriteNodes(w, e.Content...)
w.WriteString(tags[1])
}
func (w *HTMLWriter) WriteLatexFragment(l LatexFragment) {
w.WriteString(l.OpeningPair)
WriteNodes(w, l.Content...)
w.WriteString(l.ClosingPair)
}
func (w *HTMLWriter) WriteStatisticToken(s StatisticToken) {
w.WriteString(fmt.Sprintf(`[%s]
`, s.Content))
}
func (w *HTMLWriter) WriteLineBreak(l LineBreak) {
w.WriteString(strings.Repeat("\n", l.Count))
}
func (w *HTMLWriter) WriteExplicitLineBreak(l ExplicitLineBreak) {
w.WriteString(" \n")
}
func (w *HTMLWriter) WriteFootnoteLink(l FootnoteLink) {
if w.document.GetOption("f") == "nil" {
return
}
i := w.footnotes.add(l)
id := i + 1
w.WriteString(fmt.Sprintf(``, id, id, id))
}
func (w *HTMLWriter) WriteTimestamp(t Timestamp) {
if w.document.GetOption("<") == "nil" {
return
}
w.WriteString(`<`)
if t.IsDate {
w.WriteString(t.Time.Format(datestampFormat))
} else {
w.WriteString(t.Time.Format(timestampFormat))
}
if t.Interval != "" {
w.WriteString(" " + t.Interval)
}
w.WriteString(`> `)
}
func (w *HTMLWriter) WriteRegularLink(l RegularLink) {
url := html.EscapeString(l.URL)
if l.Protocol == "file" {
url = url[len("file:"):]
}
if isRelative := l.Protocol == "file" || l.Protocol == ""; isRelative && w.PrettyRelativeLinks {
if !strings.HasPrefix(url, "/") {
url = "../" + url
}
if strings.HasSuffix(url, ".org") {
url = strings.TrimSuffix(url, ".org") + "/"
}
} else if isRelative && strings.HasSuffix(url, ".org") {
url = strings.TrimSuffix(url, ".org") + ".html"
}
if prefix := w.document.Links[l.Protocol]; prefix != "" {
url = html.EscapeString(prefix) + strings.TrimPrefix(url, l.Protocol+":")
}
switch l.Kind() {
case "image":
if l.Description == nil {
w.WriteString(fmt.Sprintf(` `, url, url, url))
} else {
description := strings.TrimPrefix(String(l.Description), "file:")
w.WriteString(fmt.Sprintf(` `, url, description, description))
}
case "video":
if l.Description == nil {
w.WriteString(fmt.Sprintf(`%s `, url, url, url))
} else {
description := strings.TrimPrefix(String(l.Description), "file:")
w.WriteString(fmt.Sprintf(` `, url, description, description))
}
default:
description := url
if l.Description != nil {
description = w.WriteNodesAsString(l.Description...)
}
w.WriteString(fmt.Sprintf(`%s `, url, description))
}
}
func (w *HTMLWriter) WriteMacro(m Macro) {
if macro := w.document.Macros[m.Name]; macro != "" {
for i, param := range m.Parameters {
macro = strings.Replace(macro, fmt.Sprintf("$%d", i+1), param, -1)
}
macroDocument := w.document.Parse(strings.NewReader(macro), w.document.Path)
if macroDocument.Error != nil {
w.log.Printf("bad macro: %s -> %s: %v", m.Name, macro, macroDocument.Error)
}
WriteNodes(w, macroDocument.Nodes...)
}
}
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")
WriteNodes(w, l.Items...)
w.WriteString(tags[1] + "\n")
}
func (w *HTMLWriter) WriteListItem(li ListItem) {
if li.Status != "" {
w.WriteString(fmt.Sprintf("\n", listItemStatuses[li.Status]))
} else {
w.WriteString(" \n")
}
WriteNodes(w, li.Children...)
w.WriteString(" \n")
}
func (w *HTMLWriter) WriteDescriptiveListItem(di DescriptiveListItem) {
if di.Status != "" {
w.WriteString(fmt.Sprintf("\n", listItemStatuses[di.Status]))
} else {
w.WriteString("\n")
}
if len(di.Term) != 0 {
WriteNodes(w, di.Term...)
} else {
w.WriteString("?")
}
w.WriteString("\n \n")
w.WriteString(" \n")
WriteNodes(w, di.Details...)
w.WriteString(" \n")
}
func (w *HTMLWriter) WriteParagraph(p Paragraph) {
if len(p.Children) == 0 {
return
}
w.WriteString("")
WriteNodes(w, p.Children...)
w.WriteString("
\n")
}
func (w *HTMLWriter) WriteExample(e Example) {
w.WriteString(`` + "\n")
if len(e.Children) != 0 {
for _, n := range e.Children {
WriteNodes(w, n)
w.WriteString("\n")
}
}
w.WriteString(" \n")
}
func (w *HTMLWriter) WriteHorizontalRule(h HorizontalRule) {
w.WriteString(" \n")
}
func (w *HTMLWriter) WriteNodeWithMeta(n NodeWithMeta) {
out := w.WriteNodesAsString(n.Node)
if p, ok := n.Node.(Paragraph); ok {
if len(p.Children) == 1 && isImageOrVideoLink(p.Children[0]) {
out = w.WriteNodesAsString(p.Children[0])
}
}
for _, attributes := range n.Meta.HTMLAttributes {
out = w.withHTMLAttributes(out, attributes...) + "\n"
}
if len(n.Meta.Caption) != 0 {
caption := ""
for i, ns := range n.Meta.Caption {
if i != 0 {
caption += " "
}
caption += w.WriteNodesAsString(ns...)
}
out = fmt.Sprintf("\n%s\n%s\n \n \n", out, caption)
}
w.WriteString(out)
}
func (w *HTMLWriter) WriteNodeWithName(n NodeWithName) {
WriteNodes(w, n.Node)
}
func (w *HTMLWriter) WriteTable(t Table) {
w.WriteString("\n")
inHead := len(t.SeparatorIndices) > 0 &&
t.SeparatorIndices[0] != len(t.Rows)-1 &&
(t.SeparatorIndices[0] != 0 || len(t.SeparatorIndices) > 1 && t.SeparatorIndices[len(t.SeparatorIndices)-1] != len(t.Rows)-1)
if inHead {
w.WriteString("\n")
} else {
w.WriteString(" \n")
}
for i, row := range t.Rows {
if len(row.Columns) == 0 && i != 0 && i != len(t.Rows)-1 {
if inHead {
w.WriteString("\n \n")
inHead = false
} else {
w.WriteString(" \n\n")
}
}
if row.IsSpecial {
continue
}
if inHead {
w.writeTableColumns(row.Columns, "th")
} else {
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))
}
WriteNodes(w, column.Children...)
w.WriteString(fmt.Sprintf("%s>\n", tag))
}
w.WriteString(" \n")
}
func (w *HTMLWriter) withHTMLAttributes(input string, kvs ...string) string {
if len(kvs)%2 != 0 {
w.log.Printf("withHTMLAttributes: Len of kvs must be even: %#v", kvs)
return input
}
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 {
w.log.Printf("withHTMLAttributes: Could not extend attributes of %s: %v (%s)", input, nodes, err)
return input
}
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 {
w.log.Printf("withHTMLAttributes: Could not extend attributes of %s: %v (%s)", input, node, err)
return input
}
return out.String()
}
func (w *HTMLWriter) blockContent(name string, children []Node) string {
if isRawTextBlock(name) {
builder, htmlEscape := w.Builder, w.htmlEscape
w.Builder, w.htmlEscape = strings.Builder{}, false
WriteNodes(w, children...)
out := w.String()
w.Builder, w.htmlEscape = builder, htmlEscape
return strings.TrimRightFunc(out, unicode.IsSpace)
} else {
return w.WriteNodesAsString(children...)
}
}
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})
}
func (fs *footnotes) add(f FootnoteLink) int {
if i, ok := fs.mapping[f.Name]; ok && f.Name != "" {
return i
}
fs.list = append(fs.list, f.Definition)
i := len(fs.list) - 1
if f.Name != "" {
fs.mapping[f.Name] = i
}
return i
}
func (fs *footnotes) updateDefinition(f FootnoteDefinition) {
if i, ok := fs.mapping[f.Name]; ok {
fs.list[i] = &f
}
}