Mercurial > yakumo_izuru > aya
comparison vendor/github.com/russross/blackfriday/v2/html.go @ 66:787b5ee0289d draft
Use vendored modules
Signed-off-by: Izuru Yakumo <yakumo.izuru@chaotic.ninja>
author | yakumo.izuru |
---|---|
date | Sun, 23 Jul 2023 13:18:53 +0000 |
parents | |
children |
comparison
equal
deleted
inserted
replaced
65:6d985efa0f7a | 66:787b5ee0289d |
---|---|
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 } |