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