all repos — grayfriday @ 195dac9f5b4031be2dfd529114a6191a1b512d31

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