package org
import (
"fmt"
"html"
"log"
"regexp"
"strconv"
"strings"
"unicode"
u "net/url"
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, params map[string]string) string
PrettyRelativeLinks bool
// TopLevelHLevel determines what HTML heading to use for a
// level-1 Org headline, and by extension further headings.
//
// For example, a value of 1 means a top-level Org headline will be
// rendered as an
element, a level-2 Org headline will be
// rendered as an
element, and so on.
//
// A value of 2 (default) means a top-level Org headline will be
// rendered as an
element, a level-2 Org headline will be
// rendered as an
element, and so on.
//
// This setting and its default behavior match Org's
// :html-toplevel-hlevel export property and the associated
// org-html-toplevel-hlevel variable.
TopLevelHLevel int
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{
"/": {"", ""},
"*": {"", ""},
"+": {"", ""},
"~": {"", ""},
"=": {``, ""},
"_": {``, ""},
"_{}": {"", ""},
"^{}": {"", ""},
}
var listTags = map[string][]string{
"unordered": {"
", "
"},
"ordered": {"", ""},
"descriptive": {"
", "
"},
}
var listItemStatuses = map[string]string{
" ": "unchecked",
"-": "indeterminate",
"X": "checked",
}
var cleanHeadlineTitleForHTMLAnchorRegexp = regexp.MustCompile(`?a[^>]*>`) // nested a tags are not valid HTML
var tocHeadlineMaxLvlRegexp = regexp.MustCompile(`headlines\s+(\d+)`)
func NewHTMLWriter() *HTMLWriter {
defaultConfig := New()
return &HTMLWriter{
document: &Document{Configuration: defaultConfig},
log: defaultConfig.Log,
htmlEscape: true,
HighlightCodeBlock: func(source, lang string, inline bool, params map[string]string) string {
if inline {
return fmt.Sprintf("
` + "\n")
for i, definition := range w.footnotes.list {
id := i + 1
if definition == nil {
name := ""
for k, v := range w.footnotes.mapping {
if v == i {
name = k
}
}
w.log.Printf("Missing footnote definition for [fn:%s] (#%d)", name, id)
continue
}
w.WriteString(`
\n")
}
func (w *HTMLWriter) writeListItemContent(children []Node) {
if isParagraphNodeSlice(children) {
for i, c := range children {
out := w.WriteNodesAsString(c.(Paragraph).Children...)
if i != 0 && out != "" {
w.WriteString("\n")
}
w.WriteString(out)
}
} else {
w.WriteString("\n")
WriteNodes(w, children...)
}
}
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(strings.TrimLeftFunc(out, IsNewLineChar), 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 isParagraphNodeSlice(ns []Node) bool {
for _, n := range ns {
if _, ok := n.(Paragraph); !ok {
return false
}
}
return true
}
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
}
}