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 | }
|
---|