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