renderer.go 34 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339
  1. package html
  2. import (
  3. "bytes"
  4. "fmt"
  5. "html"
  6. "io"
  7. "regexp"
  8. "sort"
  9. "strconv"
  10. "strings"
  11. "github.com/gomarkdown/markdown/ast"
  12. "github.com/gomarkdown/markdown/parser"
  13. )
  14. // Flags control optional behavior of HTML renderer.
  15. type Flags int
  16. // IDTag is the tag used for tag identification, it defaults to "id", some renderers
  17. // may wish to override this and use e.g. "anchor".
  18. var IDTag = "id"
  19. // HTML renderer configuration options.
  20. const (
  21. FlagsNone Flags = 0
  22. SkipHTML Flags = 1 << iota // Skip preformatted HTML blocks
  23. SkipImages // Skip embedded images
  24. SkipLinks // Skip all links
  25. Safelink // Only link to trusted protocols
  26. NofollowLinks // Only link with rel="nofollow"
  27. NoreferrerLinks // Only link with rel="noreferrer"
  28. NoopenerLinks // Only link with rel="noopener"
  29. HrefTargetBlank // Add a blank target
  30. CompletePage // Generate a complete HTML page
  31. UseXHTML // Generate XHTML output instead of HTML
  32. FootnoteReturnLinks // Generate a link at the end of a footnote to return to the source
  33. FootnoteNoHRTag // Do not output an HR after starting a footnote list.
  34. Smartypants // Enable smart punctuation substitutions
  35. SmartypantsFractions // Enable smart fractions (with Smartypants)
  36. SmartypantsDashes // Enable smart dashes (with Smartypants)
  37. SmartypantsLatexDashes // Enable LaTeX-style dashes (with Smartypants)
  38. SmartypantsAngledQuotes // Enable angled double quotes (with Smartypants) for double quotes rendering
  39. SmartypantsQuotesNBSP // Enable « French guillemets » (with Smartypants)
  40. TOC // Generate a table of contents
  41. LazyLoadImages // Include loading="lazy" with images
  42. CommonFlags Flags = Smartypants | SmartypantsFractions | SmartypantsDashes | SmartypantsLatexDashes
  43. )
  44. var (
  45. htmlTagRe = regexp.MustCompile("(?i)^" + htmlTag)
  46. )
  47. const (
  48. htmlTag = "(?:" + openTag + "|" + closeTag + "|" + htmlComment + "|" +
  49. processingInstruction + "|" + declaration + "|" + cdata + ")"
  50. closeTag = "</" + tagName + "\\s*[>]"
  51. openTag = "<" + tagName + attribute + "*" + "\\s*/?>"
  52. attribute = "(?:" + "\\s+" + attributeName + attributeValueSpec + "?)"
  53. attributeValue = "(?:" + unquotedValue + "|" + singleQuotedValue + "|" + doubleQuotedValue + ")"
  54. attributeValueSpec = "(?:" + "\\s*=" + "\\s*" + attributeValue + ")"
  55. attributeName = "[a-zA-Z_:][a-zA-Z0-9:._-]*"
  56. cdata = "<!\\[CDATA\\[[\\s\\S]*?\\]\\]>"
  57. declaration = "<![A-Z]+" + "\\s+[^>]*>"
  58. doubleQuotedValue = "\"[^\"]*\""
  59. htmlComment = "<!---->|<!--(?:-?[^>-])(?:-?[^-])*-->"
  60. processingInstruction = "[<][?].*?[?][>]"
  61. singleQuotedValue = "'[^']*'"
  62. tagName = "[A-Za-z][A-Za-z0-9-]*"
  63. unquotedValue = "[^\"'=<>`\\x00-\\x20]+"
  64. )
  65. // RenderNodeFunc allows reusing most of Renderer logic and replacing
  66. // rendering of some nodes. If it returns false, Renderer.RenderNode
  67. // will execute its logic. If it returns true, Renderer.RenderNode will
  68. // skip rendering this node and will return WalkStatus
  69. type RenderNodeFunc func(w io.Writer, node ast.Node, entering bool) (ast.WalkStatus, bool)
  70. // RendererOptions is a collection of supplementary parameters tweaking
  71. // the behavior of various parts of HTML renderer.
  72. type RendererOptions struct {
  73. // Prepend this text to each relative URL.
  74. AbsolutePrefix string
  75. // Add this text to each footnote anchor, to ensure uniqueness.
  76. FootnoteAnchorPrefix string
  77. // Show this text inside the <a> tag for a footnote return link, if the
  78. // FootnoteReturnLinks flag is enabled. If blank, the string
  79. // <sup>[return]</sup> is used.
  80. FootnoteReturnLinkContents string
  81. // CitationFormatString defines how a citation is rendered. If blank, the string
  82. // <sup>[%s]</sup> is used. Where %s will be substituted with the citation target.
  83. CitationFormatString string
  84. // If set, add this text to the front of each Heading ID, to ensure uniqueness.
  85. HeadingIDPrefix string
  86. // If set, add this text to the back of each Heading ID, to ensure uniqueness.
  87. HeadingIDSuffix string
  88. // can over-write <p> for paragraph tag
  89. ParagraphTag string
  90. Title string // Document title (used if CompletePage is set)
  91. CSS string // Optional CSS file URL (used if CompletePage is set)
  92. Icon string // Optional icon file URL (used if CompletePage is set)
  93. Head []byte // Optional head data injected in the <head> section (used if CompletePage is set)
  94. Flags Flags // Flags allow customizing this renderer's behavior
  95. // if set, called at the start of RenderNode(). Allows replacing
  96. // rendering of some nodes
  97. RenderNodeHook RenderNodeFunc
  98. // Comments is a list of comments the renderer should detect when
  99. // parsing code blocks and detecting callouts.
  100. Comments [][]byte
  101. // Generator is a meta tag that is inserted in the generated HTML so show what rendered it. It should not include the closing tag.
  102. // Defaults (note content quote is not closed) to ` <meta name="GENERATOR" content="github.com/gomarkdown/markdown markdown processor for Go`
  103. Generator string
  104. }
  105. // Renderer implements Renderer interface for HTML output.
  106. //
  107. // Do not create this directly, instead use the NewRenderer function.
  108. type Renderer struct {
  109. Opts RendererOptions
  110. closeTag string // how to end singleton tags: either " />" or ">"
  111. // Track heading IDs to prevent ID collision in a single generation.
  112. headingIDs map[string]int
  113. lastOutputLen int
  114. // if > 0, will strip html tags in Out and Outs
  115. DisableTags int
  116. // IsSafeURLOverride allows overriding the default URL matcher. URL is
  117. // safe if the overriding function returns true. Can be used to extend
  118. // the default list of safe URLs.
  119. IsSafeURLOverride func(url []byte) bool
  120. sr *SPRenderer
  121. documentMatter ast.DocumentMatters // keep track of front/main/back matter.
  122. }
  123. // Escaper defines how to escape HTML special characters
  124. var Escaper = [256][]byte{
  125. '&': []byte("&amp;"),
  126. '<': []byte("&lt;"),
  127. '>': []byte("&gt;"),
  128. '"': []byte("&quot;"),
  129. }
  130. // EscapeHTML writes html-escaped d to w. It escapes &, <, > and " characters.
  131. func EscapeHTML(w io.Writer, d []byte) {
  132. var start, end int
  133. n := len(d)
  134. for end < n {
  135. escSeq := Escaper[d[end]]
  136. if escSeq != nil {
  137. w.Write(d[start:end])
  138. w.Write(escSeq)
  139. start = end + 1
  140. }
  141. end++
  142. }
  143. if start < n && end <= n {
  144. w.Write(d[start:end])
  145. }
  146. }
  147. func EscLink(w io.Writer, text []byte) {
  148. unesc := html.UnescapeString(string(text))
  149. EscapeHTML(w, []byte(unesc))
  150. }
  151. // Escape writes the text to w, but skips the escape character.
  152. func Escape(w io.Writer, text []byte) {
  153. esc := false
  154. for i := 0; i < len(text); i++ {
  155. if text[i] == '\\' {
  156. esc = !esc
  157. }
  158. if esc && text[i] == '\\' {
  159. continue
  160. }
  161. w.Write([]byte{text[i]})
  162. }
  163. }
  164. // NewRenderer creates and configures an Renderer object, which
  165. // satisfies the Renderer interface.
  166. func NewRenderer(opts RendererOptions) *Renderer {
  167. // configure the rendering engine
  168. closeTag := ">"
  169. if opts.Flags&UseXHTML != 0 {
  170. closeTag = " />"
  171. }
  172. if opts.FootnoteReturnLinkContents == "" {
  173. opts.FootnoteReturnLinkContents = `<sup>[return]</sup>`
  174. }
  175. if opts.CitationFormatString == "" {
  176. opts.CitationFormatString = `<sup>[%s]</sup>`
  177. }
  178. if opts.Generator == "" {
  179. opts.Generator = ` <meta name="GENERATOR" content="github.com/gomarkdown/markdown markdown processor for Go`
  180. }
  181. return &Renderer{
  182. Opts: opts,
  183. closeTag: closeTag,
  184. headingIDs: make(map[string]int),
  185. sr: NewSmartypantsRenderer(opts.Flags),
  186. }
  187. }
  188. func isRelativeLink(link []byte) (yes bool) {
  189. // empty links considerd relative
  190. if len(link) == 0 {
  191. return true
  192. }
  193. // a tag begin with '#'
  194. if link[0] == '#' {
  195. return true
  196. }
  197. // link begin with '/' but not '//', the second maybe a protocol relative link
  198. if len(link) >= 2 && link[0] == '/' && link[1] != '/' {
  199. return true
  200. }
  201. // only the root '/'
  202. if len(link) == 1 && link[0] == '/' {
  203. return true
  204. }
  205. // current directory : begin with "./"
  206. if bytes.HasPrefix(link, []byte("./")) {
  207. return true
  208. }
  209. // parent directory : begin with "../"
  210. if bytes.HasPrefix(link, []byte("../")) {
  211. return true
  212. }
  213. return false
  214. }
  215. func AddAbsPrefix(link []byte, prefix string) []byte {
  216. if len(link) == 0 || len(prefix) == 0 {
  217. return link
  218. }
  219. if isRelativeLink(link) && link[0] != '.' {
  220. newDest := prefix
  221. if link[0] != '/' {
  222. newDest += "/"
  223. }
  224. newDest += string(link)
  225. return []byte(newDest)
  226. }
  227. return link
  228. }
  229. func appendLinkAttrs(attrs []string, flags Flags, link []byte) []string {
  230. if isRelativeLink(link) {
  231. return attrs
  232. }
  233. var val []string
  234. if flags&NofollowLinks != 0 {
  235. val = append(val, "nofollow")
  236. }
  237. if flags&NoreferrerLinks != 0 {
  238. val = append(val, "noreferrer")
  239. }
  240. if flags&NoopenerLinks != 0 {
  241. val = append(val, "noopener")
  242. }
  243. if flags&HrefTargetBlank != 0 {
  244. attrs = append(attrs, `target="_blank"`)
  245. }
  246. if len(val) == 0 {
  247. return attrs
  248. }
  249. attr := fmt.Sprintf("rel=%q", strings.Join(val, " "))
  250. return append(attrs, attr)
  251. }
  252. func isMailto(link []byte) bool {
  253. return bytes.HasPrefix(link, []byte("mailto:"))
  254. }
  255. func needSkipLink(r *Renderer, dest []byte) bool {
  256. flags := r.Opts.Flags
  257. if flags&SkipLinks != 0 {
  258. return true
  259. }
  260. isSafeURL := r.IsSafeURLOverride
  261. if isSafeURL == nil {
  262. isSafeURL = parser.IsSafeURL
  263. }
  264. return flags&Safelink != 0 && !isSafeURL(dest) && !isMailto(dest)
  265. }
  266. func appendLanguageAttr(attrs []string, info []byte) []string {
  267. if len(info) == 0 {
  268. return attrs
  269. }
  270. endOfLang := bytes.IndexAny(info, "\t ")
  271. if endOfLang < 0 {
  272. endOfLang = len(info)
  273. }
  274. s := `class="language-` + string(info[:endOfLang]) + `"`
  275. return append(attrs, s)
  276. }
  277. func (r *Renderer) OutTag(w io.Writer, name string, attrs []string) {
  278. s := name
  279. if len(attrs) > 0 {
  280. s += " " + strings.Join(attrs, " ")
  281. }
  282. io.WriteString(w, s+">")
  283. r.lastOutputLen = 1
  284. }
  285. func FootnoteRef(prefix string, node *ast.Link) string {
  286. urlFrag := prefix + string(Slugify(node.Destination))
  287. nStr := strconv.Itoa(node.NoteID)
  288. anchor := `<a href="#fn:` + urlFrag + `">` + nStr + `</a>`
  289. return `<sup class="footnote-ref" id="fnref:` + urlFrag + `">` + anchor + `</sup>`
  290. }
  291. func FootnoteItem(prefix string, slug []byte) string {
  292. return `<li id="fn:` + prefix + string(slug) + `">`
  293. }
  294. func FootnoteReturnLink(prefix, returnLink string, slug []byte) string {
  295. return ` <a class="footnote-return" href="#fnref:` + prefix + string(slug) + `">` + returnLink + `</a>`
  296. }
  297. func ListItemOpenCR(listItem *ast.ListItem) bool {
  298. if ast.GetPrevNode(listItem) == nil {
  299. return false
  300. }
  301. ld := listItem.Parent.(*ast.List)
  302. return !ld.Tight && ld.ListFlags&ast.ListTypeDefinition == 0
  303. }
  304. func SkipParagraphTags(para *ast.Paragraph) bool {
  305. parent := para.Parent
  306. grandparent := parent.GetParent()
  307. if grandparent == nil || !IsList(grandparent) {
  308. return false
  309. }
  310. isParentTerm := IsListItemTerm(parent)
  311. grandparentListData := grandparent.(*ast.List)
  312. tightOrTerm := grandparentListData.Tight || isParentTerm
  313. return tightOrTerm
  314. }
  315. // Out is a helper to write data to writer
  316. func (r *Renderer) Out(w io.Writer, d []byte) {
  317. r.lastOutputLen = len(d)
  318. if r.DisableTags > 0 {
  319. d = htmlTagRe.ReplaceAll(d, []byte{})
  320. }
  321. w.Write(d)
  322. }
  323. // Outs is a helper to write data to writer
  324. func (r *Renderer) Outs(w io.Writer, s string) {
  325. r.lastOutputLen = len(s)
  326. if r.DisableTags > 0 {
  327. s = htmlTagRe.ReplaceAllString(s, "")
  328. }
  329. io.WriteString(w, s)
  330. }
  331. // CR writes a new line
  332. func (r *Renderer) CR(w io.Writer) {
  333. if r.lastOutputLen > 0 {
  334. r.Outs(w, "\n")
  335. }
  336. }
  337. var (
  338. openHTags = []string{"<h1", "<h2", "<h3", "<h4", "<h5"}
  339. closeHTags = []string{"</h1>", "</h2>", "</h3>", "</h4>", "</h5>"}
  340. )
  341. func HeadingOpenTagFromLevel(level int) string {
  342. if level < 1 || level > 5 {
  343. return "<h6"
  344. }
  345. return openHTags[level-1]
  346. }
  347. func HeadingCloseTagFromLevel(level int) string {
  348. if level < 1 || level > 5 {
  349. return "</h6>"
  350. }
  351. return closeHTags[level-1]
  352. }
  353. func (r *Renderer) OutHRTag(w io.Writer, attrs []string) {
  354. hr := TagWithAttributes("<hr", attrs)
  355. r.OutOneOf(w, r.Opts.Flags&UseXHTML == 0, hr, "<hr />")
  356. }
  357. // Text writes ast.Text node
  358. func (r *Renderer) Text(w io.Writer, text *ast.Text) {
  359. if r.Opts.Flags&Smartypants != 0 {
  360. var tmp bytes.Buffer
  361. EscapeHTML(&tmp, text.Literal)
  362. r.sr.Process(w, tmp.Bytes())
  363. } else {
  364. _, parentIsLink := text.Parent.(*ast.Link)
  365. if parentIsLink {
  366. EscLink(w, text.Literal)
  367. } else {
  368. EscapeHTML(w, text.Literal)
  369. }
  370. }
  371. }
  372. // HardBreak writes ast.Hardbreak node
  373. func (r *Renderer) HardBreak(w io.Writer, node *ast.Hardbreak) {
  374. r.OutOneOf(w, r.Opts.Flags&UseXHTML == 0, "<br>", "<br />")
  375. r.CR(w)
  376. }
  377. // NonBlockingSpace writes ast.NonBlockingSpace node
  378. func (r *Renderer) NonBlockingSpace(w io.Writer, node *ast.NonBlockingSpace) {
  379. r.Outs(w, "&nbsp;")
  380. }
  381. // OutOneOf writes first or second depending on outFirst
  382. func (r *Renderer) OutOneOf(w io.Writer, outFirst bool, first string, second string) {
  383. if outFirst {
  384. r.Outs(w, first)
  385. } else {
  386. r.Outs(w, second)
  387. }
  388. }
  389. // OutOneOfCr writes CR + first or second + CR depending on outFirst
  390. func (r *Renderer) OutOneOfCr(w io.Writer, outFirst bool, first string, second string) {
  391. if outFirst {
  392. r.CR(w)
  393. r.Outs(w, first)
  394. } else {
  395. r.Outs(w, second)
  396. r.CR(w)
  397. }
  398. }
  399. // HTMLSpan writes ast.HTMLSpan node
  400. func (r *Renderer) HTMLSpan(w io.Writer, span *ast.HTMLSpan) {
  401. if r.Opts.Flags&SkipHTML == 0 {
  402. r.Out(w, span.Literal)
  403. }
  404. }
  405. func (r *Renderer) linkEnter(w io.Writer, link *ast.Link) {
  406. attrs := link.AdditionalAttributes
  407. dest := link.Destination
  408. dest = AddAbsPrefix(dest, r.Opts.AbsolutePrefix)
  409. var hrefBuf bytes.Buffer
  410. hrefBuf.WriteString("href=\"")
  411. EscLink(&hrefBuf, dest)
  412. hrefBuf.WriteByte('"')
  413. attrs = append(attrs, hrefBuf.String())
  414. if link.NoteID != 0 {
  415. r.Outs(w, FootnoteRef(r.Opts.FootnoteAnchorPrefix, link))
  416. return
  417. }
  418. attrs = appendLinkAttrs(attrs, r.Opts.Flags, dest)
  419. if len(link.Title) > 0 {
  420. var titleBuff bytes.Buffer
  421. titleBuff.WriteString("title=\"")
  422. EscapeHTML(&titleBuff, link.Title)
  423. titleBuff.WriteByte('"')
  424. attrs = append(attrs, titleBuff.String())
  425. }
  426. r.OutTag(w, "<a", attrs)
  427. }
  428. func (r *Renderer) linkExit(w io.Writer, link *ast.Link) {
  429. if link.NoteID == 0 {
  430. r.Outs(w, "</a>")
  431. }
  432. }
  433. // Link writes ast.Link node
  434. func (r *Renderer) Link(w io.Writer, link *ast.Link, entering bool) {
  435. // mark it but don't link it if it is not a safe link: no smartypants
  436. if needSkipLink(r, link.Destination) {
  437. r.OutOneOf(w, entering, "<tt>", "</tt>")
  438. return
  439. }
  440. if entering {
  441. r.linkEnter(w, link)
  442. } else {
  443. r.linkExit(w, link)
  444. }
  445. }
  446. func (r *Renderer) imageEnter(w io.Writer, image *ast.Image) {
  447. r.DisableTags++
  448. if r.DisableTags > 1 {
  449. return
  450. }
  451. src := image.Destination
  452. src = AddAbsPrefix(src, r.Opts.AbsolutePrefix)
  453. attrs := BlockAttrs(image)
  454. if r.Opts.Flags&LazyLoadImages != 0 {
  455. attrs = append(attrs, `loading="lazy"`)
  456. }
  457. s := TagWithAttributes("<img", attrs)
  458. s = s[:len(s)-1] // hackish: strip off ">" from end
  459. r.Outs(w, s+` src="`)
  460. EscLink(w, src)
  461. r.Outs(w, `" alt="`)
  462. }
  463. func (r *Renderer) imageExit(w io.Writer, image *ast.Image) {
  464. r.DisableTags--
  465. if r.DisableTags > 0 {
  466. return
  467. }
  468. if image.Title != nil {
  469. r.Outs(w, `" title="`)
  470. EscapeHTML(w, image.Title)
  471. }
  472. r.Outs(w, `" />`)
  473. }
  474. // Image writes ast.Image node
  475. func (r *Renderer) Image(w io.Writer, node *ast.Image, entering bool) {
  476. if entering {
  477. r.imageEnter(w, node)
  478. } else {
  479. r.imageExit(w, node)
  480. }
  481. }
  482. func (r *Renderer) paragraphEnter(w io.Writer, para *ast.Paragraph) {
  483. // TODO: untangle this clusterfuck about when the newlines need
  484. // to be added and when not.
  485. prev := ast.GetPrevNode(para)
  486. if prev != nil {
  487. switch prev.(type) {
  488. case *ast.HTMLBlock, *ast.List, *ast.Paragraph, *ast.Heading, *ast.CaptionFigure, *ast.CodeBlock, *ast.BlockQuote, *ast.Aside, *ast.HorizontalRule:
  489. r.CR(w)
  490. }
  491. }
  492. if prev == nil {
  493. _, isParentBlockQuote := para.Parent.(*ast.BlockQuote)
  494. if isParentBlockQuote {
  495. r.CR(w)
  496. }
  497. _, isParentAside := para.Parent.(*ast.Aside)
  498. if isParentAside {
  499. r.CR(w)
  500. }
  501. }
  502. ptag := "<p"
  503. if r.Opts.ParagraphTag != "" {
  504. ptag = "<" + r.Opts.ParagraphTag
  505. }
  506. tag := TagWithAttributes(ptag, BlockAttrs(para))
  507. r.Outs(w, tag)
  508. }
  509. func (r *Renderer) paragraphExit(w io.Writer, para *ast.Paragraph) {
  510. ptag := "</p>"
  511. if r.Opts.ParagraphTag != "" {
  512. ptag = "</" + r.Opts.ParagraphTag + ">"
  513. }
  514. r.Outs(w, ptag)
  515. if !(IsListItem(para.Parent) && ast.GetNextNode(para) == nil) {
  516. r.CR(w)
  517. }
  518. }
  519. // Paragraph writes ast.Paragraph node
  520. func (r *Renderer) Paragraph(w io.Writer, para *ast.Paragraph, entering bool) {
  521. if SkipParagraphTags(para) {
  522. return
  523. }
  524. if entering {
  525. r.paragraphEnter(w, para)
  526. } else {
  527. r.paragraphExit(w, para)
  528. }
  529. }
  530. // Code writes ast.Code node
  531. func (r *Renderer) Code(w io.Writer, node *ast.Code) {
  532. r.Outs(w, "<code>")
  533. EscapeHTML(w, node.Literal)
  534. r.Outs(w, "</code>")
  535. }
  536. // HTMLBlock write ast.HTMLBlock node
  537. func (r *Renderer) HTMLBlock(w io.Writer, node *ast.HTMLBlock) {
  538. if r.Opts.Flags&SkipHTML != 0 {
  539. return
  540. }
  541. r.CR(w)
  542. r.Out(w, node.Literal)
  543. r.CR(w)
  544. }
  545. func (r *Renderer) EnsureUniqueHeadingID(id string) string {
  546. for count, found := r.headingIDs[id]; found; count, found = r.headingIDs[id] {
  547. tmp := fmt.Sprintf("%s-%d", id, count+1)
  548. if _, tmpFound := r.headingIDs[tmp]; !tmpFound {
  549. r.headingIDs[id] = count + 1
  550. id = tmp
  551. } else {
  552. id = id + "-1"
  553. }
  554. }
  555. if _, found := r.headingIDs[id]; !found {
  556. r.headingIDs[id] = 0
  557. }
  558. return id
  559. }
  560. func (r *Renderer) headingEnter(w io.Writer, nodeData *ast.Heading) {
  561. var attrs []string
  562. var class string
  563. // TODO(miek): add helper functions for coalescing these classes.
  564. if nodeData.IsTitleblock {
  565. class = "title"
  566. }
  567. if nodeData.IsSpecial {
  568. if class != "" {
  569. class += " special"
  570. } else {
  571. class = "special"
  572. }
  573. }
  574. if class != "" {
  575. attrs = []string{`class="` + class + `"`}
  576. }
  577. if nodeData.HeadingID != "" {
  578. id := r.EnsureUniqueHeadingID(nodeData.HeadingID)
  579. if r.Opts.HeadingIDPrefix != "" {
  580. id = r.Opts.HeadingIDPrefix + id
  581. }
  582. if r.Opts.HeadingIDSuffix != "" {
  583. id = id + r.Opts.HeadingIDSuffix
  584. }
  585. attrID := `id="` + id + `"`
  586. attrs = append(attrs, attrID)
  587. }
  588. attrs = append(attrs, BlockAttrs(nodeData)...)
  589. r.CR(w)
  590. r.OutTag(w, HeadingOpenTagFromLevel(nodeData.Level), attrs)
  591. }
  592. func (r *Renderer) headingExit(w io.Writer, heading *ast.Heading) {
  593. r.Outs(w, HeadingCloseTagFromLevel(heading.Level))
  594. if !(IsListItem(heading.Parent) && ast.GetNextNode(heading) == nil) {
  595. r.CR(w)
  596. }
  597. }
  598. // Heading writes ast.Heading node
  599. func (r *Renderer) Heading(w io.Writer, node *ast.Heading, entering bool) {
  600. if entering {
  601. r.headingEnter(w, node)
  602. } else {
  603. r.headingExit(w, node)
  604. }
  605. }
  606. // HorizontalRule writes ast.HorizontalRule node
  607. func (r *Renderer) HorizontalRule(w io.Writer, node *ast.HorizontalRule) {
  608. r.CR(w)
  609. r.OutHRTag(w, BlockAttrs(node))
  610. r.CR(w)
  611. }
  612. func (r *Renderer) listEnter(w io.Writer, nodeData *ast.List) {
  613. // TODO: attrs don't seem to be set
  614. var attrs []string
  615. if nodeData.IsFootnotesList {
  616. r.Outs(w, "\n<div class=\"footnotes\">\n\n")
  617. if r.Opts.Flags&FootnoteNoHRTag == 0 {
  618. r.OutHRTag(w, nil)
  619. r.CR(w)
  620. }
  621. }
  622. r.CR(w)
  623. if IsListItem(nodeData.Parent) {
  624. grand := nodeData.Parent.GetParent()
  625. if IsListTight(grand) {
  626. r.CR(w)
  627. }
  628. }
  629. openTag := "<ul"
  630. if nodeData.ListFlags&ast.ListTypeOrdered != 0 {
  631. if nodeData.Start > 0 {
  632. attrs = append(attrs, fmt.Sprintf(`start="%d"`, nodeData.Start))
  633. }
  634. openTag = "<ol"
  635. }
  636. if nodeData.ListFlags&ast.ListTypeDefinition != 0 {
  637. openTag = "<dl"
  638. }
  639. attrs = append(attrs, BlockAttrs(nodeData)...)
  640. r.OutTag(w, openTag, attrs)
  641. r.CR(w)
  642. }
  643. func (r *Renderer) listExit(w io.Writer, list *ast.List) {
  644. closeTag := "</ul>"
  645. if list.ListFlags&ast.ListTypeOrdered != 0 {
  646. closeTag = "</ol>"
  647. }
  648. if list.ListFlags&ast.ListTypeDefinition != 0 {
  649. closeTag = "</dl>"
  650. }
  651. r.Outs(w, closeTag)
  652. //cr(w)
  653. //if node.parent.Type != Item {
  654. // cr(w)
  655. //}
  656. parent := list.Parent
  657. switch parent.(type) {
  658. case *ast.ListItem:
  659. if ast.GetNextNode(list) != nil {
  660. r.CR(w)
  661. }
  662. case *ast.Document, *ast.BlockQuote, *ast.Aside:
  663. r.CR(w)
  664. }
  665. if list.IsFootnotesList {
  666. r.Outs(w, "\n</div>\n")
  667. }
  668. }
  669. // List writes ast.List node
  670. func (r *Renderer) List(w io.Writer, list *ast.List, entering bool) {
  671. if entering {
  672. r.listEnter(w, list)
  673. } else {
  674. r.listExit(w, list)
  675. }
  676. }
  677. func (r *Renderer) listItemEnter(w io.Writer, listItem *ast.ListItem) {
  678. if ListItemOpenCR(listItem) {
  679. r.CR(w)
  680. }
  681. if listItem.RefLink != nil {
  682. slug := Slugify(listItem.RefLink)
  683. r.Outs(w, FootnoteItem(r.Opts.FootnoteAnchorPrefix, slug))
  684. return
  685. }
  686. openTag := "<li>"
  687. if listItem.ListFlags&ast.ListTypeDefinition != 0 {
  688. openTag = "<dd>"
  689. }
  690. if listItem.ListFlags&ast.ListTypeTerm != 0 {
  691. openTag = "<dt>"
  692. }
  693. r.Outs(w, openTag)
  694. }
  695. func (r *Renderer) listItemExit(w io.Writer, listItem *ast.ListItem) {
  696. if listItem.RefLink != nil && r.Opts.Flags&FootnoteReturnLinks != 0 {
  697. slug := Slugify(listItem.RefLink)
  698. prefix := r.Opts.FootnoteAnchorPrefix
  699. link := r.Opts.FootnoteReturnLinkContents
  700. s := FootnoteReturnLink(prefix, link, slug)
  701. r.Outs(w, s)
  702. }
  703. closeTag := "</li>"
  704. if listItem.ListFlags&ast.ListTypeDefinition != 0 {
  705. closeTag = "</dd>"
  706. }
  707. if listItem.ListFlags&ast.ListTypeTerm != 0 {
  708. closeTag = "</dt>"
  709. }
  710. r.Outs(w, closeTag)
  711. r.CR(w)
  712. }
  713. // ListItem writes ast.ListItem node
  714. func (r *Renderer) ListItem(w io.Writer, listItem *ast.ListItem, entering bool) {
  715. if entering {
  716. r.listItemEnter(w, listItem)
  717. } else {
  718. r.listItemExit(w, listItem)
  719. }
  720. }
  721. // EscapeHTMLCallouts writes html-escaped d to w. It escapes &, <, > and " characters, *but*
  722. // expands callouts <<N>> with the callout HTML, i.e. by calling r.callout() with a newly created
  723. // ast.Callout node.
  724. func (r *Renderer) EscapeHTMLCallouts(w io.Writer, d []byte) {
  725. ld := len(d)
  726. Parse:
  727. for i := 0; i < ld; i++ {
  728. for _, comment := range r.Opts.Comments {
  729. if !bytes.HasPrefix(d[i:], comment) {
  730. break
  731. }
  732. lc := len(comment)
  733. if i+lc < ld {
  734. if id, consumed := parser.IsCallout(d[i+lc:]); consumed > 0 {
  735. // We have seen a callout
  736. callout := &ast.Callout{ID: id}
  737. r.Callout(w, callout)
  738. i += consumed + lc - 1
  739. continue Parse
  740. }
  741. }
  742. }
  743. escSeq := Escaper[d[i]]
  744. if escSeq != nil {
  745. w.Write(escSeq)
  746. } else {
  747. w.Write([]byte{d[i]})
  748. }
  749. }
  750. }
  751. // CodeBlock writes ast.CodeBlock node
  752. func (r *Renderer) CodeBlock(w io.Writer, codeBlock *ast.CodeBlock) {
  753. var attrs []string
  754. // TODO(miek): this can add multiple class= attribute, they should be coalesced into one.
  755. // This is probably true for some other elements as well
  756. attrs = appendLanguageAttr(attrs, codeBlock.Info)
  757. attrs = append(attrs, BlockAttrs(codeBlock)...)
  758. r.CR(w)
  759. r.Outs(w, "<pre>")
  760. code := TagWithAttributes("<code", attrs)
  761. r.Outs(w, code)
  762. if r.Opts.Comments != nil {
  763. r.EscapeHTMLCallouts(w, codeBlock.Literal)
  764. } else {
  765. EscapeHTML(w, codeBlock.Literal)
  766. }
  767. r.Outs(w, "</code>")
  768. r.Outs(w, "</pre>")
  769. if !IsListItem(codeBlock.Parent) {
  770. r.CR(w)
  771. }
  772. }
  773. // Caption writes ast.Caption node
  774. func (r *Renderer) Caption(w io.Writer, caption *ast.Caption, entering bool) {
  775. if entering {
  776. r.Outs(w, "<figcaption>")
  777. return
  778. }
  779. r.Outs(w, "</figcaption>")
  780. }
  781. // CaptionFigure writes ast.CaptionFigure node
  782. func (r *Renderer) CaptionFigure(w io.Writer, figure *ast.CaptionFigure, entering bool) {
  783. // TODO(miek): copy more generic ways of mmark over to here.
  784. fig := "<figure"
  785. if figure.HeadingID != "" {
  786. fig += ` id="` + figure.HeadingID + `">`
  787. } else {
  788. fig += ">"
  789. }
  790. r.OutOneOf(w, entering, fig, "\n</figure>\n")
  791. }
  792. // TableCell writes ast.TableCell node
  793. func (r *Renderer) TableCell(w io.Writer, tableCell *ast.TableCell, entering bool) {
  794. if !entering {
  795. r.OutOneOf(w, tableCell.IsHeader, "</th>", "</td>")
  796. r.CR(w)
  797. return
  798. }
  799. // entering
  800. var attrs []string
  801. openTag := "<td"
  802. if tableCell.IsHeader {
  803. openTag = "<th"
  804. }
  805. align := tableCell.Align.String()
  806. if align != "" {
  807. attrs = append(attrs, fmt.Sprintf(`align="%s"`, align))
  808. }
  809. if colspan := tableCell.ColSpan; colspan > 0 {
  810. attrs = append(attrs, fmt.Sprintf(`colspan="%d"`, colspan))
  811. }
  812. if ast.GetPrevNode(tableCell) == nil {
  813. r.CR(w)
  814. }
  815. r.OutTag(w, openTag, attrs)
  816. }
  817. // TableBody writes ast.TableBody node
  818. func (r *Renderer) TableBody(w io.Writer, node *ast.TableBody, entering bool) {
  819. if entering {
  820. r.CR(w)
  821. r.Outs(w, "<tbody>")
  822. // XXX: this is to adhere to a rather silly test. Should fix test.
  823. if ast.GetFirstChild(node) == nil {
  824. r.CR(w)
  825. }
  826. } else {
  827. r.Outs(w, "</tbody>")
  828. r.CR(w)
  829. }
  830. }
  831. // DocumentMatter writes ast.DocumentMatter
  832. func (r *Renderer) DocumentMatter(w io.Writer, node *ast.DocumentMatter, entering bool) {
  833. if !entering {
  834. return
  835. }
  836. if r.documentMatter != ast.DocumentMatterNone {
  837. r.Outs(w, "</section>\n")
  838. }
  839. switch node.Matter {
  840. case ast.DocumentMatterFront:
  841. r.Outs(w, `<section data-matter="front">`)
  842. case ast.DocumentMatterMain:
  843. r.Outs(w, `<section data-matter="main">`)
  844. case ast.DocumentMatterBack:
  845. r.Outs(w, `<section data-matter="back">`)
  846. }
  847. r.documentMatter = node.Matter
  848. }
  849. // Citation writes ast.Citation node
  850. func (r *Renderer) Citation(w io.Writer, node *ast.Citation) {
  851. for i, c := range node.Destination {
  852. attr := []string{`class="none"`}
  853. switch node.Type[i] {
  854. case ast.CitationTypeNormative:
  855. attr[0] = `class="normative"`
  856. case ast.CitationTypeInformative:
  857. attr[0] = `class="informative"`
  858. case ast.CitationTypeSuppressed:
  859. attr[0] = `class="suppressed"`
  860. }
  861. r.OutTag(w, "<cite", attr)
  862. r.Outs(w, fmt.Sprintf(`<a href="#%s">`+r.Opts.CitationFormatString+`</a>`, c, c))
  863. r.Outs(w, "</cite>")
  864. }
  865. }
  866. // Callout writes ast.Callout node
  867. func (r *Renderer) Callout(w io.Writer, node *ast.Callout) {
  868. attr := []string{`class="callout"`}
  869. r.OutTag(w, "<span", attr)
  870. r.Out(w, node.ID)
  871. r.Outs(w, "</span>")
  872. }
  873. // Index writes ast.Index node
  874. func (r *Renderer) Index(w io.Writer, node *ast.Index) {
  875. // there is no in-text representation.
  876. attr := []string{`class="index"`, fmt.Sprintf(`id="%s"`, node.ID)}
  877. r.OutTag(w, "<span", attr)
  878. r.Outs(w, "</span>")
  879. }
  880. // RenderNode renders a markdown node to HTML
  881. func (r *Renderer) RenderNode(w io.Writer, node ast.Node, entering bool) ast.WalkStatus {
  882. if r.Opts.RenderNodeHook != nil {
  883. status, didHandle := r.Opts.RenderNodeHook(w, node, entering)
  884. if didHandle {
  885. return status
  886. }
  887. }
  888. switch node := node.(type) {
  889. case *ast.Text:
  890. r.Text(w, node)
  891. case *ast.Softbreak:
  892. r.CR(w)
  893. // TODO: make it configurable via out(renderer.softbreak)
  894. case *ast.Hardbreak:
  895. r.HardBreak(w, node)
  896. case *ast.NonBlockingSpace:
  897. r.NonBlockingSpace(w, node)
  898. case *ast.Emph:
  899. r.OutOneOf(w, entering, "<em>", "</em>")
  900. case *ast.Strong:
  901. r.OutOneOf(w, entering, "<strong>", "</strong>")
  902. case *ast.Del:
  903. r.OutOneOf(w, entering, "<del>", "</del>")
  904. case *ast.BlockQuote:
  905. tag := TagWithAttributes("<blockquote", BlockAttrs(node))
  906. r.OutOneOfCr(w, entering, tag, "</blockquote>")
  907. case *ast.Aside:
  908. tag := TagWithAttributes("<aside", BlockAttrs(node))
  909. r.OutOneOfCr(w, entering, tag, "</aside>")
  910. case *ast.Link:
  911. r.Link(w, node, entering)
  912. case *ast.CrossReference:
  913. link := &ast.Link{Destination: append([]byte("#"), node.Destination...)}
  914. r.Link(w, link, entering)
  915. case *ast.Citation:
  916. r.Citation(w, node)
  917. case *ast.Image:
  918. if r.Opts.Flags&SkipImages != 0 {
  919. return ast.SkipChildren
  920. }
  921. r.Image(w, node, entering)
  922. case *ast.Code:
  923. r.Code(w, node)
  924. case *ast.CodeBlock:
  925. r.CodeBlock(w, node)
  926. case *ast.Caption:
  927. r.Caption(w, node, entering)
  928. case *ast.CaptionFigure:
  929. r.CaptionFigure(w, node, entering)
  930. case *ast.Document:
  931. // do nothing
  932. case *ast.Paragraph:
  933. r.Paragraph(w, node, entering)
  934. case *ast.HTMLSpan:
  935. r.HTMLSpan(w, node)
  936. case *ast.HTMLBlock:
  937. r.HTMLBlock(w, node)
  938. case *ast.Heading:
  939. r.Heading(w, node, entering)
  940. case *ast.HorizontalRule:
  941. r.HorizontalRule(w, node)
  942. case *ast.List:
  943. r.List(w, node, entering)
  944. case *ast.ListItem:
  945. r.ListItem(w, node, entering)
  946. case *ast.Table:
  947. tag := TagWithAttributes("<table", BlockAttrs(node))
  948. r.OutOneOfCr(w, entering, tag, "</table>")
  949. case *ast.TableCell:
  950. r.TableCell(w, node, entering)
  951. case *ast.TableHeader:
  952. r.OutOneOfCr(w, entering, "<thead>", "</thead>")
  953. case *ast.TableBody:
  954. r.TableBody(w, node, entering)
  955. case *ast.TableRow:
  956. r.OutOneOfCr(w, entering, "<tr>", "</tr>")
  957. case *ast.TableFooter:
  958. r.OutOneOfCr(w, entering, "<tfoot>", "</tfoot>")
  959. case *ast.Math:
  960. r.OutOneOf(w, true, `<span class="math inline">\(`, `\)</span>`)
  961. EscapeHTML(w, node.Literal)
  962. r.OutOneOf(w, false, `<span class="math inline">\(`, `\)</span>`)
  963. case *ast.MathBlock:
  964. r.OutOneOf(w, entering, `<p><span class="math display">\[`, `\]</span></p>`)
  965. if entering {
  966. EscapeHTML(w, node.Literal)
  967. }
  968. case *ast.DocumentMatter:
  969. r.DocumentMatter(w, node, entering)
  970. case *ast.Callout:
  971. r.Callout(w, node)
  972. case *ast.Index:
  973. r.Index(w, node)
  974. case *ast.Subscript:
  975. r.OutOneOf(w, true, "<sub>", "</sub>")
  976. if entering {
  977. Escape(w, node.Literal)
  978. }
  979. r.OutOneOf(w, false, "<sub>", "</sub>")
  980. case *ast.Superscript:
  981. r.OutOneOf(w, true, "<sup>", "</sup>")
  982. if entering {
  983. Escape(w, node.Literal)
  984. }
  985. r.OutOneOf(w, false, "<sup>", "</sup>")
  986. case *ast.Footnotes:
  987. // nothing by default; just output the list.
  988. default:
  989. panic(fmt.Sprintf("Unknown node %T", node))
  990. }
  991. return ast.GoToNext
  992. }
  993. // RenderHeader writes HTML document preamble and TOC if requested.
  994. func (r *Renderer) RenderHeader(w io.Writer, ast ast.Node) {
  995. r.writeDocumentHeader(w)
  996. if r.Opts.Flags&TOC != 0 {
  997. r.writeTOC(w, ast)
  998. }
  999. }
  1000. // RenderFooter writes HTML document footer.
  1001. func (r *Renderer) RenderFooter(w io.Writer, _ ast.Node) {
  1002. if r.documentMatter != ast.DocumentMatterNone {
  1003. r.Outs(w, "</section>\n")
  1004. }
  1005. if r.Opts.Flags&CompletePage == 0 {
  1006. return
  1007. }
  1008. io.WriteString(w, "\n</body>\n</html>\n")
  1009. }
  1010. func (r *Renderer) writeDocumentHeader(w io.Writer) {
  1011. if r.Opts.Flags&CompletePage == 0 {
  1012. return
  1013. }
  1014. ending := ""
  1015. if r.Opts.Flags&UseXHTML != 0 {
  1016. io.WriteString(w, "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" ")
  1017. io.WriteString(w, "\"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">\n")
  1018. io.WriteString(w, "<html xmlns=\"http://www.w3.org/1999/xhtml\">\n")
  1019. ending = " /"
  1020. } else {
  1021. io.WriteString(w, "<!DOCTYPE html>\n")
  1022. io.WriteString(w, "<html>\n")
  1023. }
  1024. io.WriteString(w, "<head>\n")
  1025. io.WriteString(w, " <title>")
  1026. if r.Opts.Flags&Smartypants != 0 {
  1027. r.sr.Process(w, []byte(r.Opts.Title))
  1028. } else {
  1029. EscapeHTML(w, []byte(r.Opts.Title))
  1030. }
  1031. io.WriteString(w, "</title>\n")
  1032. io.WriteString(w, r.Opts.Generator)
  1033. io.WriteString(w, "\"")
  1034. io.WriteString(w, ending)
  1035. io.WriteString(w, ">\n")
  1036. io.WriteString(w, " <meta charset=\"utf-8\"")
  1037. io.WriteString(w, ending)
  1038. io.WriteString(w, ">\n")
  1039. if r.Opts.CSS != "" {
  1040. io.WriteString(w, " <link rel=\"stylesheet\" type=\"text/css\" href=\"")
  1041. EscapeHTML(w, []byte(r.Opts.CSS))
  1042. io.WriteString(w, "\"")
  1043. io.WriteString(w, ending)
  1044. io.WriteString(w, ">\n")
  1045. }
  1046. if r.Opts.Icon != "" {
  1047. io.WriteString(w, " <link rel=\"icon\" type=\"image/x-icon\" href=\"")
  1048. EscapeHTML(w, []byte(r.Opts.Icon))
  1049. io.WriteString(w, "\"")
  1050. io.WriteString(w, ending)
  1051. io.WriteString(w, ">\n")
  1052. }
  1053. if r.Opts.Head != nil {
  1054. w.Write(r.Opts.Head)
  1055. }
  1056. io.WriteString(w, "</head>\n")
  1057. io.WriteString(w, "<body>\n\n")
  1058. }
  1059. func (r *Renderer) writeTOC(w io.Writer, doc ast.Node) {
  1060. buf := bytes.Buffer{}
  1061. inHeading := false
  1062. tocLevel := 0
  1063. headingCount := 0
  1064. ast.WalkFunc(doc, func(node ast.Node, entering bool) ast.WalkStatus {
  1065. if nodeData, ok := node.(*ast.Heading); ok && !nodeData.IsTitleblock {
  1066. inHeading = entering
  1067. if !entering {
  1068. buf.WriteString("</a>")
  1069. return ast.GoToNext
  1070. }
  1071. if nodeData.HeadingID == "" {
  1072. nodeData.HeadingID = fmt.Sprintf("toc_%d", headingCount)
  1073. }
  1074. if nodeData.Level == tocLevel {
  1075. buf.WriteString("</li>\n\n<li>")
  1076. } else if nodeData.Level < tocLevel {
  1077. for nodeData.Level < tocLevel {
  1078. tocLevel--
  1079. buf.WriteString("</li>\n</ul>")
  1080. }
  1081. buf.WriteString("</li>\n\n<li>")
  1082. } else {
  1083. for nodeData.Level > tocLevel {
  1084. tocLevel++
  1085. buf.WriteString("\n<ul>\n<li>")
  1086. }
  1087. }
  1088. fmt.Fprintf(&buf, `<a href="#%s">`, nodeData.HeadingID)
  1089. headingCount++
  1090. return ast.GoToNext
  1091. }
  1092. if inHeading {
  1093. return r.RenderNode(&buf, node, entering)
  1094. }
  1095. return ast.GoToNext
  1096. })
  1097. for ; tocLevel > 0; tocLevel-- {
  1098. buf.WriteString("</li>\n</ul>")
  1099. }
  1100. if buf.Len() > 0 {
  1101. io.WriteString(w, "<nav>\n")
  1102. w.Write(buf.Bytes())
  1103. io.WriteString(w, "\n\n</nav>\n")
  1104. }
  1105. r.lastOutputLen = buf.Len()
  1106. }
  1107. func IsList(node ast.Node) bool {
  1108. _, ok := node.(*ast.List)
  1109. return ok
  1110. }
  1111. func IsListTight(node ast.Node) bool {
  1112. if list, ok := node.(*ast.List); ok {
  1113. return list.Tight
  1114. }
  1115. return false
  1116. }
  1117. func IsListItem(node ast.Node) bool {
  1118. _, ok := node.(*ast.ListItem)
  1119. return ok
  1120. }
  1121. func IsListItemTerm(node ast.Node) bool {
  1122. data, ok := node.(*ast.ListItem)
  1123. return ok && data.ListFlags&ast.ListTypeTerm != 0
  1124. }
  1125. // TODO: move to internal package
  1126. // Create a url-safe slug for fragments
  1127. func Slugify(in []byte) []byte {
  1128. if len(in) == 0 {
  1129. return in
  1130. }
  1131. out := make([]byte, 0, len(in))
  1132. sym := false
  1133. for _, ch := range in {
  1134. if isAlnum(ch) {
  1135. sym = false
  1136. out = append(out, ch)
  1137. } else if sym {
  1138. continue
  1139. } else {
  1140. out = append(out, '-')
  1141. sym = true
  1142. }
  1143. }
  1144. var a, b int
  1145. var ch byte
  1146. for a, ch = range out {
  1147. if ch != '-' {
  1148. break
  1149. }
  1150. }
  1151. for b = len(out) - 1; b > 0; b-- {
  1152. if out[b] != '-' {
  1153. break
  1154. }
  1155. }
  1156. return out[a : b+1]
  1157. }
  1158. // BlockAttrs takes a node and checks if it has block level attributes set. If so it
  1159. // will return a slice each containing a "key=value(s)" string.
  1160. func BlockAttrs(node ast.Node) []string {
  1161. var attr *ast.Attribute
  1162. if c := node.AsContainer(); c != nil && c.Attribute != nil {
  1163. attr = c.Attribute
  1164. }
  1165. if l := node.AsLeaf(); l != nil && l.Attribute != nil {
  1166. attr = l.Attribute
  1167. }
  1168. if attr == nil {
  1169. return nil
  1170. }
  1171. var s []string
  1172. if attr.ID != nil {
  1173. s = append(s, fmt.Sprintf(`%s="%s"`, IDTag, attr.ID))
  1174. }
  1175. classes := ""
  1176. for _, c := range attr.Classes {
  1177. classes += " " + string(c)
  1178. }
  1179. if classes != "" {
  1180. s = append(s, fmt.Sprintf(`class="%s"`, classes[1:])) // skip space we added.
  1181. }
  1182. // sort the attributes so it remain stable between runs
  1183. var keys = []string{}
  1184. for k := range attr.Attrs {
  1185. keys = append(keys, k)
  1186. }
  1187. sort.Strings(keys)
  1188. for _, k := range keys {
  1189. s = append(s, fmt.Sprintf(`%s="%s"`, k, attr.Attrs[k]))
  1190. }
  1191. return s
  1192. }
  1193. // TagWithAttributes creates a HTML tag with a given name and attributes
  1194. func TagWithAttributes(name string, attrs []string) string {
  1195. s := name
  1196. if len(attrs) > 0 {
  1197. s += " " + strings.Join(attrs, " ")
  1198. }
  1199. return s + ">"
  1200. }