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