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