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