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