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