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 "html"
22 "regexp"
23 "strconv"
24 "strings"
25)
26
27type HtmlFlags int
28
29// Html renderer configuration options.
30const (
31 HtmlFlagsNone HtmlFlags = 0
32 SkipHTML HtmlFlags = 1 << iota // Skip preformatted HTML blocks
33 SkipStyle // Skip embedded <style> elements
34 SkipImages // Skip embedded images
35 SkipLinks // Skip all links
36 Safelink // Only link to trusted protocols
37 NofollowLinks // Only link with rel="nofollow"
38 NoreferrerLinks // Only link with rel="noreferrer"
39 HrefTargetBlank // Add a blank target
40 Toc // Generate a table of contents
41 OmitContents // Skip the main contents (for a standalone table of contents)
42 CompletePage // Generate a complete HTML page
43 UseXHTML // Generate XHTML output instead of HTML
44 UseSmartypants // Enable smart punctuation substitutions
45 SmartypantsFractions // Enable smart fractions (with UseSmartypants)
46 SmartypantsDashes // Enable smart dashes (with UseSmartypants)
47 SmartypantsLatexDashes // Enable LaTeX-style dashes (with UseSmartypants)
48 SmartypantsAngledQuotes // Enable angled double quotes (with UseSmartypants) for double quotes rendering
49 FootnoteReturnLinks // Generate a link at the end of a footnote to return to the source
50
51 TagName = "[A-Za-z][A-Za-z0-9-]*"
52 AttributeName = "[a-zA-Z_:][a-zA-Z0-9:._-]*"
53 UnquotedValue = "[^\"'=<>`\\x00-\\x20]+"
54 SingleQuotedValue = "'[^']*'"
55 DoubleQuotedValue = "\"[^\"]*\""
56 AttributeValue = "(?:" + UnquotedValue + "|" + SingleQuotedValue + "|" + DoubleQuotedValue + ")"
57 AttributeValueSpec = "(?:" + "\\s*=" + "\\s*" + AttributeValue + ")"
58 Attribute = "(?:" + "\\s+" + AttributeName + AttributeValueSpec + "?)"
59 OpenTag = "<" + TagName + Attribute + "*" + "\\s*/?>"
60 CloseTag = "</" + TagName + "\\s*[>]"
61 HTMLComment = "<!---->|<!--(?:-?[^>-])(?:-?[^-])*-->"
62 ProcessingInstruction = "[<][?].*?[?][>]"
63 Declaration = "<![A-Z]+" + "\\s+[^>]*>"
64 CDATA = "<!\\[CDATA\\[[\\s\\S]*?\\]\\]>"
65 HTMLTag = "(?:" + OpenTag + "|" + CloseTag + "|" + HTMLComment + "|" +
66 ProcessingInstruction + "|" + Declaration + "|" + CDATA + ")"
67)
68
69var (
70 // TODO: improve this regexp to catch all possible entities:
71 htmlEntity = regexp.MustCompile(`&[a-z]{2,5};`)
72 reHtmlTag = regexp.MustCompile("(?i)^" + HTMLTag)
73)
74
75type HtmlRendererParameters struct {
76 // Prepend this text to each relative URL.
77 AbsolutePrefix string
78 // Add this text to each footnote anchor, to ensure uniqueness.
79 FootnoteAnchorPrefix string
80 // Show this text inside the <a> tag for a footnote return link, if the
81 // HTML_FOOTNOTE_RETURN_LINKS flag is enabled. If blank, the string
82 // <sup>[return]</sup> is used.
83 FootnoteReturnLinkContents string
84 // If set, add this text to the front of each Header ID, to ensure
85 // uniqueness.
86 HeaderIDPrefix string
87 // If set, add this text to the back of each Header ID, to ensure uniqueness.
88 HeaderIDSuffix string
89}
90
91// Html is a type that implements the Renderer interface for HTML output.
92//
93// Do not create this directly, instead use the HtmlRenderer function.
94type Html struct {
95 flags HtmlFlags
96 closeTag string // how to end singleton tags: either " />" or ">"
97 title string // document title
98 css string // optional css file url (used with HTML_COMPLETE_PAGE)
99
100 parameters HtmlRendererParameters
101
102 // table of contents data
103 tocMarker int
104 headerCount int
105 currentLevel int
106 toc *bytes.Buffer
107
108 // Track header IDs to prevent ID collision in a single generation.
109 headerIDs map[string]int
110
111 smartypants *smartypantsRenderer
112 w HtmlWriter
113}
114
115const (
116 xhtmlClose = " />"
117 htmlClose = ">"
118)
119
120// HtmlRenderer creates and configures an Html object, which
121// satisfies the Renderer interface.
122//
123// flags is a set of HtmlFlags ORed together.
124// title is the title of the document, and css is a URL for the document's
125// stylesheet.
126// title and css are only used when HTML_COMPLETE_PAGE is selected.
127func HtmlRenderer(flags HtmlFlags, title string, css string) Renderer {
128 return HtmlRendererWithParameters(flags, title, css, HtmlRendererParameters{})
129}
130
131type HtmlWriter struct {
132 output bytes.Buffer
133 captureBuff *bytes.Buffer
134 copyBuff *bytes.Buffer
135 dirty bool
136}
137
138func (w *HtmlWriter) Write(p []byte) (n int, err error) {
139 w.dirty = true
140 if w.copyBuff != nil {
141 w.copyBuff.Write(p)
142 }
143 if w.captureBuff != nil {
144 w.captureBuff.Write(p)
145 return
146 }
147 return w.output.Write(p)
148}
149
150func (w *HtmlWriter) WriteString(s string) (n int, err error) {
151 w.dirty = true
152 if w.copyBuff != nil {
153 w.copyBuff.WriteString(s)
154 }
155 if w.captureBuff != nil {
156 w.captureBuff.WriteString(s)
157 return
158 }
159 return w.output.WriteString(s)
160}
161
162func (w *HtmlWriter) WriteByte(b byte) error {
163 w.dirty = true
164 if w.copyBuff != nil {
165 w.copyBuff.WriteByte(b)
166 }
167 if w.captureBuff != nil {
168 return w.captureBuff.WriteByte(b)
169 }
170 return w.output.WriteByte(b)
171}
172
173// Writes out a newline if the output is not pristine. Used at the beginning of
174// every rendering func
175func (w *HtmlWriter) Newline() {
176 if w.dirty {
177 w.WriteByte('\n')
178 }
179}
180
181func (r *Html) CaptureWrites(processor func()) []byte {
182 var output bytes.Buffer
183 // preserve old captureBuff state for possible nested captures:
184 tmp := r.w.captureBuff
185 tmpd := r.w.dirty
186 r.w.captureBuff = &output
187 r.w.dirty = false
188 processor()
189 // restore:
190 r.w.captureBuff = tmp
191 r.w.dirty = tmpd
192 return output.Bytes()
193}
194
195func (r *Html) CopyWrites(processor func()) []byte {
196 var output bytes.Buffer
197 r.w.copyBuff = &output
198 processor()
199 r.w.copyBuff = nil
200 return output.Bytes()
201}
202
203func (r *Html) Write(b []byte) (int, error) {
204 return r.w.Write(b)
205}
206
207func (r *Html) GetResult() []byte {
208 return r.w.output.Bytes()
209}
210
211func HtmlRendererWithParameters(flags HtmlFlags, title string,
212 css string, renderParameters HtmlRendererParameters) Renderer {
213 // configure the rendering engine
214 closeTag := htmlClose
215 if flags&UseXHTML != 0 {
216 closeTag = xhtmlClose
217 }
218
219 if renderParameters.FootnoteReturnLinkContents == "" {
220 renderParameters.FootnoteReturnLinkContents = `<sup>[return]</sup>`
221 }
222
223 var writer HtmlWriter
224 return &Html{
225 flags: flags,
226 closeTag: closeTag,
227 title: title,
228 css: css,
229 parameters: renderParameters,
230
231 headerCount: 0,
232 currentLevel: 0,
233 toc: new(bytes.Buffer),
234
235 headerIDs: make(map[string]int),
236
237 smartypants: smartypants(flags),
238 w: writer,
239 }
240}
241
242// Using if statements is a bit faster than a switch statement. As the compiler
243// improves, this should be unnecessary this is only worthwhile because
244// attrEscape is the single largest CPU user in normal use.
245// Also tried using map, but that gave a ~3x slowdown.
246func escapeSingleChar(char byte) (string, bool) {
247 if char == '"' {
248 return """, true
249 }
250 if char == '&' {
251 return "&", true
252 }
253 if char == '<' {
254 return "<", true
255 }
256 if char == '>' {
257 return ">", true
258 }
259 return "", false
260}
261
262func (r *Html) attrEscape(src []byte) {
263 org := 0
264 for i, ch := range src {
265 if entity, ok := escapeSingleChar(ch); ok {
266 if i > org {
267 // copy all the normal characters since the last escape
268 r.w.Write(src[org:i])
269 }
270 org = i + 1
271 r.w.WriteString(entity)
272 }
273 }
274 if org < len(src) {
275 r.w.Write(src[org:])
276 }
277}
278
279func attrEscape2(src []byte) []byte {
280 unesc := []byte(html.UnescapeString(string(src)))
281 esc1 := []byte(html.EscapeString(string(unesc)))
282 esc2 := bytes.Replace(esc1, []byte("""), []byte("""), -1)
283 return bytes.Replace(esc2, []byte("'"), []byte{'\''}, -1)
284}
285
286func (r *Html) entityEscapeWithSkip(src []byte, skipRanges [][]int) {
287 end := 0
288 for _, rang := range skipRanges {
289 r.attrEscape(src[end:rang[0]])
290 r.w.Write(src[rang[0]:rang[1]])
291 end = rang[1]
292 }
293 r.attrEscape(src[end:])
294}
295
296func (r *Html) GetFlags() HtmlFlags {
297 return r.flags
298}
299
300func (r *Html) TitleBlock(text []byte) {
301 text = bytes.TrimPrefix(text, []byte("% "))
302 text = bytes.Replace(text, []byte("\n% "), []byte("\n"), -1)
303 r.w.WriteString("<h1 class=\"title\">")
304 r.w.Write(text)
305 r.w.WriteString("\n</h1>")
306}
307
308func (r *Html) BeginHeader(level int, id string) {
309 r.w.Newline()
310
311 if id == "" && r.flags&Toc != 0 {
312 id = fmt.Sprintf("toc_%d", r.headerCount)
313 }
314
315 if id != "" {
316 id = r.ensureUniqueHeaderID(id)
317
318 if r.parameters.HeaderIDPrefix != "" {
319 id = r.parameters.HeaderIDPrefix + id
320 }
321
322 if r.parameters.HeaderIDSuffix != "" {
323 id = id + r.parameters.HeaderIDSuffix
324 }
325
326 r.w.WriteString(fmt.Sprintf("<h%d id=\"%s\">", level, id))
327 } else {
328 r.w.WriteString(fmt.Sprintf("<h%d>", level))
329 }
330}
331
332func (r *Html) EndHeader(level int, id string, header []byte) {
333 // are we building a table of contents?
334 if r.flags&Toc != 0 {
335 r.TocHeaderWithAnchor(header, level, id)
336 }
337
338 r.w.WriteString(fmt.Sprintf("</h%d>\n", level))
339}
340
341func (r *Html) BlockHtml(text []byte) {
342 if r.flags&SkipHTML != 0 {
343 return
344 }
345
346 r.w.Newline()
347 r.w.Write(text)
348 r.w.WriteByte('\n')
349}
350
351func (r *Html) HRule() {
352 r.w.Newline()
353 r.w.WriteString("<hr")
354 r.w.WriteString(r.closeTag)
355 r.w.WriteByte('\n')
356}
357
358func (r *Html) BlockCode(text []byte, lang string) {
359 r.w.Newline()
360
361 // parse out the language names/classes
362 count := 0
363 for _, elt := range strings.Fields(lang) {
364 if elt[0] == '.' {
365 elt = elt[1:]
366 }
367 if len(elt) == 0 {
368 continue
369 }
370 if count == 0 {
371 r.w.WriteString("<pre><code class=\"language-")
372 } else {
373 r.w.WriteByte(' ')
374 }
375 r.attrEscape([]byte(elt))
376 count++
377 }
378
379 if count == 0 {
380 r.w.WriteString("<pre><code>")
381 } else {
382 r.w.WriteString("\">")
383 }
384
385 r.attrEscape(text)
386 r.w.WriteString("</code></pre>\n")
387}
388
389func (r *Html) BlockQuote(text []byte) {
390 r.w.Newline()
391 r.w.WriteString("<blockquote>\n")
392 r.w.Write(text)
393 r.w.WriteString("</blockquote>\n")
394}
395
396func (r *Html) Table(header []byte, body []byte, columnData []int) {
397 r.w.Newline()
398 r.w.WriteString("<table>\n<thead>\n")
399 r.w.Write(header)
400 r.w.WriteString("</thead>\n\n<tbody>\n")
401 r.w.Write(body)
402 r.w.WriteString("</tbody>\n</table>\n")
403}
404
405func (r *Html) TableRow(text []byte) {
406 r.w.Newline()
407 r.w.WriteString("<tr>\n")
408 r.w.Write(text)
409 r.w.WriteString("\n</tr>\n")
410}
411
412func leadingNewline(out *bytes.Buffer) {
413 if out.Len() > 0 {
414 out.WriteByte('\n')
415 }
416}
417
418func (r *Html) TableHeaderCell(out *bytes.Buffer, text []byte, align int) {
419 leadingNewline(out)
420 switch align {
421 case TableAlignmentLeft:
422 out.WriteString("<th align=\"left\">")
423 case TableAlignmentRight:
424 out.WriteString("<th align=\"right\">")
425 case TableAlignmentCenter:
426 out.WriteString("<th align=\"center\">")
427 default:
428 out.WriteString("<th>")
429 }
430
431 out.Write(text)
432 out.WriteString("</th>")
433}
434
435func (r *Html) TableCell(out *bytes.Buffer, text []byte, align int) {
436 leadingNewline(out)
437 switch align {
438 case TableAlignmentLeft:
439 out.WriteString("<td align=\"left\">")
440 case TableAlignmentRight:
441 out.WriteString("<td align=\"right\">")
442 case TableAlignmentCenter:
443 out.WriteString("<td align=\"center\">")
444 default:
445 out.WriteString("<td>")
446 }
447
448 out.Write(text)
449 out.WriteString("</td>")
450}
451
452func (r *Html) BeginFootnotes() {
453 r.w.WriteString("<div class=\"footnotes\">\n")
454 r.HRule()
455 r.BeginList(ListTypeOrdered)
456}
457
458func (r *Html) EndFootnotes() {
459 r.EndList(ListTypeOrdered)
460 r.w.WriteString("</div>\n")
461}
462
463func (r *Html) FootnoteItem(name, text []byte, flags ListType) {
464 if flags&ListItemContainsBlock != 0 || flags&ListItemBeginningOfList != 0 {
465 r.w.Newline()
466 }
467 slug := slugify(name)
468 r.w.WriteString(`<li id="`)
469 r.w.WriteString(`fn:`)
470 r.w.WriteString(r.parameters.FootnoteAnchorPrefix)
471 r.w.Write(slug)
472 r.w.WriteString(`">`)
473 r.w.Write(text)
474 if r.flags&FootnoteReturnLinks != 0 {
475 r.w.WriteString(` <a class="footnote-return" href="#`)
476 r.w.WriteString(`fnref:`)
477 r.w.WriteString(r.parameters.FootnoteAnchorPrefix)
478 r.w.Write(slug)
479 r.w.WriteString(`">`)
480 r.w.WriteString(r.parameters.FootnoteReturnLinkContents)
481 r.w.WriteString(`</a>`)
482 }
483 r.w.WriteString("</li>\n")
484}
485
486func (r *Html) BeginList(flags ListType) {
487 r.w.Newline()
488
489 if flags&ListTypeDefinition != 0 {
490 r.w.WriteString("<dl>")
491 } else if flags&ListTypeOrdered != 0 {
492 r.w.WriteString("<ol>")
493 } else {
494 r.w.WriteString("<ul>")
495 }
496}
497
498func (r *Html) EndList(flags ListType) {
499 if flags&ListTypeDefinition != 0 {
500 r.w.WriteString("</dl>\n")
501 } else if flags&ListTypeOrdered != 0 {
502 r.w.WriteString("</ol>\n")
503 } else {
504 r.w.WriteString("</ul>\n")
505 }
506}
507
508func (r *Html) ListItem(text []byte, flags ListType) {
509 if (flags&ListItemContainsBlock != 0 && flags&ListTypeDefinition == 0) ||
510 flags&ListItemBeginningOfList != 0 {
511 r.w.Newline()
512 }
513 if flags&ListTypeTerm != 0 {
514 r.w.WriteString("<dt>")
515 } else if flags&ListTypeDefinition != 0 {
516 r.w.WriteString("<dd>")
517 } else {
518 r.w.WriteString("<li>")
519 }
520 r.w.Write(text)
521 if flags&ListTypeTerm != 0 {
522 r.w.WriteString("</dt>\n")
523 } else if flags&ListTypeDefinition != 0 {
524 r.w.WriteString("</dd>\n")
525 } else {
526 r.w.WriteString("</li>\n")
527 }
528}
529
530func (r *Html) BeginParagraph() {
531 r.w.Newline()
532 r.w.WriteString("<p>")
533}
534
535func (r *Html) EndParagraph() {
536 r.w.WriteString("</p>\n")
537}
538
539func (r *Html) AutoLink(link []byte, kind LinkType) {
540 skipRanges := htmlEntity.FindAllIndex(link, -1)
541 if r.flags&Safelink != 0 && !isSafeLink(link) && kind != LinkTypeEmail {
542 // mark it but don't link it if it is not a safe link: no smartypants
543 r.w.WriteString("<tt>")
544 r.entityEscapeWithSkip(link, skipRanges)
545 r.w.WriteString("</tt>")
546 return
547 }
548
549 r.w.WriteString("<a href=\"")
550 if kind == LinkTypeEmail {
551 r.w.WriteString("mailto:")
552 } else {
553 r.maybeWriteAbsolutePrefix(link)
554 }
555
556 r.entityEscapeWithSkip(link, skipRanges)
557
558 var relAttrs []string
559 if r.flags&NofollowLinks != 0 && !isRelativeLink(link) {
560 relAttrs = append(relAttrs, "nofollow")
561 }
562 if r.flags&NoreferrerLinks != 0 && !isRelativeLink(link) {
563 relAttrs = append(relAttrs, "noreferrer")
564 }
565 if len(relAttrs) > 0 {
566 r.w.WriteString(fmt.Sprintf("\" rel=\"%s", strings.Join(relAttrs, " ")))
567 }
568
569 // blank target only add to external link
570 if r.flags&HrefTargetBlank != 0 && !isRelativeLink(link) {
571 r.w.WriteString("\" target=\"_blank")
572 }
573
574 r.w.WriteString("\">")
575
576 // Pretty print: if we get an email address as
577 // an actual URI, e.g. `mailto:foo@bar.com`, we don't
578 // want to print the `mailto:` prefix
579 switch {
580 case bytes.HasPrefix(link, []byte("mailto://")):
581 r.attrEscape(link[len("mailto://"):])
582 case bytes.HasPrefix(link, []byte("mailto:")):
583 r.attrEscape(link[len("mailto:"):])
584 default:
585 r.entityEscapeWithSkip(link, skipRanges)
586 }
587
588 r.w.WriteString("</a>")
589}
590
591func (r *Html) CodeSpan(text []byte) {
592 r.w.WriteString("<code>")
593 r.attrEscape(text)
594 r.w.WriteString("</code>")
595}
596
597func (r *Html) DoubleEmphasis(text []byte) {
598 r.w.WriteString("<strong>")
599 r.w.Write(text)
600 r.w.WriteString("</strong>")
601}
602
603func (r *Html) Emphasis(text []byte) {
604 if len(text) == 0 {
605 return
606 }
607 r.w.WriteString("<em>")
608 r.w.Write(text)
609 r.w.WriteString("</em>")
610}
611
612func (r *Html) maybeWriteAbsolutePrefix(link []byte) {
613 if r.parameters.AbsolutePrefix != "" && isRelativeLink(link) && link[0] != '.' {
614 r.w.WriteString(r.parameters.AbsolutePrefix)
615 if link[0] != '/' {
616 r.w.WriteByte('/')
617 }
618 }
619}
620
621func (r *Html) Image(link []byte, title []byte, alt []byte) {
622 if r.flags&SkipImages != 0 {
623 return
624 }
625
626 r.w.WriteString("<img src=\"")
627 r.maybeWriteAbsolutePrefix(link)
628 r.attrEscape(link)
629 r.w.WriteString("\" alt=\"")
630 if len(alt) > 0 {
631 r.attrEscape(alt)
632 }
633 if len(title) > 0 {
634 r.w.WriteString("\" title=\"")
635 r.attrEscape(title)
636 }
637
638 r.w.WriteByte('"')
639 r.w.WriteString(r.closeTag)
640}
641
642func (r *Html) LineBreak() {
643 r.w.WriteString("<br")
644 r.w.WriteString(r.closeTag)
645 r.w.WriteByte('\n')
646}
647
648func (r *Html) Link(link []byte, title []byte, content []byte) {
649 if r.flags&SkipLinks != 0 {
650 // write the link text out but don't link it, just mark it with typewriter font
651 r.w.WriteString("<tt>")
652 r.attrEscape(content)
653 r.w.WriteString("</tt>")
654 return
655 }
656
657 if r.flags&Safelink != 0 && !isSafeLink(link) {
658 // write the link text out but don't link it, just mark it with typewriter font
659 r.w.WriteString("<tt>")
660 r.attrEscape(content)
661 r.w.WriteString("</tt>")
662 return
663 }
664
665 r.w.WriteString("<a href=\"")
666 r.maybeWriteAbsolutePrefix(link)
667 r.attrEscape(link)
668 if len(title) > 0 {
669 r.w.WriteString("\" title=\"")
670 r.attrEscape(title)
671 }
672 var relAttrs []string
673 if r.flags&NofollowLinks != 0 && !isRelativeLink(link) {
674 relAttrs = append(relAttrs, "nofollow")
675 }
676 if r.flags&NoreferrerLinks != 0 && !isRelativeLink(link) {
677 relAttrs = append(relAttrs, "noreferrer")
678 }
679 if len(relAttrs) > 0 {
680 r.w.WriteString(fmt.Sprintf("\" rel=\"%s", strings.Join(relAttrs, " ")))
681 }
682
683 // blank target only add to external link
684 if r.flags&HrefTargetBlank != 0 && !isRelativeLink(link) {
685 r.w.WriteString("\" target=\"_blank")
686 }
687
688 r.w.WriteString("\">")
689 r.w.Write(content)
690 r.w.WriteString("</a>")
691 return
692}
693
694func (r *Html) RawHtmlTag(text []byte) {
695 if r.flags&SkipHTML != 0 {
696 return
697 }
698 if r.flags&SkipStyle != 0 && isHtmlTag(text, "style") {
699 return
700 }
701 if r.flags&SkipLinks != 0 && isHtmlTag(text, "a") {
702 return
703 }
704 if r.flags&SkipImages != 0 && isHtmlTag(text, "img") {
705 return
706 }
707 r.w.Write(text)
708}
709
710func (r *Html) TripleEmphasis(text []byte) {
711 r.w.WriteString("<strong><em>")
712 r.w.Write(text)
713 r.w.WriteString("</em></strong>")
714}
715
716func (r *Html) StrikeThrough(text []byte) {
717 r.w.WriteString("<del>")
718 r.w.Write(text)
719 r.w.WriteString("</del>")
720}
721
722func (r *Html) FootnoteRef(ref []byte, id int) {
723 slug := slugify(ref)
724 r.w.WriteString(`<sup class="footnote-ref" id="`)
725 r.w.WriteString(`fnref:`)
726 r.w.WriteString(r.parameters.FootnoteAnchorPrefix)
727 r.w.Write(slug)
728 r.w.WriteString(`"><a rel="footnote" href="#`)
729 r.w.WriteString(`fn:`)
730 r.w.WriteString(r.parameters.FootnoteAnchorPrefix)
731 r.w.Write(slug)
732 r.w.WriteString(`">`)
733 r.w.WriteString(strconv.Itoa(id))
734 r.w.WriteString(`</a></sup>`)
735}
736
737func (r *Html) Entity(entity []byte) {
738 r.w.Write(entity)
739}
740
741func (r *Html) NormalText(text []byte) {
742 if r.flags&UseSmartypants != 0 {
743 r.Smartypants(text)
744 } else {
745 r.attrEscape(text)
746 }
747}
748
749func (r *Html) Smartypants2(text []byte) []byte {
750 smrt := smartypantsData{false, false}
751 var buff bytes.Buffer
752 // first do normal entity escaping
753 text = attrEscape2(text)
754 mark := 0
755 for i := 0; i < len(text); i++ {
756 if action := r.smartypants[text[i]]; action != nil {
757 if i > mark {
758 buff.Write(text[mark:i])
759 }
760 previousChar := byte(0)
761 if i > 0 {
762 previousChar = text[i-1]
763 }
764 var tmp bytes.Buffer
765 i += action(&tmp, &smrt, previousChar, text[i:])
766 buff.Write(tmp.Bytes())
767 mark = i + 1
768 }
769 }
770 if mark < len(text) {
771 buff.Write(text[mark:])
772 }
773 return buff.Bytes()
774}
775
776func (r *Html) Smartypants(text []byte) {
777 smrt := smartypantsData{false, false}
778
779 // first do normal entity escaping
780 text = r.CaptureWrites(func() {
781 r.attrEscape(text)
782 })
783
784 mark := 0
785 for i := 0; i < len(text); i++ {
786 if action := r.smartypants[text[i]]; action != nil {
787 if i > mark {
788 r.w.Write(text[mark:i])
789 }
790
791 previousChar := byte(0)
792 if i > 0 {
793 previousChar = text[i-1]
794 }
795 var tmp bytes.Buffer
796 i += action(&tmp, &smrt, previousChar, text[i:])
797 r.w.Write(tmp.Bytes())
798 mark = i + 1
799 }
800 }
801
802 if mark < len(text) {
803 r.w.Write(text[mark:])
804 }
805}
806
807func (r *Html) DocumentHeader() {
808 if r.flags&CompletePage == 0 {
809 return
810 }
811
812 ending := ""
813 if r.flags&UseXHTML != 0 {
814 r.w.WriteString("<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" ")
815 r.w.WriteString("\"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">\n")
816 r.w.WriteString("<html xmlns=\"http://www.w3.org/1999/xhtml\">\n")
817 ending = " /"
818 } else {
819 r.w.WriteString("<!DOCTYPE html>\n")
820 r.w.WriteString("<html>\n")
821 }
822 r.w.WriteString("<head>\n")
823 r.w.WriteString(" <title>")
824 r.NormalText([]byte(r.title))
825 r.w.WriteString("</title>\n")
826 r.w.WriteString(" <meta name=\"GENERATOR\" content=\"Blackfriday Markdown Processor v")
827 r.w.WriteString(VERSION)
828 r.w.WriteString("\"")
829 r.w.WriteString(ending)
830 r.w.WriteString(">\n")
831 r.w.WriteString(" <meta charset=\"utf-8\"")
832 r.w.WriteString(ending)
833 r.w.WriteString(">\n")
834 if r.css != "" {
835 r.w.WriteString(" <link rel=\"stylesheet\" type=\"text/css\" href=\"")
836 r.attrEscape([]byte(r.css))
837 r.w.WriteString("\"")
838 r.w.WriteString(ending)
839 r.w.WriteString(">\n")
840 }
841 r.w.WriteString("</head>\n")
842 r.w.WriteString("<body>\n")
843
844 r.tocMarker = r.w.output.Len() // XXX
845}
846
847func (r *Html) DocumentFooter() {
848 // finalize and insert the table of contents
849 if r.flags&Toc != 0 {
850 r.TocFinalize()
851
852 // now we have to insert the table of contents into the document
853 var temp bytes.Buffer
854
855 // start by making a copy of everything after the document header
856 temp.Write(r.w.output.Bytes()[r.tocMarker:])
857
858 // now clear the copied material from the main output buffer
859 r.w.output.Truncate(r.tocMarker)
860
861 // corner case spacing issue
862 if r.flags&CompletePage != 0 {
863 r.w.WriteByte('\n')
864 }
865
866 // insert the table of contents
867 r.w.WriteString("<nav>\n")
868 r.w.Write(r.toc.Bytes())
869 r.w.WriteString("</nav>\n")
870
871 // corner case spacing issue
872 if r.flags&CompletePage == 0 && r.flags&OmitContents == 0 {
873 r.w.WriteByte('\n')
874 }
875
876 // write out everything that came after it
877 if r.flags&OmitContents == 0 {
878 r.w.Write(temp.Bytes())
879 }
880 }
881
882 if r.flags&CompletePage != 0 {
883 r.w.WriteString("\n</body>\n")
884 r.w.WriteString("</html>\n")
885 }
886
887}
888
889func (r *Html) TocHeaderWithAnchor(text []byte, level int, anchor string) {
890 for level > r.currentLevel {
891 switch {
892 case bytes.HasSuffix(r.toc.Bytes(), []byte("</li>\n")):
893 // this sublist can nest underneath a header
894 size := r.toc.Len()
895 r.toc.Truncate(size - len("</li>\n"))
896
897 case r.currentLevel > 0:
898 r.toc.WriteString("<li>")
899 }
900 if r.toc.Len() > 0 {
901 r.toc.WriteByte('\n')
902 }
903 r.toc.WriteString("<ul>\n")
904 r.currentLevel++
905 }
906
907 for level < r.currentLevel {
908 r.toc.WriteString("</ul>")
909 if r.currentLevel > 1 {
910 r.toc.WriteString("</li>\n")
911 }
912 r.currentLevel--
913 }
914
915 r.toc.WriteString("<li><a href=\"#")
916 if anchor != "" {
917 r.toc.WriteString(anchor)
918 } else {
919 r.toc.WriteString("toc_")
920 r.toc.WriteString(strconv.Itoa(r.headerCount))
921 }
922 r.toc.WriteString("\">")
923 r.headerCount++
924
925 r.toc.Write(text)
926
927 r.toc.WriteString("</a></li>\n")
928}
929
930func (r *Html) TocHeader(text []byte, level int) {
931 r.TocHeaderWithAnchor(text, level, "")
932}
933
934func (r *Html) TocFinalize() {
935 for r.currentLevel > 1 {
936 r.toc.WriteString("</ul></li>\n")
937 r.currentLevel--
938 }
939
940 if r.currentLevel > 0 {
941 r.toc.WriteString("</ul>\n")
942 }
943}
944
945func isHtmlTag(tag []byte, tagname string) bool {
946 found, _ := findHtmlTagPos(tag, tagname)
947 return found
948}
949
950// Look for a character, but ignore it when it's in any kind of quotes, it
951// might be JavaScript
952func skipUntilCharIgnoreQuotes(html []byte, start int, char byte) int {
953 inSingleQuote := false
954 inDoubleQuote := false
955 inGraveQuote := false
956 i := start
957 for i < len(html) {
958 switch {
959 case html[i] == char && !inSingleQuote && !inDoubleQuote && !inGraveQuote:
960 return i
961 case html[i] == '\'':
962 inSingleQuote = !inSingleQuote
963 case html[i] == '"':
964 inDoubleQuote = !inDoubleQuote
965 case html[i] == '`':
966 inGraveQuote = !inGraveQuote
967 }
968 i++
969 }
970 return start
971}
972
973func findHtmlTagPos(tag []byte, tagname string) (bool, int) {
974 i := 0
975 if i < len(tag) && tag[0] != '<' {
976 return false, -1
977 }
978 i++
979 i = skipSpace(tag, i)
980
981 if i < len(tag) && tag[i] == '/' {
982 i++
983 }
984
985 i = skipSpace(tag, i)
986 j := 0
987 for ; i < len(tag); i, j = i+1, j+1 {
988 if j >= len(tagname) {
989 break
990 }
991
992 if strings.ToLower(string(tag[i]))[0] != tagname[j] {
993 return false, -1
994 }
995 }
996
997 if i == len(tag) {
998 return false, -1
999 }
1000
1001 rightAngle := skipUntilCharIgnoreQuotes(tag, i, '>')
1002 if rightAngle > i {
1003 return true, rightAngle
1004 }
1005
1006 return false, -1
1007}
1008
1009func skipUntilChar(text []byte, start int, char byte) int {
1010 i := start
1011 for i < len(text) && text[i] != char {
1012 i++
1013 }
1014 return i
1015}
1016
1017func skipSpace(tag []byte, i int) int {
1018 for i < len(tag) && isspace(tag[i]) {
1019 i++
1020 }
1021 return i
1022}
1023
1024func skipChar(data []byte, start int, char byte) int {
1025 i := start
1026 for i < len(data) && data[i] == char {
1027 i++
1028 }
1029 return i
1030}
1031
1032func isRelativeLink(link []byte) (yes bool) {
1033 // a tag begin with '#'
1034 if link[0] == '#' {
1035 return true
1036 }
1037
1038 // link begin with '/' but not '//', the second maybe a protocol relative link
1039 if len(link) >= 2 && link[0] == '/' && link[1] != '/' {
1040 return true
1041 }
1042
1043 // only the root '/'
1044 if len(link) == 1 && link[0] == '/' {
1045 return true
1046 }
1047
1048 // current directory : begin with "./"
1049 if bytes.HasPrefix(link, []byte("./")) {
1050 return true
1051 }
1052
1053 // parent directory : begin with "../"
1054 if bytes.HasPrefix(link, []byte("../")) {
1055 return true
1056 }
1057
1058 return false
1059}
1060
1061func (r *Html) ensureUniqueHeaderID(id string) string {
1062 for count, found := r.headerIDs[id]; found; count, found = r.headerIDs[id] {
1063 tmp := fmt.Sprintf("%s-%d", id, count+1)
1064
1065 if _, tmpFound := r.headerIDs[tmp]; !tmpFound {
1066 r.headerIDs[id] = count + 1
1067 id = tmp
1068 } else {
1069 id = id + "-1"
1070 }
1071 }
1072
1073 if _, found := r.headerIDs[id]; !found {
1074 r.headerIDs[id] = 0
1075 }
1076
1077 return id
1078}
1079
1080func (r *Html) addAbsPrefix(link []byte) []byte {
1081 if r.parameters.AbsolutePrefix != "" && isRelativeLink(link) && link[0] != '.' {
1082 newDest := r.parameters.AbsolutePrefix
1083 if link[0] != '/' {
1084 newDest += "/"
1085 }
1086 newDest += string(link)
1087 return []byte(newDest)
1088 }
1089 return link
1090}
1091
1092func appendLinkAttrs(attrs []string, flags HtmlFlags, link []byte) []string {
1093 if isRelativeLink(link) {
1094 return attrs
1095 }
1096 val := []string{}
1097 if flags&NofollowLinks != 0 {
1098 val = append(val, "nofollow")
1099 }
1100 if flags&NoreferrerLinks != 0 {
1101 val = append(val, "noreferrer")
1102 }
1103 if flags&HrefTargetBlank != 0 {
1104 attrs = append(attrs, "target=\"_blank\"")
1105 }
1106 if len(val) == 0 {
1107 return attrs
1108 }
1109 attr := fmt.Sprintf("rel=%q", strings.Join(val, " "))
1110 return append(attrs, attr)
1111}
1112
1113func isMailto(link []byte) bool {
1114 return bytes.HasPrefix(link, []byte("mailto:"))
1115}
1116
1117func isSmartypantable(node *Node) bool {
1118 pt := node.Parent.Type
1119 return pt != Link && pt != CodeBlock && pt != Code
1120}
1121
1122func appendLanguageAttr(attrs []string, info []byte) []string {
1123 infoWords := bytes.Split(info, []byte("\t "))
1124 if len(infoWords) > 0 && len(infoWords[0]) > 0 {
1125 attrs = append(attrs, fmt.Sprintf("class=\"language-%s\"", infoWords[0]))
1126 }
1127 return attrs
1128}
1129
1130func tag(name string, attrs []string, selfClosing bool) []byte {
1131 result := "<" + name
1132 if attrs != nil && len(attrs) > 0 {
1133 result += " " + strings.Join(attrs, " ")
1134 }
1135 if selfClosing {
1136 result += " /"
1137 }
1138 return []byte(result + ">")
1139}
1140
1141func footnoteRef(prefix string, node *Node) []byte {
1142 urlFrag := prefix + string(slugify(node.Destination))
1143 anchor := fmt.Sprintf(`<a rel="footnote" href="#fn:%s">%d</a>`, urlFrag, node.NoteID)
1144 return []byte(fmt.Sprintf(`<sup class="footnote-ref" id="fnref:%s">%s</sup>`, urlFrag, anchor))
1145}
1146
1147func footnoteItem(prefix string, slug []byte) []byte {
1148 return []byte(fmt.Sprintf(`<li id="fn:%s%s">`, prefix, slug))
1149}
1150
1151func footnoteReturnLink(prefix, returnLink string, slug []byte) []byte {
1152 const format = ` <a class="footnote-return" href="#fnref:%s%s">%s</a>`
1153 return []byte(fmt.Sprintf(format, prefix, slug, returnLink))
1154}
1155
1156func itemOpenCR(node *Node) bool {
1157 if node.Prev == nil {
1158 return false
1159 }
1160 ld := node.Parent.ListData
1161 return !ld.Tight && ld.Flags&ListTypeDefinition == 0
1162}
1163
1164func skipParagraphTags(node *Node) bool {
1165 grandparent := node.Parent.Parent
1166 if grandparent == nil || grandparent.ListData == nil {
1167 return false
1168 }
1169 tightOrTerm := grandparent.ListData.Tight || node.Parent.ListData.Flags&ListTypeTerm != 0
1170 return grandparent.Type == List && tightOrTerm
1171}
1172
1173func cellAlignment(align int) string {
1174 switch align {
1175 case TableAlignmentLeft:
1176 return "left"
1177 case TableAlignmentRight:
1178 return "right"
1179 case TableAlignmentCenter:
1180 return "center"
1181 default:
1182 return ""
1183 }
1184}
1185
1186func (r *Html) Render(ast *Node) []byte {
1187 //println("render_Blackfriday")
1188 //dump(ast)
1189 var buff bytes.Buffer
1190 var lastOutputLen int
1191 disableTags := 0
1192 out := func(text []byte) {
1193 if disableTags > 0 {
1194 buff.Write(reHtmlTag.ReplaceAll(text, []byte{}))
1195 } else {
1196 buff.Write(text)
1197 }
1198 lastOutputLen = len(text)
1199 }
1200 esc := func(text []byte, preserveEntities bool) []byte {
1201 return attrEscape2(text)
1202 }
1203 escCode := func(text []byte, preserveEntities bool) []byte {
1204 e1 := []byte(html.EscapeString(string(text)))
1205 e2 := bytes.Replace(e1, []byte("""), []byte("""), -1)
1206 return bytes.Replace(e2, []byte("'"), []byte{'\''}, -1)
1207 }
1208 cr := func() {
1209 if lastOutputLen > 0 {
1210 out([]byte{'\n'})
1211 }
1212 }
1213 ForEachNode(ast, func(node *Node, entering bool) {
1214 attrs := []string{}
1215 switch node.Type {
1216 case Text:
1217 if r.flags&UseSmartypants != 0 && isSmartypantable(node) {
1218 // TODO: don't do that in renderer, do that at parse time!
1219 out(r.Smartypants2(node.Literal))
1220 } else {
1221 out(esc(node.Literal, false))
1222 }
1223 break
1224 case Softbreak:
1225 out([]byte("\n"))
1226 // TODO: make it configurable via out(renderer.softbreak)
1227 case Hardbreak:
1228 out(tag("br", nil, true))
1229 cr()
1230 case Emph:
1231 if entering {
1232 out(tag("em", nil, false))
1233 } else {
1234 out(tag("/em", nil, false))
1235 }
1236 break
1237 case Strong:
1238 if entering {
1239 out(tag("strong", nil, false))
1240 } else {
1241 out(tag("/strong", nil, false))
1242 }
1243 break
1244 case Del:
1245 if entering {
1246 out(tag("del", nil, false))
1247 } else {
1248 out(tag("/del", nil, false))
1249 }
1250 case HtmlSpan:
1251 //if options.safe {
1252 // out("<!-- raw HTML omitted -->")
1253 //} else {
1254 out(node.Literal)
1255 //}
1256 case Link:
1257 // mark it but don't link it if it is not a safe link: no smartypants
1258 dest := node.LinkData.Destination
1259 if r.flags&Safelink != 0 && !isSafeLink(dest) && !isMailto(dest) {
1260 if entering {
1261 out(tag("tt", nil, false))
1262 } else {
1263 out(tag("/tt", nil, false))
1264 }
1265 } else {
1266 if entering {
1267 dest = r.addAbsPrefix(dest)
1268 //if (!(options.safe && potentiallyUnsafe(node.destination))) {
1269 attrs = append(attrs, fmt.Sprintf("href=%q", esc(dest, true)))
1270 //}
1271 if node.NoteID != 0 {
1272 out(footnoteRef(r.parameters.FootnoteAnchorPrefix, node))
1273 break
1274 }
1275 attrs = appendLinkAttrs(attrs, r.flags, dest)
1276 if len(node.LinkData.Title) > 0 {
1277 attrs = append(attrs, fmt.Sprintf("title=%q", esc(node.LinkData.Title, true)))
1278 }
1279 out(tag("a", attrs, false))
1280 } else {
1281 if node.NoteID != 0 {
1282 break
1283 }
1284 out(tag("/a", nil, false))
1285 }
1286 }
1287 case Image:
1288 if entering {
1289 dest := node.LinkData.Destination
1290 dest = r.addAbsPrefix(dest)
1291 if disableTags == 0 {
1292 //if options.safe && potentiallyUnsafe(dest) {
1293 //out(`<img src="" alt="`)
1294 //} else {
1295 out([]byte(fmt.Sprintf(`<img src="%s" alt="`, esc(dest, true))))
1296 //}
1297 }
1298 disableTags++
1299 } else {
1300 disableTags--
1301 if disableTags == 0 {
1302 if node.LinkData.Title != nil {
1303 out([]byte(`" title="`))
1304 out(esc(node.LinkData.Title, true))
1305 }
1306 out([]byte(`" />`))
1307 }
1308 }
1309 case Code:
1310 out(tag("code", nil, false))
1311 out(escCode(node.Literal, false))
1312 out(tag("/code", nil, false))
1313 case Document:
1314 break
1315 case Paragraph:
1316 if skipParagraphTags(node) {
1317 break
1318 }
1319 if entering {
1320 // TODO: untangle this clusterfuck about when the newlines need
1321 // to be added and when not.
1322 if node.Prev != nil {
1323 t := node.Prev.Type
1324 if t == HtmlBlock || t == List || t == Paragraph || t == Header || t == CodeBlock || t == BlockQuote || t == HorizontalRule {
1325 cr()
1326 }
1327 }
1328 if node.Parent.Type == BlockQuote && node.Prev == nil {
1329 cr()
1330 }
1331 out(tag("p", attrs, false))
1332 } else {
1333 out(tag("/p", attrs, false))
1334 if !(node.Parent.Type == Item && node.Next == nil) {
1335 cr()
1336 }
1337 }
1338 break
1339 case BlockQuote:
1340 if entering {
1341 cr()
1342 out(tag("blockquote", attrs, false))
1343 } else {
1344 out(tag("/blockquote", nil, false))
1345 cr()
1346 }
1347 break
1348 case HtmlBlock:
1349 cr()
1350 out(node.Literal)
1351 cr()
1352 case Header:
1353 tagname := fmt.Sprintf("h%d", node.Level)
1354 if entering {
1355 if node.IsTitleblock {
1356 attrs = append(attrs, `class="title"`)
1357 }
1358 if node.HeaderID != "" {
1359 id := r.ensureUniqueHeaderID(node.HeaderID)
1360 if r.parameters.HeaderIDPrefix != "" {
1361 id = r.parameters.HeaderIDPrefix + id
1362 }
1363 if r.parameters.HeaderIDSuffix != "" {
1364 id = id + r.parameters.HeaderIDSuffix
1365 }
1366 attrs = append(attrs, fmt.Sprintf(`id="%s"`, id))
1367 }
1368 cr()
1369 out(tag(tagname, attrs, false))
1370 } else {
1371 out(tag("/"+tagname, nil, false))
1372 if !(node.Parent.Type == Item && node.Next == nil) {
1373 cr()
1374 }
1375 }
1376 break
1377 case HorizontalRule:
1378 cr()
1379 out(tag("hr", attrs, r.flags&UseXHTML != 0))
1380 cr()
1381 break
1382 case List:
1383 tagName := "ul"
1384 if node.ListData.Flags&ListTypeOrdered != 0 {
1385 tagName = "ol"
1386 }
1387 if node.ListData.Flags&ListTypeDefinition != 0 {
1388 tagName = "dl"
1389 }
1390 if entering {
1391 // var start = node.listStart;
1392 // if (start !== null && start !== 1) {
1393 // attrs.push(['start', start.toString()]);
1394 // }
1395 cr()
1396 if node.Parent.Type == Item && node.Parent.Parent.ListData.Tight {
1397 cr()
1398 }
1399 out(tag(tagName, attrs, false))
1400 cr()
1401 } else {
1402 out(tag("/"+tagName, nil, false))
1403 //cr()
1404 //if node.parent.Type != Item {
1405 // cr()
1406 //}
1407 if node.Parent.Type == Item && node.Next != nil {
1408 cr()
1409 }
1410 if node.Parent.Type == Document || node.Parent.Type == BlockQuote {
1411 cr()
1412 }
1413 }
1414 case Item:
1415 tagName := "li"
1416 if node.ListData.Flags&ListTypeDefinition != 0 {
1417 tagName = "dd"
1418 }
1419 if node.ListData.Flags&ListTypeTerm != 0 {
1420 tagName = "dt"
1421 }
1422 if entering {
1423 if itemOpenCR(node) {
1424 cr()
1425 }
1426 if node.ListData.RefLink != nil {
1427 slug := slugify(node.ListData.RefLink)
1428 out(footnoteItem(r.parameters.FootnoteAnchorPrefix, slug))
1429 break
1430 }
1431 out(tag(tagName, nil, false))
1432 } else {
1433 if node.ListData.RefLink != nil {
1434 slug := slugify(node.ListData.RefLink)
1435 if r.flags&FootnoteReturnLinks != 0 {
1436 out(footnoteReturnLink(r.parameters.FootnoteAnchorPrefix, r.parameters.FootnoteReturnLinkContents, slug))
1437 }
1438 }
1439 out(tag("/"+tagName, nil, false))
1440 cr()
1441 }
1442 case CodeBlock:
1443 attrs = appendLanguageAttr(attrs, node.Info)
1444 cr()
1445 out(tag("pre", nil, false))
1446 out(tag("code", attrs, false))
1447 out(escCode(node.Literal, false))
1448 out(tag("/code", nil, false))
1449 out(tag("/pre", nil, false))
1450 if node.Parent.Type != Item {
1451 cr()
1452 }
1453 case Table:
1454 if entering {
1455 cr()
1456 out(tag("table", nil, false))
1457 } else {
1458 out(tag("/table", nil, false))
1459 cr()
1460 }
1461 case TableCell:
1462 tagName := "td"
1463 if node.IsHeader {
1464 tagName = "th"
1465 }
1466 if entering {
1467 align := cellAlignment(node.Align)
1468 if align != "" {
1469 attrs = append(attrs, fmt.Sprintf(`align="%s"`, align))
1470 }
1471 if node.Prev == nil {
1472 cr()
1473 }
1474 out(tag(tagName, attrs, false))
1475 } else {
1476 out(tag("/"+tagName, nil, false))
1477 cr()
1478 }
1479 case TableHead:
1480 if entering {
1481 cr()
1482 out(tag("thead", nil, false))
1483 } else {
1484 out(tag("/thead", nil, false))
1485 cr()
1486 }
1487 case TableBody:
1488 if entering {
1489 cr()
1490 out(tag("tbody", nil, false))
1491 // XXX: this is to adhere to a rather silly test. Should fix test.
1492 if node.FirstChild == nil {
1493 cr()
1494 }
1495 } else {
1496 out(tag("/tbody", nil, false))
1497 cr()
1498 }
1499 case TableRow:
1500 if entering {
1501 cr()
1502 out(tag("tr", nil, false))
1503 } else {
1504 out(tag("/tr", nil, false))
1505 cr()
1506 }
1507 default:
1508 panic("Unknown node type " + node.Type.String())
1509 }
1510 })
1511 return buff.Bytes()
1512}