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