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