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