html.go

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