1 |
yakumo_izuru |
1.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 |
|
|
} |