all repos — grayfriday @ a5e88a33508709183047034d861fa7b8641d2ccb

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