all repos — grayfriday @ ee98bc0bf4ddd4670dfad41751d5162470c18dca

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