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