66
|
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
|
|
16 package blackfriday
|
|
17
|
|
18 import (
|
|
19 "bytes"
|
|
20 "fmt"
|
|
21 "io"
|
|
22 "regexp"
|
|
23 "strings"
|
|
24 )
|
|
25
|
|
26 // HTMLFlags control optional behavior of HTML renderer.
|
|
27 type HTMLFlags int
|
|
28
|
|
29 // HTML renderer configuration options.
|
|
30 const (
|
|
31 HTMLFlagsNone HTMLFlags = 0
|
|
32 SkipHTML HTMLFlags = 1 << iota // Skip preformatted HTML blocks
|
|
33 SkipImages // Skip embedded images
|
|
34 SkipLinks // Skip all links
|
|
35 Safelink // Only link to trusted protocols
|
|
36 NofollowLinks // Only link with rel="nofollow"
|
|
37 NoreferrerLinks // Only link with rel="noreferrer"
|
|
38 NoopenerLinks // Only link with rel="noopener"
|
|
39 HrefTargetBlank // Add a blank target
|
|
40 CompletePage // Generate a complete HTML page
|
|
41 UseXHTML // Generate XHTML output instead of HTML
|
|
42 FootnoteReturnLinks // Generate a link at the end of a footnote to return to the source
|
|
43 Smartypants // Enable smart punctuation substitutions
|
|
44 SmartypantsFractions // Enable smart fractions (with Smartypants)
|
|
45 SmartypantsDashes // Enable smart dashes (with Smartypants)
|
|
46 SmartypantsLatexDashes // Enable LaTeX-style dashes (with Smartypants)
|
|
47 SmartypantsAngledQuotes // Enable angled double quotes (with Smartypants) for double quotes rendering
|
|
48 SmartypantsQuotesNBSP // Enable « French guillemets » (with Smartypants)
|
|
49 TOC // Generate a table of contents
|
|
50 )
|
|
51
|
|
52 var (
|
|
53 htmlTagRe = regexp.MustCompile("(?i)^" + htmlTag)
|
|
54 )
|
|
55
|
|
56 const (
|
|
57 htmlTag = "(?:" + openTag + "|" + closeTag + "|" + htmlComment + "|" +
|
|
58 processingInstruction + "|" + declaration + "|" + cdata + ")"
|
|
59 closeTag = "</" + tagName + "\\s*[>]"
|
|
60 openTag = "<" + tagName + attribute + "*" + "\\s*/?>"
|
|
61 attribute = "(?:" + "\\s+" + attributeName + attributeValueSpec + "?)"
|
|
62 attributeValue = "(?:" + unquotedValue + "|" + singleQuotedValue + "|" + doubleQuotedValue + ")"
|
|
63 attributeValueSpec = "(?:" + "\\s*=" + "\\s*" + attributeValue + ")"
|
|
64 attributeName = "[a-zA-Z_:][a-zA-Z0-9:._-]*"
|
|
65 cdata = "<!\\[CDATA\\[[\\s\\S]*?\\]\\]>"
|
|
66 declaration = "<![A-Z]+" + "\\s+[^>]*>"
|
|
67 doubleQuotedValue = "\"[^\"]*\""
|
|
68 htmlComment = "<!---->|<!--(?:-?[^>-])(?:-?[^-])*-->"
|
|
69 processingInstruction = "[<][?].*?[?][>]"
|
|
70 singleQuotedValue = "'[^']*'"
|
|
71 tagName = "[A-Za-z][A-Za-z0-9-]*"
|
|
72 unquotedValue = "[^\"'=<>`\\x00-\\x20]+"
|
|
73 )
|
|
74
|
|
75 // HTMLRendererParameters is a collection of supplementary parameters tweaking
|
|
76 // the behavior of various parts of HTML renderer.
|
|
77 type HTMLRendererParameters struct {
|
|
78 // Prepend this text to each relative URL.
|
|
79 AbsolutePrefix string
|
|
80 // Add this text to each footnote anchor, to ensure uniqueness.
|
|
81 FootnoteAnchorPrefix string
|
|
82 // Show this text inside the <a> tag for a footnote return link, if the
|
|
83 // HTML_FOOTNOTE_RETURN_LINKS flag is enabled. If blank, the string
|
|
84 // <sup>[return]</sup> is used.
|
|
85 FootnoteReturnLinkContents string
|
|
86 // If set, add this text to the front of each Heading ID, to ensure
|
|
87 // uniqueness.
|
|
88 HeadingIDPrefix string
|
|
89 // If set, add this text to the back of each Heading ID, to ensure uniqueness.
|
|
90 HeadingIDSuffix string
|
|
91 // Increase heading levels: if the offset is 1, <h1> becomes <h2> etc.
|
|
92 // Negative offset is also valid.
|
|
93 // Resulting levels are clipped between 1 and 6.
|
|
94 HeadingLevelOffset int
|
|
95
|
|
96 Title string // Document title (used if CompletePage is set)
|
|
97 CSS string // Optional CSS file URL (used if CompletePage is set)
|
|
98 Icon string // Optional icon file URL (used if CompletePage is set)
|
|
99
|
|
100 Flags HTMLFlags // Flags allow customizing this renderer's behavior
|
|
101 }
|
|
102
|
|
103 // HTMLRenderer is a type that implements the Renderer interface for HTML output.
|
|
104 //
|
|
105 // Do not create this directly, instead use the NewHTMLRenderer function.
|
|
106 type HTMLRenderer struct {
|
|
107 HTMLRendererParameters
|
|
108
|
|
109 closeTag string // how to end singleton tags: either " />" or ">"
|
|
110
|
|
111 // Track heading IDs to prevent ID collision in a single generation.
|
|
112 headingIDs map[string]int
|
|
113
|
|
114 lastOutputLen int
|
|
115 disableTags int
|
|
116
|
|
117 sr *SPRenderer
|
|
118 }
|
|
119
|
|
120 const (
|
|
121 xhtmlClose = " />"
|
|
122 htmlClose = ">"
|
|
123 )
|
|
124
|
|
125 // NewHTMLRenderer creates and configures an HTMLRenderer object, which
|
|
126 // satisfies the Renderer interface.
|
|
127 func NewHTMLRenderer(params HTMLRendererParameters) *HTMLRenderer {
|
|
128 // configure the rendering engine
|
|
129 closeTag := htmlClose
|
|
130 if params.Flags&UseXHTML != 0 {
|
|
131 closeTag = xhtmlClose
|
|
132 }
|
|
133
|
|
134 if params.FootnoteReturnLinkContents == "" {
|
|
135 // U+FE0E is VARIATION SELECTOR-15.
|
|
136 // It suppresses automatic emoji presentation of the preceding
|
|
137 // U+21A9 LEFTWARDS ARROW WITH HOOK on iOS and iPadOS.
|
|
138 params.FootnoteReturnLinkContents = "<span aria-label='Return'>↩\ufe0e</span>"
|
|
139 }
|
|
140
|
|
141 return &HTMLRenderer{
|
|
142 HTMLRendererParameters: params,
|
|
143
|
|
144 closeTag: closeTag,
|
|
145 headingIDs: make(map[string]int),
|
|
146
|
|
147 sr: NewSmartypantsRenderer(params.Flags),
|
|
148 }
|
|
149 }
|
|
150
|
|
151 func isHTMLTag(tag []byte, tagname string) bool {
|
|
152 found, _ := findHTMLTagPos(tag, tagname)
|
|
153 return found
|
|
154 }
|
|
155
|
|
156 // Look for a character, but ignore it when it's in any kind of quotes, it
|
|
157 // might be JavaScript
|
|
158 func skipUntilCharIgnoreQuotes(html []byte, start int, char byte) int {
|
|
159 inSingleQuote := false
|
|
160 inDoubleQuote := false
|
|
161 inGraveQuote := false
|
|
162 i := start
|
|
163 for i < len(html) {
|
|
164 switch {
|
|
165 case html[i] == char && !inSingleQuote && !inDoubleQuote && !inGraveQuote:
|
|
166 return i
|
|
167 case html[i] == '\'':
|
|
168 inSingleQuote = !inSingleQuote
|
|
169 case html[i] == '"':
|
|
170 inDoubleQuote = !inDoubleQuote
|
|
171 case html[i] == '`':
|
|
172 inGraveQuote = !inGraveQuote
|
|
173 }
|
|
174 i++
|
|
175 }
|
|
176 return start
|
|
177 }
|
|
178
|
|
179 func findHTMLTagPos(tag []byte, tagname string) (bool, int) {
|
|
180 i := 0
|
|
181 if i < len(tag) && tag[0] != '<' {
|
|
182 return false, -1
|
|
183 }
|
|
184 i++
|
|
185 i = skipSpace(tag, i)
|
|
186
|
|
187 if i < len(tag) && tag[i] == '/' {
|
|
188 i++
|
|
189 }
|
|
190
|
|
191 i = skipSpace(tag, i)
|
|
192 j := 0
|
|
193 for ; i < len(tag); i, j = i+1, j+1 {
|
|
194 if j >= len(tagname) {
|
|
195 break
|
|
196 }
|
|
197
|
|
198 if strings.ToLower(string(tag[i]))[0] != tagname[j] {
|
|
199 return false, -1
|
|
200 }
|
|
201 }
|
|
202
|
|
203 if i == len(tag) {
|
|
204 return false, -1
|
|
205 }
|
|
206
|
|
207 rightAngle := skipUntilCharIgnoreQuotes(tag, i, '>')
|
|
208 if rightAngle >= i {
|
|
209 return true, rightAngle
|
|
210 }
|
|
211
|
|
212 return false, -1
|
|
213 }
|
|
214
|
|
215 func skipSpace(tag []byte, i int) int {
|
|
216 for i < len(tag) && isspace(tag[i]) {
|
|
217 i++
|
|
218 }
|
|
219 return i
|
|
220 }
|
|
221
|
|
222 func isRelativeLink(link []byte) (yes bool) {
|
|
223 // a tag begin with '#'
|
|
224 if link[0] == '#' {
|
|
225 return true
|
|
226 }
|
|
227
|
|
228 // link begin with '/' but not '//', the second maybe a protocol relative link
|
|
229 if len(link) >= 2 && link[0] == '/' && link[1] != '/' {
|
|
230 return true
|
|
231 }
|
|
232
|
|
233 // only the root '/'
|
|
234 if len(link) == 1 && link[0] == '/' {
|
|
235 return true
|
|
236 }
|
|
237
|
|
238 // current directory : begin with "./"
|
|
239 if bytes.HasPrefix(link, []byte("./")) {
|
|
240 return true
|
|
241 }
|
|
242
|
|
243 // parent directory : begin with "../"
|
|
244 if bytes.HasPrefix(link, []byte("../")) {
|
|
245 return true
|
|
246 }
|
|
247
|
|
248 return false
|
|
249 }
|
|
250
|
|
251 func (r *HTMLRenderer) ensureUniqueHeadingID(id string) string {
|
|
252 for count, found := r.headingIDs[id]; found; count, found = r.headingIDs[id] {
|
|
253 tmp := fmt.Sprintf("%s-%d", id, count+1)
|
|
254
|
|
255 if _, tmpFound := r.headingIDs[tmp]; !tmpFound {
|
|
256 r.headingIDs[id] = count + 1
|
|
257 id = tmp
|
|
258 } else {
|
|
259 id = id + "-1"
|
|
260 }
|
|
261 }
|
|
262
|
|
263 if _, found := r.headingIDs[id]; !found {
|
|
264 r.headingIDs[id] = 0
|
|
265 }
|
|
266
|
|
267 return id
|
|
268 }
|
|
269
|
|
270 func (r *HTMLRenderer) addAbsPrefix(link []byte) []byte {
|
|
271 if r.AbsolutePrefix != "" && isRelativeLink(link) && link[0] != '.' {
|
|
272 newDest := r.AbsolutePrefix
|
|
273 if link[0] != '/' {
|
|
274 newDest += "/"
|
|
275 }
|
|
276 newDest += string(link)
|
|
277 return []byte(newDest)
|
|
278 }
|
|
279 return link
|
|
280 }
|
|
281
|
|
282 func appendLinkAttrs(attrs []string, flags HTMLFlags, link []byte) []string {
|
|
283 if isRelativeLink(link) {
|
|
284 return attrs
|
|
285 }
|
|
286 val := []string{}
|
|
287 if flags&NofollowLinks != 0 {
|
|
288 val = append(val, "nofollow")
|
|
289 }
|
|
290 if flags&NoreferrerLinks != 0 {
|
|
291 val = append(val, "noreferrer")
|
|
292 }
|
|
293 if flags&NoopenerLinks != 0 {
|
|
294 val = append(val, "noopener")
|
|
295 }
|
|
296 if flags&HrefTargetBlank != 0 {
|
|
297 attrs = append(attrs, "target=\"_blank\"")
|
|
298 }
|
|
299 if len(val) == 0 {
|
|
300 return attrs
|
|
301 }
|
|
302 attr := fmt.Sprintf("rel=%q", strings.Join(val, " "))
|
|
303 return append(attrs, attr)
|
|
304 }
|
|
305
|
|
306 func isMailto(link []byte) bool {
|
|
307 return bytes.HasPrefix(link, []byte("mailto:"))
|
|
308 }
|
|
309
|
|
310 func needSkipLink(flags HTMLFlags, dest []byte) bool {
|
|
311 if flags&SkipLinks != 0 {
|
|
312 return true
|
|
313 }
|
|
314 return flags&Safelink != 0 && !isSafeLink(dest) && !isMailto(dest)
|
|
315 }
|
|
316
|
|
317 func isSmartypantable(node *Node) bool {
|
|
318 pt := node.Parent.Type
|
|
319 return pt != Link && pt != CodeBlock && pt != Code
|
|
320 }
|
|
321
|
|
322 func appendLanguageAttr(attrs []string, info []byte) []string {
|
|
323 if len(info) == 0 {
|
|
324 return attrs
|
|
325 }
|
|
326 endOfLang := bytes.IndexAny(info, "\t ")
|
|
327 if endOfLang < 0 {
|
|
328 endOfLang = len(info)
|
|
329 }
|
|
330 return append(attrs, fmt.Sprintf("class=\"language-%s\"", info[:endOfLang]))
|
|
331 }
|
|
332
|
|
333 func (r *HTMLRenderer) tag(w io.Writer, name []byte, attrs []string) {
|
|
334 w.Write(name)
|
|
335 if len(attrs) > 0 {
|
|
336 w.Write(spaceBytes)
|
|
337 w.Write([]byte(strings.Join(attrs, " ")))
|
|
338 }
|
|
339 w.Write(gtBytes)
|
|
340 r.lastOutputLen = 1
|
|
341 }
|
|
342
|
|
343 func footnoteRef(prefix string, node *Node) []byte {
|
|
344 urlFrag := prefix + string(slugify(node.Destination))
|
|
345 anchor := fmt.Sprintf(`<a href="#fn:%s">%d</a>`, urlFrag, node.NoteID)
|
|
346 return []byte(fmt.Sprintf(`<sup class="footnote-ref" id="fnref:%s">%s</sup>`, urlFrag, anchor))
|
|
347 }
|
|
348
|
|
349 func footnoteItem(prefix string, slug []byte) []byte {
|
|
350 return []byte(fmt.Sprintf(`<li id="fn:%s%s">`, prefix, slug))
|
|
351 }
|
|
352
|
|
353 func footnoteReturnLink(prefix, returnLink string, slug []byte) []byte {
|
|
354 const format = ` <a class="footnote-return" href="#fnref:%s%s">%s</a>`
|
|
355 return []byte(fmt.Sprintf(format, prefix, slug, returnLink))
|
|
356 }
|
|
357
|
|
358 func itemOpenCR(node *Node) bool {
|
|
359 if node.Prev == nil {
|
|
360 return false
|
|
361 }
|
|
362 ld := node.Parent.ListData
|
|
363 return !ld.Tight && ld.ListFlags&ListTypeDefinition == 0
|
|
364 }
|
|
365
|
|
366 func skipParagraphTags(node *Node) bool {
|
|
367 grandparent := node.Parent.Parent
|
|
368 if grandparent == nil || grandparent.Type != List {
|
|
369 return false
|
|
370 }
|
|
371 tightOrTerm := grandparent.Tight || node.Parent.ListFlags&ListTypeTerm != 0
|
|
372 return grandparent.Type == List && tightOrTerm
|
|
373 }
|
|
374
|
|
375 func cellAlignment(align CellAlignFlags) string {
|
|
376 switch align {
|
|
377 case TableAlignmentLeft:
|
|
378 return "left"
|
|
379 case TableAlignmentRight:
|
|
380 return "right"
|
|
381 case TableAlignmentCenter:
|
|
382 return "center"
|
|
383 default:
|
|
384 return ""
|
|
385 }
|
|
386 }
|
|
387
|
|
388 func (r *HTMLRenderer) out(w io.Writer, text []byte) {
|
|
389 if r.disableTags > 0 {
|
|
390 w.Write(htmlTagRe.ReplaceAll(text, []byte{}))
|
|
391 } else {
|
|
392 w.Write(text)
|
|
393 }
|
|
394 r.lastOutputLen = len(text)
|
|
395 }
|
|
396
|
|
397 func (r *HTMLRenderer) cr(w io.Writer) {
|
|
398 if r.lastOutputLen > 0 {
|
|
399 r.out(w, nlBytes)
|
|
400 }
|
|
401 }
|
|
402
|
|
403 var (
|
|
404 nlBytes = []byte{'\n'}
|
|
405 gtBytes = []byte{'>'}
|
|
406 spaceBytes = []byte{' '}
|
|
407 )
|
|
408
|
|
409 var (
|
|
410 brTag = []byte("<br>")
|
|
411 brXHTMLTag = []byte("<br />")
|
|
412 emTag = []byte("<em>")
|
|
413 emCloseTag = []byte("</em>")
|
|
414 strongTag = []byte("<strong>")
|
|
415 strongCloseTag = []byte("</strong>")
|
|
416 delTag = []byte("<del>")
|
|
417 delCloseTag = []byte("</del>")
|
|
418 ttTag = []byte("<tt>")
|
|
419 ttCloseTag = []byte("</tt>")
|
|
420 aTag = []byte("<a")
|
|
421 aCloseTag = []byte("</a>")
|
|
422 preTag = []byte("<pre>")
|
|
423 preCloseTag = []byte("</pre>")
|
|
424 codeTag = []byte("<code>")
|
|
425 codeCloseTag = []byte("</code>")
|
|
426 pTag = []byte("<p>")
|
|
427 pCloseTag = []byte("</p>")
|
|
428 blockquoteTag = []byte("<blockquote>")
|
|
429 blockquoteCloseTag = []byte("</blockquote>")
|
|
430 hrTag = []byte("<hr>")
|
|
431 hrXHTMLTag = []byte("<hr />")
|
|
432 ulTag = []byte("<ul>")
|
|
433 ulCloseTag = []byte("</ul>")
|
|
434 olTag = []byte("<ol>")
|
|
435 olCloseTag = []byte("</ol>")
|
|
436 dlTag = []byte("<dl>")
|
|
437 dlCloseTag = []byte("</dl>")
|
|
438 liTag = []byte("<li>")
|
|
439 liCloseTag = []byte("</li>")
|
|
440 ddTag = []byte("<dd>")
|
|
441 ddCloseTag = []byte("</dd>")
|
|
442 dtTag = []byte("<dt>")
|
|
443 dtCloseTag = []byte("</dt>")
|
|
444 tableTag = []byte("<table>")
|
|
445 tableCloseTag = []byte("</table>")
|
|
446 tdTag = []byte("<td")
|
|
447 tdCloseTag = []byte("</td>")
|
|
448 thTag = []byte("<th")
|
|
449 thCloseTag = []byte("</th>")
|
|
450 theadTag = []byte("<thead>")
|
|
451 theadCloseTag = []byte("</thead>")
|
|
452 tbodyTag = []byte("<tbody>")
|
|
453 tbodyCloseTag = []byte("</tbody>")
|
|
454 trTag = []byte("<tr>")
|
|
455 trCloseTag = []byte("</tr>")
|
|
456 h1Tag = []byte("<h1")
|
|
457 h1CloseTag = []byte("</h1>")
|
|
458 h2Tag = []byte("<h2")
|
|
459 h2CloseTag = []byte("</h2>")
|
|
460 h3Tag = []byte("<h3")
|
|
461 h3CloseTag = []byte("</h3>")
|
|
462 h4Tag = []byte("<h4")
|
|
463 h4CloseTag = []byte("</h4>")
|
|
464 h5Tag = []byte("<h5")
|
|
465 h5CloseTag = []byte("</h5>")
|
|
466 h6Tag = []byte("<h6")
|
|
467 h6CloseTag = []byte("</h6>")
|
|
468
|
|
469 footnotesDivBytes = []byte("\n<div class=\"footnotes\">\n\n")
|
|
470 footnotesCloseDivBytes = []byte("\n</div>\n")
|
|
471 )
|
|
472
|
|
473 func headingTagsFromLevel(level int) ([]byte, []byte) {
|
|
474 if level <= 1 {
|
|
475 return h1Tag, h1CloseTag
|
|
476 }
|
|
477 switch level {
|
|
478 case 2:
|
|
479 return h2Tag, h2CloseTag
|
|
480 case 3:
|
|
481 return h3Tag, h3CloseTag
|
|
482 case 4:
|
|
483 return h4Tag, h4CloseTag
|
|
484 case 5:
|
|
485 return h5Tag, h5CloseTag
|
|
486 }
|
|
487 return h6Tag, h6CloseTag
|
|
488 }
|
|
489
|
|
490 func (r *HTMLRenderer) outHRTag(w io.Writer) {
|
|
491 if r.Flags&UseXHTML == 0 {
|
|
492 r.out(w, hrTag)
|
|
493 } else {
|
|
494 r.out(w, hrXHTMLTag)
|
|
495 }
|
|
496 }
|
|
497
|
|
498 // RenderNode is a default renderer of a single node of a syntax tree. For
|
|
499 // block nodes it will be called twice: first time with entering=true, second
|
|
500 // time with entering=false, so that it could know when it's working on an open
|
|
501 // tag and when on close. It writes the result to w.
|
|
502 //
|
|
503 // The return value is a way to tell the calling walker to adjust its walk
|
|
504 // pattern: e.g. it can terminate the traversal by returning Terminate. Or it
|
|
505 // can ask the walker to skip a subtree of this node by returning SkipChildren.
|
|
506 // The typical behavior is to return GoToNext, which asks for the usual
|
|
507 // traversal to the next node.
|
|
508 func (r *HTMLRenderer) RenderNode(w io.Writer, node *Node, entering bool) WalkStatus {
|
|
509 attrs := []string{}
|
|
510 switch node.Type {
|
|
511 case Text:
|
|
512 if r.Flags&Smartypants != 0 {
|
|
513 var tmp bytes.Buffer
|
|
514 escapeHTML(&tmp, node.Literal)
|
|
515 r.sr.Process(w, tmp.Bytes())
|
|
516 } else {
|
|
517 if node.Parent.Type == Link {
|
|
518 escLink(w, node.Literal)
|
|
519 } else {
|
|
520 escapeHTML(w, node.Literal)
|
|
521 }
|
|
522 }
|
|
523 case Softbreak:
|
|
524 r.cr(w)
|
|
525 // TODO: make it configurable via out(renderer.softbreak)
|
|
526 case Hardbreak:
|
|
527 if r.Flags&UseXHTML == 0 {
|
|
528 r.out(w, brTag)
|
|
529 } else {
|
|
530 r.out(w, brXHTMLTag)
|
|
531 }
|
|
532 r.cr(w)
|
|
533 case Emph:
|
|
534 if entering {
|
|
535 r.out(w, emTag)
|
|
536 } else {
|
|
537 r.out(w, emCloseTag)
|
|
538 }
|
|
539 case Strong:
|
|
540 if entering {
|
|
541 r.out(w, strongTag)
|
|
542 } else {
|
|
543 r.out(w, strongCloseTag)
|
|
544 }
|
|
545 case Del:
|
|
546 if entering {
|
|
547 r.out(w, delTag)
|
|
548 } else {
|
|
549 r.out(w, delCloseTag)
|
|
550 }
|
|
551 case HTMLSpan:
|
|
552 if r.Flags&SkipHTML != 0 {
|
|
553 break
|
|
554 }
|
|
555 r.out(w, node.Literal)
|
|
556 case Link:
|
|
557 // mark it but don't link it if it is not a safe link: no smartypants
|
|
558 dest := node.LinkData.Destination
|
|
559 if needSkipLink(r.Flags, dest) {
|
|
560 if entering {
|
|
561 r.out(w, ttTag)
|
|
562 } else {
|
|
563 r.out(w, ttCloseTag)
|
|
564 }
|
|
565 } else {
|
|
566 if entering {
|
|
567 dest = r.addAbsPrefix(dest)
|
|
568 var hrefBuf bytes.Buffer
|
|
569 hrefBuf.WriteString("href=\"")
|
|
570 escLink(&hrefBuf, dest)
|
|
571 hrefBuf.WriteByte('"')
|
|
572 attrs = append(attrs, hrefBuf.String())
|
|
573 if node.NoteID != 0 {
|
|
574 r.out(w, footnoteRef(r.FootnoteAnchorPrefix, node))
|
|
575 break
|
|
576 }
|
|
577 attrs = appendLinkAttrs(attrs, r.Flags, dest)
|
|
578 if len(node.LinkData.Title) > 0 {
|
|
579 var titleBuff bytes.Buffer
|
|
580 titleBuff.WriteString("title=\"")
|
|
581 escapeHTML(&titleBuff, node.LinkData.Title)
|
|
582 titleBuff.WriteByte('"')
|
|
583 attrs = append(attrs, titleBuff.String())
|
|
584 }
|
|
585 r.tag(w, aTag, attrs)
|
|
586 } else {
|
|
587 if node.NoteID != 0 {
|
|
588 break
|
|
589 }
|
|
590 r.out(w, aCloseTag)
|
|
591 }
|
|
592 }
|
|
593 case Image:
|
|
594 if r.Flags&SkipImages != 0 {
|
|
595 return SkipChildren
|
|
596 }
|
|
597 if entering {
|
|
598 dest := node.LinkData.Destination
|
|
599 dest = r.addAbsPrefix(dest)
|
|
600 if r.disableTags == 0 {
|
|
601 //if options.safe && potentiallyUnsafe(dest) {
|
|
602 //out(w, `<img src="" alt="`)
|
|
603 //} else {
|
|
604 r.out(w, []byte(`<img src="`))
|
|
605 escLink(w, dest)
|
|
606 r.out(w, []byte(`" alt="`))
|
|
607 //}
|
|
608 }
|
|
609 r.disableTags++
|
|
610 } else {
|
|
611 r.disableTags--
|
|
612 if r.disableTags == 0 {
|
|
613 if node.LinkData.Title != nil {
|
|
614 r.out(w, []byte(`" title="`))
|
|
615 escapeHTML(w, node.LinkData.Title)
|
|
616 }
|
|
617 r.out(w, []byte(`" />`))
|
|
618 }
|
|
619 }
|
|
620 case Code:
|
|
621 r.out(w, codeTag)
|
|
622 escapeAllHTML(w, node.Literal)
|
|
623 r.out(w, codeCloseTag)
|
|
624 case Document:
|
|
625 break
|
|
626 case Paragraph:
|
|
627 if skipParagraphTags(node) {
|
|
628 break
|
|
629 }
|
|
630 if entering {
|
|
631 // TODO: untangle this clusterfuck about when the newlines need
|
|
632 // to be added and when not.
|
|
633 if node.Prev != nil {
|
|
634 switch node.Prev.Type {
|
|
635 case HTMLBlock, List, Paragraph, Heading, CodeBlock, BlockQuote, HorizontalRule:
|
|
636 r.cr(w)
|
|
637 }
|
|
638 }
|
|
639 if node.Parent.Type == BlockQuote && node.Prev == nil {
|
|
640 r.cr(w)
|
|
641 }
|
|
642 r.out(w, pTag)
|
|
643 } else {
|
|
644 r.out(w, pCloseTag)
|
|
645 if !(node.Parent.Type == Item && node.Next == nil) {
|
|
646 r.cr(w)
|
|
647 }
|
|
648 }
|
|
649 case BlockQuote:
|
|
650 if entering {
|
|
651 r.cr(w)
|
|
652 r.out(w, blockquoteTag)
|
|
653 } else {
|
|
654 r.out(w, blockquoteCloseTag)
|
|
655 r.cr(w)
|
|
656 }
|
|
657 case HTMLBlock:
|
|
658 if r.Flags&SkipHTML != 0 {
|
|
659 break
|
|
660 }
|
|
661 r.cr(w)
|
|
662 r.out(w, node.Literal)
|
|
663 r.cr(w)
|
|
664 case Heading:
|
|
665 headingLevel := r.HTMLRendererParameters.HeadingLevelOffset + node.Level
|
|
666 openTag, closeTag := headingTagsFromLevel(headingLevel)
|
|
667 if entering {
|
|
668 if node.IsTitleblock {
|
|
669 attrs = append(attrs, `class="title"`)
|
|
670 }
|
|
671 if node.HeadingID != "" {
|
|
672 id := r.ensureUniqueHeadingID(node.HeadingID)
|
|
673 if r.HeadingIDPrefix != "" {
|
|
674 id = r.HeadingIDPrefix + id
|
|
675 }
|
|
676 if r.HeadingIDSuffix != "" {
|
|
677 id = id + r.HeadingIDSuffix
|
|
678 }
|
|
679 attrs = append(attrs, fmt.Sprintf(`id="%s"`, id))
|
|
680 }
|
|
681 r.cr(w)
|
|
682 r.tag(w, openTag, attrs)
|
|
683 } else {
|
|
684 r.out(w, closeTag)
|
|
685 if !(node.Parent.Type == Item && node.Next == nil) {
|
|
686 r.cr(w)
|
|
687 }
|
|
688 }
|
|
689 case HorizontalRule:
|
|
690 r.cr(w)
|
|
691 r.outHRTag(w)
|
|
692 r.cr(w)
|
|
693 case List:
|
|
694 openTag := ulTag
|
|
695 closeTag := ulCloseTag
|
|
696 if node.ListFlags&ListTypeOrdered != 0 {
|
|
697 openTag = olTag
|
|
698 closeTag = olCloseTag
|
|
699 }
|
|
700 if node.ListFlags&ListTypeDefinition != 0 {
|
|
701 openTag = dlTag
|
|
702 closeTag = dlCloseTag
|
|
703 }
|
|
704 if entering {
|
|
705 if node.IsFootnotesList {
|
|
706 r.out(w, footnotesDivBytes)
|
|
707 r.outHRTag(w)
|
|
708 r.cr(w)
|
|
709 }
|
|
710 r.cr(w)
|
|
711 if node.Parent.Type == Item && node.Parent.Parent.Tight {
|
|
712 r.cr(w)
|
|
713 }
|
|
714 r.tag(w, openTag[:len(openTag)-1], attrs)
|
|
715 r.cr(w)
|
|
716 } else {
|
|
717 r.out(w, closeTag)
|
|
718 //cr(w)
|
|
719 //if node.parent.Type != Item {
|
|
720 // cr(w)
|
|
721 //}
|
|
722 if node.Parent.Type == Item && node.Next != nil {
|
|
723 r.cr(w)
|
|
724 }
|
|
725 if node.Parent.Type == Document || node.Parent.Type == BlockQuote {
|
|
726 r.cr(w)
|
|
727 }
|
|
728 if node.IsFootnotesList {
|
|
729 r.out(w, footnotesCloseDivBytes)
|
|
730 }
|
|
731 }
|
|
732 case Item:
|
|
733 openTag := liTag
|
|
734 closeTag := liCloseTag
|
|
735 if node.ListFlags&ListTypeDefinition != 0 {
|
|
736 openTag = ddTag
|
|
737 closeTag = ddCloseTag
|
|
738 }
|
|
739 if node.ListFlags&ListTypeTerm != 0 {
|
|
740 openTag = dtTag
|
|
741 closeTag = dtCloseTag
|
|
742 }
|
|
743 if entering {
|
|
744 if itemOpenCR(node) {
|
|
745 r.cr(w)
|
|
746 }
|
|
747 if node.ListData.RefLink != nil {
|
|
748 slug := slugify(node.ListData.RefLink)
|
|
749 r.out(w, footnoteItem(r.FootnoteAnchorPrefix, slug))
|
|
750 break
|
|
751 }
|
|
752 r.out(w, openTag)
|
|
753 } else {
|
|
754 if node.ListData.RefLink != nil {
|
|
755 slug := slugify(node.ListData.RefLink)
|
|
756 if r.Flags&FootnoteReturnLinks != 0 {
|
|
757 r.out(w, footnoteReturnLink(r.FootnoteAnchorPrefix, r.FootnoteReturnLinkContents, slug))
|
|
758 }
|
|
759 }
|
|
760 r.out(w, closeTag)
|
|
761 r.cr(w)
|
|
762 }
|
|
763 case CodeBlock:
|
|
764 attrs = appendLanguageAttr(attrs, node.Info)
|
|
765 r.cr(w)
|
|
766 r.out(w, preTag)
|
|
767 r.tag(w, codeTag[:len(codeTag)-1], attrs)
|
|
768 escapeAllHTML(w, node.Literal)
|
|
769 r.out(w, codeCloseTag)
|
|
770 r.out(w, preCloseTag)
|
|
771 if node.Parent.Type != Item {
|
|
772 r.cr(w)
|
|
773 }
|
|
774 case Table:
|
|
775 if entering {
|
|
776 r.cr(w)
|
|
777 r.out(w, tableTag)
|
|
778 } else {
|
|
779 r.out(w, tableCloseTag)
|
|
780 r.cr(w)
|
|
781 }
|
|
782 case TableCell:
|
|
783 openTag := tdTag
|
|
784 closeTag := tdCloseTag
|
|
785 if node.IsHeader {
|
|
786 openTag = thTag
|
|
787 closeTag = thCloseTag
|
|
788 }
|
|
789 if entering {
|
|
790 align := cellAlignment(node.Align)
|
|
791 if align != "" {
|
|
792 attrs = append(attrs, fmt.Sprintf(`align="%s"`, align))
|
|
793 }
|
|
794 if node.Prev == nil {
|
|
795 r.cr(w)
|
|
796 }
|
|
797 r.tag(w, openTag, attrs)
|
|
798 } else {
|
|
799 r.out(w, closeTag)
|
|
800 r.cr(w)
|
|
801 }
|
|
802 case TableHead:
|
|
803 if entering {
|
|
804 r.cr(w)
|
|
805 r.out(w, theadTag)
|
|
806 } else {
|
|
807 r.out(w, theadCloseTag)
|
|
808 r.cr(w)
|
|
809 }
|
|
810 case TableBody:
|
|
811 if entering {
|
|
812 r.cr(w)
|
|
813 r.out(w, tbodyTag)
|
|
814 // XXX: this is to adhere to a rather silly test. Should fix test.
|
|
815 if node.FirstChild == nil {
|
|
816 r.cr(w)
|
|
817 }
|
|
818 } else {
|
|
819 r.out(w, tbodyCloseTag)
|
|
820 r.cr(w)
|
|
821 }
|
|
822 case TableRow:
|
|
823 if entering {
|
|
824 r.cr(w)
|
|
825 r.out(w, trTag)
|
|
826 } else {
|
|
827 r.out(w, trCloseTag)
|
|
828 r.cr(w)
|
|
829 }
|
|
830 default:
|
|
831 panic("Unknown node type " + node.Type.String())
|
|
832 }
|
|
833 return GoToNext
|
|
834 }
|
|
835
|
|
836 // RenderHeader writes HTML document preamble and TOC if requested.
|
|
837 func (r *HTMLRenderer) RenderHeader(w io.Writer, ast *Node) {
|
|
838 r.writeDocumentHeader(w)
|
|
839 if r.Flags&TOC != 0 {
|
|
840 r.writeTOC(w, ast)
|
|
841 }
|
|
842 }
|
|
843
|
|
844 // RenderFooter writes HTML document footer.
|
|
845 func (r *HTMLRenderer) RenderFooter(w io.Writer, ast *Node) {
|
|
846 if r.Flags&CompletePage == 0 {
|
|
847 return
|
|
848 }
|
|
849 io.WriteString(w, "\n</body>\n</html>\n")
|
|
850 }
|
|
851
|
|
852 func (r *HTMLRenderer) writeDocumentHeader(w io.Writer) {
|
|
853 if r.Flags&CompletePage == 0 {
|
|
854 return
|
|
855 }
|
|
856 ending := ""
|
|
857 if r.Flags&UseXHTML != 0 {
|
|
858 io.WriteString(w, "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" ")
|
|
859 io.WriteString(w, "\"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">\n")
|
|
860 io.WriteString(w, "<html xmlns=\"http://www.w3.org/1999/xhtml\">\n")
|
|
861 ending = " /"
|
|
862 } else {
|
|
863 io.WriteString(w, "<!DOCTYPE html>\n")
|
|
864 io.WriteString(w, "<html>\n")
|
|
865 }
|
|
866 io.WriteString(w, "<head>\n")
|
|
867 io.WriteString(w, " <title>")
|
|
868 if r.Flags&Smartypants != 0 {
|
|
869 r.sr.Process(w, []byte(r.Title))
|
|
870 } else {
|
|
871 escapeHTML(w, []byte(r.Title))
|
|
872 }
|
|
873 io.WriteString(w, "</title>\n")
|
|
874 io.WriteString(w, " <meta name=\"GENERATOR\" content=\"Blackfriday Markdown Processor v")
|
|
875 io.WriteString(w, Version)
|
|
876 io.WriteString(w, "\"")
|
|
877 io.WriteString(w, ending)
|
|
878 io.WriteString(w, ">\n")
|
|
879 io.WriteString(w, " <meta charset=\"utf-8\"")
|
|
880 io.WriteString(w, ending)
|
|
881 io.WriteString(w, ">\n")
|
|
882 if r.CSS != "" {
|
|
883 io.WriteString(w, " <link rel=\"stylesheet\" type=\"text/css\" href=\"")
|
|
884 escapeHTML(w, []byte(r.CSS))
|
|
885 io.WriteString(w, "\"")
|
|
886 io.WriteString(w, ending)
|
|
887 io.WriteString(w, ">\n")
|
|
888 }
|
|
889 if r.Icon != "" {
|
|
890 io.WriteString(w, " <link rel=\"icon\" type=\"image/x-icon\" href=\"")
|
|
891 escapeHTML(w, []byte(r.Icon))
|
|
892 io.WriteString(w, "\"")
|
|
893 io.WriteString(w, ending)
|
|
894 io.WriteString(w, ">\n")
|
|
895 }
|
|
896 io.WriteString(w, "</head>\n")
|
|
897 io.WriteString(w, "<body>\n\n")
|
|
898 }
|
|
899
|
|
900 func (r *HTMLRenderer) writeTOC(w io.Writer, ast *Node) {
|
|
901 buf := bytes.Buffer{}
|
|
902
|
|
903 inHeading := false
|
|
904 tocLevel := 0
|
|
905 headingCount := 0
|
|
906
|
|
907 ast.Walk(func(node *Node, entering bool) WalkStatus {
|
|
908 if node.Type == Heading && !node.HeadingData.IsTitleblock {
|
|
909 inHeading = entering
|
|
910 if entering {
|
|
911 node.HeadingID = fmt.Sprintf("toc_%d", headingCount)
|
|
912 if node.Level == tocLevel {
|
|
913 buf.WriteString("</li>\n\n<li>")
|
|
914 } else if node.Level < tocLevel {
|
|
915 for node.Level < tocLevel {
|
|
916 tocLevel--
|
|
917 buf.WriteString("</li>\n</ul>")
|
|
918 }
|
|
919 buf.WriteString("</li>\n\n<li>")
|
|
920 } else {
|
|
921 for node.Level > tocLevel {
|
|
922 tocLevel++
|
|
923 buf.WriteString("\n<ul>\n<li>")
|
|
924 }
|
|
925 }
|
|
926
|
|
927 fmt.Fprintf(&buf, `<a href="#toc_%d">`, headingCount)
|
|
928 headingCount++
|
|
929 } else {
|
|
930 buf.WriteString("</a>")
|
|
931 }
|
|
932 return GoToNext
|
|
933 }
|
|
934
|
|
935 if inHeading {
|
|
936 return r.RenderNode(&buf, node, entering)
|
|
937 }
|
|
938
|
|
939 return GoToNext
|
|
940 })
|
|
941
|
|
942 for ; tocLevel > 0; tocLevel-- {
|
|
943 buf.WriteString("</li>\n</ul>")
|
|
944 }
|
|
945
|
|
946 if buf.Len() > 0 {
|
|
947 io.WriteString(w, "<nav>\n")
|
|
948 w.Write(buf.Bytes())
|
|
949 io.WriteString(w, "\n\n</nav>\n")
|
|
950 }
|
|
951 r.lastOutputLen = buf.Len()
|
|
952 }
|