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 14KB

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