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 "io"
22 "regexp"
23 "strings"
24)
25
26// HTMLFlags control optional behavior of HTML renderer.
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 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 (r *HTMLRenderer) tag(w io.Writer, name []byte, attrs []string) {
318 w.Write(name)
319 if len(attrs) > 0 {
320 w.Write(spaceBytes)
321 w.Write([]byte(strings.Join(attrs, " ")))
322 }
323 w.Write(gtBytes)
324 r.lastOutputLen = 1
325}
326
327func footnoteRef(prefix string, node *Node) []byte {
328 urlFrag := prefix + string(slugify(node.Destination))
329 anchor := fmt.Sprintf(`<a rel="footnote" href="#fn:%s">%d</a>`, urlFrag, node.NoteID)
330 return []byte(fmt.Sprintf(`<sup class="footnote-ref" id="fnref:%s">%s</sup>`, urlFrag, anchor))
331}
332
333func footnoteItem(prefix string, slug []byte) []byte {
334 return []byte(fmt.Sprintf(`<li id="fn:%s%s">`, prefix, slug))
335}
336
337func footnoteReturnLink(prefix, returnLink string, slug []byte) []byte {
338 const format = ` <a class="footnote-return" href="#fnref:%s%s">%s</a>`
339 return []byte(fmt.Sprintf(format, prefix, slug, returnLink))
340}
341
342func itemOpenCR(node *Node) bool {
343 if node.Prev == nil {
344 return false
345 }
346 ld := node.Parent.ListData
347 return !ld.Tight && ld.ListFlags&ListTypeDefinition == 0
348}
349
350func skipParagraphTags(node *Node) bool {
351 grandparent := node.Parent.Parent
352 if grandparent == nil || grandparent.Type != List {
353 return false
354 }
355 tightOrTerm := grandparent.Tight || node.Parent.ListFlags&ListTypeTerm != 0
356 return grandparent.Type == List && tightOrTerm
357}
358
359func cellAlignment(align CellAlignFlags) string {
360 switch align {
361 case TableAlignmentLeft:
362 return "left"
363 case TableAlignmentRight:
364 return "right"
365 case TableAlignmentCenter:
366 return "center"
367 default:
368 return ""
369 }
370}
371
372func (r *HTMLRenderer) out(w io.Writer, text []byte) {
373 if r.disableTags > 0 {
374 w.Write(htmlTagRe.ReplaceAll(text, []byte{}))
375 } else {
376 w.Write(text)
377 }
378 r.lastOutputLen = len(text)
379}
380
381func (r *HTMLRenderer) cr(w io.Writer) {
382 if r.lastOutputLen > 0 {
383 r.out(w, nlBytes)
384 }
385}
386
387var (
388 nlBytes = []byte{'\n'}
389 gtBytes = []byte{'>'}
390 spaceBytes = []byte{' '}
391)
392
393var (
394 brTag = []byte("<br>")
395 brXHTMLTag = []byte("<br />")
396 emTag = []byte("<em>")
397 emCloseTag = []byte("</em>")
398 strongTag = []byte("<strong>")
399 strongCloseTag = []byte("</strong>")
400 delTag = []byte("<del>")
401 delCloseTag = []byte("</del>")
402 ttTag = []byte("<tt>")
403 ttCloseTag = []byte("</tt>")
404 aTag = []byte("<a")
405 aCloseTag = []byte("</a>")
406 preTag = []byte("<pre>")
407 preCloseTag = []byte("</pre>")
408 codeTag = []byte("<code>")
409 codeCloseTag = []byte("</code>")
410 pTag = []byte("<p>")
411 pCloseTag = []byte("</p>")
412 blockquoteTag = []byte("<blockquote>")
413 blockquoteCloseTag = []byte("</blockquote>")
414 hrTag = []byte("<hr>")
415 hrXHTMLTag = []byte("<hr />")
416 ulTag = []byte("<ul>")
417 ulCloseTag = []byte("</ul>")
418 olTag = []byte("<ol>")
419 olCloseTag = []byte("</ol>")
420 dlTag = []byte("<dl>")
421 dlCloseTag = []byte("</dl>")
422 liTag = []byte("<li>")
423 liCloseTag = []byte("</li>")
424 ddTag = []byte("<dd>")
425 ddCloseTag = []byte("</dd>")
426 dtTag = []byte("<dt>")
427 dtCloseTag = []byte("</dt>")
428 tableTag = []byte("<table>")
429 tableCloseTag = []byte("</table>")
430 tdTag = []byte("<td")
431 tdCloseTag = []byte("</td>")
432 thTag = []byte("<th")
433 thCloseTag = []byte("</th>")
434 theadTag = []byte("<thead>")
435 theadCloseTag = []byte("</thead>")
436 tbodyTag = []byte("<tbody>")
437 tbodyCloseTag = []byte("</tbody>")
438 trTag = []byte("<tr>")
439 trCloseTag = []byte("</tr>")
440 h1Tag = []byte("<h1")
441 h1CloseTag = []byte("</h1>")
442 h2Tag = []byte("<h2")
443 h2CloseTag = []byte("</h2>")
444 h3Tag = []byte("<h3")
445 h3CloseTag = []byte("</h3>")
446 h4Tag = []byte("<h4")
447 h4CloseTag = []byte("</h4>")
448 h5Tag = []byte("<h5")
449 h5CloseTag = []byte("</h5>")
450 h6Tag = []byte("<h6")
451 h6CloseTag = []byte("</h6>")
452
453 footnotesDivBytes = []byte("\n<div class=\"footnotes\">\n\n")
454 footnotesCloseDivBytes = []byte("\n</div>\n")
455)
456
457func headerTagsFromLevel(level int) ([]byte, []byte) {
458 switch level {
459 case 1:
460 return h1Tag, h1CloseTag
461 case 2:
462 return h2Tag, h2CloseTag
463 case 3:
464 return h3Tag, h3CloseTag
465 case 4:
466 return h4Tag, h4CloseTag
467 case 5:
468 return h5Tag, h5CloseTag
469 default:
470 return h6Tag, h6CloseTag
471 }
472}
473
474func (r *HTMLRenderer) outHRTag(w io.Writer) {
475 if r.Flags&UseXHTML == 0 {
476 r.out(w, hrTag)
477 } else {
478 r.out(w, hrXHTMLTag)
479 }
480}
481
482// RenderNode is a default renderer of a single node of a syntax tree. For
483// block nodes it will be called twice: first time with entering=true, second
484// time with entering=false, so that it could know when it's working on an open
485// tag and when on close. It writes the result to w.
486//
487// The return value is a way to tell the calling walker to adjust its walk
488// pattern: e.g. it can terminate the traversal by returning Terminate. Or it
489// can ask the walker to skip a subtree of this node by returning SkipChildren.
490// The typical behavior is to return GoToNext, which asks for the usual
491// traversal to the next node.
492func (r *HTMLRenderer) RenderNode(w io.Writer, node *Node, entering bool) WalkStatus {
493 attrs := []string{}
494 switch node.Type {
495 case Text:
496 if r.Flags&Smartypants != 0 {
497 var tmp bytes.Buffer
498 escapeHTML(&tmp, node.Literal)
499 r.sr.Process(w, tmp.Bytes())
500 } else {
501 if node.Parent.Type == Link {
502 escLink(w, node.Literal)
503 } else {
504 escapeHTML(w, node.Literal)
505 }
506 }
507 case Softbreak:
508 r.cr(w)
509 // TODO: make it configurable via out(renderer.softbreak)
510 case Hardbreak:
511 if r.Flags&UseXHTML == 0 {
512 r.out(w, brTag)
513 } else {
514 r.out(w, brXHTMLTag)
515 }
516 r.cr(w)
517 case Emph:
518 if entering {
519 r.out(w, emTag)
520 } else {
521 r.out(w, emCloseTag)
522 }
523 case Strong:
524 if entering {
525 r.out(w, strongTag)
526 } else {
527 r.out(w, strongCloseTag)
528 }
529 case Del:
530 if entering {
531 r.out(w, delTag)
532 } else {
533 r.out(w, delCloseTag)
534 }
535 case HTMLSpan:
536 if r.Flags&SkipHTML != 0 {
537 break
538 }
539 if r.Flags&SkipStyle != 0 && isHTMLTag(node.Literal, "style") {
540 break
541 }
542 //if options.safe {
543 // out(w, "<!-- raw HTML omitted -->")
544 //} else {
545 r.out(w, node.Literal)
546 //}
547 case Link:
548 // mark it but don't link it if it is not a safe link: no smartypants
549 dest := node.LinkData.Destination
550 if needSkipLink(r.Flags, dest) {
551 if entering {
552 r.out(w, ttTag)
553 } else {
554 r.out(w, ttCloseTag)
555 }
556 } else {
557 if entering {
558 dest = r.addAbsPrefix(dest)
559 var hrefBuff bytes.Buffer
560 hrefBuff.WriteString("href=\"")
561 escLink(&hrefBuff, dest)
562 hrefBuff.WriteByte('"')
563 attrs = append(attrs, hrefBuff.String())
564 if node.NoteID != 0 {
565 r.out(w, footnoteRef(r.FootnoteAnchorPrefix, node))
566 break
567 }
568 attrs = appendLinkAttrs(attrs, r.Flags, dest)
569 if len(node.LinkData.Title) > 0 {
570 var titleBuff bytes.Buffer
571 titleBuff.WriteString("title=\"")
572 escapeHTML(&titleBuff, node.LinkData.Title)
573 titleBuff.WriteByte('"')
574 attrs = append(attrs, titleBuff.String())
575 }
576 r.tag(w, aTag, attrs)
577 } else {
578 if node.NoteID != 0 {
579 break
580 }
581 r.out(w, aCloseTag)
582 }
583 }
584 case Image:
585 if r.Flags&SkipImages != 0 {
586 return SkipChildren
587 }
588 if entering {
589 dest := node.LinkData.Destination
590 dest = r.addAbsPrefix(dest)
591 if r.disableTags == 0 {
592 //if options.safe && potentiallyUnsafe(dest) {
593 //out(w, `<img src="" alt="`)
594 //} else {
595 r.out(w, []byte(`<img src="`))
596 escLink(w, dest)
597 r.out(w, []byte(`" alt="`))
598 //}
599 }
600 r.disableTags++
601 } else {
602 r.disableTags--
603 if r.disableTags == 0 {
604 if node.LinkData.Title != nil {
605 r.out(w, []byte(`" title="`))
606 escapeHTML(w, node.LinkData.Title)
607 }
608 r.out(w, []byte(`" />`))
609 }
610 }
611 case Code:
612 r.out(w, codeTag)
613 escapeHTML(w, node.Literal)
614 r.out(w, codeCloseTag)
615 case Document:
616 break
617 case Paragraph:
618 if skipParagraphTags(node) {
619 break
620 }
621 if entering {
622 // TODO: untangle this clusterfuck about when the newlines need
623 // to be added and when not.
624 if node.Prev != nil {
625 switch node.Prev.Type {
626 case HTMLBlock, List, Paragraph, Header, CodeBlock, BlockQuote, HorizontalRule:
627 r.cr(w)
628 }
629 }
630 if node.Parent.Type == BlockQuote && node.Prev == nil {
631 r.cr(w)
632 }
633 r.out(w, pTag)
634 } else {
635 r.out(w, pCloseTag)
636 if !(node.Parent.Type == Item && node.Next == nil) {
637 r.cr(w)
638 }
639 }
640 case BlockQuote:
641 if entering {
642 r.cr(w)
643 r.out(w, blockquoteTag)
644 } else {
645 r.out(w, blockquoteCloseTag)
646 r.cr(w)
647 }
648 case HTMLBlock:
649 if r.Flags&SkipHTML != 0 {
650 break
651 }
652 r.cr(w)
653 r.out(w, node.Literal)
654 r.cr(w)
655 case Header:
656 openTag, closeTag := headerTagsFromLevel(node.Level)
657 if entering {
658 if node.IsTitleblock {
659 attrs = append(attrs, `class="title"`)
660 }
661 if node.HeaderID != "" {
662 id := r.ensureUniqueHeaderID(node.HeaderID)
663 if r.HeaderIDPrefix != "" {
664 id = r.HeaderIDPrefix + id
665 }
666 if r.HeaderIDSuffix != "" {
667 id = id + r.HeaderIDSuffix
668 }
669 attrs = append(attrs, fmt.Sprintf(`id="%s"`, id))
670 }
671 r.cr(w)
672 r.tag(w, openTag, attrs)
673 } else {
674 r.out(w, closeTag)
675 if !(node.Parent.Type == Item && node.Next == nil) {
676 r.cr(w)
677 }
678 }
679 case HorizontalRule:
680 r.cr(w)
681 r.outHRTag(w)
682 r.cr(w)
683 case List:
684 openTag := ulTag
685 closeTag := ulCloseTag
686 if node.ListFlags&ListTypeOrdered != 0 {
687 openTag = olTag
688 closeTag = olCloseTag
689 }
690 if node.ListFlags&ListTypeDefinition != 0 {
691 openTag = dlTag
692 closeTag = dlCloseTag
693 }
694 if entering {
695 if node.IsFootnotesList {
696 r.out(w, footnotesDivBytes)
697 r.outHRTag(w)
698 r.cr(w)
699 }
700 r.cr(w)
701 if node.Parent.Type == Item && node.Parent.Parent.Tight {
702 r.cr(w)
703 }
704 r.tag(w, openTag[:len(openTag)-1], attrs)
705 r.cr(w)
706 } else {
707 r.out(w, closeTag)
708 //cr(w)
709 //if node.parent.Type != Item {
710 // cr(w)
711 //}
712 if node.Parent.Type == Item && node.Next != nil {
713 r.cr(w)
714 }
715 if node.Parent.Type == Document || node.Parent.Type == BlockQuote {
716 r.cr(w)
717 }
718 if node.IsFootnotesList {
719 r.out(w, footnotesCloseDivBytes)
720 }
721 }
722 case Item:
723 openTag := liTag
724 closeTag := liCloseTag
725 if node.ListFlags&ListTypeDefinition != 0 {
726 openTag = ddTag
727 closeTag = ddCloseTag
728 }
729 if node.ListFlags&ListTypeTerm != 0 {
730 openTag = dtTag
731 closeTag = dtCloseTag
732 }
733 if entering {
734 if itemOpenCR(node) {
735 r.cr(w)
736 }
737 if node.ListData.RefLink != nil {
738 slug := slugify(node.ListData.RefLink)
739 r.out(w, footnoteItem(r.FootnoteAnchorPrefix, slug))
740 break
741 }
742 r.out(w, openTag)
743 } else {
744 if node.ListData.RefLink != nil {
745 slug := slugify(node.ListData.RefLink)
746 if r.Flags&FootnoteReturnLinks != 0 {
747 r.out(w, footnoteReturnLink(r.FootnoteAnchorPrefix, r.FootnoteReturnLinkContents, slug))
748 }
749 }
750 r.out(w, closeTag)
751 r.cr(w)
752 }
753 case CodeBlock:
754 attrs = appendLanguageAttr(attrs, node.Info)
755 r.cr(w)
756 r.out(w, preTag)
757 r.tag(w, codeTag[:len(codeTag)-1], attrs)
758 escapeHTML(w, node.Literal)
759 r.out(w, codeCloseTag)
760 r.out(w, preCloseTag)
761 if node.Parent.Type != Item {
762 r.cr(w)
763 }
764 case Table:
765 if entering {
766 r.cr(w)
767 r.out(w, tableTag)
768 } else {
769 r.out(w, tableCloseTag)
770 r.cr(w)
771 }
772 case TableCell:
773 openTag := tdTag
774 closeTag := tdCloseTag
775 if node.IsHeader {
776 openTag = thTag
777 closeTag = thCloseTag
778 }
779 if entering {
780 align := cellAlignment(node.Align)
781 if align != "" {
782 attrs = append(attrs, fmt.Sprintf(`align="%s"`, align))
783 }
784 if node.Prev == nil {
785 r.cr(w)
786 }
787 r.tag(w, openTag, attrs)
788 } else {
789 r.out(w, closeTag)
790 r.cr(w)
791 }
792 case TableHead:
793 if entering {
794 r.cr(w)
795 r.out(w, theadTag)
796 } else {
797 r.out(w, theadCloseTag)
798 r.cr(w)
799 }
800 case TableBody:
801 if entering {
802 r.cr(w)
803 r.out(w, tbodyTag)
804 // XXX: this is to adhere to a rather silly test. Should fix test.
805 if node.FirstChild == nil {
806 r.cr(w)
807 }
808 } else {
809 r.out(w, tbodyCloseTag)
810 r.cr(w)
811 }
812 case TableRow:
813 if entering {
814 r.cr(w)
815 r.out(w, trTag)
816 } else {
817 r.out(w, trCloseTag)
818 r.cr(w)
819 }
820 default:
821 panic("Unknown node type " + node.Type.String())
822 }
823 return GoToNext
824}
825
826func (r *HTMLRenderer) writeDocumentHeader(w *bytes.Buffer) {
827 if r.Flags&CompletePage == 0 {
828 return
829 }
830 ending := ""
831 if r.Flags&UseXHTML != 0 {
832 w.WriteString("<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" ")
833 w.WriteString("\"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">\n")
834 w.WriteString("<html xmlns=\"http://www.w3.org/1999/xhtml\">\n")
835 ending = " /"
836 } else {
837 w.WriteString("<!DOCTYPE html>\n")
838 w.WriteString("<html>\n")
839 }
840 w.WriteString("<head>\n")
841 w.WriteString(" <title>")
842 if r.Flags&Smartypants != 0 {
843 r.sr.Process(w, []byte(r.Title))
844 } else {
845 escapeHTML(w, []byte(r.Title))
846 }
847 w.WriteString("</title>\n")
848 w.WriteString(" <meta name=\"GENERATOR\" content=\"Blackfriday Markdown Processor v")
849 w.WriteString(Version)
850 w.WriteString("\"")
851 w.WriteString(ending)
852 w.WriteString(">\n")
853 w.WriteString(" <meta charset=\"utf-8\"")
854 w.WriteString(ending)
855 w.WriteString(">\n")
856 if r.CSS != "" {
857 w.WriteString(" <link rel=\"stylesheet\" type=\"text/css\" href=\"")
858 escapeHTML(w, []byte(r.CSS))
859 w.WriteString("\"")
860 w.WriteString(ending)
861 w.WriteString(">\n")
862 }
863 if r.Icon != "" {
864 w.WriteString(" <link rel=\"icon\" type=\"image/x-icon\" href=\"")
865 escapeHTML(w, []byte(r.Icon))
866 w.WriteString("\"")
867 w.WriteString(ending)
868 w.WriteString(">\n")
869 }
870 w.WriteString("</head>\n")
871 w.WriteString("<body>\n\n")
872}
873
874func (r *HTMLRenderer) writeTOC(w *bytes.Buffer, ast *Node) {
875 buf := bytes.Buffer{}
876
877 inHeader := false
878 tocLevel := 0
879 headerCount := 0
880
881 ast.Walk(func(node *Node, entering bool) WalkStatus {
882 if node.Type == Header && !node.HeaderData.IsTitleblock {
883 inHeader = entering
884 if entering {
885 node.HeaderID = fmt.Sprintf("toc_%d", headerCount)
886 if node.Level == tocLevel {
887 buf.WriteString("</li>\n\n<li>")
888 } else if node.Level < tocLevel {
889 for node.Level < tocLevel {
890 tocLevel--
891 buf.WriteString("</li>\n</ul>")
892 }
893 buf.WriteString("</li>\n\n<li>")
894 } else {
895 for node.Level > tocLevel {
896 tocLevel++
897 buf.WriteString("\n<ul>\n<li>")
898 }
899 }
900
901 fmt.Fprintf(&buf, `<a href="#toc_%d">`, headerCount)
902 headerCount++
903 } else {
904 buf.WriteString("</a>")
905 }
906 return GoToNext
907 }
908
909 if inHeader {
910 return r.RenderNode(&buf, node, entering)
911 }
912
913 return GoToNext
914 })
915
916 for ; tocLevel > 0; tocLevel-- {
917 buf.WriteString("</li>\n</ul>")
918 }
919
920 if buf.Len() > 0 {
921 w.WriteString("<nav>\n")
922 w.Write(buf.Bytes())
923 w.WriteString("\n\n</nav>\n")
924 }
925 r.lastOutputLen = buf.Len()
926}
927
928func (r *HTMLRenderer) writeDocumentFooter(w *bytes.Buffer) {
929 if r.Flags&CompletePage == 0 {
930 return
931 }
932 w.WriteString("\n</body>\n</html>\n")
933}
934
935// Render walks the specified syntax (sub)tree and returns a HTML document.
936func (r *HTMLRenderer) Render(ast *Node) []byte {
937 //println("render_Blackfriday")
938 //dump(ast)
939 var buff bytes.Buffer
940 r.writeDocumentHeader(&buff)
941 if r.Extensions&TOC != 0 || r.Extensions&OmitContents != 0 {
942 r.writeTOC(&buff, ast)
943 if r.Extensions&OmitContents != 0 {
944 return buff.Bytes()
945 }
946 }
947 ast.Walk(func(node *Node, entering bool) WalkStatus {
948 return r.RenderNode(&buff, node, entering)
949 })
950 r.writeDocumentFooter(&buff)
951 return buff.Bytes()
952}