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