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 "regexp"
22 "strconv"
23 "strings"
24)
25
26type HtmlFlags int
27
28// Html renderer configuration options.
29const (
30 HtmlFlagsNone HtmlFlags = 0
31 SkipHTML HtmlFlags = 1 << iota // Skip preformatted HTML blocks
32 SkipStyle // Skip embedded <style> elements
33 SkipImages // Skip embedded images
34 SkipLinks // Skip all links
35 Safelink // Only link to trusted protocols
36 NofollowLinks // Only link with rel="nofollow"
37 NoreferrerLinks // Only link with rel="noreferrer"
38 HrefTargetBlank // Add a blank target
39 Toc // Generate a table of contents
40 OmitContents // Skip the main contents (for a standalone table of contents)
41 CompletePage // Generate a complete HTML page
42 UseXHTML // Generate XHTML output instead of HTML
43 UseSmartypants // Enable smart punctuation substitutions
44 SmartypantsFractions // Enable smart fractions (with UseSmartypants)
45 SmartypantsDashes // Enable smart dashes (with UseSmartypants)
46 SmartypantsLatexDashes // Enable LaTeX-style dashes (with UseSmartypants)
47 SmartypantsAngledQuotes // Enable angled double quotes (with UseSmartypants) for double quotes rendering
48 FootnoteReturnLinks // Generate a link at the end of a footnote to return to the source
49)
50
51var (
52 alignments = []string{
53 "left",
54 "right",
55 "center",
56 }
57
58 // TODO: improve this regexp to catch all possible entities:
59 htmlEntity = regexp.MustCompile(`&[a-z]{2,5};`)
60)
61
62type HtmlRendererParameters struct {
63 // Prepend this text to each relative URL.
64 AbsolutePrefix string
65 // Add this text to each footnote anchor, to ensure uniqueness.
66 FootnoteAnchorPrefix string
67 // Show this text inside the <a> tag for a footnote return link, if the
68 // HTML_FOOTNOTE_RETURN_LINKS flag is enabled. If blank, the string
69 // <sup>[return]</sup> is used.
70 FootnoteReturnLinkContents string
71 // If set, add this text to the front of each Header ID, to ensure
72 // uniqueness.
73 HeaderIDPrefix string
74 // If set, add this text to the back of each Header ID, to ensure uniqueness.
75 HeaderIDSuffix string
76}
77
78// Html is a type that implements the Renderer interface for HTML output.
79//
80// Do not create this directly, instead use the HtmlRenderer function.
81type Html struct {
82 flags HtmlFlags
83 closeTag string // how to end singleton tags: either " />" or ">"
84 title string // document title
85 css string // optional css file url (used with HTML_COMPLETE_PAGE)
86
87 parameters HtmlRendererParameters
88
89 // table of contents data
90 tocMarker int
91 headerCount int
92 currentLevel int
93 toc *bytes.Buffer
94
95 // Track header IDs to prevent ID collision in a single generation.
96 headerIDs map[string]int
97
98 smartypants *smartypantsRenderer
99 w HtmlWriter
100}
101
102const (
103 xhtmlClose = " />"
104 htmlClose = ">"
105)
106
107// HtmlRenderer creates and configures an Html object, which
108// satisfies the Renderer interface.
109//
110// flags is a set of HtmlFlags ORed together.
111// title is the title of the document, and css is a URL for the document's
112// stylesheet.
113// title and css are only used when HTML_COMPLETE_PAGE is selected.
114func HtmlRenderer(flags HtmlFlags, title string, css string) Renderer {
115 return HtmlRendererWithParameters(flags, title, css, HtmlRendererParameters{})
116}
117
118type HtmlWriter struct {
119 output bytes.Buffer
120 captureBuff *bytes.Buffer
121 copyBuff *bytes.Buffer
122 dirty bool
123}
124
125func (w *HtmlWriter) Write(p []byte) (n int, err error) {
126 w.dirty = true
127 if w.copyBuff != nil {
128 w.copyBuff.Write(p)
129 }
130 if w.captureBuff != nil {
131 w.captureBuff.Write(p)
132 return
133 }
134 return w.output.Write(p)
135}
136
137func (w *HtmlWriter) WriteString(s string) (n int, err error) {
138 w.dirty = true
139 if w.copyBuff != nil {
140 w.copyBuff.WriteString(s)
141 }
142 if w.captureBuff != nil {
143 w.captureBuff.WriteString(s)
144 return
145 }
146 return w.output.WriteString(s)
147}
148
149func (w *HtmlWriter) WriteByte(b byte) error {
150 w.dirty = true
151 if w.copyBuff != nil {
152 w.copyBuff.WriteByte(b)
153 }
154 if w.captureBuff != nil {
155 return w.captureBuff.WriteByte(b)
156 }
157 return w.output.WriteByte(b)
158}
159
160// Writes out a newline if the output is not pristine. Used at the beginning of
161// every rendering func
162func (w *HtmlWriter) Newline() {
163 if w.dirty {
164 w.WriteByte('\n')
165 }
166}
167
168func (r *Html) CaptureWrites(processor func()) []byte {
169 var output bytes.Buffer
170 // preserve old captureBuff state for possible nested captures:
171 tmp := r.w.captureBuff
172 tmpd := r.w.dirty
173 r.w.captureBuff = &output
174 r.w.dirty = false
175 processor()
176 // restore:
177 r.w.captureBuff = tmp
178 r.w.dirty = tmpd
179 return output.Bytes()
180}
181
182func (r *Html) CopyWrites(processor func()) []byte {
183 var output bytes.Buffer
184 r.w.copyBuff = &output
185 processor()
186 r.w.copyBuff = nil
187 return output.Bytes()
188}
189
190func (r *Html) GetResult() []byte {
191 return r.w.output.Bytes()
192}
193
194func HtmlRendererWithParameters(flags HtmlFlags, title string,
195 css string, renderParameters HtmlRendererParameters) Renderer {
196 // configure the rendering engine
197 closeTag := htmlClose
198 if flags&UseXHTML != 0 {
199 closeTag = xhtmlClose
200 }
201
202 if renderParameters.FootnoteReturnLinkContents == "" {
203 renderParameters.FootnoteReturnLinkContents = `<sup>[return]</sup>`
204 }
205
206 var writer HtmlWriter
207 return &Html{
208 flags: flags,
209 closeTag: closeTag,
210 title: title,
211 css: css,
212 parameters: renderParameters,
213
214 headerCount: 0,
215 currentLevel: 0,
216 toc: new(bytes.Buffer),
217
218 headerIDs: make(map[string]int),
219
220 smartypants: smartypants(flags),
221 w: writer,
222 }
223}
224
225// Using if statements is a bit faster than a switch statement. As the compiler
226// improves, this should be unnecessary this is only worthwhile because
227// attrEscape is the single largest CPU user in normal use.
228// Also tried using map, but that gave a ~3x slowdown.
229func escapeSingleChar(char byte) (string, bool) {
230 if char == '"' {
231 return """, true
232 }
233 if char == '&' {
234 return "&", true
235 }
236 if char == '<' {
237 return "<", true
238 }
239 if char == '>' {
240 return ">", true
241 }
242 return "", false
243}
244
245func attrEscape(src []byte) {
246 org := 0
247 for i, ch := range src {
248 if entity, ok := escapeSingleChar(ch); ok {
249 if i > org {
250 // copy all the normal characters since the last escape
251 out.Write(src[org:i])
252 }
253 org = i + 1
254 out.WriteString(entity)
255 }
256 }
257 if org < len(src) {
258 out.Write(src[org:])
259 }
260}
261
262func entityEscapeWithSkip(src []byte, skipRanges [][]int) {
263 end := 0
264 for _, rang := range skipRanges {
265 attrEscape(out, src[end:rang[0]])
266 out.Write(src[rang[0]:rang[1]])
267 end = rang[1]
268 }
269 attrEscape(out, src[end:])
270}
271
272func (r *Html) GetFlags() HtmlFlags {
273 return r.flags
274}
275
276func (r *Html) TitleBlock(text []byte) {
277 text = bytes.TrimPrefix(text, []byte("% "))
278 text = bytes.Replace(text, []byte("\n% "), []byte("\n"), -1)
279 out.WriteString("<h1 class=\"title\">")
280 out.Write(text)
281 out.WriteString("\n</h1>")
282}
283
284func (r *Html) BeginHeader(level int, id string) int {
285 doubleSpace(out)
286
287 if id == "" && r.flags&Toc != 0 {
288 id = fmt.Sprintf("toc_%d", r.headerCount)
289 }
290
291 if id != "" {
292 id = r.ensureUniqueHeaderID(id)
293
294 if r.parameters.HeaderIDPrefix != "" {
295 id = r.parameters.HeaderIDPrefix + id
296 }
297
298 if r.parameters.HeaderIDSuffix != "" {
299 id = id + r.parameters.HeaderIDSuffix
300 }
301
302 out.WriteString(fmt.Sprintf("<h%d id=\"%s\">", level, id))
303 } else {
304 out.WriteString(fmt.Sprintf("<h%d>", level))
305 }
306
307 return out.Len()
308}
309
310func (r *Html) EndHeader(level int, id string, tocMarker int) {
311 // are we building a table of contents?
312 if r.flags&Toc != 0 {
313 r.TocHeaderWithAnchor(out.Bytes()[tocMarker:], level, id)
314 }
315
316 out.WriteString(fmt.Sprintf("</h%d>\n", level))
317}
318
319func (r *Html) BlockHtml(text []byte) {
320 if r.flags&SkipHTML != 0 {
321 return
322 }
323
324 doubleSpace(out)
325 out.Write(text)
326 out.WriteByte('\n')
327}
328
329func (r *Html) HRule() {
330 doubleSpace(out)
331 out.WriteString("<hr")
332 out.WriteString(r.closeTag)
333 out.WriteByte('\n')
334}
335
336func (r *Html) BlockCode(text []byte, lang string) {
337 doubleSpace(out)
338
339 // parse out the language names/classes
340 count := 0
341 for _, elt := range strings.Fields(lang) {
342 if elt[0] == '.' {
343 elt = elt[1:]
344 }
345 if len(elt) == 0 {
346 continue
347 }
348 if count == 0 {
349 out.WriteString("<pre><code class=\"language-")
350 } else {
351 out.WriteByte(' ')
352 }
353 attrEscape(out, []byte(elt))
354 count++
355 }
356
357 if count == 0 {
358 out.WriteString("<pre><code>")
359 } else {
360 out.WriteString("\">")
361 }
362
363 attrEscape(out, text)
364 out.WriteString("</code></pre>\n")
365}
366
367func (r *Html) BlockQuote(text []byte) {
368 doubleSpace(out)
369 out.WriteString("<blockquote>\n")
370 out.Write(text)
371 out.WriteString("</blockquote>\n")
372}
373
374func (r *Html) Table(header []byte, body []byte, columnData []int) {
375 doubleSpace(out)
376 out.WriteString("<table>\n<thead>\n")
377 out.Write(header)
378 out.WriteString("</thead>\n\n<tbody>\n")
379 out.Write(body)
380 out.WriteString("</tbody>\n</table>\n")
381}
382
383func (r *Html) TableRow(text []byte) {
384 doubleSpace(out)
385 out.WriteString("<tr>\n")
386 out.Write(text)
387 out.WriteString("\n</tr>\n")
388}
389
390func (r *Html) TableHeaderCell(out *bytes.Buffer, text []byte, align int) {
391 doubleSpace(out)
392 switch align {
393 case TableAlignmentLeft:
394 out.WriteString("<th align=\"left\">")
395 case TableAlignmentRight:
396 out.WriteString("<th align=\"right\">")
397 case TableAlignmentCenter:
398 out.WriteString("<th align=\"center\">")
399 default:
400 out.WriteString("<th>")
401 }
402
403 out.Write(text)
404 out.WriteString("</th>")
405}
406
407func (r *Html) TableCell(out *bytes.Buffer, text []byte, align int) {
408 doubleSpace(out)
409 switch align {
410 case TableAlignmentLeft:
411 out.WriteString("<td align=\"left\">")
412 case TableAlignmentRight:
413 out.WriteString("<td align=\"right\">")
414 case TableAlignmentCenter:
415 out.WriteString("<td align=\"center\">")
416 default:
417 out.WriteString("<td>")
418 }
419
420 out.Write(text)
421 out.WriteString("</td>")
422}
423
424func (r *Html) BeginFootnotes() {
425 out.WriteString("<div class=\"footnotes\">\n")
426 r.HRule()
427 r.BeginList(ListTypeOrdered)
428}
429
430func (r *Html) EndFootnotes() {
431 r.EndList(ListTypeOrdered)
432 out.WriteString("</div>\n")
433}
434
435func (r *Html) FootnoteItem(name, text []byte, flags ListType) {
436 if flags&ListItemContainsBlock != 0 || flags&ListItemBeginningOfList != 0 {
437 doubleSpace(out)
438 }
439 slug := slugify(name)
440 out.WriteString(`<li id="`)
441 out.WriteString(`fn:`)
442 out.WriteString(r.parameters.FootnoteAnchorPrefix)
443 out.Write(slug)
444 out.WriteString(`">`)
445 out.Write(text)
446 if r.flags&FootnoteReturnLinks != 0 {
447 out.WriteString(` <a class="footnote-return" href="#`)
448 out.WriteString(`fnref:`)
449 out.WriteString(r.parameters.FootnoteAnchorPrefix)
450 out.Write(slug)
451 out.WriteString(`">`)
452 out.WriteString(r.parameters.FootnoteReturnLinkContents)
453 out.WriteString(`</a>`)
454 }
455 out.WriteString("</li>\n")
456}
457
458func (r *Html) BeginList(flags ListType) {
459 doubleSpace(out)
460
461 if flags&ListTypeDefinition != 0 {
462 out.WriteString("<dl>")
463 } else if flags&ListTypeOrdered != 0 {
464 out.WriteString("<ol>")
465 } else {
466 out.WriteString("<ul>")
467 }
468}
469
470func (r *Html) EndList(flags ListType) {
471 if flags&ListTypeDefinition != 0 {
472 out.WriteString("</dl>\n")
473 } else if flags&ListTypeOrdered != 0 {
474 out.WriteString("</ol>\n")
475 } else {
476 out.WriteString("</ul>\n")
477 }
478}
479
480func (r *Html) ListItem(text []byte, flags ListType) {
481 if (flags&ListItemContainsBlock != 0 && flags&ListTypeDefinition == 0) ||
482 flags&ListItemBeginningOfList != 0 {
483 doubleSpace(out)
484 }
485 if flags&ListTypeTerm != 0 {
486 out.WriteString("<dt>")
487 } else if flags&ListTypeDefinition != 0 {
488 out.WriteString("<dd>")
489 } else {
490 out.WriteString("<li>")
491 }
492 out.Write(text)
493 if flags&ListTypeTerm != 0 {
494 out.WriteString("</dt>\n")
495 } else if flags&ListTypeDefinition != 0 {
496 out.WriteString("</dd>\n")
497 } else {
498 out.WriteString("</li>\n")
499 }
500}
501
502func (r *Html) BeginParagraph() {
503 doubleSpace(out)
504 out.WriteString("<p>")
505}
506
507func (r *Html) EndParagraph() {
508 out.WriteString("</p>\n")
509}
510
511func (r *Html) AutoLink(link []byte, kind LinkType) {
512 skipRanges := htmlEntity.FindAllIndex(link, -1)
513 if r.flags&Safelink != 0 && !isSafeLink(link) && kind != LinkTypeEmail {
514 // mark it but don't link it if it is not a safe link: no smartypants
515 out.WriteString("<tt>")
516 entityEscapeWithSkip(out, link, skipRanges)
517 out.WriteString("</tt>")
518 return
519 }
520
521 out.WriteString("<a href=\"")
522 if kind == LinkTypeEmail {
523 out.WriteString("mailto:")
524 } else {
525 r.maybeWriteAbsolutePrefix(out, link)
526 }
527
528 entityEscapeWithSkip(out, link, skipRanges)
529
530 var relAttrs []string
531 if r.flags&NofollowLinks != 0 && !isRelativeLink(link) {
532 relAttrs = append(relAttrs, "nofollow")
533 }
534 if r.flags&NoreferrerLinks != 0 && !isRelativeLink(link) {
535 relAttrs = append(relAttrs, "noreferrer")
536 }
537 if len(relAttrs) > 0 {
538 out.WriteString(fmt.Sprintf("\" rel=\"%s", strings.Join(relAttrs, " ")))
539 }
540
541 // blank target only add to external link
542 if r.flags&HrefTargetBlank != 0 && !isRelativeLink(link) {
543 out.WriteString("\" target=\"_blank")
544 }
545
546 out.WriteString("\">")
547
548 // Pretty print: if we get an email address as
549 // an actual URI, e.g. `mailto:foo@bar.com`, we don't
550 // want to print the `mailto:` prefix
551 switch {
552 case bytes.HasPrefix(link, []byte("mailto://")):
553 attrEscape(out, link[len("mailto://"):])
554 case bytes.HasPrefix(link, []byte("mailto:")):
555 attrEscape(out, link[len("mailto:"):])
556 default:
557 entityEscapeWithSkip(out, link, skipRanges)
558 }
559
560 out.WriteString("</a>")
561}
562
563func (r *Html) CodeSpan(text []byte) {
564 out.WriteString("<code>")
565 attrEscape(out, text)
566 out.WriteString("</code>")
567}
568
569func (r *Html) DoubleEmphasis(text []byte) {
570 out.WriteString("<strong>")
571 out.Write(text)
572 out.WriteString("</strong>")
573}
574
575func (r *Html) Emphasis(text []byte) {
576 if len(text) == 0 {
577 return
578 }
579 out.WriteString("<em>")
580 out.Write(text)
581 out.WriteString("</em>")
582}
583
584func (r *Html) maybeWriteAbsolutePrefix(link []byte) {
585 if r.parameters.AbsolutePrefix != "" && isRelativeLink(link) && link[0] != '.' {
586 out.WriteString(r.parameters.AbsolutePrefix)
587 if link[0] != '/' {
588 out.WriteByte('/')
589 }
590 }
591}
592
593func (r *Html) Image(link []byte, title []byte, alt []byte) {
594 if r.flags&SkipImages != 0 {
595 return
596 }
597
598 out.WriteString("<img src=\"")
599 r.maybeWriteAbsolutePrefix(out, link)
600 attrEscape(out, link)
601 out.WriteString("\" alt=\"")
602 if len(alt) > 0 {
603 attrEscape(out, alt)
604 }
605 if len(title) > 0 {
606 out.WriteString("\" title=\"")
607 attrEscape(out, title)
608 }
609
610 out.WriteByte('"')
611 out.WriteString(r.closeTag)
612}
613
614func (r *Html) LineBreak() {
615 out.WriteString("<br")
616 out.WriteString(r.closeTag)
617 out.WriteByte('\n')
618}
619
620func (r *Html) Link(link []byte, title []byte, content []byte) {
621 if r.flags&SkipLinks != 0 {
622 // write the link text out but don't link it, just mark it with typewriter font
623 out.WriteString("<tt>")
624 attrEscape(out, content)
625 out.WriteString("</tt>")
626 return
627 }
628
629 if r.flags&Safelink != 0 && !isSafeLink(link) {
630 // write the link text out but don't link it, just mark it with typewriter font
631 out.WriteString("<tt>")
632 attrEscape(out, content)
633 out.WriteString("</tt>")
634 return
635 }
636
637 out.WriteString("<a href=\"")
638 r.maybeWriteAbsolutePrefix(out, link)
639 attrEscape(out, link)
640 if len(title) > 0 {
641 out.WriteString("\" title=\"")
642 attrEscape(out, title)
643 }
644 var relAttrs []string
645 if r.flags&NofollowLinks != 0 && !isRelativeLink(link) {
646 relAttrs = append(relAttrs, "nofollow")
647 }
648 if r.flags&NoreferrerLinks != 0 && !isRelativeLink(link) {
649 relAttrs = append(relAttrs, "noreferrer")
650 }
651 if len(relAttrs) > 0 {
652 out.WriteString(fmt.Sprintf("\" rel=\"%s", strings.Join(relAttrs, " ")))
653 }
654
655 // blank target only add to external link
656 if r.flags&HrefTargetBlank != 0 && !isRelativeLink(link) {
657 out.WriteString("\" target=\"_blank")
658 }
659
660 out.WriteString("\">")
661 out.Write(content)
662 out.WriteString("</a>")
663 return
664}
665
666func (r *Html) RawHtmlTag(text []byte) {
667 if r.flags&SkipHTML != 0 {
668 return
669 }
670 if r.flags&SkipStyle != 0 && isHtmlTag(text, "style") {
671 return
672 }
673 if r.flags&SkipLinks != 0 && isHtmlTag(text, "a") {
674 return
675 }
676 if r.flags&SkipImages != 0 && isHtmlTag(text, "img") {
677 return
678 }
679 out.Write(text)
680}
681
682func (r *Html) TripleEmphasis(text []byte) {
683 out.WriteString("<strong><em>")
684 out.Write(text)
685 out.WriteString("</em></strong>")
686}
687
688func (r *Html) StrikeThrough(text []byte) {
689 out.WriteString("<del>")
690 out.Write(text)
691 out.WriteString("</del>")
692}
693
694func (r *Html) FootnoteRef(ref []byte, id int) {
695 slug := slugify(ref)
696 out.WriteString(`<sup class="footnote-ref" id="`)
697 out.WriteString(`fnref:`)
698 out.WriteString(r.parameters.FootnoteAnchorPrefix)
699 out.Write(slug)
700 out.WriteString(`"><a rel="footnote" href="#`)
701 out.WriteString(`fn:`)
702 out.WriteString(r.parameters.FootnoteAnchorPrefix)
703 out.Write(slug)
704 out.WriteString(`">`)
705 out.WriteString(strconv.Itoa(id))
706 out.WriteString(`</a></sup>`)
707}
708
709func (r *Html) Entity(entity []byte) {
710 out.Write(entity)
711}
712
713func (r *Html) NormalText(text []byte) {
714 if r.flags&UseSmartypants != 0 {
715 r.Smartypants(out, text)
716 } else {
717 attrEscape(out, text)
718 }
719}
720
721func (r *Html) Smartypants(text []byte) {
722 smrt := smartypantsData{false, false}
723
724 // first do normal entity escaping
725 var escaped bytes.Buffer
726 attrEscape(&escaped, text)
727 text = escaped.Bytes()
728
729 mark := 0
730 for i := 0; i < len(text); i++ {
731 if action := r.smartypants[text[i]]; action != nil {
732 if i > mark {
733 out.Write(text[mark:i])
734 }
735
736 previousChar := byte(0)
737 if i > 0 {
738 previousChar = text[i-1]
739 }
740 i += action(out, &smrt, previousChar, text[i:])
741 mark = i + 1
742 }
743 }
744
745 if mark < len(text) {
746 out.Write(text[mark:])
747 }
748}
749
750func (r *Html) DocumentHeader() {
751 if r.flags&CompletePage == 0 {
752 return
753 }
754
755 ending := ""
756 if r.flags&UseXHTML != 0 {
757 out.WriteString("<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" ")
758 out.WriteString("\"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">\n")
759 out.WriteString("<html xmlns=\"http://www.w3.org/1999/xhtml\">\n")
760 ending = " /"
761 } else {
762 out.WriteString("<!DOCTYPE html>\n")
763 out.WriteString("<html>\n")
764 }
765 out.WriteString("<head>\n")
766 out.WriteString(" <title>")
767 r.NormalText([]byte(r.title))
768 out.WriteString("</title>\n")
769 out.WriteString(" <meta name=\"GENERATOR\" content=\"Blackfriday Markdown Processor v")
770 out.WriteString(VERSION)
771 out.WriteString("\"")
772 out.WriteString(ending)
773 out.WriteString(">\n")
774 out.WriteString(" <meta charset=\"utf-8\"")
775 out.WriteString(ending)
776 out.WriteString(">\n")
777 if r.css != "" {
778 out.WriteString(" <link rel=\"stylesheet\" type=\"text/css\" href=\"")
779 attrEscape(out, []byte(r.css))
780 out.WriteString("\"")
781 out.WriteString(ending)
782 out.WriteString(">\n")
783 }
784 out.WriteString("</head>\n")
785 out.WriteString("<body>\n")
786
787 r.tocMarker = out.Len()
788}
789
790func (r *Html) DocumentFooter() {
791 // finalize and insert the table of contents
792 if r.flags&Toc != 0 {
793 r.TocFinalize()
794
795 // now we have to insert the table of contents into the document
796 var temp bytes.Buffer
797
798 // start by making a copy of everything after the document header
799 temp.Write(out.Bytes()[r.tocMarker:])
800
801 // now clear the copied material from the main output buffer
802 out.Truncate(r.tocMarker)
803
804 // corner case spacing issue
805 if r.flags&CompletePage != 0 {
806 out.WriteByte('\n')
807 }
808
809 // insert the table of contents
810 out.WriteString("<nav>\n")
811 out.Write(r.toc.Bytes())
812 out.WriteString("</nav>\n")
813
814 // corner case spacing issue
815 if r.flags&CompletePage == 0 && r.flags&OmitContents == 0 {
816 out.WriteByte('\n')
817 }
818
819 // write out everything that came after it
820 if r.flags&OmitContents == 0 {
821 out.Write(temp.Bytes())
822 }
823 }
824
825 if r.flags&CompletePage != 0 {
826 out.WriteString("\n</body>\n")
827 out.WriteString("</html>\n")
828 }
829
830}
831
832func (r *Html) TocHeaderWithAnchor(text []byte, level int, anchor string) {
833 for level > r.currentLevel {
834 switch {
835 case bytes.HasSuffix(r.toc.Bytes(), []byte("</li>\n")):
836 // this sublist can nest underneath a header
837 size := r.toc.Len()
838 r.toc.Truncate(size - len("</li>\n"))
839
840 case r.currentLevel > 0:
841 r.toc.WriteString("<li>")
842 }
843 if r.toc.Len() > 0 {
844 r.toc.WriteByte('\n')
845 }
846 r.toc.WriteString("<ul>\n")
847 r.currentLevel++
848 }
849
850 for level < r.currentLevel {
851 r.toc.WriteString("</ul>")
852 if r.currentLevel > 1 {
853 r.toc.WriteString("</li>\n")
854 }
855 r.currentLevel--
856 }
857
858 r.toc.WriteString("<li><a href=\"#")
859 if anchor != "" {
860 r.toc.WriteString(anchor)
861 } else {
862 r.toc.WriteString("toc_")
863 r.toc.WriteString(strconv.Itoa(r.headerCount))
864 }
865 r.toc.WriteString("\">")
866 r.headerCount++
867
868 r.toc.Write(text)
869
870 r.toc.WriteString("</a></li>\n")
871}
872
873func (r *Html) TocHeader(text []byte, level int) {
874 r.TocHeaderWithAnchor(text, level, "")
875}
876
877func (r *Html) TocFinalize() {
878 for r.currentLevel > 1 {
879 r.toc.WriteString("</ul></li>\n")
880 r.currentLevel--
881 }
882
883 if r.currentLevel > 0 {
884 r.toc.WriteString("</ul>\n")
885 }
886}
887
888func isHtmlTag(tag []byte, tagname string) bool {
889 found, _ := findHtmlTagPos(tag, tagname)
890 return found
891}
892
893// Look for a character, but ignore it when it's in any kind of quotes, it
894// might be JavaScript
895func skipUntilCharIgnoreQuotes(html []byte, start int, char byte) int {
896 inSingleQuote := false
897 inDoubleQuote := false
898 inGraveQuote := false
899 i := start
900 for i < len(html) {
901 switch {
902 case html[i] == char && !inSingleQuote && !inDoubleQuote && !inGraveQuote:
903 return i
904 case html[i] == '\'':
905 inSingleQuote = !inSingleQuote
906 case html[i] == '"':
907 inDoubleQuote = !inDoubleQuote
908 case html[i] == '`':
909 inGraveQuote = !inGraveQuote
910 }
911 i++
912 }
913 return start
914}
915
916func findHtmlTagPos(tag []byte, tagname string) (bool, int) {
917 i := 0
918 if i < len(tag) && tag[0] != '<' {
919 return false, -1
920 }
921 i++
922 i = skipSpace(tag, i)
923
924 if i < len(tag) && tag[i] == '/' {
925 i++
926 }
927
928 i = skipSpace(tag, i)
929 j := 0
930 for ; i < len(tag); i, j = i+1, j+1 {
931 if j >= len(tagname) {
932 break
933 }
934
935 if strings.ToLower(string(tag[i]))[0] != tagname[j] {
936 return false, -1
937 }
938 }
939
940 if i == len(tag) {
941 return false, -1
942 }
943
944 rightAngle := skipUntilCharIgnoreQuotes(tag, i, '>')
945 if rightAngle > i {
946 return true, rightAngle
947 }
948
949 return false, -1
950}
951
952func skipUntilChar(text []byte, start int, char byte) int {
953 i := start
954 for i < len(text) && text[i] != char {
955 i++
956 }
957 return i
958}
959
960func skipSpace(tag []byte, i int) int {
961 for i < len(tag) && isspace(tag[i]) {
962 i++
963 }
964 return i
965}
966
967func skipChar(data []byte, start int, char byte) int {
968 i := start
969 for i < len(data) && data[i] == char {
970 i++
971 }
972 return i
973}
974
975func doubleSpace(out *bytes.Buffer) {
976 if out.Len() > 0 {
977 out.WriteByte('\n')
978 }
979}
980
981func isRelativeLink(link []byte) (yes bool) {
982 // a tag begin with '#'
983 if link[0] == '#' {
984 return true
985 }
986
987 // link begin with '/' but not '//', the second maybe a protocol relative link
988 if len(link) >= 2 && link[0] == '/' && link[1] != '/' {
989 return true
990 }
991
992 // only the root '/'
993 if len(link) == 1 && link[0] == '/' {
994 return true
995 }
996
997 // current directory : begin with "./"
998 if bytes.HasPrefix(link, []byte("./")) {
999 return true
1000 }
1001
1002 // parent directory : begin with "../"
1003 if bytes.HasPrefix(link, []byte("../")) {
1004 return true
1005 }
1006
1007 return false
1008}
1009
1010func (r *Html) ensureUniqueHeaderID(id string) string {
1011 for count, found := r.headerIDs[id]; found; count, found = r.headerIDs[id] {
1012 tmp := fmt.Sprintf("%s-%d", id, count+1)
1013
1014 if _, tmpFound := r.headerIDs[tmp]; !tmpFound {
1015 r.headerIDs[id] = count + 1
1016 id = tmp
1017 } else {
1018 id = id + "-1"
1019 }
1020 }
1021
1022 if _, found := r.headerIDs[id]; !found {
1023 r.headerIDs[id] = 0
1024 }
1025
1026 return id
1027}