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 "io"
23 "regexp"
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 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
44 TagName = "[A-Za-z][A-Za-z0-9-]*"
45 AttributeName = "[a-zA-Z_:][a-zA-Z0-9:._-]*"
46 UnquotedValue = "[^\"'=<>`\\x00-\\x20]+"
47 SingleQuotedValue = "'[^']*'"
48 DoubleQuotedValue = "\"[^\"]*\""
49 AttributeValue = "(?:" + UnquotedValue + "|" + SingleQuotedValue + "|" + DoubleQuotedValue + ")"
50 AttributeValueSpec = "(?:" + "\\s*=" + "\\s*" + AttributeValue + ")"
51 Attribute = "(?:" + "\\s+" + AttributeName + AttributeValueSpec + "?)"
52 OpenTag = "<" + TagName + Attribute + "*" + "\\s*/?>"
53 CloseTag = "</" + TagName + "\\s*[>]"
54 HTMLComment = "<!---->|<!--(?:-?[^>-])(?:-?[^-])*-->"
55 ProcessingInstruction = "[<][?].*?[?][>]"
56 Declaration = "<![A-Z]+" + "\\s+[^>]*>"
57 CDATA = "<!\\[CDATA\\[[\\s\\S]*?\\]\\]>"
58 HTMLTag = "(?:" + OpenTag + "|" + CloseTag + "|" + HTMLComment + "|" +
59 ProcessingInstruction + "|" + Declaration + "|" + CDATA + ")"
60)
61
62var (
63 htmlTagRe = regexp.MustCompile("(?i)^" + HTMLTag)
64)
65
66type HTMLRendererParameters struct {
67 // Prepend this text to each relative URL.
68 AbsolutePrefix string
69 // Add this text to each footnote anchor, to ensure uniqueness.
70 FootnoteAnchorPrefix string
71 // Show this text inside the <a> tag for a footnote return link, if the
72 // HTML_FOOTNOTE_RETURN_LINKS flag is enabled. If blank, the string
73 // <sup>[return]</sup> is used.
74 FootnoteReturnLinkContents string
75 // If set, add this text to the front of each Header ID, to ensure
76 // uniqueness.
77 HeaderIDPrefix string
78 // If set, add this text to the back of each Header ID, to ensure uniqueness.
79 HeaderIDSuffix string
80
81 Title string // Document title (used if CompletePage is set)
82 CSS string // Optional CSS file URL (used if CompletePage is set)
83
84 Flags HTMLFlags // Flags allow customizing this renderer's behavior
85 Extensions Extensions // Extensions give Smartypants and HTML renderer access to Blackfriday's global extensions
86}
87
88// HTMLRenderer is a type that implements the Renderer interface for HTML output.
89//
90// Do not create this directly, instead use the NewHTMLRenderer function.
91type HTMLRenderer struct {
92 HTMLRendererParameters
93
94 closeTag string // how to end singleton tags: either " />" or ">"
95
96 // Track header IDs to prevent ID collision in a single generation.
97 headerIDs map[string]int
98
99 lastOutputLen int
100 disableTags int
101}
102
103const (
104 xhtmlClose = " />"
105 htmlClose = ">"
106)
107
108// NewHTMLRenderer creates and configures an HTMLRenderer object, which
109// satisfies the Renderer interface.
110func NewHTMLRenderer(params HTMLRendererParameters) Renderer {
111 // configure the rendering engine
112 closeTag := htmlClose
113 if params.Flags&UseXHTML != 0 {
114 closeTag = xhtmlClose
115 }
116
117 if params.FootnoteReturnLinkContents == "" {
118 params.FootnoteReturnLinkContents = `<sup>[return]</sup>`
119 }
120
121 return &HTMLRenderer{
122 HTMLRendererParameters: params,
123
124 closeTag: closeTag,
125 headerIDs: make(map[string]int),
126 }
127}
128
129func isHtmlTag(tag []byte, tagname string) bool {
130 found, _ := findHtmlTagPos(tag, tagname)
131 return found
132}
133
134// Look for a character, but ignore it when it's in any kind of quotes, it
135// might be JavaScript
136func skipUntilCharIgnoreQuotes(html []byte, start int, char byte) int {
137 inSingleQuote := false
138 inDoubleQuote := false
139 inGraveQuote := false
140 i := start
141 for i < len(html) {
142 switch {
143 case html[i] == char && !inSingleQuote && !inDoubleQuote && !inGraveQuote:
144 return i
145 case html[i] == '\'':
146 inSingleQuote = !inSingleQuote
147 case html[i] == '"':
148 inDoubleQuote = !inDoubleQuote
149 case html[i] == '`':
150 inGraveQuote = !inGraveQuote
151 }
152 i++
153 }
154 return start
155}
156
157func findHtmlTagPos(tag []byte, tagname string) (bool, int) {
158 i := 0
159 if i < len(tag) && tag[0] != '<' {
160 return false, -1
161 }
162 i++
163 i = skipSpace(tag, i)
164
165 if i < len(tag) && tag[i] == '/' {
166 i++
167 }
168
169 i = skipSpace(tag, i)
170 j := 0
171 for ; i < len(tag); i, j = i+1, j+1 {
172 if j >= len(tagname) {
173 break
174 }
175
176 if strings.ToLower(string(tag[i]))[0] != tagname[j] {
177 return false, -1
178 }
179 }
180
181 if i == len(tag) {
182 return false, -1
183 }
184
185 rightAngle := skipUntilCharIgnoreQuotes(tag, i, '>')
186 if rightAngle >= i {
187 return true, rightAngle
188 }
189
190 return false, -1
191}
192
193func skipSpace(tag []byte, i int) int {
194 for i < len(tag) && isspace(tag[i]) {
195 i++
196 }
197 return i
198}
199
200func isRelativeLink(link []byte) (yes bool) {
201 // a tag begin with '#'
202 if link[0] == '#' {
203 return true
204 }
205
206 // link begin with '/' but not '//', the second maybe a protocol relative link
207 if len(link) >= 2 && link[0] == '/' && link[1] != '/' {
208 return true
209 }
210
211 // only the root '/'
212 if len(link) == 1 && link[0] == '/' {
213 return true
214 }
215
216 // current directory : begin with "./"
217 if bytes.HasPrefix(link, []byte("./")) {
218 return true
219 }
220
221 // parent directory : begin with "../"
222 if bytes.HasPrefix(link, []byte("../")) {
223 return true
224 }
225
226 return false
227}
228
229func (r *HTMLRenderer) ensureUniqueHeaderID(id string) string {
230 for count, found := r.headerIDs[id]; found; count, found = r.headerIDs[id] {
231 tmp := fmt.Sprintf("%s-%d", id, count+1)
232
233 if _, tmpFound := r.headerIDs[tmp]; !tmpFound {
234 r.headerIDs[id] = count + 1
235 id = tmp
236 } else {
237 id = id + "-1"
238 }
239 }
240
241 if _, found := r.headerIDs[id]; !found {
242 r.headerIDs[id] = 0
243 }
244
245 return id
246}
247
248func (r *HTMLRenderer) addAbsPrefix(link []byte) []byte {
249 if r.AbsolutePrefix != "" && isRelativeLink(link) && link[0] != '.' {
250 newDest := r.AbsolutePrefix
251 if link[0] != '/' {
252 newDest += "/"
253 }
254 newDest += string(link)
255 return []byte(newDest)
256 }
257 return link
258}
259
260func appendLinkAttrs(attrs []string, flags HTMLFlags, link []byte) []string {
261 if isRelativeLink(link) {
262 return attrs
263 }
264 val := []string{}
265 if flags&NofollowLinks != 0 {
266 val = append(val, "nofollow")
267 }
268 if flags&NoreferrerLinks != 0 {
269 val = append(val, "noreferrer")
270 }
271 if flags&HrefTargetBlank != 0 {
272 attrs = append(attrs, "target=\"_blank\"")
273 }
274 if len(val) == 0 {
275 return attrs
276 }
277 attr := fmt.Sprintf("rel=%q", strings.Join(val, " "))
278 return append(attrs, attr)
279}
280
281func isMailto(link []byte) bool {
282 return bytes.HasPrefix(link, []byte("mailto:"))
283}
284
285func needSkipLink(flags HTMLFlags, dest []byte) bool {
286 if flags&SkipLinks != 0 {
287 return true
288 }
289 return flags&Safelink != 0 && !isSafeLink(dest) && !isMailto(dest)
290}
291
292func isSmartypantable(node *Node) bool {
293 pt := node.Parent.Type
294 return pt != Link && pt != CodeBlock && pt != Code
295}
296
297func appendLanguageAttr(attrs []string, info []byte) []string {
298 infoWords := bytes.Split(info, []byte("\t "))
299 if len(infoWords) > 0 && len(infoWords[0]) > 0 {
300 attrs = append(attrs, fmt.Sprintf("class=\"language-%s\"", infoWords[0]))
301 }
302 return attrs
303}
304
305func tag(name string, attrs []string, selfClosing bool) []byte {
306 result := "<" + name
307 if attrs != nil && len(attrs) > 0 {
308 result += " " + strings.Join(attrs, " ")
309 }
310 if selfClosing {
311 result += " /"
312 }
313 return []byte(result + ">")
314}
315
316func footnoteRef(prefix string, node *Node) []byte {
317 urlFrag := prefix + string(slugify(node.Destination))
318 anchor := fmt.Sprintf(`<a rel="footnote" href="#fn:%s">%d</a>`, urlFrag, node.NoteID)
319 return []byte(fmt.Sprintf(`<sup class="footnote-ref" id="fnref:%s">%s</sup>`, urlFrag, anchor))
320}
321
322func footnoteItem(prefix string, slug []byte) []byte {
323 return []byte(fmt.Sprintf(`<li id="fn:%s%s">`, prefix, slug))
324}
325
326func footnoteReturnLink(prefix, returnLink string, slug []byte) []byte {
327 const format = ` <a class="footnote-return" href="#fnref:%s%s">%s</a>`
328 return []byte(fmt.Sprintf(format, prefix, slug, returnLink))
329}
330
331func itemOpenCR(node *Node) bool {
332 if node.Prev == nil {
333 return false
334 }
335 ld := node.Parent.ListData
336 return !ld.Tight && ld.ListFlags&ListTypeDefinition == 0
337}
338
339func skipParagraphTags(node *Node) bool {
340 grandparent := node.Parent.Parent
341 if grandparent == nil || grandparent.Type != List {
342 return false
343 }
344 tightOrTerm := grandparent.Tight || node.Parent.ListFlags&ListTypeTerm != 0
345 return grandparent.Type == List && tightOrTerm
346}
347
348func cellAlignment(align CellAlignFlags) string {
349 switch align {
350 case TableAlignmentLeft:
351 return "left"
352 case TableAlignmentRight:
353 return "right"
354 case TableAlignmentCenter:
355 return "center"
356 default:
357 return ""
358 }
359}
360
361func esc(text []byte) []byte {
362 unesc := []byte(html.UnescapeString(string(text)))
363 return escCode(unesc)
364}
365
366func escCode(text []byte) []byte {
367 e1 := []byte(html.EscapeString(string(text)))
368 e2 := bytes.Replace(e1, []byte("""), []byte("""), -1)
369 return bytes.Replace(e2, []byte("'"), []byte{'\''}, -1)
370}
371
372func (r *HTMLRenderer) out(w io.Writer, text []byte) {
373 if r.disableTags > 0 {
374 w.Write(htmlTagRe.ReplaceAll(text, []byte{}))
375 } else {
376 w.Write(text)
377 }
378 r.lastOutputLen = len(text)
379}
380
381func (r *HTMLRenderer) cr(w io.Writer) {
382 if r.lastOutputLen > 0 {
383 r.out(w, []byte{'\n'})
384 }
385}
386
387func (r *HTMLRenderer) RenderNode(w io.Writer, node *Node, entering bool) WalkStatus {
388 attrs := []string{}
389 switch node.Type {
390 case Text:
391 r.out(w, node.Literal)
392 break
393 case Softbreak:
394 r.out(w, []byte("\n"))
395 // TODO: make it configurable via out(renderer.softbreak)
396 case Hardbreak:
397 r.out(w, tag("br", nil, true))
398 r.cr(w)
399 case Emph:
400 if entering {
401 r.out(w, tag("em", nil, false))
402 } else {
403 r.out(w, tag("/em", nil, false))
404 }
405 break
406 case Strong:
407 if entering {
408 r.out(w, tag("strong", nil, false))
409 } else {
410 r.out(w, tag("/strong", nil, false))
411 }
412 break
413 case Del:
414 if entering {
415 r.out(w, tag("del", nil, false))
416 } else {
417 r.out(w, tag("/del", nil, false))
418 }
419 case HTMLSpan:
420 if r.Flags&SkipHTML != 0 {
421 break
422 }
423 if r.Flags&SkipStyle != 0 && isHtmlTag(node.Literal, "style") {
424 break
425 }
426 //if options.safe {
427 // out(w, "<!-- raw HTML omitted -->")
428 //} else {
429 r.out(w, node.Literal)
430 //}
431 case Link:
432 // mark it but don't link it if it is not a safe link: no smartypants
433 dest := node.LinkData.Destination
434 if needSkipLink(r.Flags, dest) {
435 if entering {
436 r.out(w, tag("tt", nil, false))
437 } else {
438 r.out(w, tag("/tt", nil, false))
439 }
440 } else {
441 if entering {
442 dest = r.addAbsPrefix(dest)
443 //if (!(options.safe && potentiallyUnsafe(node.destination))) {
444 attrs = append(attrs, fmt.Sprintf("href=%q", esc(dest)))
445 //}
446 if node.NoteID != 0 {
447 r.out(w, footnoteRef(r.FootnoteAnchorPrefix, node))
448 break
449 }
450 attrs = appendLinkAttrs(attrs, r.Flags, dest)
451 if len(node.LinkData.Title) > 0 {
452 attrs = append(attrs, fmt.Sprintf("title=%q", esc(node.LinkData.Title)))
453 }
454 r.out(w, tag("a", attrs, false))
455 } else {
456 if node.NoteID != 0 {
457 break
458 }
459 r.out(w, tag("/a", nil, false))
460 }
461 }
462 case Image:
463 if r.Flags&SkipImages != 0 {
464 return SkipChildren
465 }
466 if entering {
467 dest := node.LinkData.Destination
468 dest = r.addAbsPrefix(dest)
469 if r.disableTags == 0 {
470 //if options.safe && potentiallyUnsafe(dest) {
471 //out(w, `<img src="" alt="`)
472 //} else {
473 r.out(w, []byte(fmt.Sprintf(`<img src="%s" alt="`, esc(dest))))
474 //}
475 }
476 r.disableTags++
477 } else {
478 r.disableTags--
479 if r.disableTags == 0 {
480 if node.LinkData.Title != nil {
481 r.out(w, []byte(`" title="`))
482 r.out(w, esc(node.LinkData.Title))
483 }
484 r.out(w, []byte(`" />`))
485 }
486 }
487 case Code:
488 r.out(w, tag("code", nil, false))
489 r.out(w, escCode(node.Literal))
490 r.out(w, tag("/code", nil, false))
491 case Document:
492 break
493 case Paragraph:
494 if skipParagraphTags(node) {
495 break
496 }
497 if entering {
498 // TODO: untangle this clusterfuck about when the newlines need
499 // to be added and when not.
500 if node.Prev != nil {
501 t := node.Prev.Type
502 if t == HTMLBlock || t == List || t == Paragraph || t == Header || t == CodeBlock || t == BlockQuote || t == HorizontalRule {
503 r.cr(w)
504 }
505 }
506 if node.Parent.Type == BlockQuote && node.Prev == nil {
507 r.cr(w)
508 }
509 r.out(w, tag("p", attrs, false))
510 } else {
511 r.out(w, tag("/p", attrs, false))
512 if !(node.Parent.Type == Item && node.Next == nil) {
513 r.cr(w)
514 }
515 }
516 break
517 case BlockQuote:
518 if entering {
519 r.cr(w)
520 r.out(w, tag("blockquote", attrs, false))
521 } else {
522 r.out(w, tag("/blockquote", nil, false))
523 r.cr(w)
524 }
525 break
526 case HTMLBlock:
527 if r.Flags&SkipHTML != 0 {
528 break
529 }
530 r.cr(w)
531 r.out(w, node.Literal)
532 r.cr(w)
533 case Header:
534 tagname := fmt.Sprintf("h%d", node.Level)
535 if entering {
536 if node.IsTitleblock {
537 attrs = append(attrs, `class="title"`)
538 }
539 if node.HeaderID != "" {
540 id := r.ensureUniqueHeaderID(node.HeaderID)
541 if r.HeaderIDPrefix != "" {
542 id = r.HeaderIDPrefix + id
543 }
544 if r.HeaderIDSuffix != "" {
545 id = id + r.HeaderIDSuffix
546 }
547 attrs = append(attrs, fmt.Sprintf(`id="%s"`, id))
548 }
549 r.cr(w)
550 r.out(w, tag(tagname, attrs, false))
551 } else {
552 r.out(w, tag("/"+tagname, nil, false))
553 if !(node.Parent.Type == Item && node.Next == nil) {
554 r.cr(w)
555 }
556 }
557 break
558 case HorizontalRule:
559 r.cr(w)
560 r.out(w, tag("hr", attrs, r.Flags&UseXHTML != 0))
561 r.cr(w)
562 break
563 case List:
564 tagName := "ul"
565 if node.ListFlags&ListTypeOrdered != 0 {
566 tagName = "ol"
567 }
568 if node.ListFlags&ListTypeDefinition != 0 {
569 tagName = "dl"
570 }
571 if entering {
572 // var start = node.listStart;
573 // if (start !== null && start !== 1) {
574 // attrs.push(['start', start.toString()]);
575 // }
576 r.cr(w)
577 if node.Parent.Type == Item && node.Parent.Parent.Tight {
578 r.cr(w)
579 }
580 r.out(w, tag(tagName, attrs, false))
581 r.cr(w)
582 } else {
583 r.out(w, tag("/"+tagName, nil, false))
584 //cr(w)
585 //if node.parent.Type != Item {
586 // cr(w)
587 //}
588 if node.Parent.Type == Item && node.Next != nil {
589 r.cr(w)
590 }
591 if node.Parent.Type == Document || node.Parent.Type == BlockQuote {
592 r.cr(w)
593 }
594 }
595 case Item:
596 tagName := "li"
597 if node.ListFlags&ListTypeDefinition != 0 {
598 tagName = "dd"
599 }
600 if node.ListFlags&ListTypeTerm != 0 {
601 tagName = "dt"
602 }
603 if entering {
604 if itemOpenCR(node) {
605 r.cr(w)
606 }
607 if node.ListData.RefLink != nil {
608 slug := slugify(node.ListData.RefLink)
609 r.out(w, footnoteItem(r.FootnoteAnchorPrefix, slug))
610 break
611 }
612 r.out(w, tag(tagName, nil, false))
613 } else {
614 if node.ListData.RefLink != nil {
615 slug := slugify(node.ListData.RefLink)
616 if r.Flags&FootnoteReturnLinks != 0 {
617 r.out(w, footnoteReturnLink(r.FootnoteAnchorPrefix, r.FootnoteReturnLinkContents, slug))
618 }
619 }
620 r.out(w, tag("/"+tagName, nil, false))
621 r.cr(w)
622 }
623 case CodeBlock:
624 attrs = appendLanguageAttr(attrs, node.Info)
625 r.cr(w)
626 r.out(w, tag("pre", nil, false))
627 r.out(w, tag("code", attrs, false))
628 r.out(w, escCode(node.Literal))
629 r.out(w, tag("/code", nil, false))
630 r.out(w, tag("/pre", nil, false))
631 if node.Parent.Type != Item {
632 r.cr(w)
633 }
634 case Table:
635 if entering {
636 r.cr(w)
637 r.out(w, tag("table", nil, false))
638 } else {
639 r.out(w, tag("/table", nil, false))
640 r.cr(w)
641 }
642 case TableCell:
643 tagName := "td"
644 if node.IsHeader {
645 tagName = "th"
646 }
647 if entering {
648 align := cellAlignment(node.Align)
649 if align != "" {
650 attrs = append(attrs, fmt.Sprintf(`align="%s"`, align))
651 }
652 if node.Prev == nil {
653 r.cr(w)
654 }
655 r.out(w, tag(tagName, attrs, false))
656 } else {
657 r.out(w, tag("/"+tagName, nil, false))
658 r.cr(w)
659 }
660 case TableHead:
661 if entering {
662 r.cr(w)
663 r.out(w, tag("thead", nil, false))
664 } else {
665 r.out(w, tag("/thead", nil, false))
666 r.cr(w)
667 }
668 case TableBody:
669 if entering {
670 r.cr(w)
671 r.out(w, tag("tbody", nil, false))
672 // XXX: this is to adhere to a rather silly test. Should fix test.
673 if node.FirstChild == nil {
674 r.cr(w)
675 }
676 } else {
677 r.out(w, tag("/tbody", nil, false))
678 r.cr(w)
679 }
680 case TableRow:
681 if entering {
682 r.cr(w)
683 r.out(w, tag("tr", nil, false))
684 } else {
685 r.out(w, tag("/tr", nil, false))
686 r.cr(w)
687 }
688 default:
689 panic("Unknown node type " + node.Type.String())
690 }
691 return GoToNext
692}
693
694func (r *HTMLRenderer) writeDocumentHeader(w *bytes.Buffer, sr *SPRenderer) {
695 if r.Flags&CompletePage == 0 {
696 return
697 }
698 ending := ""
699 if r.Flags&UseXHTML != 0 {
700 w.WriteString("<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" ")
701 w.WriteString("\"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">\n")
702 w.WriteString("<html xmlns=\"http://www.w3.org/1999/xhtml\">\n")
703 ending = " /"
704 } else {
705 w.WriteString("<!DOCTYPE html>\n")
706 w.WriteString("<html>\n")
707 }
708 w.WriteString("<head>\n")
709 w.WriteString(" <title>")
710 if r.Extensions&Smartypants != 0 {
711 w.Write(sr.Process([]byte(r.Title)))
712 } else {
713 w.Write(esc([]byte(r.Title)))
714 }
715 w.WriteString("</title>\n")
716 w.WriteString(" <meta name=\"GENERATOR\" content=\"Blackfriday Markdown Processor v")
717 w.WriteString(VERSION)
718 w.WriteString("\"")
719 w.WriteString(ending)
720 w.WriteString(">\n")
721 w.WriteString(" <meta charset=\"utf-8\"")
722 w.WriteString(ending)
723 w.WriteString(">\n")
724 if r.CSS != "" {
725 w.WriteString(" <link rel=\"stylesheet\" type=\"text/css\" href=\"")
726 w.Write(esc([]byte(r.CSS)))
727 w.WriteString("\"")
728 w.WriteString(ending)
729 w.WriteString(">\n")
730 }
731 w.WriteString("</head>\n")
732 w.WriteString("<body>\n\n")
733}
734
735func (r *HTMLRenderer) writeDocumentFooter(w *bytes.Buffer) {
736 if r.Flags&CompletePage == 0 {
737 return
738 }
739 w.WriteString("\n</body>\n</html>\n")
740}
741
742func (r *HTMLRenderer) Render(ast *Node) []byte {
743 //println("render_Blackfriday")
744 //dump(ast)
745 // Run Smartypants if it's enabled or simply escape text if not
746 sr := NewSmartypantsRenderer(r.Extensions)
747 ast.Walk(func(node *Node, entering bool) WalkStatus {
748 if node.Type == Text {
749 if r.Extensions&Smartypants != 0 {
750 node.Literal = sr.Process(node.Literal)
751 } else {
752 node.Literal = esc(node.Literal)
753 }
754 }
755 return GoToNext
756 })
757 var buff bytes.Buffer
758 r.writeDocumentHeader(&buff, sr)
759 ast.Walk(func(node *Node, entering bool) WalkStatus {
760 return r.RenderNode(&buff, node, entering)
761 })
762 r.writeDocumentFooter(&buff)
763 return buff.Bytes()
764}