1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 package blackfriday
17
18 import (
19 "bytes"
20 "fmt"
21 "io"
22 "regexp"
23 "strings"
24 )
25
26
27 type HTMLFlags int
28
29
30 const (
31 HTMLFlagsNone HTMLFlags = 0
32 SkipHTML HTMLFlags = 1 << iota
33 SkipImages
34 SkipLinks
35 Safelink
36 NofollowLinks
37 NoreferrerLinks
38 NoopenerLinks
39 HrefTargetBlank
40 CompletePage
41 UseXHTML
42 FootnoteReturnLinks
43 Smartypants
44 SmartypantsFractions
45 SmartypantsDashes
46 SmartypantsLatexDashes
47 SmartypantsAngledQuotes
48 SmartypantsQuotesNBSP
49 TOC
50 )
51
52 var (
53 htmlTagRe = regexp.MustCompile("(?i)^" + htmlTag)
54 )
55
56 const (
57 htmlTag = "(?:" + openTag + "|" + closeTag + "|" + htmlComment + "|" +
58 processingInstruction + "|" + declaration + "|" + cdata + ")"
59 closeTag = "</" + tagName + "\\s*[>]"
60 openTag = "<" + tagName + attribute + "*" + "\\s*/?>"
61 attribute = "(?:" + "\\s+" + attributeName + attributeValueSpec + "?)"
62 attributeValue = "(?:" + unquotedValue + "|" + singleQuotedValue + "|" + doubleQuotedValue + ")"
63 attributeValueSpec = "(?:" + "\\s*=" + "\\s*" + attributeValue + ")"
64 attributeName = "[a-zA-Z_:][a-zA-Z0-9:._-]*"
65 cdata = "<!\\[CDATA\\[[\\s\\S]*?\\]\\]>"
66 declaration = "<![A-Z]+" + "\\s+[^>]*>"
67 doubleQuotedValue = "\"[^\"]*\""
68 htmlComment = "<!---->|<!--(?:-?[^>-])(?:-?[^-])*-->"
69 processingInstruction = "[<][?].*?[?][>]"
70 singleQuotedValue = "'[^']*'"
71 tagName = "[A-Za-z][A-Za-z0-9-]*"
72 unquotedValue = "[^\"'=<>`\\x00-\\x20]+"
73 )
74
75
76
77 type HTMLRendererParameters struct {
78
79 AbsolutePrefix string
80
81 FootnoteAnchorPrefix string
82
83
84
85 FootnoteReturnLinkContents string
86
87
88 HeadingIDPrefix string
89
90 HeadingIDSuffix string
91
92
93
94 HeadingLevelOffset int
95
96 Title string
97 CSS string
98 Icon string
99
100 Flags HTMLFlags
101 }
102
103
104
105
106 type HTMLRenderer struct {
107 HTMLRendererParameters
108
109 closeTag string
110
111
112 headingIDs map[string]int
113
114 lastOutputLen int
115 disableTags int
116
117 sr *SPRenderer
118 }
119
120 const (
121 xhtmlClose = " />"
122 htmlClose = ">"
123 )
124
125
126
127 func NewHTMLRenderer(params HTMLRendererParameters) *HTMLRenderer {
128
129 closeTag := htmlClose
130 if params.Flags&UseXHTML != 0 {
131 closeTag = xhtmlClose
132 }
133
134 if params.FootnoteReturnLinkContents == "" {
135
136
137
138 params.FootnoteReturnLinkContents = "<span aria-label='Return'>↩\ufe0e</span>"
139 }
140
141 return &HTMLRenderer{
142 HTMLRendererParameters: params,
143
144 closeTag: closeTag,
145 headingIDs: make(map[string]int),
146
147 sr: NewSmartypantsRenderer(params.Flags),
148 }
149 }
150
151 func isHTMLTag(tag []byte, tagname string) bool {
152 found, _ := findHTMLTagPos(tag, tagname)
153 return found
154 }
155
156
157
158 func skipUntilCharIgnoreQuotes(html []byte, start int, char byte) int {
159 inSingleQuote := false
160 inDoubleQuote := false
161 inGraveQuote := false
162 i := start
163 for i < len(html) {
164 switch {
165 case html[i] == char && !inSingleQuote && !inDoubleQuote && !inGraveQuote:
166 return i
167 case html[i] == '\'':
168 inSingleQuote = !inSingleQuote
169 case html[i] == '"':
170 inDoubleQuote = !inDoubleQuote
171 case html[i] == '`':
172 inGraveQuote = !inGraveQuote
173 }
174 i++
175 }
176 return start
177 }
178
179 func findHTMLTagPos(tag []byte, tagname string) (bool, int) {
180 i := 0
181 if i < len(tag) && tag[0] != '<' {
182 return false, -1
183 }
184 i++
185 i = skipSpace(tag, i)
186
187 if i < len(tag) && tag[i] == '/' {
188 i++
189 }
190
191 i = skipSpace(tag, i)
192 j := 0
193 for ; i < len(tag); i, j = i+1, j+1 {
194 if j >= len(tagname) {
195 break
196 }
197
198 if strings.ToLower(string(tag[i]))[0] != tagname[j] {
199 return false, -1
200 }
201 }
202
203 if i == len(tag) {
204 return false, -1
205 }
206
207 rightAngle := skipUntilCharIgnoreQuotes(tag, i, '>')
208 if rightAngle >= i {
209 return true, rightAngle
210 }
211
212 return false, -1
213 }
214
215 func skipSpace(tag []byte, i int) int {
216 for i < len(tag) && isspace(tag[i]) {
217 i++
218 }
219 return i
220 }
221
222 func isRelativeLink(link []byte) (yes bool) {
223
224 if link[0] == '#' {
225 return true
226 }
227
228
229 if len(link) >= 2 && link[0] == '/' && link[1] != '/' {
230 return true
231 }
232
233
234 if len(link) == 1 && link[0] == '/' {
235 return true
236 }
237
238
239 if bytes.HasPrefix(link, []byte("./")) {
240 return true
241 }
242
243
244 if bytes.HasPrefix(link, []byte("../")) {
245 return true
246 }
247
248 return false
249 }
250
251 func (r *HTMLRenderer) ensureUniqueHeadingID(id string) string {
252 for count, found := r.headingIDs[id]; found; count, found = r.headingIDs[id] {
253 tmp := fmt.Sprintf("%s-%d", id, count+1)
254
255 if _, tmpFound := r.headingIDs[tmp]; !tmpFound {
256 r.headingIDs[id] = count + 1
257 id = tmp
258 } else {
259 id = id + "-1"
260 }
261 }
262
263 if _, found := r.headingIDs[id]; !found {
264 r.headingIDs[id] = 0
265 }
266
267 return id
268 }
269
270 func (r *HTMLRenderer) addAbsPrefix(link []byte) []byte {
271 if r.AbsolutePrefix != "" && isRelativeLink(link) && link[0] != '.' {
272 newDest := r.AbsolutePrefix
273 if link[0] != '/' {
274 newDest += "/"
275 }
276 newDest += string(link)
277 return []byte(newDest)
278 }
279 return link
280 }
281
282 func appendLinkAttrs(attrs []string, flags HTMLFlags, link []byte) []string {
283 if isRelativeLink(link) {
284 return attrs
285 }
286 val := []string{}
287 if flags&NofollowLinks != 0 {
288 val = append(val, "nofollow")
289 }
290 if flags&NoreferrerLinks != 0 {
291 val = append(val, "noreferrer")
292 }
293 if flags&NoopenerLinks != 0 {
294 val = append(val, "noopener")
295 }
296 if flags&HrefTargetBlank != 0 {
297 attrs = append(attrs, "target=\"_blank\"")
298 }
299 if len(val) == 0 {
300 return attrs
301 }
302 attr := fmt.Sprintf("rel=%q", strings.Join(val, " "))
303 return append(attrs, attr)
304 }
305
306 func isMailto(link []byte) bool {
307 return bytes.HasPrefix(link, []byte("mailto:"))
308 }
309
310 func needSkipLink(flags HTMLFlags, dest []byte) bool {
311 if flags&SkipLinks != 0 {
312 return true
313 }
314 return flags&Safelink != 0 && !isSafeLink(dest) && !isMailto(dest)
315 }
316
317 func isSmartypantable(node *Node) bool {
318 pt := node.Parent.Type
319 return pt != Link && pt != CodeBlock && pt != Code
320 }
321
322 func appendLanguageAttr(attrs []string, info []byte) []string {
323 if len(info) == 0 {
324 return attrs
325 }
326 endOfLang := bytes.IndexAny(info, "\t ")
327 if endOfLang < 0 {
328 endOfLang = len(info)
329 }
330 return append(attrs, fmt.Sprintf("class=\"language-%s\"", info[:endOfLang]))
331 }
332
333 func (r *HTMLRenderer) tag(w io.Writer, name []byte, attrs []string) {
334 w.Write(name)
335 if len(attrs) > 0 {
336 w.Write(spaceBytes)
337 w.Write([]byte(strings.Join(attrs, " ")))
338 }
339 w.Write(gtBytes)
340 r.lastOutputLen = 1
341 }
342
343 func footnoteRef(prefix string, node *Node) []byte {
344 urlFrag := prefix + string(slugify(node.Destination))
345 anchor := fmt.Sprintf(`<a href="#fn:%s">%d</a>`, urlFrag, node.NoteID)
346 return []byte(fmt.Sprintf(`<sup class="footnote-ref" id="fnref:%s">%s</sup>`, urlFrag, anchor))
347 }
348
349 func footnoteItem(prefix string, slug []byte) []byte {
350 return []byte(fmt.Sprintf(`<li id="fn:%s%s">`, prefix, slug))
351 }
352
353 func footnoteReturnLink(prefix, returnLink string, slug []byte) []byte {
354 const format = ` <a class="footnote-return" href="#fnref:%s%s">%s</a>`
355 return []byte(fmt.Sprintf(format, prefix, slug, returnLink))
356 }
357
358 func itemOpenCR(node *Node) bool {
359 if node.Prev == nil {
360 return false
361 }
362 ld := node.Parent.ListData
363 return !ld.Tight && ld.ListFlags&ListTypeDefinition == 0
364 }
365
366 func skipParagraphTags(node *Node) bool {
367 grandparent := node.Parent.Parent
368 if grandparent == nil || grandparent.Type != List {
369 return false
370 }
371 tightOrTerm := grandparent.Tight || node.Parent.ListFlags&ListTypeTerm != 0
372 return grandparent.Type == List && tightOrTerm
373 }
374
375 func cellAlignment(align CellAlignFlags) string {
376 switch align {
377 case TableAlignmentLeft:
378 return "left"
379 case TableAlignmentRight:
380 return "right"
381 case TableAlignmentCenter:
382 return "center"
383 default:
384 return ""
385 }
386 }
387
388 func (r *HTMLRenderer) out(w io.Writer, text []byte) {
389 if r.disableTags > 0 {
390 w.Write(htmlTagRe.ReplaceAll(text, []byte{}))
391 } else {
392 w.Write(text)
393 }
394 r.lastOutputLen = len(text)
395 }
396
397 func (r *HTMLRenderer) cr(w io.Writer) {
398 if r.lastOutputLen > 0 {
399 r.out(w, nlBytes)
400 }
401 }
402
403 var (
404 nlBytes = []byte{'\n'}
405 gtBytes = []byte{'>'}
406 spaceBytes = []byte{' '}
407 )
408
409 var (
410 brTag = []byte("<br>")
411 brXHTMLTag = []byte("<br />")
412 emTag = []byte("<em>")
413 emCloseTag = []byte("</em>")
414 strongTag = []byte("<strong>")
415 strongCloseTag = []byte("</strong>")
416 delTag = []byte("<del>")
417 delCloseTag = []byte("</del>")
418 ttTag = []byte("<tt>")
419 ttCloseTag = []byte("</tt>")
420 aTag = []byte("<a")
421 aCloseTag = []byte("</a>")
422 preTag = []byte("<pre>")
423 preCloseTag = []byte("</pre>")
424 codeTag = []byte("<code>")
425 codeCloseTag = []byte("</code>")
426 pTag = []byte("<p>")
427 pCloseTag = []byte("</p>")
428 blockquoteTag = []byte("<blockquote>")
429 blockquoteCloseTag = []byte("</blockquote>")
430 hrTag = []byte("<hr>")
431 hrXHTMLTag = []byte("<hr />")
432 ulTag = []byte("<ul>")
433 ulCloseTag = []byte("</ul>")
434 olTag = []byte("<ol>")
435 olCloseTag = []byte("</ol>")
436 dlTag = []byte("<dl>")
437 dlCloseTag = []byte("</dl>")
438 liTag = []byte("<li>")
439 liCloseTag = []byte("</li>")
440 ddTag = []byte("<dd>")
441 ddCloseTag = []byte("</dd>")
442 dtTag = []byte("<dt>")
443 dtCloseTag = []byte("</dt>")
444 tableTag = []byte("<table>")
445 tableCloseTag = []byte("</table>")
446 tdTag = []byte("<td")
447 tdCloseTag = []byte("</td>")
448 thTag = []byte("<th")
449 thCloseTag = []byte("</th>")
450 theadTag = []byte("<thead>")
451 theadCloseTag = []byte("</thead>")
452 tbodyTag = []byte("<tbody>")
453 tbodyCloseTag = []byte("</tbody>")
454 trTag = []byte("<tr>")
455 trCloseTag = []byte("</tr>")
456 h1Tag = []byte("<h1")
457 h1CloseTag = []byte("</h1>")
458 h2Tag = []byte("<h2")
459 h2CloseTag = []byte("</h2>")
460 h3Tag = []byte("<h3")
461 h3CloseTag = []byte("</h3>")
462 h4Tag = []byte("<h4")
463 h4CloseTag = []byte("</h4>")
464 h5Tag = []byte("<h5")
465 h5CloseTag = []byte("</h5>")
466 h6Tag = []byte("<h6")
467 h6CloseTag = []byte("</h6>")
468
469 footnotesDivBytes = []byte("\n<div class=\"footnotes\">\n\n")
470 footnotesCloseDivBytes = []byte("\n</div>\n")
471 )
472
473 func headingTagsFromLevel(level int) ([]byte, []byte) {
474 if level <= 1 {
475 return h1Tag, h1CloseTag
476 }
477 switch level {
478 case 2:
479 return h2Tag, h2CloseTag
480 case 3:
481 return h3Tag, h3CloseTag
482 case 4:
483 return h4Tag, h4CloseTag
484 case 5:
485 return h5Tag, h5CloseTag
486 }
487 return h6Tag, h6CloseTag
488 }
489
490 func (r *HTMLRenderer) outHRTag(w io.Writer) {
491 if r.Flags&UseXHTML == 0 {
492 r.out(w, hrTag)
493 } else {
494 r.out(w, hrXHTMLTag)
495 }
496 }
497
498
499
500
501
502
503
504
505
506
507
508 func (r *HTMLRenderer) RenderNode(w io.Writer, node *Node, entering bool) WalkStatus {
509 attrs := []string{}
510 switch node.Type {
511 case Text:
512 if r.Flags&Smartypants != 0 {
513 var tmp bytes.Buffer
514 escapeHTML(&tmp, node.Literal)
515 r.sr.Process(w, tmp.Bytes())
516 } else {
517 if node.Parent.Type == Link {
518 escLink(w, node.Literal)
519 } else {
520 escapeHTML(w, node.Literal)
521 }
522 }
523 case Softbreak:
524 r.cr(w)
525
526 case Hardbreak:
527 if r.Flags&UseXHTML == 0 {
528 r.out(w, brTag)
529 } else {
530 r.out(w, brXHTMLTag)
531 }
532 r.cr(w)
533 case Emph:
534 if entering {
535 r.out(w, emTag)
536 } else {
537 r.out(w, emCloseTag)
538 }
539 case Strong:
540 if entering {
541 r.out(w, strongTag)
542 } else {
543 r.out(w, strongCloseTag)
544 }
545 case Del:
546 if entering {
547 r.out(w, delTag)
548 } else {
549 r.out(w, delCloseTag)
550 }
551 case HTMLSpan:
552 if r.Flags&SkipHTML != 0 {
553 break
554 }
555 r.out(w, node.Literal)
556 case Link:
557
558 dest := node.LinkData.Destination
559 if needSkipLink(r.Flags, dest) {
560 if entering {
561 r.out(w, ttTag)
562 } else {
563 r.out(w, ttCloseTag)
564 }
565 } else {
566 if entering {
567 dest = r.addAbsPrefix(dest)
568 var hrefBuf bytes.Buffer
569 hrefBuf.WriteString("href=\"")
570 escLink(&hrefBuf, dest)
571 hrefBuf.WriteByte('"')
572 attrs = append(attrs, hrefBuf.String())
573 if node.NoteID != 0 {
574 r.out(w, footnoteRef(r.FootnoteAnchorPrefix, node))
575 break
576 }
577 attrs = appendLinkAttrs(attrs, r.Flags, dest)
578 if len(node.LinkData.Title) > 0 {
579 var titleBuff bytes.Buffer
580 titleBuff.WriteString("title=\"")
581 escapeHTML(&titleBuff, node.LinkData.Title)
582 titleBuff.WriteByte('"')
583 attrs = append(attrs, titleBuff.String())
584 }
585 r.tag(w, aTag, attrs)
586 } else {
587 if node.NoteID != 0 {
588 break
589 }
590 r.out(w, aCloseTag)
591 }
592 }
593 case Image:
594 if r.Flags&SkipImages != 0 {
595 return SkipChildren
596 }
597 if entering {
598 dest := node.LinkData.Destination
599 dest = r.addAbsPrefix(dest)
600 if r.disableTags == 0 {
601
602
603
604 r.out(w, []byte(`<img src="`))
605 escLink(w, dest)
606 r.out(w, []byte(`" alt="`))
607
608 }
609 r.disableTags++
610 } else {
611 r.disableTags--
612 if r.disableTags == 0 {
613 if node.LinkData.Title != nil {
614 r.out(w, []byte(`" title="`))
615 escapeHTML(w, node.LinkData.Title)
616 }
617 r.out(w, []byte(`" />`))
618 }
619 }
620 case Code:
621 r.out(w, codeTag)
622 escapeAllHTML(w, node.Literal)
623 r.out(w, codeCloseTag)
624 case Document:
625 break
626 case Paragraph:
627 if skipParagraphTags(node) {
628 break
629 }
630 if entering {
631
632
633 if node.Prev != nil {
634 switch node.Prev.Type {
635 case HTMLBlock, List, Paragraph, Heading, CodeBlock, BlockQuote, HorizontalRule:
636 r.cr(w)
637 }
638 }
639 if node.Parent.Type == BlockQuote && node.Prev == nil {
640 r.cr(w)
641 }
642 r.out(w, pTag)
643 } else {
644 r.out(w, pCloseTag)
645 if !(node.Parent.Type == Item && node.Next == nil) {
646 r.cr(w)
647 }
648 }
649 case BlockQuote:
650 if entering {
651 r.cr(w)
652 r.out(w, blockquoteTag)
653 } else {
654 r.out(w, blockquoteCloseTag)
655 r.cr(w)
656 }
657 case HTMLBlock:
658 if r.Flags&SkipHTML != 0 {
659 break
660 }
661 r.cr(w)
662 r.out(w, node.Literal)
663 r.cr(w)
664 case Heading:
665 headingLevel := r.HTMLRendererParameters.HeadingLevelOffset + node.Level
666 openTag, closeTag := headingTagsFromLevel(headingLevel)
667 if entering {
668 if node.IsTitleblock {
669 attrs = append(attrs, `class="title"`)
670 }
671 if node.HeadingID != "" {
672 id := r.ensureUniqueHeadingID(node.HeadingID)
673 if r.HeadingIDPrefix != "" {
674 id = r.HeadingIDPrefix + id
675 }
676 if r.HeadingIDSuffix != "" {
677 id = id + r.HeadingIDSuffix
678 }
679 attrs = append(attrs, fmt.Sprintf(`id="%s"`, id))
680 }
681 r.cr(w)
682 r.tag(w, openTag, attrs)
683 } else {
684 r.out(w, closeTag)
685 if !(node.Parent.Type == Item && node.Next == nil) {
686 r.cr(w)
687 }
688 }
689 case HorizontalRule:
690 r.cr(w)
691 r.outHRTag(w)
692 r.cr(w)
693 case List:
694 openTag := ulTag
695 closeTag := ulCloseTag
696 if node.ListFlags&ListTypeOrdered != 0 {
697 openTag = olTag
698 closeTag = olCloseTag
699 }
700 if node.ListFlags&ListTypeDefinition != 0 {
701 openTag = dlTag
702 closeTag = dlCloseTag
703 }
704 if entering {
705 if node.IsFootnotesList {
706 r.out(w, footnotesDivBytes)
707 r.outHRTag(w)
708 r.cr(w)
709 }
710 r.cr(w)
711 if node.Parent.Type == Item && node.Parent.Parent.Tight {
712 r.cr(w)
713 }
714 r.tag(w, openTag[:len(openTag)-1], attrs)
715 r.cr(w)
716 } else {
717 r.out(w, closeTag)
718
719
720
721
722 if node.Parent.Type == Item && node.Next != nil {
723 r.cr(w)
724 }
725 if node.Parent.Type == Document || node.Parent.Type == BlockQuote {
726 r.cr(w)
727 }
728 if node.IsFootnotesList {
729 r.out(w, footnotesCloseDivBytes)
730 }
731 }
732 case Item:
733 openTag := liTag
734 closeTag := liCloseTag
735 if node.ListFlags&ListTypeDefinition != 0 {
736 openTag = ddTag
737 closeTag = ddCloseTag
738 }
739 if node.ListFlags&ListTypeTerm != 0 {
740 openTag = dtTag
741 closeTag = dtCloseTag
742 }
743 if entering {
744 if itemOpenCR(node) {
745 r.cr(w)
746 }
747 if node.ListData.RefLink != nil {
748 slug := slugify(node.ListData.RefLink)
749 r.out(w, footnoteItem(r.FootnoteAnchorPrefix, slug))
750 break
751 }
752 r.out(w, openTag)
753 } else {
754 if node.ListData.RefLink != nil {
755 slug := slugify(node.ListData.RefLink)
756 if r.Flags&FootnoteReturnLinks != 0 {
757 r.out(w, footnoteReturnLink(r.FootnoteAnchorPrefix, r.FootnoteReturnLinkContents, slug))
758 }
759 }
760 r.out(w, closeTag)
761 r.cr(w)
762 }
763 case CodeBlock:
764 attrs = appendLanguageAttr(attrs, node.Info)
765 r.cr(w)
766 r.out(w, preTag)
767 r.tag(w, codeTag[:len(codeTag)-1], attrs)
768 escapeAllHTML(w, node.Literal)
769 r.out(w, codeCloseTag)
770 r.out(w, preCloseTag)
771 if node.Parent.Type != Item {
772 r.cr(w)
773 }
774 case Table:
775 if entering {
776 r.cr(w)
777 r.out(w, tableTag)
778 } else {
779 r.out(w, tableCloseTag)
780 r.cr(w)
781 }
782 case TableCell:
783 openTag := tdTag
784 closeTag := tdCloseTag
785 if node.IsHeader {
786 openTag = thTag
787 closeTag = thCloseTag
788 }
789 if entering {
790 align := cellAlignment(node.Align)
791 if align != "" {
792 attrs = append(attrs, fmt.Sprintf(`align="%s"`, align))
793 }
794 if node.Prev == nil {
795 r.cr(w)
796 }
797 r.tag(w, openTag, attrs)
798 } else {
799 r.out(w, closeTag)
800 r.cr(w)
801 }
802 case TableHead:
803 if entering {
804 r.cr(w)
805 r.out(w, theadTag)
806 } else {
807 r.out(w, theadCloseTag)
808 r.cr(w)
809 }
810 case TableBody:
811 if entering {
812 r.cr(w)
813 r.out(w, tbodyTag)
814
815 if node.FirstChild == nil {
816 r.cr(w)
817 }
818 } else {
819 r.out(w, tbodyCloseTag)
820 r.cr(w)
821 }
822 case TableRow:
823 if entering {
824 r.cr(w)
825 r.out(w, trTag)
826 } else {
827 r.out(w, trCloseTag)
828 r.cr(w)
829 }
830 default:
831 panic("Unknown node type " + node.Type.String())
832 }
833 return GoToNext
834 }
835
836
837 func (r *HTMLRenderer) RenderHeader(w io.Writer, ast *Node) {
838 r.writeDocumentHeader(w)
839 if r.Flags&TOC != 0 {
840 r.writeTOC(w, ast)
841 }
842 }
843
844
845 func (r *HTMLRenderer) RenderFooter(w io.Writer, ast *Node) {
846 if r.Flags&CompletePage == 0 {
847 return
848 }
849 io.WriteString(w, "\n</body>\n</html>\n")
850 }
851
852 func (r *HTMLRenderer) writeDocumentHeader(w io.Writer) {
853 if r.Flags&CompletePage == 0 {
854 return
855 }
856 ending := ""
857 if r.Flags&UseXHTML != 0 {
858 io.WriteString(w, "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" ")
859 io.WriteString(w, "\"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">\n")
860 io.WriteString(w, "<html xmlns=\"http://www.w3.org/1999/xhtml\">\n")
861 ending = " /"
862 } else {
863 io.WriteString(w, "<!DOCTYPE html>\n")
864 io.WriteString(w, "<html>\n")
865 }
866 io.WriteString(w, "<head>\n")
867 io.WriteString(w, " <title>")
868 if r.Flags&Smartypants != 0 {
869 r.sr.Process(w, []byte(r.Title))
870 } else {
871 escapeHTML(w, []byte(r.Title))
872 }
873 io.WriteString(w, "</title>\n")
874 io.WriteString(w, " <meta name=\"GENERATOR\" content=\"Blackfriday Markdown Processor v")
875 io.WriteString(w, Version)
876 io.WriteString(w, "\"")
877 io.WriteString(w, ending)
878 io.WriteString(w, ">\n")
879 io.WriteString(w, " <meta charset=\"utf-8\"")
880 io.WriteString(w, ending)
881 io.WriteString(w, ">\n")
882 if r.CSS != "" {
883 io.WriteString(w, " <link rel=\"stylesheet\" type=\"text/css\" href=\"")
884 escapeHTML(w, []byte(r.CSS))
885 io.WriteString(w, "\"")
886 io.WriteString(w, ending)
887 io.WriteString(w, ">\n")
888 }
889 if r.Icon != "" {
890 io.WriteString(w, " <link rel=\"icon\" type=\"image/x-icon\" href=\"")
891 escapeHTML(w, []byte(r.Icon))
892 io.WriteString(w, "\"")
893 io.WriteString(w, ending)
894 io.WriteString(w, ">\n")
895 }
896 io.WriteString(w, "</head>\n")
897 io.WriteString(w, "<body>\n\n")
898 }
899
900 func (r *HTMLRenderer) writeTOC(w io.Writer, ast *Node) {
901 buf := bytes.Buffer{}
902
903 inHeading := false
904 tocLevel := 0
905 headingCount := 0
906
907 ast.Walk(func(node *Node, entering bool) WalkStatus {
908 if node.Type == Heading && !node.HeadingData.IsTitleblock {
909 inHeading = entering
910 if entering {
911 node.HeadingID = fmt.Sprintf("toc_%d", headingCount)
912 if node.Level == tocLevel {
913 buf.WriteString("</li>\n\n<li>")
914 } else if node.Level < tocLevel {
915 for node.Level < tocLevel {
916 tocLevel--
917 buf.WriteString("</li>\n</ul>")
918 }
919 buf.WriteString("</li>\n\n<li>")
920 } else {
921 for node.Level > tocLevel {
922 tocLevel++
923 buf.WriteString("\n<ul>\n<li>")
924 }
925 }
926
927 fmt.Fprintf(&buf, `<a href="#toc_%d">`, headingCount)
928 headingCount++
929 } else {
930 buf.WriteString("</a>")
931 }
932 return GoToNext
933 }
934
935 if inHeading {
936 return r.RenderNode(&buf, node, entering)
937 }
938
939 return GoToNext
940 })
941
942 for ; tocLevel > 0; tocLevel-- {
943 buf.WriteString("</li>\n</ul>")
944 }
945
946 if buf.Len() > 0 {
947 io.WriteString(w, "<nav>\n")
948 w.Write(buf.Bytes())
949 io.WriteString(w, "\n\n</nav>\n")
950 }
951 r.lastOutputLen = buf.Len()
952 }
953
View as plain text