all repos — grayfriday @ bd11a52f1ea4de72f2f9e2f121e048bcc693dccb

blackfriday fork with a few changes

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 "&quot;", true
148	}
149	if char == '&' {
150		return "&amp;", true
151	}
152	if char == '<' {
153		return "&lt;", true
154	}
155	if char == '>' {
156		return "&gt;", 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}