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