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