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