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