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