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