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