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