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