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 """, true
137 }
138 if char == '&' {
139 return "&", true
140 }
141 if char == '<' {
142 return "<", true
143 }
144 if char == '>' {
145 return ">", 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}