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 "html"
22 "regexp"
23 "strconv"
24 "strings"
25)
26
27type HtmlFlags int
28
29// Html renderer configuration options.
30const (
31 HtmlFlagsNone HtmlFlags = 0
32 SkipHTML HtmlFlags = 1 << iota // Skip preformatted HTML blocks
33 SkipStyle // Skip embedded <style> elements
34 SkipImages // Skip embedded images
35 SkipLinks // Skip all links
36 Safelink // Only link to trusted protocols
37 NofollowLinks // Only link with rel="nofollow"
38 NoreferrerLinks // Only link with rel="noreferrer"
39 HrefTargetBlank // Add a blank target
40 Toc // Generate a table of contents
41 OmitContents // Skip the main contents (for a standalone table of contents)
42 CompletePage // Generate a complete HTML page
43 UseXHTML // Generate XHTML output instead of HTML
44 UseSmartypants // Enable smart punctuation substitutions
45 SmartypantsFractions // Enable smart fractions (with UseSmartypants)
46 SmartypantsDashes // Enable smart dashes (with UseSmartypants)
47 SmartypantsLatexDashes // Enable LaTeX-style dashes (with UseSmartypants)
48 SmartypantsAngledQuotes // Enable angled double quotes (with UseSmartypants) for double quotes rendering
49 FootnoteReturnLinks // Generate a link at the end of a footnote to return to the source
50
51 TagName = "[A-Za-z][A-Za-z0-9-]*"
52 AttributeName = "[a-zA-Z_:][a-zA-Z0-9:._-]*"
53 UnquotedValue = "[^\"'=<>`\\x00-\\x20]+"
54 SingleQuotedValue = "'[^']*'"
55 DoubleQuotedValue = "\"[^\"]*\""
56 AttributeValue = "(?:" + UnquotedValue + "|" + SingleQuotedValue + "|" + DoubleQuotedValue + ")"
57 AttributeValueSpec = "(?:" + "\\s*=" + "\\s*" + AttributeValue + ")"
58 Attribute = "(?:" + "\\s+" + AttributeName + AttributeValueSpec + "?)"
59 OpenTag = "<" + TagName + Attribute + "*" + "\\s*/?>"
60 CloseTag = "</" + TagName + "\\s*[>]"
61 HTMLComment = "<!---->|<!--(?:-?[^>-])(?:-?[^-])*-->"
62 ProcessingInstruction = "[<][?].*?[?][>]"
63 Declaration = "<![A-Z]+" + "\\s+[^>]*>"
64 CDATA = "<!\\[CDATA\\[[\\s\\S]*?\\]\\]>"
65 HTMLTag = "(?:" + OpenTag + "|" + CloseTag + "|" + HTMLComment + "|" +
66 ProcessingInstruction + "|" + Declaration + "|" + CDATA + ")"
67)
68
69var (
70 // TODO: improve this regexp to catch all possible entities:
71 htmlEntity = regexp.MustCompile(`&[a-z]{2,5};`)
72 reHtmlTag = regexp.MustCompile("(?i)^" + HTMLTag)
73)
74
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 Header ID, to ensure
85 // uniqueness.
86 HeaderIDPrefix string
87 // If set, add this text to the back of each Header ID, to ensure uniqueness.
88 HeaderIDSuffix string
89}
90
91// Html is a type that implements the Renderer interface for HTML output.
92//
93// Do not create this directly, instead use the HtmlRenderer function.
94type Html struct {
95 flags HtmlFlags
96 closeTag string // how to end singleton tags: either " />" or ">"
97 title string // document title
98 css string // optional css file url (used with HTML_COMPLETE_PAGE)
99
100 parameters HtmlRendererParameters
101
102 // table of contents data
103 tocMarker int
104 headerCount int
105 currentLevel int
106 toc *bytes.Buffer
107
108 // Track header IDs to prevent ID collision in a single generation.
109 headerIDs map[string]int
110
111 smartypants *smartypantsRenderer
112 w HtmlWriter
113 lastOutputLen int
114 disableTags int
115 renderBuffer bytes.Buffer
116}
117
118const (
119 xhtmlClose = " />"
120 htmlClose = ">"
121)
122
123// HtmlRenderer creates and configures an Html object, which
124// satisfies the Renderer interface.
125//
126// flags is a set of HtmlFlags ORed together.
127// title is the title of the document, and css is a URL for the document's
128// stylesheet.
129// title and css are only used when HTML_COMPLETE_PAGE is selected.
130func HtmlRenderer(flags HtmlFlags, title string, css string) Renderer {
131 return HtmlRendererWithParameters(flags, title, css, HtmlRendererParameters{})
132}
133
134type HtmlWriter struct {
135 output bytes.Buffer
136 captureBuff *bytes.Buffer
137 copyBuff *bytes.Buffer
138 dirty bool
139}
140
141func (w *HtmlWriter) Write(p []byte) (n int, err error) {
142 w.dirty = true
143 if w.copyBuff != nil {
144 w.copyBuff.Write(p)
145 }
146 if w.captureBuff != nil {
147 w.captureBuff.Write(p)
148 return
149 }
150 return w.output.Write(p)
151}
152
153func (w *HtmlWriter) WriteString(s string) (n int, err error) {
154 w.dirty = true
155 if w.copyBuff != nil {
156 w.copyBuff.WriteString(s)
157 }
158 if w.captureBuff != nil {
159 w.captureBuff.WriteString(s)
160 return
161 }
162 return w.output.WriteString(s)
163}
164
165func (w *HtmlWriter) WriteByte(b byte) error {
166 w.dirty = true
167 if w.copyBuff != nil {
168 w.copyBuff.WriteByte(b)
169 }
170 if w.captureBuff != nil {
171 return w.captureBuff.WriteByte(b)
172 }
173 return w.output.WriteByte(b)
174}
175
176// Writes out a newline if the output is not pristine. Used at the beginning of
177// every rendering func
178func (w *HtmlWriter) Newline() {
179 if w.dirty {
180 w.WriteByte('\n')
181 }
182}
183
184func (r *Html) CaptureWrites(processor func()) []byte {
185 var output bytes.Buffer
186 // preserve old captureBuff state for possible nested captures:
187 tmp := r.w.captureBuff
188 tmpd := r.w.dirty
189 r.w.captureBuff = &output
190 r.w.dirty = false
191 processor()
192 // restore:
193 r.w.captureBuff = tmp
194 r.w.dirty = tmpd
195 return output.Bytes()
196}
197
198func (r *Html) CopyWrites(processor func()) []byte {
199 var output bytes.Buffer
200 r.w.copyBuff = &output
201 processor()
202 r.w.copyBuff = nil
203 return output.Bytes()
204}
205
206func (r *Html) Write(b []byte) (int, error) {
207 return r.w.Write(b)
208}
209
210func (r *Html) GetResult() []byte {
211 return r.w.output.Bytes()
212}
213
214func HtmlRendererWithParameters(flags HtmlFlags, title string,
215 css string, renderParameters HtmlRendererParameters) Renderer {
216 // configure the rendering engine
217 closeTag := htmlClose
218 if flags&UseXHTML != 0 {
219 closeTag = xhtmlClose
220 }
221
222 if renderParameters.FootnoteReturnLinkContents == "" {
223 renderParameters.FootnoteReturnLinkContents = `<sup>[return]</sup>`
224 }
225
226 var writer HtmlWriter
227 return &Html{
228 flags: flags,
229 closeTag: closeTag,
230 title: title,
231 css: css,
232 parameters: renderParameters,
233
234 headerCount: 0,
235 currentLevel: 0,
236 toc: new(bytes.Buffer),
237
238 headerIDs: make(map[string]int),
239
240 smartypants: smartypants(flags),
241 w: writer,
242 }
243}
244
245// Using if statements is a bit faster than a switch statement. As the compiler
246// improves, this should be unnecessary this is only worthwhile because
247// attrEscape is the single largest CPU user in normal use.
248// Also tried using map, but that gave a ~3x slowdown.
249func escapeSingleChar(char byte) (string, bool) {
250 if char == '"' {
251 return """, true
252 }
253 if char == '&' {
254 return "&", true
255 }
256 if char == '<' {
257 return "<", true
258 }
259 if char == '>' {
260 return ">", true
261 }
262 return "", false
263}
264
265func (r *Html) attrEscape(src []byte) {
266 org := 0
267 for i, ch := range src {
268 if entity, ok := escapeSingleChar(ch); ok {
269 if i > org {
270 // copy all the normal characters since the last escape
271 r.w.Write(src[org:i])
272 }
273 org = i + 1
274 r.w.WriteString(entity)
275 }
276 }
277 if org < len(src) {
278 r.w.Write(src[org:])
279 }
280}
281
282func attrEscape2(src []byte) []byte {
283 unesc := []byte(html.UnescapeString(string(src)))
284 esc1 := []byte(html.EscapeString(string(unesc)))
285 esc2 := bytes.Replace(esc1, []byte("""), []byte("""), -1)
286 return bytes.Replace(esc2, []byte("'"), []byte{'\''}, -1)
287}
288
289func (r *Html) entityEscapeWithSkip(src []byte, skipRanges [][]int) {
290 end := 0
291 for _, rang := range skipRanges {
292 r.attrEscape(src[end:rang[0]])
293 r.w.Write(src[rang[0]:rang[1]])
294 end = rang[1]
295 }
296 r.attrEscape(src[end:])
297}
298
299func (r *Html) GetFlags() HtmlFlags {
300 return r.flags
301}
302
303func (r *Html) TitleBlock(text []byte) {
304 text = bytes.TrimPrefix(text, []byte("% "))
305 text = bytes.Replace(text, []byte("\n% "), []byte("\n"), -1)
306 r.w.WriteString("<h1 class=\"title\">")
307 r.w.Write(text)
308 r.w.WriteString("\n</h1>")
309}
310
311func (r *Html) BeginHeader(level int, id string) {
312 r.w.Newline()
313
314 if id == "" && r.flags&Toc != 0 {
315 id = fmt.Sprintf("toc_%d", r.headerCount)
316 }
317
318 if id != "" {
319 id = r.ensureUniqueHeaderID(id)
320
321 if r.parameters.HeaderIDPrefix != "" {
322 id = r.parameters.HeaderIDPrefix + id
323 }
324
325 if r.parameters.HeaderIDSuffix != "" {
326 id = id + r.parameters.HeaderIDSuffix
327 }
328
329 r.w.WriteString(fmt.Sprintf("<h%d id=\"%s\">", level, id))
330 } else {
331 r.w.WriteString(fmt.Sprintf("<h%d>", level))
332 }
333}
334
335func (r *Html) EndHeader(level int, id string, header []byte) {
336 // are we building a table of contents?
337 if r.flags&Toc != 0 {
338 r.TocHeaderWithAnchor(header, level, id)
339 }
340
341 r.w.WriteString(fmt.Sprintf("</h%d>\n", level))
342}
343
344func (r *Html) BlockHtml(text []byte) {
345 if r.flags&SkipHTML != 0 {
346 return
347 }
348
349 r.w.Newline()
350 r.w.Write(text)
351 r.w.WriteByte('\n')
352}
353
354func (r *Html) HRule() {
355 r.w.Newline()
356 r.w.WriteString("<hr")
357 r.w.WriteString(r.closeTag)
358 r.w.WriteByte('\n')
359}
360
361func (r *Html) BlockCode(text []byte, lang string) {
362 r.w.Newline()
363
364 // parse out the language names/classes
365 count := 0
366 for _, elt := range strings.Fields(lang) {
367 if elt[0] == '.' {
368 elt = elt[1:]
369 }
370 if len(elt) == 0 {
371 continue
372 }
373 if count == 0 {
374 r.w.WriteString("<pre><code class=\"language-")
375 } else {
376 r.w.WriteByte(' ')
377 }
378 r.attrEscape([]byte(elt))
379 count++
380 }
381
382 if count == 0 {
383 r.w.WriteString("<pre><code>")
384 } else {
385 r.w.WriteString("\">")
386 }
387
388 r.attrEscape(text)
389 r.w.WriteString("</code></pre>\n")
390}
391
392func (r *Html) BlockQuote(text []byte) {
393 r.w.Newline()
394 r.w.WriteString("<blockquote>\n")
395 r.w.Write(text)
396 r.w.WriteString("</blockquote>\n")
397}
398
399func (r *Html) Table(header []byte, body []byte, columnData []int) {
400 r.w.Newline()
401 r.w.WriteString("<table>\n<thead>\n")
402 r.w.Write(header)
403 r.w.WriteString("</thead>\n\n<tbody>\n")
404 r.w.Write(body)
405 r.w.WriteString("</tbody>\n</table>\n")
406}
407
408func (r *Html) TableRow(text []byte) {
409 r.w.Newline()
410 r.w.WriteString("<tr>\n")
411 r.w.Write(text)
412 r.w.WriteString("\n</tr>\n")
413}
414
415func leadingNewline(out *bytes.Buffer) {
416 if out.Len() > 0 {
417 out.WriteByte('\n')
418 }
419}
420
421func (r *Html) TableHeaderCell(out *bytes.Buffer, text []byte, align int) {
422 leadingNewline(out)
423 switch align {
424 case TableAlignmentLeft:
425 out.WriteString("<th align=\"left\">")
426 case TableAlignmentRight:
427 out.WriteString("<th align=\"right\">")
428 case TableAlignmentCenter:
429 out.WriteString("<th align=\"center\">")
430 default:
431 out.WriteString("<th>")
432 }
433
434 out.Write(text)
435 out.WriteString("</th>")
436}
437
438func (r *Html) TableCell(out *bytes.Buffer, text []byte, align int) {
439 leadingNewline(out)
440 switch align {
441 case TableAlignmentLeft:
442 out.WriteString("<td align=\"left\">")
443 case TableAlignmentRight:
444 out.WriteString("<td align=\"right\">")
445 case TableAlignmentCenter:
446 out.WriteString("<td align=\"center\">")
447 default:
448 out.WriteString("<td>")
449 }
450
451 out.Write(text)
452 out.WriteString("</td>")
453}
454
455func (r *Html) BeginFootnotes() {
456 r.w.WriteString("<div class=\"footnotes\">\n")
457 r.HRule()
458 r.BeginList(ListTypeOrdered)
459}
460
461func (r *Html) EndFootnotes() {
462 r.EndList(ListTypeOrdered)
463 r.w.WriteString("</div>\n")
464}
465
466func (r *Html) FootnoteItem(name, text []byte, flags ListType) {
467 if flags&ListItemContainsBlock != 0 || flags&ListItemBeginningOfList != 0 {
468 r.w.Newline()
469 }
470 slug := slugify(name)
471 r.w.WriteString(`<li id="`)
472 r.w.WriteString(`fn:`)
473 r.w.WriteString(r.parameters.FootnoteAnchorPrefix)
474 r.w.Write(slug)
475 r.w.WriteString(`">`)
476 r.w.Write(text)
477 if r.flags&FootnoteReturnLinks != 0 {
478 r.w.WriteString(` <a class="footnote-return" href="#`)
479 r.w.WriteString(`fnref:`)
480 r.w.WriteString(r.parameters.FootnoteAnchorPrefix)
481 r.w.Write(slug)
482 r.w.WriteString(`">`)
483 r.w.WriteString(r.parameters.FootnoteReturnLinkContents)
484 r.w.WriteString(`</a>`)
485 }
486 r.w.WriteString("</li>\n")
487}
488
489func (r *Html) BeginList(flags ListType) {
490 r.w.Newline()
491
492 if flags&ListTypeDefinition != 0 {
493 r.w.WriteString("<dl>")
494 } else if flags&ListTypeOrdered != 0 {
495 r.w.WriteString("<ol>")
496 } else {
497 r.w.WriteString("<ul>")
498 }
499}
500
501func (r *Html) EndList(flags ListType) {
502 if flags&ListTypeDefinition != 0 {
503 r.w.WriteString("</dl>\n")
504 } else if flags&ListTypeOrdered != 0 {
505 r.w.WriteString("</ol>\n")
506 } else {
507 r.w.WriteString("</ul>\n")
508 }
509}
510
511func (r *Html) ListItem(text []byte, flags ListType) {
512 if (flags&ListItemContainsBlock != 0 && flags&ListTypeDefinition == 0) ||
513 flags&ListItemBeginningOfList != 0 {
514 r.w.Newline()
515 }
516 if flags&ListTypeTerm != 0 {
517 r.w.WriteString("<dt>")
518 } else if flags&ListTypeDefinition != 0 {
519 r.w.WriteString("<dd>")
520 } else {
521 r.w.WriteString("<li>")
522 }
523 r.w.Write(text)
524 if flags&ListTypeTerm != 0 {
525 r.w.WriteString("</dt>\n")
526 } else if flags&ListTypeDefinition != 0 {
527 r.w.WriteString("</dd>\n")
528 } else {
529 r.w.WriteString("</li>\n")
530 }
531}
532
533func (r *Html) BeginParagraph() {
534 r.w.Newline()
535 r.w.WriteString("<p>")
536}
537
538func (r *Html) EndParagraph() {
539 r.w.WriteString("</p>\n")
540}
541
542func (r *Html) AutoLink(link []byte, kind LinkType) {
543 skipRanges := htmlEntity.FindAllIndex(link, -1)
544 if r.flags&Safelink != 0 && !isSafeLink(link) && kind != LinkTypeEmail {
545 // mark it but don't link it if it is not a safe link: no smartypants
546 r.w.WriteString("<tt>")
547 r.entityEscapeWithSkip(link, skipRanges)
548 r.w.WriteString("</tt>")
549 return
550 }
551
552 r.w.WriteString("<a href=\"")
553 if kind == LinkTypeEmail {
554 r.w.WriteString("mailto:")
555 } else {
556 r.maybeWriteAbsolutePrefix(link)
557 }
558
559 r.entityEscapeWithSkip(link, skipRanges)
560
561 var relAttrs []string
562 if r.flags&NofollowLinks != 0 && !isRelativeLink(link) {
563 relAttrs = append(relAttrs, "nofollow")
564 }
565 if r.flags&NoreferrerLinks != 0 && !isRelativeLink(link) {
566 relAttrs = append(relAttrs, "noreferrer")
567 }
568 if len(relAttrs) > 0 {
569 r.w.WriteString(fmt.Sprintf("\" rel=\"%s", strings.Join(relAttrs, " ")))
570 }
571
572 // blank target only add to external link
573 if r.flags&HrefTargetBlank != 0 && !isRelativeLink(link) {
574 r.w.WriteString("\" target=\"_blank")
575 }
576
577 r.w.WriteString("\">")
578
579 // Pretty print: if we get an email address as
580 // an actual URI, e.g. `mailto:foo@bar.com`, we don't
581 // want to print the `mailto:` prefix
582 switch {
583 case bytes.HasPrefix(link, []byte("mailto://")):
584 r.attrEscape(link[len("mailto://"):])
585 case bytes.HasPrefix(link, []byte("mailto:")):
586 r.attrEscape(link[len("mailto:"):])
587 default:
588 r.entityEscapeWithSkip(link, skipRanges)
589 }
590
591 r.w.WriteString("</a>")
592}
593
594func (r *Html) CodeSpan(text []byte) {
595 r.w.WriteString("<code>")
596 r.attrEscape(text)
597 r.w.WriteString("</code>")
598}
599
600func (r *Html) DoubleEmphasis(text []byte) {
601 r.w.WriteString("<strong>")
602 r.w.Write(text)
603 r.w.WriteString("</strong>")
604}
605
606func (r *Html) Emphasis(text []byte) {
607 if len(text) == 0 {
608 return
609 }
610 r.w.WriteString("<em>")
611 r.w.Write(text)
612 r.w.WriteString("</em>")
613}
614
615func (r *Html) maybeWriteAbsolutePrefix(link []byte) {
616 if r.parameters.AbsolutePrefix != "" && isRelativeLink(link) && link[0] != '.' {
617 r.w.WriteString(r.parameters.AbsolutePrefix)
618 if link[0] != '/' {
619 r.w.WriteByte('/')
620 }
621 }
622}
623
624func (r *Html) Image(link []byte, title []byte, alt []byte) {
625 if r.flags&SkipImages != 0 {
626 return
627 }
628
629 r.w.WriteString("<img src=\"")
630 r.maybeWriteAbsolutePrefix(link)
631 r.attrEscape(link)
632 r.w.WriteString("\" alt=\"")
633 if len(alt) > 0 {
634 r.attrEscape(alt)
635 }
636 if len(title) > 0 {
637 r.w.WriteString("\" title=\"")
638 r.attrEscape(title)
639 }
640
641 r.w.WriteByte('"')
642 r.w.WriteString(r.closeTag)
643}
644
645func (r *Html) LineBreak() {
646 r.w.WriteString("<br")
647 r.w.WriteString(r.closeTag)
648 r.w.WriteByte('\n')
649}
650
651func (r *Html) Link(link []byte, title []byte, content []byte) {
652 if r.flags&SkipLinks != 0 {
653 // write the link text out but don't link it, just mark it with typewriter font
654 r.w.WriteString("<tt>")
655 r.attrEscape(content)
656 r.w.WriteString("</tt>")
657 return
658 }
659
660 if r.flags&Safelink != 0 && !isSafeLink(link) {
661 // write the link text out but don't link it, just mark it with typewriter font
662 r.w.WriteString("<tt>")
663 r.attrEscape(content)
664 r.w.WriteString("</tt>")
665 return
666 }
667
668 r.w.WriteString("<a href=\"")
669 r.maybeWriteAbsolutePrefix(link)
670 r.attrEscape(link)
671 if len(title) > 0 {
672 r.w.WriteString("\" title=\"")
673 r.attrEscape(title)
674 }
675 var relAttrs []string
676 if r.flags&NofollowLinks != 0 && !isRelativeLink(link) {
677 relAttrs = append(relAttrs, "nofollow")
678 }
679 if r.flags&NoreferrerLinks != 0 && !isRelativeLink(link) {
680 relAttrs = append(relAttrs, "noreferrer")
681 }
682 if len(relAttrs) > 0 {
683 r.w.WriteString(fmt.Sprintf("\" rel=\"%s", strings.Join(relAttrs, " ")))
684 }
685
686 // blank target only add to external link
687 if r.flags&HrefTargetBlank != 0 && !isRelativeLink(link) {
688 r.w.WriteString("\" target=\"_blank")
689 }
690
691 r.w.WriteString("\">")
692 r.w.Write(content)
693 r.w.WriteString("</a>")
694 return
695}
696
697func (r *Html) RawHtmlTag(text []byte) {
698 if r.flags&SkipHTML != 0 {
699 return
700 }
701 if r.flags&SkipStyle != 0 && isHtmlTag(text, "style") {
702 return
703 }
704 if r.flags&SkipLinks != 0 && isHtmlTag(text, "a") {
705 return
706 }
707 if r.flags&SkipImages != 0 && isHtmlTag(text, "img") {
708 return
709 }
710 r.w.Write(text)
711}
712
713func (r *Html) TripleEmphasis(text []byte) {
714 r.w.WriteString("<strong><em>")
715 r.w.Write(text)
716 r.w.WriteString("</em></strong>")
717}
718
719func (r *Html) StrikeThrough(text []byte) {
720 r.w.WriteString("<del>")
721 r.w.Write(text)
722 r.w.WriteString("</del>")
723}
724
725func (r *Html) FootnoteRef(ref []byte, id int) {
726 slug := slugify(ref)
727 r.w.WriteString(`<sup class="footnote-ref" id="`)
728 r.w.WriteString(`fnref:`)
729 r.w.WriteString(r.parameters.FootnoteAnchorPrefix)
730 r.w.Write(slug)
731 r.w.WriteString(`"><a rel="footnote" href="#`)
732 r.w.WriteString(`fn:`)
733 r.w.WriteString(r.parameters.FootnoteAnchorPrefix)
734 r.w.Write(slug)
735 r.w.WriteString(`">`)
736 r.w.WriteString(strconv.Itoa(id))
737 r.w.WriteString(`</a></sup>`)
738}
739
740func (r *Html) Entity(entity []byte) {
741 r.w.Write(entity)
742}
743
744func (r *Html) NormalText(text []byte) {
745 if r.flags&UseSmartypants != 0 {
746 r.Smartypants(text)
747 } else {
748 r.attrEscape(text)
749 }
750}
751
752func (r *Html) Smartypants2(text []byte) []byte {
753 smrt := smartypantsData{false, false}
754 var buff bytes.Buffer
755 // first do normal entity escaping
756 text = attrEscape2(text)
757 mark := 0
758 for i := 0; i < len(text); i++ {
759 if action := r.smartypants[text[i]]; action != nil {
760 if i > mark {
761 buff.Write(text[mark:i])
762 }
763 previousChar := byte(0)
764 if i > 0 {
765 previousChar = text[i-1]
766 }
767 var tmp bytes.Buffer
768 i += action(&tmp, &smrt, previousChar, text[i:])
769 buff.Write(tmp.Bytes())
770 mark = i + 1
771 }
772 }
773 if mark < len(text) {
774 buff.Write(text[mark:])
775 }
776 return buff.Bytes()
777}
778
779func (r *Html) Smartypants(text []byte) {
780 smrt := smartypantsData{false, false}
781
782 // first do normal entity escaping
783 text = r.CaptureWrites(func() {
784 r.attrEscape(text)
785 })
786
787 mark := 0
788 for i := 0; i < len(text); i++ {
789 if action := r.smartypants[text[i]]; action != nil {
790 if i > mark {
791 r.w.Write(text[mark:i])
792 }
793
794 previousChar := byte(0)
795 if i > 0 {
796 previousChar = text[i-1]
797 }
798 var tmp bytes.Buffer
799 i += action(&tmp, &smrt, previousChar, text[i:])
800 r.w.Write(tmp.Bytes())
801 mark = i + 1
802 }
803 }
804
805 if mark < len(text) {
806 r.w.Write(text[mark:])
807 }
808}
809
810func (r *Html) DocumentHeader() {
811 if r.flags&CompletePage == 0 {
812 return
813 }
814
815 ending := ""
816 if r.flags&UseXHTML != 0 {
817 r.w.WriteString("<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" ")
818 r.w.WriteString("\"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">\n")
819 r.w.WriteString("<html xmlns=\"http://www.w3.org/1999/xhtml\">\n")
820 ending = " /"
821 } else {
822 r.w.WriteString("<!DOCTYPE html>\n")
823 r.w.WriteString("<html>\n")
824 }
825 r.w.WriteString("<head>\n")
826 r.w.WriteString(" <title>")
827 r.NormalText([]byte(r.title))
828 r.w.WriteString("</title>\n")
829 r.w.WriteString(" <meta name=\"GENERATOR\" content=\"Blackfriday Markdown Processor v")
830 r.w.WriteString(VERSION)
831 r.w.WriteString("\"")
832 r.w.WriteString(ending)
833 r.w.WriteString(">\n")
834 r.w.WriteString(" <meta charset=\"utf-8\"")
835 r.w.WriteString(ending)
836 r.w.WriteString(">\n")
837 if r.css != "" {
838 r.w.WriteString(" <link rel=\"stylesheet\" type=\"text/css\" href=\"")
839 r.attrEscape([]byte(r.css))
840 r.w.WriteString("\"")
841 r.w.WriteString(ending)
842 r.w.WriteString(">\n")
843 }
844 r.w.WriteString("</head>\n")
845 r.w.WriteString("<body>\n")
846
847 r.tocMarker = r.w.output.Len() // XXX
848}
849
850func (r *Html) DocumentFooter() {
851 // finalize and insert the table of contents
852 if r.flags&Toc != 0 {
853 r.TocFinalize()
854
855 // now we have to insert the table of contents into the document
856 var temp bytes.Buffer
857
858 // start by making a copy of everything after the document header
859 temp.Write(r.w.output.Bytes()[r.tocMarker:])
860
861 // now clear the copied material from the main output buffer
862 r.w.output.Truncate(r.tocMarker)
863
864 // corner case spacing issue
865 if r.flags&CompletePage != 0 {
866 r.w.WriteByte('\n')
867 }
868
869 // insert the table of contents
870 r.w.WriteString("<nav>\n")
871 r.w.Write(r.toc.Bytes())
872 r.w.WriteString("</nav>\n")
873
874 // corner case spacing issue
875 if r.flags&CompletePage == 0 && r.flags&OmitContents == 0 {
876 r.w.WriteByte('\n')
877 }
878
879 // write out everything that came after it
880 if r.flags&OmitContents == 0 {
881 r.w.Write(temp.Bytes())
882 }
883 }
884
885 if r.flags&CompletePage != 0 {
886 r.w.WriteString("\n</body>\n")
887 r.w.WriteString("</html>\n")
888 }
889
890}
891
892func (r *Html) TocHeaderWithAnchor(text []byte, level int, anchor string) {
893 for level > r.currentLevel {
894 switch {
895 case bytes.HasSuffix(r.toc.Bytes(), []byte("</li>\n")):
896 // this sublist can nest underneath a header
897 size := r.toc.Len()
898 r.toc.Truncate(size - len("</li>\n"))
899
900 case r.currentLevel > 0:
901 r.toc.WriteString("<li>")
902 }
903 if r.toc.Len() > 0 {
904 r.toc.WriteByte('\n')
905 }
906 r.toc.WriteString("<ul>\n")
907 r.currentLevel++
908 }
909
910 for level < r.currentLevel {
911 r.toc.WriteString("</ul>")
912 if r.currentLevel > 1 {
913 r.toc.WriteString("</li>\n")
914 }
915 r.currentLevel--
916 }
917
918 r.toc.WriteString("<li><a href=\"#")
919 if anchor != "" {
920 r.toc.WriteString(anchor)
921 } else {
922 r.toc.WriteString("toc_")
923 r.toc.WriteString(strconv.Itoa(r.headerCount))
924 }
925 r.toc.WriteString("\">")
926 r.headerCount++
927
928 r.toc.Write(text)
929
930 r.toc.WriteString("</a></li>\n")
931}
932
933func (r *Html) TocHeader(text []byte, level int) {
934 r.TocHeaderWithAnchor(text, level, "")
935}
936
937func (r *Html) TocFinalize() {
938 for r.currentLevel > 1 {
939 r.toc.WriteString("</ul></li>\n")
940 r.currentLevel--
941 }
942
943 if r.currentLevel > 0 {
944 r.toc.WriteString("</ul>\n")
945 }
946}
947
948func isHtmlTag(tag []byte, tagname string) bool {
949 found, _ := findHtmlTagPos(tag, tagname)
950 return found
951}
952
953// Look for a character, but ignore it when it's in any kind of quotes, it
954// might be JavaScript
955func skipUntilCharIgnoreQuotes(html []byte, start int, char byte) int {
956 inSingleQuote := false
957 inDoubleQuote := false
958 inGraveQuote := false
959 i := start
960 for i < len(html) {
961 switch {
962 case html[i] == char && !inSingleQuote && !inDoubleQuote && !inGraveQuote:
963 return i
964 case html[i] == '\'':
965 inSingleQuote = !inSingleQuote
966 case html[i] == '"':
967 inDoubleQuote = !inDoubleQuote
968 case html[i] == '`':
969 inGraveQuote = !inGraveQuote
970 }
971 i++
972 }
973 return start
974}
975
976func findHtmlTagPos(tag []byte, tagname string) (bool, int) {
977 i := 0
978 if i < len(tag) && tag[0] != '<' {
979 return false, -1
980 }
981 i++
982 i = skipSpace(tag, i)
983
984 if i < len(tag) && tag[i] == '/' {
985 i++
986 }
987
988 i = skipSpace(tag, i)
989 j := 0
990 for ; i < len(tag); i, j = i+1, j+1 {
991 if j >= len(tagname) {
992 break
993 }
994
995 if strings.ToLower(string(tag[i]))[0] != tagname[j] {
996 return false, -1
997 }
998 }
999
1000 if i == len(tag) {
1001 return false, -1
1002 }
1003
1004 rightAngle := skipUntilCharIgnoreQuotes(tag, i, '>')
1005 if rightAngle > i {
1006 return true, rightAngle
1007 }
1008
1009 return false, -1
1010}
1011
1012func skipUntilChar(text []byte, start int, char byte) int {
1013 i := start
1014 for i < len(text) && text[i] != char {
1015 i++
1016 }
1017 return i
1018}
1019
1020func skipSpace(tag []byte, i int) int {
1021 for i < len(tag) && isspace(tag[i]) {
1022 i++
1023 }
1024 return i
1025}
1026
1027func skipChar(data []byte, start int, char byte) int {
1028 i := start
1029 for i < len(data) && data[i] == char {
1030 i++
1031 }
1032 return i
1033}
1034
1035func isRelativeLink(link []byte) (yes bool) {
1036 // a tag begin with '#'
1037 if link[0] == '#' {
1038 return true
1039 }
1040
1041 // link begin with '/' but not '//', the second maybe a protocol relative link
1042 if len(link) >= 2 && link[0] == '/' && link[1] != '/' {
1043 return true
1044 }
1045
1046 // only the root '/'
1047 if len(link) == 1 && link[0] == '/' {
1048 return true
1049 }
1050
1051 // current directory : begin with "./"
1052 if bytes.HasPrefix(link, []byte("./")) {
1053 return true
1054 }
1055
1056 // parent directory : begin with "../"
1057 if bytes.HasPrefix(link, []byte("../")) {
1058 return true
1059 }
1060
1061 return false
1062}
1063
1064func (r *Html) ensureUniqueHeaderID(id string) string {
1065 for count, found := r.headerIDs[id]; found; count, found = r.headerIDs[id] {
1066 tmp := fmt.Sprintf("%s-%d", id, count+1)
1067
1068 if _, tmpFound := r.headerIDs[tmp]; !tmpFound {
1069 r.headerIDs[id] = count + 1
1070 id = tmp
1071 } else {
1072 id = id + "-1"
1073 }
1074 }
1075
1076 if _, found := r.headerIDs[id]; !found {
1077 r.headerIDs[id] = 0
1078 }
1079
1080 return id
1081}
1082
1083func (r *Html) addAbsPrefix(link []byte) []byte {
1084 if r.parameters.AbsolutePrefix != "" && isRelativeLink(link) && link[0] != '.' {
1085 newDest := r.parameters.AbsolutePrefix
1086 if link[0] != '/' {
1087 newDest += "/"
1088 }
1089 newDest += string(link)
1090 return []byte(newDest)
1091 }
1092 return link
1093}
1094
1095func appendLinkAttrs(attrs []string, flags HtmlFlags, link []byte) []string {
1096 if isRelativeLink(link) {
1097 return attrs
1098 }
1099 val := []string{}
1100 if flags&NofollowLinks != 0 {
1101 val = append(val, "nofollow")
1102 }
1103 if flags&NoreferrerLinks != 0 {
1104 val = append(val, "noreferrer")
1105 }
1106 if flags&HrefTargetBlank != 0 {
1107 attrs = append(attrs, "target=\"_blank\"")
1108 }
1109 if len(val) == 0 {
1110 return attrs
1111 }
1112 attr := fmt.Sprintf("rel=%q", strings.Join(val, " "))
1113 return append(attrs, attr)
1114}
1115
1116func isMailto(link []byte) bool {
1117 return bytes.HasPrefix(link, []byte("mailto:"))
1118}
1119
1120func isSmartypantable(node *Node) bool {
1121 pt := node.Parent.Type
1122 return pt != Link && pt != CodeBlock && pt != Code
1123}
1124
1125func appendLanguageAttr(attrs []string, info []byte) []string {
1126 infoWords := bytes.Split(info, []byte("\t "))
1127 if len(infoWords) > 0 && len(infoWords[0]) > 0 {
1128 attrs = append(attrs, fmt.Sprintf("class=\"language-%s\"", infoWords[0]))
1129 }
1130 return attrs
1131}
1132
1133func tag(name string, attrs []string, selfClosing bool) []byte {
1134 result := "<" + name
1135 if attrs != nil && len(attrs) > 0 {
1136 result += " " + strings.Join(attrs, " ")
1137 }
1138 if selfClosing {
1139 result += " /"
1140 }
1141 return []byte(result + ">")
1142}
1143
1144func footnoteRef(prefix string, node *Node) []byte {
1145 urlFrag := prefix + string(slugify(node.Destination))
1146 anchor := fmt.Sprintf(`<a rel="footnote" href="#fn:%s">%d</a>`, urlFrag, node.NoteID)
1147 return []byte(fmt.Sprintf(`<sup class="footnote-ref" id="fnref:%s">%s</sup>`, urlFrag, anchor))
1148}
1149
1150func footnoteItem(prefix string, slug []byte) []byte {
1151 return []byte(fmt.Sprintf(`<li id="fn:%s%s">`, prefix, slug))
1152}
1153
1154func footnoteReturnLink(prefix, returnLink string, slug []byte) []byte {
1155 const format = ` <a class="footnote-return" href="#fnref:%s%s">%s</a>`
1156 return []byte(fmt.Sprintf(format, prefix, slug, returnLink))
1157}
1158
1159func itemOpenCR(node *Node) bool {
1160 if node.Prev == nil {
1161 return false
1162 }
1163 ld := node.Parent.ListData
1164 return !ld.Tight && ld.Flags&ListTypeDefinition == 0
1165}
1166
1167func skipParagraphTags(node *Node) bool {
1168 grandparent := node.Parent.Parent
1169 if grandparent == nil || grandparent.ListData == nil {
1170 return false
1171 }
1172 tightOrTerm := grandparent.ListData.Tight || node.Parent.ListData.Flags&ListTypeTerm != 0
1173 return grandparent.Type == List && tightOrTerm
1174}
1175
1176func cellAlignment(align int) string {
1177 switch align {
1178 case TableAlignmentLeft:
1179 return "left"
1180 case TableAlignmentRight:
1181 return "right"
1182 case TableAlignmentCenter:
1183 return "center"
1184 default:
1185 return ""
1186 }
1187}
1188
1189func esc(text []byte, preserveEntities bool) []byte {
1190 return attrEscape2(text)
1191}
1192
1193func escCode(text []byte, preserveEntities bool) []byte {
1194 e1 := []byte(html.EscapeString(string(text)))
1195 e2 := bytes.Replace(e1, []byte("""), []byte("""), -1)
1196 return bytes.Replace(e2, []byte("'"), []byte{'\''}, -1)
1197}
1198
1199func (r *Html) out(text []byte) {
1200 if r.disableTags > 0 {
1201 r.renderBuffer.Write(reHtmlTag.ReplaceAll(text, []byte{}))
1202 } else {
1203 r.renderBuffer.Write(text)
1204 }
1205 r.lastOutputLen = len(text)
1206}
1207
1208func (r *Html) cr() {
1209 if r.lastOutputLen > 0 {
1210 r.out([]byte{'\n'})
1211 }
1212}
1213
1214func (r *Html) Render(ast *Node) []byte {
1215 //println("render_Blackfriday")
1216 //dump(ast)
1217 ForEachNode(ast, func(node *Node, entering bool) {
1218 attrs := []string{}
1219 switch node.Type {
1220 case Text:
1221 if r.flags&UseSmartypants != 0 && isSmartypantable(node) {
1222 // TODO: don't do that in renderer, do that at parse time!
1223 r.out(r.Smartypants2(node.Literal))
1224 } else {
1225 r.out(esc(node.Literal, false))
1226 }
1227 break
1228 case Softbreak:
1229 r.out([]byte("\n"))
1230 // TODO: make it configurable via out(renderer.softbreak)
1231 case Hardbreak:
1232 r.out(tag("br", nil, true))
1233 r.cr()
1234 case Emph:
1235 if entering {
1236 r.out(tag("em", nil, false))
1237 } else {
1238 r.out(tag("/em", nil, false))
1239 }
1240 break
1241 case Strong:
1242 if entering {
1243 r.out(tag("strong", nil, false))
1244 } else {
1245 r.out(tag("/strong", nil, false))
1246 }
1247 break
1248 case Del:
1249 if entering {
1250 r.out(tag("del", nil, false))
1251 } else {
1252 r.out(tag("/del", nil, false))
1253 }
1254 case HtmlSpan:
1255 //if options.safe {
1256 // out("<!-- raw HTML omitted -->")
1257 //} else {
1258 r.out(node.Literal)
1259 //}
1260 case Link:
1261 // mark it but don't link it if it is not a safe link: no smartypants
1262 dest := node.LinkData.Destination
1263 if r.flags&Safelink != 0 && !isSafeLink(dest) && !isMailto(dest) {
1264 if entering {
1265 r.out(tag("tt", nil, false))
1266 } else {
1267 r.out(tag("/tt", nil, false))
1268 }
1269 } else {
1270 if entering {
1271 dest = r.addAbsPrefix(dest)
1272 //if (!(options.safe && potentiallyUnsafe(node.destination))) {
1273 attrs = append(attrs, fmt.Sprintf("href=%q", esc(dest, true)))
1274 //}
1275 if node.NoteID != 0 {
1276 r.out(footnoteRef(r.parameters.FootnoteAnchorPrefix, node))
1277 break
1278 }
1279 attrs = appendLinkAttrs(attrs, r.flags, dest)
1280 if len(node.LinkData.Title) > 0 {
1281 attrs = append(attrs, fmt.Sprintf("title=%q", esc(node.LinkData.Title, true)))
1282 }
1283 r.out(tag("a", attrs, false))
1284 } else {
1285 if node.NoteID != 0 {
1286 break
1287 }
1288 r.out(tag("/a", nil, false))
1289 }
1290 }
1291 case Image:
1292 if entering {
1293 dest := node.LinkData.Destination
1294 dest = r.addAbsPrefix(dest)
1295 if r.disableTags == 0 {
1296 //if options.safe && potentiallyUnsafe(dest) {
1297 //out(`<img src="" alt="`)
1298 //} else {
1299 r.out([]byte(fmt.Sprintf(`<img src="%s" alt="`, esc(dest, true))))
1300 //}
1301 }
1302 r.disableTags++
1303 } else {
1304 r.disableTags--
1305 if r.disableTags == 0 {
1306 if node.LinkData.Title != nil {
1307 r.out([]byte(`" title="`))
1308 r.out(esc(node.LinkData.Title, true))
1309 }
1310 r.out([]byte(`" />`))
1311 }
1312 }
1313 case Code:
1314 r.out(tag("code", nil, false))
1315 r.out(escCode(node.Literal, false))
1316 r.out(tag("/code", nil, false))
1317 case Document:
1318 break
1319 case Paragraph:
1320 if skipParagraphTags(node) {
1321 break
1322 }
1323 if entering {
1324 // TODO: untangle this clusterfuck about when the newlines need
1325 // to be added and when not.
1326 if node.Prev != nil {
1327 t := node.Prev.Type
1328 if t == HtmlBlock || t == List || t == Paragraph || t == Header || t == CodeBlock || t == BlockQuote || t == HorizontalRule {
1329 r.cr()
1330 }
1331 }
1332 if node.Parent.Type == BlockQuote && node.Prev == nil {
1333 r.cr()
1334 }
1335 r.out(tag("p", attrs, false))
1336 } else {
1337 r.out(tag("/p", attrs, false))
1338 if !(node.Parent.Type == Item && node.Next == nil) {
1339 r.cr()
1340 }
1341 }
1342 break
1343 case BlockQuote:
1344 if entering {
1345 r.cr()
1346 r.out(tag("blockquote", attrs, false))
1347 } else {
1348 r.out(tag("/blockquote", nil, false))
1349 r.cr()
1350 }
1351 break
1352 case HtmlBlock:
1353 r.cr()
1354 r.out(node.Literal)
1355 r.cr()
1356 case Header:
1357 tagname := fmt.Sprintf("h%d", node.Level)
1358 if entering {
1359 if node.IsTitleblock {
1360 attrs = append(attrs, `class="title"`)
1361 }
1362 if node.HeaderID != "" {
1363 id := r.ensureUniqueHeaderID(node.HeaderID)
1364 if r.parameters.HeaderIDPrefix != "" {
1365 id = r.parameters.HeaderIDPrefix + id
1366 }
1367 if r.parameters.HeaderIDSuffix != "" {
1368 id = id + r.parameters.HeaderIDSuffix
1369 }
1370 attrs = append(attrs, fmt.Sprintf(`id="%s"`, id))
1371 }
1372 r.cr()
1373 r.out(tag(tagname, attrs, false))
1374 } else {
1375 r.out(tag("/"+tagname, nil, false))
1376 if !(node.Parent.Type == Item && node.Next == nil) {
1377 r.cr()
1378 }
1379 }
1380 break
1381 case HorizontalRule:
1382 r.cr()
1383 r.out(tag("hr", attrs, r.flags&UseXHTML != 0))
1384 r.cr()
1385 break
1386 case List:
1387 tagName := "ul"
1388 if node.ListData.Flags&ListTypeOrdered != 0 {
1389 tagName = "ol"
1390 }
1391 if node.ListData.Flags&ListTypeDefinition != 0 {
1392 tagName = "dl"
1393 }
1394 if entering {
1395 // var start = node.listStart;
1396 // if (start !== null && start !== 1) {
1397 // attrs.push(['start', start.toString()]);
1398 // }
1399 r.cr()
1400 if node.Parent.Type == Item && node.Parent.Parent.ListData.Tight {
1401 r.cr()
1402 }
1403 r.out(tag(tagName, attrs, false))
1404 r.cr()
1405 } else {
1406 r.out(tag("/"+tagName, nil, false))
1407 //cr()
1408 //if node.parent.Type != Item {
1409 // cr()
1410 //}
1411 if node.Parent.Type == Item && node.Next != nil {
1412 r.cr()
1413 }
1414 if node.Parent.Type == Document || node.Parent.Type == BlockQuote {
1415 r.cr()
1416 }
1417 }
1418 case Item:
1419 tagName := "li"
1420 if node.ListData.Flags&ListTypeDefinition != 0 {
1421 tagName = "dd"
1422 }
1423 if node.ListData.Flags&ListTypeTerm != 0 {
1424 tagName = "dt"
1425 }
1426 if entering {
1427 if itemOpenCR(node) {
1428 r.cr()
1429 }
1430 if node.ListData.RefLink != nil {
1431 slug := slugify(node.ListData.RefLink)
1432 r.out(footnoteItem(r.parameters.FootnoteAnchorPrefix, slug))
1433 break
1434 }
1435 r.out(tag(tagName, nil, false))
1436 } else {
1437 if node.ListData.RefLink != nil {
1438 slug := slugify(node.ListData.RefLink)
1439 if r.flags&FootnoteReturnLinks != 0 {
1440 r.out(footnoteReturnLink(r.parameters.FootnoteAnchorPrefix, r.parameters.FootnoteReturnLinkContents, slug))
1441 }
1442 }
1443 r.out(tag("/"+tagName, nil, false))
1444 r.cr()
1445 }
1446 case CodeBlock:
1447 attrs = appendLanguageAttr(attrs, node.Info)
1448 r.cr()
1449 r.out(tag("pre", nil, false))
1450 r.out(tag("code", attrs, false))
1451 r.out(escCode(node.Literal, false))
1452 r.out(tag("/code", nil, false))
1453 r.out(tag("/pre", nil, false))
1454 if node.Parent.Type != Item {
1455 r.cr()
1456 }
1457 case Table:
1458 if entering {
1459 r.cr()
1460 r.out(tag("table", nil, false))
1461 } else {
1462 r.out(tag("/table", nil, false))
1463 r.cr()
1464 }
1465 case TableCell:
1466 tagName := "td"
1467 if node.IsHeader {
1468 tagName = "th"
1469 }
1470 if entering {
1471 align := cellAlignment(node.Align)
1472 if align != "" {
1473 attrs = append(attrs, fmt.Sprintf(`align="%s"`, align))
1474 }
1475 if node.Prev == nil {
1476 r.cr()
1477 }
1478 r.out(tag(tagName, attrs, false))
1479 } else {
1480 r.out(tag("/"+tagName, nil, false))
1481 r.cr()
1482 }
1483 case TableHead:
1484 if entering {
1485 r.cr()
1486 r.out(tag("thead", nil, false))
1487 } else {
1488 r.out(tag("/thead", nil, false))
1489 r.cr()
1490 }
1491 case TableBody:
1492 if entering {
1493 r.cr()
1494 r.out(tag("tbody", nil, false))
1495 // XXX: this is to adhere to a rather silly test. Should fix test.
1496 if node.FirstChild == nil {
1497 r.cr()
1498 }
1499 } else {
1500 r.out(tag("/tbody", nil, false))
1501 r.cr()
1502 }
1503 case TableRow:
1504 if entering {
1505 r.cr()
1506 r.out(tag("tr", nil, false))
1507 } else {
1508 r.out(tag("/tr", nil, false))
1509 r.cr()
1510 }
1511 default:
1512 panic("Unknown node type " + node.Type.String())
1513 }
1514 })
1515 return r.renderBuffer.Bytes()
1516}