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