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