You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

html_writer.go 13KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503
  1. package org
  2. import (
  3. "fmt"
  4. "html"
  5. "log"
  6. "regexp"
  7. "strings"
  8. "unicode"
  9. h "golang.org/x/net/html"
  10. "golang.org/x/net/html/atom"
  11. )
  12. // HTMLWriter exports an org document into a html document.
  13. type HTMLWriter struct {
  14. ExtendingWriter Writer
  15. HighlightCodeBlock func(source, lang string) string
  16. strings.Builder
  17. document *Document
  18. htmlEscape bool
  19. log *log.Logger
  20. footnotes *footnotes
  21. }
  22. type footnotes struct {
  23. mapping map[string]int
  24. list []*FootnoteDefinition
  25. }
  26. var emphasisTags = map[string][]string{
  27. "/": []string{"<em>", "</em>"},
  28. "*": []string{"<strong>", "</strong>"},
  29. "+": []string{"<del>", "</del>"},
  30. "~": []string{"<code>", "</code>"},
  31. "=": []string{`<code class="verbatim">`, "</code>"},
  32. "_": []string{`<span style="text-decoration: underline;">`, "</span>"},
  33. "_{}": []string{"<sub>", "</sub>"},
  34. "^{}": []string{"<sup>", "</sup>"},
  35. }
  36. var listTags = map[string][]string{
  37. "unordered": []string{"<ul>", "</ul>"},
  38. "ordered": []string{"<ol>", "</ol>"},
  39. "descriptive": []string{"<dl>", "</dl>"},
  40. }
  41. var listItemStatuses = map[string]string{
  42. " ": "unchecked",
  43. "-": "indeterminate",
  44. "X": "checked",
  45. }
  46. var cleanHeadlineTitleForHTMLAnchorRegexp = regexp.MustCompile(`</?a[^>]*>`) // nested a tags are not valid HTML
  47. func NewHTMLWriter() *HTMLWriter {
  48. defaultConfig := New()
  49. return &HTMLWriter{
  50. document: &Document{Configuration: defaultConfig},
  51. log: defaultConfig.Log,
  52. htmlEscape: true,
  53. HighlightCodeBlock: func(source, lang string) string {
  54. return fmt.Sprintf("<div class=\"highlight\">\n<pre>\n%s\n</pre>\n</div>", html.EscapeString(source))
  55. },
  56. footnotes: &footnotes{
  57. mapping: map[string]int{},
  58. },
  59. }
  60. }
  61. func (w *HTMLWriter) WriteNodesAsString(nodes ...Node) string {
  62. original := w.Builder
  63. w.Builder = strings.Builder{}
  64. WriteNodes(w, nodes...)
  65. out := w.String()
  66. w.Builder = original
  67. return out
  68. }
  69. func (w *HTMLWriter) WriterWithExtensions() Writer {
  70. if w.ExtendingWriter != nil {
  71. return w.ExtendingWriter
  72. }
  73. return w
  74. }
  75. func (w *HTMLWriter) Before(d *Document) {
  76. w.document = d
  77. w.log = d.Log
  78. w.WriteOutline(d)
  79. }
  80. func (w *HTMLWriter) After(d *Document) {
  81. w.WriteFootnotes(d)
  82. }
  83. func (w *HTMLWriter) WriteComment(Comment) {}
  84. func (w *HTMLWriter) WritePropertyDrawer(PropertyDrawer) {}
  85. func (w *HTMLWriter) WriteBlock(b Block) {
  86. content := ""
  87. if isRawTextBlock(b.Name) {
  88. builder, htmlEscape := w.Builder, w.htmlEscape
  89. w.Builder, w.htmlEscape = strings.Builder{}, false
  90. WriteNodes(w, b.Children...)
  91. out := w.String()
  92. w.Builder, w.htmlEscape = builder, htmlEscape
  93. content = strings.TrimRightFunc(out, unicode.IsSpace)
  94. } else {
  95. content = w.WriteNodesAsString(b.Children...)
  96. }
  97. switch name := b.Name; {
  98. case name == "SRC":
  99. lang := "text"
  100. if len(b.Parameters) >= 1 {
  101. lang = strings.ToLower(b.Parameters[0])
  102. }
  103. content = w.HighlightCodeBlock(content, lang)
  104. w.WriteString(fmt.Sprintf("<div class=\"src src-%s\">\n%s\n</div>\n", lang, content))
  105. case name == "EXAMPLE":
  106. w.WriteString(`<pre class="example">` + "\n" + content + "\n</pre>\n")
  107. case name == "EXPORT" && len(b.Parameters) >= 1 && strings.ToLower(b.Parameters[0]) == "html":
  108. w.WriteString(content + "\n")
  109. case name == "QUOTE":
  110. w.WriteString("<blockquote>\n" + content + "</blockquote>\n")
  111. case name == "CENTER":
  112. w.WriteString(`<div class="center-block" style="text-align: center; margin-left: auto; margin-right: auto;">` + "\n")
  113. w.WriteString(content + "</div>\n")
  114. default:
  115. w.WriteString(fmt.Sprintf(`<div class="%s-block">`, strings.ToLower(b.Name)) + "\n")
  116. w.WriteString(content + "</div>\n")
  117. }
  118. }
  119. func (w *HTMLWriter) WriteDrawer(d Drawer) {
  120. WriteNodes(w, d.Children...)
  121. }
  122. func (w *HTMLWriter) WriteKeyword(k Keyword) {
  123. if k.Key == "HTML" {
  124. w.WriteString(k.Value + "\n")
  125. }
  126. }
  127. func (w *HTMLWriter) WriteInclude(i Include) {
  128. WriteNodes(w, i.Resolve())
  129. }
  130. func (w *HTMLWriter) WriteFootnoteDefinition(f FootnoteDefinition) {
  131. w.footnotes.updateDefinition(f)
  132. }
  133. func (w *HTMLWriter) WriteFootnotes(d *Document) {
  134. if !w.document.GetOption("f") || len(w.footnotes.list) == 0 {
  135. return
  136. }
  137. w.WriteString(`<div class="footnotes">` + "\n")
  138. w.WriteString(`<hr class="footnotes-separatator">` + "\n")
  139. w.WriteString(`<div class="footnote-definitions">` + "\n")
  140. for i, definition := range w.footnotes.list {
  141. id := i + 1
  142. if definition == nil {
  143. name := ""
  144. for k, v := range w.footnotes.mapping {
  145. if v == i {
  146. name = k
  147. }
  148. }
  149. w.log.Printf("Missing footnote definition for [fn:%s] (#%d)", name, id)
  150. continue
  151. }
  152. w.WriteString(`<div class="footnote-definition">` + "\n")
  153. w.WriteString(fmt.Sprintf(`<sup id="footnote-%d"><a href="#footnote-reference-%d">%d</a></sup>`, id, id, id) + "\n")
  154. w.WriteString(`<div class="footnote-body">` + "\n")
  155. WriteNodes(w, definition.Children...)
  156. w.WriteString("</div>\n</div>\n")
  157. }
  158. w.WriteString("</div>\n</div>\n")
  159. }
  160. func (w *HTMLWriter) WriteOutline(d *Document) {
  161. if w.document.GetOption("toc") && len(d.Outline.Children) != 0 {
  162. w.WriteString("<nav>\n<ul>\n")
  163. for _, section := range d.Outline.Children {
  164. w.writeSection(section)
  165. }
  166. w.WriteString("</ul>\n</nav>\n")
  167. }
  168. }
  169. func (w *HTMLWriter) writeSection(section *Section) {
  170. // NOTE: To satisfy hugo ExtractTOC() check we cannot use `<li>\n` here. Doesn't really matter, just a note.
  171. w.WriteString("<li>")
  172. h := section.Headline
  173. title := cleanHeadlineTitleForHTMLAnchorRegexp.ReplaceAllString(w.WriteNodesAsString(h.Title...), "")
  174. w.WriteString(fmt.Sprintf("<a href=\"#%s\">%s</a>\n", h.ID(), title))
  175. if len(section.Children) != 0 {
  176. w.WriteString("<ul>\n")
  177. for _, section := range section.Children {
  178. w.writeSection(section)
  179. }
  180. w.WriteString("</ul>\n")
  181. }
  182. w.WriteString("</li>\n")
  183. }
  184. func (w *HTMLWriter) WriteHeadline(h Headline) {
  185. for _, excludeTag := range strings.Fields(w.document.Get("EXCLUDE_TAGS")) {
  186. for _, tag := range h.Tags {
  187. if excludeTag == tag {
  188. return
  189. }
  190. }
  191. }
  192. w.WriteString(fmt.Sprintf(`<h%d id="%s">`, h.Lvl, h.ID()) + "\n")
  193. if w.document.GetOption("todo") && h.Status != "" {
  194. w.WriteString(fmt.Sprintf(`<span class="todo">%s</span>`, h.Status) + "\n")
  195. }
  196. if w.document.GetOption("pri") && h.Priority != "" {
  197. w.WriteString(fmt.Sprintf(`<span class="priority">[%s]</span>`, h.Priority) + "\n")
  198. }
  199. WriteNodes(w, h.Title...)
  200. if w.document.GetOption("tags") && len(h.Tags) != 0 {
  201. tags := make([]string, len(h.Tags))
  202. for i, tag := range h.Tags {
  203. tags[i] = fmt.Sprintf(`<span>%s</span>`, tag)
  204. }
  205. w.WriteString("&#xa0;&#xa0;&#xa0;")
  206. w.WriteString(fmt.Sprintf(`<span class="tags">%s</span>`, strings.Join(tags, "&#xa0;")))
  207. }
  208. w.WriteString(fmt.Sprintf("\n</h%d>\n", h.Lvl))
  209. WriteNodes(w, h.Children...)
  210. }
  211. func (w *HTMLWriter) WriteText(t Text) {
  212. if !w.htmlEscape {
  213. w.WriteString(t.Content)
  214. } else if !w.document.GetOption("e") || t.IsRaw {
  215. w.WriteString(html.EscapeString(t.Content))
  216. } else {
  217. w.WriteString(html.EscapeString(htmlEntityReplacer.Replace(t.Content)))
  218. }
  219. }
  220. func (w *HTMLWriter) WriteEmphasis(e Emphasis) {
  221. tags, ok := emphasisTags[e.Kind]
  222. if !ok {
  223. panic(fmt.Sprintf("bad emphasis %#v", e))
  224. }
  225. w.WriteString(tags[0])
  226. WriteNodes(w, e.Content...)
  227. w.WriteString(tags[1])
  228. }
  229. func (w *HTMLWriter) WriteLatexFragment(l LatexFragment) {
  230. w.WriteString(l.OpeningPair)
  231. WriteNodes(w, l.Content...)
  232. w.WriteString(l.ClosingPair)
  233. }
  234. func (w *HTMLWriter) WriteStatisticToken(s StatisticToken) {
  235. w.WriteString(fmt.Sprintf(`<code class="statistic">[%s]</code>`, s.Content))
  236. }
  237. func (w *HTMLWriter) WriteLineBreak(l LineBreak) {
  238. w.WriteString(strings.Repeat("\n", l.Count))
  239. }
  240. func (w *HTMLWriter) WriteExplicitLineBreak(l ExplicitLineBreak) {
  241. w.WriteString("<br>\n")
  242. }
  243. func (w *HTMLWriter) WriteFootnoteLink(l FootnoteLink) {
  244. if !w.document.GetOption("f") {
  245. return
  246. }
  247. i := w.footnotes.add(l)
  248. id := i + 1
  249. w.WriteString(fmt.Sprintf(`<sup class="footnote-reference"><a id="footnote-reference-%d" href="#footnote-%d">%d</a></sup>`, id, id, id))
  250. }
  251. func (w *HTMLWriter) WriteTimestamp(t Timestamp) {
  252. if !w.document.GetOption("<") {
  253. return
  254. }
  255. w.WriteString(`<span class="timestamp">&lt;`)
  256. if t.IsDate {
  257. w.WriteString(t.Time.Format(datestampFormat))
  258. } else {
  259. w.WriteString(t.Time.Format(timestampFormat))
  260. }
  261. if t.Interval != "" {
  262. w.WriteString(" " + t.Interval)
  263. }
  264. w.WriteString(`&gt;</span>`)
  265. }
  266. func (w *HTMLWriter) WriteRegularLink(l RegularLink) {
  267. url := html.EscapeString(l.URL)
  268. if l.Protocol == "file" {
  269. url = url[len("file:"):]
  270. }
  271. description := url
  272. if l.Description != nil {
  273. description = w.WriteNodesAsString(l.Description...)
  274. }
  275. switch l.Kind() {
  276. case "image":
  277. w.WriteString(fmt.Sprintf(`<img src="%s" alt="%s" title="%s" />`, url, description, description))
  278. case "video":
  279. w.WriteString(fmt.Sprintf(`<video src="%s" title="%s">%s</video>`, url, description, description))
  280. default:
  281. w.WriteString(fmt.Sprintf(`<a href="%s">%s</a>`, url, description))
  282. }
  283. }
  284. func (w *HTMLWriter) WriteList(l List) {
  285. tags, ok := listTags[l.Kind]
  286. if !ok {
  287. panic(fmt.Sprintf("bad list kind %#v", l))
  288. }
  289. w.WriteString(tags[0] + "\n")
  290. WriteNodes(w, l.Items...)
  291. w.WriteString(tags[1] + "\n")
  292. }
  293. func (w *HTMLWriter) WriteListItem(li ListItem) {
  294. if li.Status != "" {
  295. w.WriteString(fmt.Sprintf("<li class=\"%s\">\n", listItemStatuses[li.Status]))
  296. } else {
  297. w.WriteString("<li>\n")
  298. }
  299. WriteNodes(w, li.Children...)
  300. w.WriteString("</li>\n")
  301. }
  302. func (w *HTMLWriter) WriteDescriptiveListItem(di DescriptiveListItem) {
  303. if di.Status != "" {
  304. w.WriteString(fmt.Sprintf("<dt class=\"%s\">\n", listItemStatuses[di.Status]))
  305. } else {
  306. w.WriteString("<dt>\n")
  307. }
  308. if len(di.Term) != 0 {
  309. WriteNodes(w, di.Term...)
  310. } else {
  311. w.WriteString("?")
  312. }
  313. w.WriteString("\n</dt>\n")
  314. w.WriteString("<dd>\n")
  315. WriteNodes(w, di.Details...)
  316. w.WriteString("</dd>\n")
  317. }
  318. func (w *HTMLWriter) WriteParagraph(p Paragraph) {
  319. if len(p.Children) == 0 {
  320. return
  321. }
  322. w.WriteString("<p>")
  323. if _, ok := p.Children[0].(LineBreak); !ok {
  324. w.WriteString("\n")
  325. }
  326. WriteNodes(w, p.Children...)
  327. w.WriteString("\n</p>\n")
  328. }
  329. func (w *HTMLWriter) WriteExample(e Example) {
  330. w.WriteString(`<pre class="example">` + "\n")
  331. if len(e.Children) != 0 {
  332. for _, n := range e.Children {
  333. WriteNodes(w, n)
  334. w.WriteString("\n")
  335. }
  336. }
  337. w.WriteString("</pre>\n")
  338. }
  339. func (w *HTMLWriter) WriteHorizontalRule(h HorizontalRule) {
  340. w.WriteString("<hr>\n")
  341. }
  342. func (w *HTMLWriter) WriteNodeWithMeta(n NodeWithMeta) {
  343. out := w.WriteNodesAsString(n.Node)
  344. if p, ok := n.Node.(Paragraph); ok {
  345. if len(p.Children) == 1 && isImageOrVideoLink(p.Children[0]) {
  346. out = w.WriteNodesAsString(p.Children[0])
  347. }
  348. }
  349. for _, attributes := range n.Meta.HTMLAttributes {
  350. out = w.withHTMLAttributes(out, attributes...) + "\n"
  351. }
  352. if len(n.Meta.Caption) != 0 {
  353. caption := ""
  354. for i, ns := range n.Meta.Caption {
  355. if i != 0 {
  356. caption += " "
  357. }
  358. caption += w.WriteNodesAsString(ns...)
  359. }
  360. out = fmt.Sprintf("<figure>\n%s<figcaption>\n%s\n</figcaption>\n</figure>\n", out, caption)
  361. }
  362. w.WriteString(out)
  363. }
  364. func (w *HTMLWriter) WriteNodeWithName(n NodeWithName) {
  365. WriteNodes(w, n.Node)
  366. }
  367. func (w *HTMLWriter) WriteTable(t Table) {
  368. w.WriteString("<table>\n")
  369. beforeFirstContentRow := true
  370. for i, row := range t.Rows {
  371. if row.IsSpecial || len(row.Columns) == 0 {
  372. continue
  373. }
  374. if beforeFirstContentRow {
  375. beforeFirstContentRow = false
  376. if i+1 < len(t.Rows) && len(t.Rows[i+1].Columns) == 0 {
  377. w.WriteString("<thead>\n")
  378. w.writeTableColumns(row.Columns, "th")
  379. w.WriteString("</thead>\n<tbody>\n")
  380. continue
  381. } else {
  382. w.WriteString("<tbody>\n")
  383. }
  384. }
  385. w.writeTableColumns(row.Columns, "td")
  386. }
  387. w.WriteString("</tbody>\n</table>\n")
  388. }
  389. func (w *HTMLWriter) writeTableColumns(columns []Column, tag string) {
  390. w.WriteString("<tr>\n")
  391. for _, column := range columns {
  392. if column.Align == "" {
  393. w.WriteString(fmt.Sprintf("<%s>", tag))
  394. } else {
  395. w.WriteString(fmt.Sprintf(`<%s class="align-%s">`, tag, column.Align))
  396. }
  397. WriteNodes(w, column.Children...)
  398. w.WriteString(fmt.Sprintf("</%s>\n", tag))
  399. }
  400. w.WriteString("</tr>\n")
  401. }
  402. func (w *HTMLWriter) withHTMLAttributes(input string, kvs ...string) string {
  403. if len(kvs)%2 != 0 {
  404. w.log.Printf("withHTMLAttributes: Len of kvs must be even: %#v", kvs)
  405. return input
  406. }
  407. context := &h.Node{Type: h.ElementNode, Data: "body", DataAtom: atom.Body}
  408. nodes, err := h.ParseFragment(strings.NewReader(strings.TrimSpace(input)), context)
  409. if err != nil || len(nodes) != 1 {
  410. w.log.Printf("withHTMLAttributes: Could not extend attributes of %s: %v (%s)", input, nodes, err)
  411. return input
  412. }
  413. out, node := strings.Builder{}, nodes[0]
  414. for i := 0; i < len(kvs)-1; i += 2 {
  415. node.Attr = setHTMLAttribute(node.Attr, strings.TrimPrefix(kvs[i], ":"), kvs[i+1])
  416. }
  417. err = h.Render(&out, nodes[0])
  418. if err != nil {
  419. w.log.Printf("withHTMLAttributes: Could not extend attributes of %s: %v (%s)", input, node, err)
  420. return input
  421. }
  422. return out.String()
  423. }
  424. func setHTMLAttribute(attributes []h.Attribute, k, v string) []h.Attribute {
  425. for i, a := range attributes {
  426. if strings.ToLower(a.Key) == strings.ToLower(k) {
  427. switch strings.ToLower(k) {
  428. case "class", "style":
  429. attributes[i].Val += " " + v
  430. default:
  431. attributes[i].Val = v
  432. }
  433. return attributes
  434. }
  435. }
  436. return append(attributes, h.Attribute{Namespace: "", Key: k, Val: v})
  437. }
  438. func (fs *footnotes) add(f FootnoteLink) int {
  439. if i, ok := fs.mapping[f.Name]; ok && f.Name != "" {
  440. return i
  441. }
  442. fs.list = append(fs.list, f.Definition)
  443. i := len(fs.list) - 1
  444. if f.Name != "" {
  445. fs.mapping[f.Name] = i
  446. }
  447. return i
  448. }
  449. func (fs *footnotes) updateDefinition(f FootnoteDefinition) {
  450. if i, ok := fs.mapping[f.Name]; ok {
  451. fs.list[i] = &f
  452. }
  453. }