html.go 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952
  1. //
  2. // Blackfriday Markdown Processor
  3. // Available at http://github.com/russross/blackfriday
  4. //
  5. // Copyright © 2011 Russ Ross <russ@russross.com>.
  6. // Distributed under the Simplified BSD License.
  7. // See README.md for details.
  8. //
  9. //
  10. //
  11. // HTML rendering backend
  12. //
  13. //
  14. package blackfriday
  15. import (
  16. "bytes"
  17. "fmt"
  18. "io"
  19. "regexp"
  20. "strings"
  21. )
  22. // HTMLFlags control optional behavior of HTML renderer.
  23. type HTMLFlags int
  24. // HTML renderer configuration options.
  25. const (
  26. HTMLFlagsNone HTMLFlags = 0
  27. SkipHTML HTMLFlags = 1 << iota // Skip preformatted HTML blocks
  28. SkipImages // Skip embedded images
  29. SkipLinks // Skip all links
  30. Safelink // Only link to trusted protocols
  31. NofollowLinks // Only link with rel="nofollow"
  32. NoreferrerLinks // Only link with rel="noreferrer"
  33. NoopenerLinks // Only link with rel="noopener"
  34. HrefTargetBlank // Add a blank target
  35. CompletePage // Generate a complete HTML page
  36. UseXHTML // Generate XHTML output instead of HTML
  37. FootnoteReturnLinks // Generate a link at the end of a footnote to return to the source
  38. Smartypants // Enable smart punctuation substitutions
  39. SmartypantsFractions // Enable smart fractions (with Smartypants)
  40. SmartypantsDashes // Enable smart dashes (with Smartypants)
  41. SmartypantsLatexDashes // Enable LaTeX-style dashes (with Smartypants)
  42. SmartypantsAngledQuotes // Enable angled double quotes (with Smartypants) for double quotes rendering
  43. SmartypantsQuotesNBSP // Enable « French guillemets » (with Smartypants)
  44. TOC // Generate a table of contents
  45. )
  46. var (
  47. htmlTagRe = regexp.MustCompile("(?i)^" + htmlTag)
  48. )
  49. const (
  50. htmlTag = "(?:" + openTag + "|" + closeTag + "|" + htmlComment + "|" +
  51. processingInstruction + "|" + declaration + "|" + cdata + ")"
  52. closeTag = "</" + tagName + "\\s*[>]"
  53. openTag = "<" + tagName + attribute + "*" + "\\s*/?>"
  54. attribute = "(?:" + "\\s+" + attributeName + attributeValueSpec + "?)"
  55. attributeValue = "(?:" + unquotedValue + "|" + singleQuotedValue + "|" + doubleQuotedValue + ")"
  56. attributeValueSpec = "(?:" + "\\s*=" + "\\s*" + attributeValue + ")"
  57. attributeName = "[a-zA-Z_:][a-zA-Z0-9:._-]*"
  58. cdata = "<!\\[CDATA\\[[\\s\\S]*?\\]\\]>"
  59. declaration = "<![A-Z]+" + "\\s+[^>]*>"
  60. doubleQuotedValue = "\"[^\"]*\""
  61. htmlComment = "<!---->|<!--(?:-?[^>-])(?:-?[^-])*-->"
  62. processingInstruction = "[<][?].*?[?][>]"
  63. singleQuotedValue = "'[^']*'"
  64. tagName = "[A-Za-z][A-Za-z0-9-]*"
  65. unquotedValue = "[^\"'=<>`\\x00-\\x20]+"
  66. )
  67. // HTMLRendererParameters is a collection of supplementary parameters tweaking
  68. // the behavior of various parts of HTML renderer.
  69. type HTMLRendererParameters struct {
  70. // Prepend this text to each relative URL.
  71. AbsolutePrefix string
  72. // Add this text to each footnote anchor, to ensure uniqueness.
  73. FootnoteAnchorPrefix string
  74. // Show this text inside the <a> tag for a footnote return link, if the
  75. // HTML_FOOTNOTE_RETURN_LINKS flag is enabled. If blank, the string
  76. // <sup>[return]</sup> is used.
  77. FootnoteReturnLinkContents string
  78. // If set, add this text to the front of each Heading ID, to ensure
  79. // uniqueness.
  80. HeadingIDPrefix string
  81. // If set, add this text to the back of each Heading ID, to ensure uniqueness.
  82. HeadingIDSuffix string
  83. // Increase heading levels: if the offset is 1, <h1> becomes <h2> etc.
  84. // Negative offset is also valid.
  85. // Resulting levels are clipped between 1 and 6.
  86. HeadingLevelOffset int
  87. Title string // Document title (used if CompletePage is set)
  88. CSS string // Optional CSS file URL (used if CompletePage is set)
  89. Icon string // Optional icon file URL (used if CompletePage is set)
  90. Flags HTMLFlags // Flags allow customizing this renderer's behavior
  91. }
  92. // HTMLRenderer is a type that implements the Renderer interface for HTML output.
  93. //
  94. // Do not create this directly, instead use the NewHTMLRenderer function.
  95. type HTMLRenderer struct {
  96. HTMLRendererParameters
  97. closeTag string // how to end singleton tags: either " />" or ">"
  98. // Track heading IDs to prevent ID collision in a single generation.
  99. headingIDs map[string]int
  100. lastOutputLen int
  101. disableTags int
  102. sr *SPRenderer
  103. }
  104. const (
  105. xhtmlClose = " />"
  106. htmlClose = ">"
  107. )
  108. // NewHTMLRenderer creates and configures an HTMLRenderer object, which
  109. // satisfies the Renderer interface.
  110. func NewHTMLRenderer(params HTMLRendererParameters) *HTMLRenderer {
  111. // configure the rendering engine
  112. closeTag := htmlClose
  113. if params.Flags&UseXHTML != 0 {
  114. closeTag = xhtmlClose
  115. }
  116. if params.FootnoteReturnLinkContents == "" {
  117. // U+FE0E is VARIATION SELECTOR-15.
  118. // It suppresses automatic emoji presentation of the preceding
  119. // U+21A9 LEFTWARDS ARROW WITH HOOK on iOS and iPadOS.
  120. params.FootnoteReturnLinkContents = "<span aria-label='Return'>↩\ufe0e</span>"
  121. }
  122. return &HTMLRenderer{
  123. HTMLRendererParameters: params,
  124. closeTag: closeTag,
  125. headingIDs: make(map[string]int),
  126. sr: NewSmartypantsRenderer(params.Flags),
  127. }
  128. }
  129. func isHTMLTag(tag []byte, tagname string) bool {
  130. found, _ := findHTMLTagPos(tag, tagname)
  131. return found
  132. }
  133. // Look for a character, but ignore it when it's in any kind of quotes, it
  134. // might be JavaScript
  135. func skipUntilCharIgnoreQuotes(html []byte, start int, char byte) int {
  136. inSingleQuote := false
  137. inDoubleQuote := false
  138. inGraveQuote := false
  139. i := start
  140. for i < len(html) {
  141. switch {
  142. case html[i] == char && !inSingleQuote && !inDoubleQuote && !inGraveQuote:
  143. return i
  144. case html[i] == '\'':
  145. inSingleQuote = !inSingleQuote
  146. case html[i] == '"':
  147. inDoubleQuote = !inDoubleQuote
  148. case html[i] == '`':
  149. inGraveQuote = !inGraveQuote
  150. }
  151. i++
  152. }
  153. return start
  154. }
  155. func findHTMLTagPos(tag []byte, tagname string) (bool, int) {
  156. i := 0
  157. if i < len(tag) && tag[0] != '<' {
  158. return false, -1
  159. }
  160. i++
  161. i = skipSpace(tag, i)
  162. if i < len(tag) && tag[i] == '/' {
  163. i++
  164. }
  165. i = skipSpace(tag, i)
  166. j := 0
  167. for ; i < len(tag); i, j = i+1, j+1 {
  168. if j >= len(tagname) {
  169. break
  170. }
  171. if strings.ToLower(string(tag[i]))[0] != tagname[j] {
  172. return false, -1
  173. }
  174. }
  175. if i == len(tag) {
  176. return false, -1
  177. }
  178. rightAngle := skipUntilCharIgnoreQuotes(tag, i, '>')
  179. if rightAngle >= i {
  180. return true, rightAngle
  181. }
  182. return false, -1
  183. }
  184. func skipSpace(tag []byte, i int) int {
  185. for i < len(tag) && isspace(tag[i]) {
  186. i++
  187. }
  188. return i
  189. }
  190. func isRelativeLink(link []byte) (yes bool) {
  191. // a tag begin with '#'
  192. if link[0] == '#' {
  193. return true
  194. }
  195. // link begin with '/' but not '//', the second maybe a protocol relative link
  196. if len(link) >= 2 && link[0] == '/' && link[1] != '/' {
  197. return true
  198. }
  199. // only the root '/'
  200. if len(link) == 1 && link[0] == '/' {
  201. return true
  202. }
  203. // current directory : begin with "./"
  204. if bytes.HasPrefix(link, []byte("./")) {
  205. return true
  206. }
  207. // parent directory : begin with "../"
  208. if bytes.HasPrefix(link, []byte("../")) {
  209. return true
  210. }
  211. return false
  212. }
  213. func (r *HTMLRenderer) ensureUniqueHeadingID(id string) string {
  214. for count, found := r.headingIDs[id]; found; count, found = r.headingIDs[id] {
  215. tmp := fmt.Sprintf("%s-%d", id, count+1)
  216. if _, tmpFound := r.headingIDs[tmp]; !tmpFound {
  217. r.headingIDs[id] = count + 1
  218. id = tmp
  219. } else {
  220. id = id + "-1"
  221. }
  222. }
  223. if _, found := r.headingIDs[id]; !found {
  224. r.headingIDs[id] = 0
  225. }
  226. return id
  227. }
  228. func (r *HTMLRenderer) addAbsPrefix(link []byte) []byte {
  229. if r.AbsolutePrefix != "" && isRelativeLink(link) && link[0] != '.' {
  230. newDest := r.AbsolutePrefix
  231. if link[0] != '/' {
  232. newDest += "/"
  233. }
  234. newDest += string(link)
  235. return []byte(newDest)
  236. }
  237. return link
  238. }
  239. func appendLinkAttrs(attrs []string, flags HTMLFlags, link []byte) []string {
  240. if isRelativeLink(link) {
  241. return attrs
  242. }
  243. val := []string{}
  244. if flags&NofollowLinks != 0 {
  245. val = append(val, "nofollow")
  246. }
  247. if flags&NoreferrerLinks != 0 {
  248. val = append(val, "noreferrer")
  249. }
  250. if flags&NoopenerLinks != 0 {
  251. val = append(val, "noopener")
  252. }
  253. if flags&HrefTargetBlank != 0 {
  254. attrs = append(attrs, "target=\"_blank\"")
  255. }
  256. if len(val) == 0 {
  257. return attrs
  258. }
  259. attr := fmt.Sprintf("rel=%q", strings.Join(val, " "))
  260. return append(attrs, attr)
  261. }
  262. func isMailto(link []byte) bool {
  263. return bytes.HasPrefix(link, []byte("mailto:"))
  264. }
  265. func needSkipLink(flags HTMLFlags, dest []byte) bool {
  266. if flags&SkipLinks != 0 {
  267. return true
  268. }
  269. return flags&Safelink != 0 && !isSafeLink(dest) && !isMailto(dest)
  270. }
  271. func isSmartypantable(node *Node) bool {
  272. pt := node.Parent.Type
  273. return pt != Link && pt != CodeBlock && pt != Code
  274. }
  275. func appendLanguageAttr(attrs []string, info []byte) []string {
  276. if len(info) == 0 {
  277. return attrs
  278. }
  279. endOfLang := bytes.IndexAny(info, "\t ")
  280. if endOfLang < 0 {
  281. endOfLang = len(info)
  282. }
  283. return append(attrs, fmt.Sprintf("class=\"language-%s\"", info[:endOfLang]))
  284. }
  285. func (r *HTMLRenderer) tag(w io.Writer, name []byte, attrs []string) {
  286. w.Write(name)
  287. if len(attrs) > 0 {
  288. w.Write(spaceBytes)
  289. w.Write([]byte(strings.Join(attrs, " ")))
  290. }
  291. w.Write(gtBytes)
  292. r.lastOutputLen = 1
  293. }
  294. func footnoteRef(prefix string, node *Node) []byte {
  295. urlFrag := prefix + string(slugify(node.Destination))
  296. anchor := fmt.Sprintf(`<a href="#fn:%s">%d</a>`, urlFrag, node.NoteID)
  297. return []byte(fmt.Sprintf(`<sup class="footnote-ref" id="fnref:%s">%s</sup>`, urlFrag, anchor))
  298. }
  299. func footnoteItem(prefix string, slug []byte) []byte {
  300. return []byte(fmt.Sprintf(`<li id="fn:%s%s">`, prefix, slug))
  301. }
  302. func footnoteReturnLink(prefix, returnLink string, slug []byte) []byte {
  303. const format = ` <a class="footnote-return" href="#fnref:%s%s">%s</a>`
  304. return []byte(fmt.Sprintf(format, prefix, slug, returnLink))
  305. }
  306. func itemOpenCR(node *Node) bool {
  307. if node.Prev == nil {
  308. return false
  309. }
  310. ld := node.Parent.ListData
  311. return !ld.Tight && ld.ListFlags&ListTypeDefinition == 0
  312. }
  313. func skipParagraphTags(node *Node) bool {
  314. grandparent := node.Parent.Parent
  315. if grandparent == nil || grandparent.Type != List {
  316. return false
  317. }
  318. tightOrTerm := grandparent.Tight || node.Parent.ListFlags&ListTypeTerm != 0
  319. return grandparent.Type == List && tightOrTerm
  320. }
  321. func cellAlignment(align CellAlignFlags) string {
  322. switch align {
  323. case TableAlignmentLeft:
  324. return "left"
  325. case TableAlignmentRight:
  326. return "right"
  327. case TableAlignmentCenter:
  328. return "center"
  329. default:
  330. return ""
  331. }
  332. }
  333. func (r *HTMLRenderer) out(w io.Writer, text []byte) {
  334. if r.disableTags > 0 {
  335. w.Write(htmlTagRe.ReplaceAll(text, []byte{}))
  336. } else {
  337. w.Write(text)
  338. }
  339. r.lastOutputLen = len(text)
  340. }
  341. func (r *HTMLRenderer) cr(w io.Writer) {
  342. if r.lastOutputLen > 0 {
  343. r.out(w, nlBytes)
  344. }
  345. }
  346. var (
  347. nlBytes = []byte{'\n'}
  348. gtBytes = []byte{'>'}
  349. spaceBytes = []byte{' '}
  350. )
  351. var (
  352. brTag = []byte("<br>")
  353. brXHTMLTag = []byte("<br />")
  354. emTag = []byte("<em>")
  355. emCloseTag = []byte("</em>")
  356. strongTag = []byte("<strong>")
  357. strongCloseTag = []byte("</strong>")
  358. delTag = []byte("<del>")
  359. delCloseTag = []byte("</del>")
  360. ttTag = []byte("<tt>")
  361. ttCloseTag = []byte("</tt>")
  362. aTag = []byte("<a")
  363. aCloseTag = []byte("</a>")
  364. preTag = []byte("<pre>")
  365. preCloseTag = []byte("</pre>")
  366. codeTag = []byte("<code>")
  367. codeCloseTag = []byte("</code>")
  368. pTag = []byte("<p>")
  369. pCloseTag = []byte("</p>")
  370. blockquoteTag = []byte("<blockquote>")
  371. blockquoteCloseTag = []byte("</blockquote>")
  372. hrTag = []byte("<hr>")
  373. hrXHTMLTag = []byte("<hr />")
  374. ulTag = []byte("<ul>")
  375. ulCloseTag = []byte("</ul>")
  376. olTag = []byte("<ol>")
  377. olCloseTag = []byte("</ol>")
  378. dlTag = []byte("<dl>")
  379. dlCloseTag = []byte("</dl>")
  380. liTag = []byte("<li>")
  381. liCloseTag = []byte("</li>")
  382. ddTag = []byte("<dd>")
  383. ddCloseTag = []byte("</dd>")
  384. dtTag = []byte("<dt>")
  385. dtCloseTag = []byte("</dt>")
  386. tableTag = []byte("<table>")
  387. tableCloseTag = []byte("</table>")
  388. tdTag = []byte("<td")
  389. tdCloseTag = []byte("</td>")
  390. thTag = []byte("<th")
  391. thCloseTag = []byte("</th>")
  392. theadTag = []byte("<thead>")
  393. theadCloseTag = []byte("</thead>")
  394. tbodyTag = []byte("<tbody>")
  395. tbodyCloseTag = []byte("</tbody>")
  396. trTag = []byte("<tr>")
  397. trCloseTag = []byte("</tr>")
  398. h1Tag = []byte("<h1")
  399. h1CloseTag = []byte("</h1>")
  400. h2Tag = []byte("<h2")
  401. h2CloseTag = []byte("</h2>")
  402. h3Tag = []byte("<h3")
  403. h3CloseTag = []byte("</h3>")
  404. h4Tag = []byte("<h4")
  405. h4CloseTag = []byte("</h4>")
  406. h5Tag = []byte("<h5")
  407. h5CloseTag = []byte("</h5>")
  408. h6Tag = []byte("<h6")
  409. h6CloseTag = []byte("</h6>")
  410. footnotesDivBytes = []byte("\n<div class=\"footnotes\">\n\n")
  411. footnotesCloseDivBytes = []byte("\n</div>\n")
  412. )
  413. func headingTagsFromLevel(level int) ([]byte, []byte) {
  414. if level <= 1 {
  415. return h1Tag, h1CloseTag
  416. }
  417. switch level {
  418. case 2:
  419. return h2Tag, h2CloseTag
  420. case 3:
  421. return h3Tag, h3CloseTag
  422. case 4:
  423. return h4Tag, h4CloseTag
  424. case 5:
  425. return h5Tag, h5CloseTag
  426. }
  427. return h6Tag, h6CloseTag
  428. }
  429. func (r *HTMLRenderer) outHRTag(w io.Writer) {
  430. if r.Flags&UseXHTML == 0 {
  431. r.out(w, hrTag)
  432. } else {
  433. r.out(w, hrXHTMLTag)
  434. }
  435. }
  436. // RenderNode is a default renderer of a single node of a syntax tree. For
  437. // block nodes it will be called twice: first time with entering=true, second
  438. // time with entering=false, so that it could know when it's working on an open
  439. // tag and when on close. It writes the result to w.
  440. //
  441. // The return value is a way to tell the calling walker to adjust its walk
  442. // pattern: e.g. it can terminate the traversal by returning Terminate. Or it
  443. // can ask the walker to skip a subtree of this node by returning SkipChildren.
  444. // The typical behavior is to return GoToNext, which asks for the usual
  445. // traversal to the next node.
  446. func (r *HTMLRenderer) RenderNode(w io.Writer, node *Node, entering bool) WalkStatus {
  447. attrs := []string{}
  448. switch node.Type {
  449. case Text:
  450. if r.Flags&Smartypants != 0 {
  451. var tmp bytes.Buffer
  452. escapeHTML(&tmp, node.Literal)
  453. r.sr.Process(w, tmp.Bytes())
  454. } else {
  455. if node.Parent.Type == Link {
  456. escLink(w, node.Literal)
  457. } else {
  458. escapeHTML(w, node.Literal)
  459. }
  460. }
  461. case Softbreak:
  462. r.cr(w)
  463. // TODO: make it configurable via out(renderer.softbreak)
  464. case Hardbreak:
  465. if r.Flags&UseXHTML == 0 {
  466. r.out(w, brTag)
  467. } else {
  468. r.out(w, brXHTMLTag)
  469. }
  470. r.cr(w)
  471. case Emph:
  472. if entering {
  473. r.out(w, emTag)
  474. } else {
  475. r.out(w, emCloseTag)
  476. }
  477. case Strong:
  478. if entering {
  479. r.out(w, strongTag)
  480. } else {
  481. r.out(w, strongCloseTag)
  482. }
  483. case Del:
  484. if entering {
  485. r.out(w, delTag)
  486. } else {
  487. r.out(w, delCloseTag)
  488. }
  489. case HTMLSpan:
  490. if r.Flags&SkipHTML != 0 {
  491. break
  492. }
  493. r.out(w, node.Literal)
  494. case Link:
  495. // mark it but don't link it if it is not a safe link: no smartypants
  496. dest := node.LinkData.Destination
  497. if needSkipLink(r.Flags, dest) {
  498. if entering {
  499. r.out(w, ttTag)
  500. } else {
  501. r.out(w, ttCloseTag)
  502. }
  503. } else {
  504. if entering {
  505. dest = r.addAbsPrefix(dest)
  506. var hrefBuf bytes.Buffer
  507. hrefBuf.WriteString("href=\"")
  508. escLink(&hrefBuf, dest)
  509. hrefBuf.WriteByte('"')
  510. attrs = append(attrs, hrefBuf.String())
  511. if node.NoteID != 0 {
  512. r.out(w, footnoteRef(r.FootnoteAnchorPrefix, node))
  513. break
  514. }
  515. attrs = appendLinkAttrs(attrs, r.Flags, dest)
  516. if len(node.LinkData.Title) > 0 {
  517. var titleBuff bytes.Buffer
  518. titleBuff.WriteString("title=\"")
  519. escapeHTML(&titleBuff, node.LinkData.Title)
  520. titleBuff.WriteByte('"')
  521. attrs = append(attrs, titleBuff.String())
  522. }
  523. r.tag(w, aTag, attrs)
  524. } else {
  525. if node.NoteID != 0 {
  526. break
  527. }
  528. r.out(w, aCloseTag)
  529. }
  530. }
  531. case Image:
  532. if r.Flags&SkipImages != 0 {
  533. return SkipChildren
  534. }
  535. if entering {
  536. dest := node.LinkData.Destination
  537. dest = r.addAbsPrefix(dest)
  538. if r.disableTags == 0 {
  539. //if options.safe && potentiallyUnsafe(dest) {
  540. //out(w, `<img src="" alt="`)
  541. //} else {
  542. r.out(w, []byte(`<img src="`))
  543. escLink(w, dest)
  544. r.out(w, []byte(`" alt="`))
  545. //}
  546. }
  547. r.disableTags++
  548. } else {
  549. r.disableTags--
  550. if r.disableTags == 0 {
  551. if node.LinkData.Title != nil {
  552. r.out(w, []byte(`" title="`))
  553. escapeHTML(w, node.LinkData.Title)
  554. }
  555. r.out(w, []byte(`" />`))
  556. }
  557. }
  558. case Code:
  559. r.out(w, codeTag)
  560. escapeAllHTML(w, node.Literal)
  561. r.out(w, codeCloseTag)
  562. case Document:
  563. break
  564. case Paragraph:
  565. if skipParagraphTags(node) {
  566. break
  567. }
  568. if entering {
  569. // TODO: untangle this clusterfuck about when the newlines need
  570. // to be added and when not.
  571. if node.Prev != nil {
  572. switch node.Prev.Type {
  573. case HTMLBlock, List, Paragraph, Heading, CodeBlock, BlockQuote, HorizontalRule:
  574. r.cr(w)
  575. }
  576. }
  577. if node.Parent.Type == BlockQuote && node.Prev == nil {
  578. r.cr(w)
  579. }
  580. r.out(w, pTag)
  581. } else {
  582. r.out(w, pCloseTag)
  583. if !(node.Parent.Type == Item && node.Next == nil) {
  584. r.cr(w)
  585. }
  586. }
  587. case BlockQuote:
  588. if entering {
  589. r.cr(w)
  590. r.out(w, blockquoteTag)
  591. } else {
  592. r.out(w, blockquoteCloseTag)
  593. r.cr(w)
  594. }
  595. case HTMLBlock:
  596. if r.Flags&SkipHTML != 0 {
  597. break
  598. }
  599. r.cr(w)
  600. r.out(w, node.Literal)
  601. r.cr(w)
  602. case Heading:
  603. headingLevel := r.HTMLRendererParameters.HeadingLevelOffset + node.Level
  604. openTag, closeTag := headingTagsFromLevel(headingLevel)
  605. if entering {
  606. if node.IsTitleblock {
  607. attrs = append(attrs, `class="title"`)
  608. }
  609. if node.HeadingID != "" {
  610. id := r.ensureUniqueHeadingID(node.HeadingID)
  611. if r.HeadingIDPrefix != "" {
  612. id = r.HeadingIDPrefix + id
  613. }
  614. if r.HeadingIDSuffix != "" {
  615. id = id + r.HeadingIDSuffix
  616. }
  617. attrs = append(attrs, fmt.Sprintf(`id="%s"`, id))
  618. }
  619. r.cr(w)
  620. r.tag(w, openTag, attrs)
  621. } else {
  622. r.out(w, closeTag)
  623. if !(node.Parent.Type == Item && node.Next == nil) {
  624. r.cr(w)
  625. }
  626. }
  627. case HorizontalRule:
  628. r.cr(w)
  629. r.outHRTag(w)
  630. r.cr(w)
  631. case List:
  632. openTag := ulTag
  633. closeTag := ulCloseTag
  634. if node.ListFlags&ListTypeOrdered != 0 {
  635. openTag = olTag
  636. closeTag = olCloseTag
  637. }
  638. if node.ListFlags&ListTypeDefinition != 0 {
  639. openTag = dlTag
  640. closeTag = dlCloseTag
  641. }
  642. if entering {
  643. if node.IsFootnotesList {
  644. r.out(w, footnotesDivBytes)
  645. r.outHRTag(w)
  646. r.cr(w)
  647. }
  648. r.cr(w)
  649. if node.Parent.Type == Item && node.Parent.Parent.Tight {
  650. r.cr(w)
  651. }
  652. r.tag(w, openTag[:len(openTag)-1], attrs)
  653. r.cr(w)
  654. } else {
  655. r.out(w, closeTag)
  656. //cr(w)
  657. //if node.parent.Type != Item {
  658. // cr(w)
  659. //}
  660. if node.Parent.Type == Item && node.Next != nil {
  661. r.cr(w)
  662. }
  663. if node.Parent.Type == Document || node.Parent.Type == BlockQuote {
  664. r.cr(w)
  665. }
  666. if node.IsFootnotesList {
  667. r.out(w, footnotesCloseDivBytes)
  668. }
  669. }
  670. case Item:
  671. openTag := liTag
  672. closeTag := liCloseTag
  673. if node.ListFlags&ListTypeDefinition != 0 {
  674. openTag = ddTag
  675. closeTag = ddCloseTag
  676. }
  677. if node.ListFlags&ListTypeTerm != 0 {
  678. openTag = dtTag
  679. closeTag = dtCloseTag
  680. }
  681. if entering {
  682. if itemOpenCR(node) {
  683. r.cr(w)
  684. }
  685. if node.ListData.RefLink != nil {
  686. slug := slugify(node.ListData.RefLink)
  687. r.out(w, footnoteItem(r.FootnoteAnchorPrefix, slug))
  688. break
  689. }
  690. r.out(w, openTag)
  691. } else {
  692. if node.ListData.RefLink != nil {
  693. slug := slugify(node.ListData.RefLink)
  694. if r.Flags&FootnoteReturnLinks != 0 {
  695. r.out(w, footnoteReturnLink(r.FootnoteAnchorPrefix, r.FootnoteReturnLinkContents, slug))
  696. }
  697. }
  698. r.out(w, closeTag)
  699. r.cr(w)
  700. }
  701. case CodeBlock:
  702. attrs = appendLanguageAttr(attrs, node.Info)
  703. r.cr(w)
  704. r.out(w, preTag)
  705. r.tag(w, codeTag[:len(codeTag)-1], attrs)
  706. escapeAllHTML(w, node.Literal)
  707. r.out(w, codeCloseTag)
  708. r.out(w, preCloseTag)
  709. if node.Parent.Type != Item {
  710. r.cr(w)
  711. }
  712. case Table:
  713. if entering {
  714. r.cr(w)
  715. r.out(w, tableTag)
  716. } else {
  717. r.out(w, tableCloseTag)
  718. r.cr(w)
  719. }
  720. case TableCell:
  721. openTag := tdTag
  722. closeTag := tdCloseTag
  723. if node.IsHeader {
  724. openTag = thTag
  725. closeTag = thCloseTag
  726. }
  727. if entering {
  728. align := cellAlignment(node.Align)
  729. if align != "" {
  730. attrs = append(attrs, fmt.Sprintf(`align="%s"`, align))
  731. }
  732. if node.Prev == nil {
  733. r.cr(w)
  734. }
  735. r.tag(w, openTag, attrs)
  736. } else {
  737. r.out(w, closeTag)
  738. r.cr(w)
  739. }
  740. case TableHead:
  741. if entering {
  742. r.cr(w)
  743. r.out(w, theadTag)
  744. } else {
  745. r.out(w, theadCloseTag)
  746. r.cr(w)
  747. }
  748. case TableBody:
  749. if entering {
  750. r.cr(w)
  751. r.out(w, tbodyTag)
  752. // XXX: this is to adhere to a rather silly test. Should fix test.
  753. if node.FirstChild == nil {
  754. r.cr(w)
  755. }
  756. } else {
  757. r.out(w, tbodyCloseTag)
  758. r.cr(w)
  759. }
  760. case TableRow:
  761. if entering {
  762. r.cr(w)
  763. r.out(w, trTag)
  764. } else {
  765. r.out(w, trCloseTag)
  766. r.cr(w)
  767. }
  768. default:
  769. panic("Unknown node type " + node.Type.String())
  770. }
  771. return GoToNext
  772. }
  773. // RenderHeader writes HTML document preamble and TOC if requested.
  774. func (r *HTMLRenderer) RenderHeader(w io.Writer, ast *Node) {
  775. r.writeDocumentHeader(w)
  776. if r.Flags&TOC != 0 {
  777. r.writeTOC(w, ast)
  778. }
  779. }
  780. // RenderFooter writes HTML document footer.
  781. func (r *HTMLRenderer) RenderFooter(w io.Writer, ast *Node) {
  782. if r.Flags&CompletePage == 0 {
  783. return
  784. }
  785. io.WriteString(w, "\n</body>\n</html>\n")
  786. }
  787. func (r *HTMLRenderer) writeDocumentHeader(w io.Writer) {
  788. if r.Flags&CompletePage == 0 {
  789. return
  790. }
  791. ending := ""
  792. if r.Flags&UseXHTML != 0 {
  793. io.WriteString(w, "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" ")
  794. io.WriteString(w, "\"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">\n")
  795. io.WriteString(w, "<html xmlns=\"http://www.w3.org/1999/xhtml\">\n")
  796. ending = " /"
  797. } else {
  798. io.WriteString(w, "<!DOCTYPE html>\n")
  799. io.WriteString(w, "<html>\n")
  800. }
  801. io.WriteString(w, "<head>\n")
  802. io.WriteString(w, " <title>")
  803. if r.Flags&Smartypants != 0 {
  804. r.sr.Process(w, []byte(r.Title))
  805. } else {
  806. escapeHTML(w, []byte(r.Title))
  807. }
  808. io.WriteString(w, "</title>\n")
  809. io.WriteString(w, " <meta name=\"GENERATOR\" content=\"Blackfriday Markdown Processor v")
  810. io.WriteString(w, Version)
  811. io.WriteString(w, "\"")
  812. io.WriteString(w, ending)
  813. io.WriteString(w, ">\n")
  814. io.WriteString(w, " <meta charset=\"utf-8\"")
  815. io.WriteString(w, ending)
  816. io.WriteString(w, ">\n")
  817. if r.CSS != "" {
  818. io.WriteString(w, " <link rel=\"stylesheet\" type=\"text/css\" href=\"")
  819. escapeHTML(w, []byte(r.CSS))
  820. io.WriteString(w, "\"")
  821. io.WriteString(w, ending)
  822. io.WriteString(w, ">\n")
  823. }
  824. if r.Icon != "" {
  825. io.WriteString(w, " <link rel=\"icon\" type=\"image/x-icon\" href=\"")
  826. escapeHTML(w, []byte(r.Icon))
  827. io.WriteString(w, "\"")
  828. io.WriteString(w, ending)
  829. io.WriteString(w, ">\n")
  830. }
  831. io.WriteString(w, "</head>\n")
  832. io.WriteString(w, "<body>\n\n")
  833. }
  834. func (r *HTMLRenderer) writeTOC(w io.Writer, ast *Node) {
  835. buf := bytes.Buffer{}
  836. inHeading := false
  837. tocLevel := 0
  838. headingCount := 0
  839. ast.Walk(func(node *Node, entering bool) WalkStatus {
  840. if node.Type == Heading && !node.HeadingData.IsTitleblock {
  841. inHeading = entering
  842. if entering {
  843. node.HeadingID = fmt.Sprintf("toc_%d", headingCount)
  844. if node.Level == tocLevel {
  845. buf.WriteString("</li>\n\n<li>")
  846. } else if node.Level < tocLevel {
  847. for node.Level < tocLevel {
  848. tocLevel--
  849. buf.WriteString("</li>\n</ul>")
  850. }
  851. buf.WriteString("</li>\n\n<li>")
  852. } else {
  853. for node.Level > tocLevel {
  854. tocLevel++
  855. buf.WriteString("\n<ul>\n<li>")
  856. }
  857. }
  858. fmt.Fprintf(&buf, `<a href="#toc_%d">`, headingCount)
  859. headingCount++
  860. } else {
  861. buf.WriteString("</a>")
  862. }
  863. return GoToNext
  864. }
  865. if inHeading {
  866. return r.RenderNode(&buf, node, entering)
  867. }
  868. return GoToNext
  869. })
  870. for ; tocLevel > 0; tocLevel-- {
  871. buf.WriteString("</li>\n</ul>")
  872. }
  873. if buf.Len() > 0 {
  874. io.WriteString(w, "<nav>\n")
  875. w.Write(buf.Bytes())
  876. io.WriteString(w, "\n\n</nav>\n")
  877. }
  878. r.lastOutputLen = buf.Len()
  879. }