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