html.go (24872B)
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 censoredTag = []byte(`<span class="censored">`) 415 censoredCloseTag = []byte("</span>") 416 strongTag = []byte("<strong>") 417 strongCloseTag = []byte("</strong>") 418 delTag = []byte("<del>") 419 delCloseTag = []byte("</del>") 420 ttTag = []byte("<tt>") 421 ttCloseTag = []byte("</tt>") 422 aTag = []byte("<a") 423 aCloseTag = []byte("</a>") 424 preTag = []byte("<pre>") 425 preCloseTag = []byte("</pre>") 426 codeTag = []byte("<code>") 427 codeCloseTag = []byte("</code>") 428 pTag = []byte("<p>") 429 pCloseTag = []byte("</p>") 430 blockquoteTag = []byte("<blockquote>") 431 blockquoteCloseTag = []byte("</blockquote>") 432 hrTag = []byte("<hr>") 433 hrXHTMLTag = []byte("<hr />") 434 ulTag = []byte("<ul>") 435 ulCloseTag = []byte("</ul>") 436 olTag = []byte("<ol>") 437 olCloseTag = []byte("</ol>") 438 dlTag = []byte("<dl>") 439 dlCloseTag = []byte("</dl>") 440 liTag = []byte("<li>") 441 liCloseTag = []byte("</li>") 442 ddTag = []byte("<dd>") 443 ddCloseTag = []byte("</dd>") 444 dtTag = []byte("<dt>") 445 dtCloseTag = []byte("</dt>") 446 tableTag = []byte("<table>") 447 tableCloseTag = []byte("</table>") 448 tdTag = []byte("<td") 449 tdCloseTag = []byte("</td>") 450 thTag = []byte("<th") 451 thCloseTag = []byte("</th>") 452 theadTag = []byte("<thead>") 453 theadCloseTag = []byte("</thead>") 454 tbodyTag = []byte("<tbody>") 455 tbodyCloseTag = []byte("</tbody>") 456 trTag = []byte("<tr>") 457 trCloseTag = []byte("</tr>") 458 h1Tag = []byte("<h1") 459 h1CloseTag = []byte("</h1>") 460 h2Tag = []byte("<h2") 461 h2CloseTag = []byte("</h2>") 462 h3Tag = []byte("<h3") 463 h3CloseTag = []byte("</h3>") 464 h4Tag = []byte("<h4") 465 h4CloseTag = []byte("</h4>") 466 h5Tag = []byte("<h5") 467 h5CloseTag = []byte("</h5>") 468 h6Tag = []byte("<h6") 469 h6CloseTag = []byte("</h6>") 470 471 footnotesDivBytes = []byte("\n<div class=\"footnotes\">\n\n") 472 footnotesCloseDivBytes = []byte("\n</div>\n") 473 ) 474 475 func headingTagsFromLevel(level int) ([]byte, []byte) { 476 if level <= 1 { 477 return h1Tag, h1CloseTag 478 } 479 switch level { 480 case 2: 481 return h2Tag, h2CloseTag 482 case 3: 483 return h3Tag, h3CloseTag 484 case 4: 485 return h4Tag, h4CloseTag 486 case 5: 487 return h5Tag, h5CloseTag 488 } 489 return h6Tag, h6CloseTag 490 } 491 492 func (r *HTMLRenderer) outHRTag(w io.Writer) { 493 if r.Flags&UseXHTML == 0 { 494 r.out(w, hrTag) 495 } else { 496 r.out(w, hrXHTMLTag) 497 } 498 } 499 500 // RenderNode is a default renderer of a single node of a syntax tree. For 501 // block nodes it will be called twice: first time with entering=true, second 502 // time with entering=false, so that it could know when it's working on an open 503 // tag and when on close. It writes the result to w. 504 // 505 // The return value is a way to tell the calling walker to adjust its walk 506 // pattern: e.g. it can terminate the traversal by returning Terminate. Or it 507 // can ask the walker to skip a subtree of this node by returning SkipChildren. 508 // The typical behavior is to return GoToNext, which asks for the usual 509 // traversal to the next node. 510 func (r *HTMLRenderer) RenderNode(w io.Writer, node *Node, entering bool) WalkStatus { 511 attrs := []string{} 512 switch node.Type { 513 case Text: 514 if r.Flags&Smartypants != 0 { 515 var tmp bytes.Buffer 516 escapeHTML(&tmp, node.Literal) 517 r.sr.Process(w, tmp.Bytes()) 518 } else { 519 if node.Parent.Type == Link { 520 escLink(w, node.Literal) 521 } else { 522 escapeHTML(w, node.Literal) 523 } 524 } 525 case Softbreak: 526 r.cr(w) 527 // TODO: make it configurable via out(renderer.softbreak) 528 case Hardbreak: 529 if r.Flags&UseXHTML == 0 { 530 r.out(w, brTag) 531 } else { 532 r.out(w, brXHTMLTag) 533 } 534 r.cr(w) 535 case Emph: 536 if entering { 537 r.out(w, emTag) 538 } else { 539 r.out(w, emCloseTag) 540 } 541 case Censored: 542 if entering { 543 r.out(w, censoredTag) 544 } else { 545 r.out(w, censoredCloseTag) 546 } 547 case Strong: 548 if entering { 549 r.out(w, strongTag) 550 } else { 551 r.out(w, strongCloseTag) 552 } 553 case Del: 554 if entering { 555 r.out(w, delTag) 556 } else { 557 r.out(w, delCloseTag) 558 } 559 case HTMLSpan: 560 if r.Flags&SkipHTML != 0 { 561 break 562 } 563 r.out(w, node.Literal) 564 case Link: 565 // mark it but don't link it if it is not a safe link: no smartypants 566 dest := node.LinkData.Destination 567 if needSkipLink(r.Flags, dest) { 568 if entering { 569 r.out(w, ttTag) 570 } else { 571 r.out(w, ttCloseTag) 572 } 573 } else { 574 if entering { 575 dest = r.addAbsPrefix(dest) 576 var hrefBuf bytes.Buffer 577 hrefBuf.WriteString("href=\"") 578 escLink(&hrefBuf, dest) 579 hrefBuf.WriteByte('"') 580 attrs = append(attrs, hrefBuf.String()) 581 if node.NoteID != 0 { 582 r.out(w, footnoteRef(r.FootnoteAnchorPrefix, node)) 583 break 584 } 585 attrs = appendLinkAttrs(attrs, r.Flags, dest) 586 if len(node.LinkData.Title) > 0 { 587 var titleBuff bytes.Buffer 588 titleBuff.WriteString("title=\"") 589 escapeHTML(&titleBuff, node.LinkData.Title) 590 titleBuff.WriteByte('"') 591 attrs = append(attrs, titleBuff.String()) 592 } 593 r.tag(w, aTag, attrs) 594 } else { 595 if node.NoteID != 0 { 596 break 597 } 598 r.out(w, aCloseTag) 599 } 600 } 601 case Image: 602 if r.Flags&SkipImages != 0 { 603 return SkipChildren 604 } 605 if entering { 606 dest := node.LinkData.Destination 607 dest = r.addAbsPrefix(dest) 608 if r.disableTags == 0 { 609 //if options.safe && potentiallyUnsafe(dest) { 610 //out(w, `<img src="" alt="`) 611 //} else { 612 r.out(w, []byte(`<img src="`)) 613 escLink(w, dest) 614 r.out(w, []byte(`" alt="`)) 615 //} 616 } 617 r.disableTags++ 618 } else { 619 r.disableTags-- 620 if r.disableTags == 0 { 621 if node.LinkData.Title != nil { 622 r.out(w, []byte(`" title="`)) 623 escapeHTML(w, node.LinkData.Title) 624 } 625 r.out(w, []byte(`" />`)) 626 } 627 } 628 case Code: 629 r.out(w, codeTag) 630 escapeAllHTML(w, node.Literal) 631 r.out(w, codeCloseTag) 632 case Document: 633 break 634 case Paragraph: 635 if skipParagraphTags(node) { 636 break 637 } 638 if entering { 639 // TODO: untangle this clusterfuck about when the newlines need 640 // to be added and when not. 641 if node.Prev != nil { 642 switch node.Prev.Type { 643 case HTMLBlock, List, Paragraph, Heading, CodeBlock, BlockQuote, HorizontalRule: 644 r.cr(w) 645 } 646 } 647 if node.Parent.Type == BlockQuote && node.Prev == nil { 648 r.cr(w) 649 } 650 r.out(w, pTag) 651 } else { 652 r.out(w, pCloseTag) 653 if !(node.Parent.Type == Item && node.Next == nil) { 654 r.cr(w) 655 } 656 } 657 case BlockQuote: 658 if entering { 659 r.cr(w) 660 r.out(w, blockquoteTag) 661 } else { 662 r.out(w, blockquoteCloseTag) 663 r.cr(w) 664 } 665 case HTMLBlock: 666 if r.Flags&SkipHTML != 0 { 667 break 668 } 669 r.cr(w) 670 r.out(w, node.Literal) 671 r.cr(w) 672 case Heading: 673 headingLevel := r.HTMLRendererParameters.HeadingLevelOffset + node.Level 674 openTag, closeTag := headingTagsFromLevel(headingLevel) 675 if entering { 676 if node.IsTitleblock { 677 attrs = append(attrs, `class="title"`) 678 } 679 if node.HeadingID != "" { 680 id := r.ensureUniqueHeadingID(node.HeadingID) 681 if r.HeadingIDPrefix != "" { 682 id = r.HeadingIDPrefix + id 683 } 684 if r.HeadingIDSuffix != "" { 685 id = id + r.HeadingIDSuffix 686 } 687 attrs = append(attrs, fmt.Sprintf(`id="%s"`, id)) 688 } 689 r.cr(w) 690 r.tag(w, openTag, attrs) 691 } else { 692 r.out(w, closeTag) 693 if !(node.Parent.Type == Item && node.Next == nil) { 694 r.cr(w) 695 } 696 } 697 case HorizontalRule: 698 r.cr(w) 699 r.outHRTag(w) 700 r.cr(w) 701 case List: 702 openTag := ulTag 703 closeTag := ulCloseTag 704 if node.ListFlags&ListTypeOrdered != 0 { 705 openTag = olTag 706 closeTag = olCloseTag 707 } 708 if node.ListFlags&ListTypeDefinition != 0 { 709 openTag = dlTag 710 closeTag = dlCloseTag 711 } 712 if entering { 713 if node.IsFootnotesList { 714 r.out(w, footnotesDivBytes) 715 r.outHRTag(w) 716 r.cr(w) 717 } 718 r.cr(w) 719 if node.Parent.Type == Item && node.Parent.Parent.Tight { 720 r.cr(w) 721 } 722 r.tag(w, openTag[:len(openTag)-1], attrs) 723 r.cr(w) 724 } else { 725 r.out(w, closeTag) 726 //cr(w) 727 //if node.parent.Type != Item { 728 // cr(w) 729 //} 730 if node.Parent.Type == Item && node.Next != nil { 731 r.cr(w) 732 } 733 if node.Parent.Type == Document || node.Parent.Type == BlockQuote { 734 r.cr(w) 735 } 736 if node.IsFootnotesList { 737 r.out(w, footnotesCloseDivBytes) 738 } 739 } 740 case Item: 741 openTag := liTag 742 closeTag := liCloseTag 743 if node.ListFlags&ListTypeDefinition != 0 { 744 openTag = ddTag 745 closeTag = ddCloseTag 746 } 747 if node.ListFlags&ListTypeTerm != 0 { 748 openTag = dtTag 749 closeTag = dtCloseTag 750 } 751 if entering { 752 if itemOpenCR(node) { 753 r.cr(w) 754 } 755 if node.ListData.RefLink != nil { 756 slug := slugify(node.ListData.RefLink) 757 r.out(w, footnoteItem(r.FootnoteAnchorPrefix, slug)) 758 break 759 } 760 r.out(w, openTag) 761 } else { 762 if node.ListData.RefLink != nil { 763 slug := slugify(node.ListData.RefLink) 764 if r.Flags&FootnoteReturnLinks != 0 { 765 r.out(w, footnoteReturnLink(r.FootnoteAnchorPrefix, r.FootnoteReturnLinkContents, slug)) 766 } 767 } 768 r.out(w, closeTag) 769 r.cr(w) 770 } 771 case CodeBlock: 772 attrs = appendLanguageAttr(attrs, node.Info) 773 r.cr(w) 774 r.out(w, preTag) 775 r.tag(w, codeTag[:len(codeTag)-1], attrs) 776 escapeAllHTML(w, node.Literal) 777 r.out(w, codeCloseTag) 778 r.out(w, preCloseTag) 779 if node.Parent.Type != Item { 780 r.cr(w) 781 } 782 case Table: 783 if entering { 784 r.cr(w) 785 r.out(w, tableTag) 786 } else { 787 r.out(w, tableCloseTag) 788 r.cr(w) 789 } 790 case TableCell: 791 openTag := tdTag 792 closeTag := tdCloseTag 793 if node.IsHeader { 794 openTag = thTag 795 closeTag = thCloseTag 796 } 797 if entering { 798 align := cellAlignment(node.Align) 799 if align != "" { 800 attrs = append(attrs, fmt.Sprintf(`align="%s"`, align)) 801 } 802 if node.Prev == nil { 803 r.cr(w) 804 } 805 r.tag(w, openTag, attrs) 806 } else { 807 r.out(w, closeTag) 808 r.cr(w) 809 } 810 case TableHead: 811 if entering { 812 r.cr(w) 813 r.out(w, theadTag) 814 } else { 815 r.out(w, theadCloseTag) 816 r.cr(w) 817 } 818 case TableBody: 819 if entering { 820 r.cr(w) 821 r.out(w, tbodyTag) 822 // XXX: this is to adhere to a rather silly test. Should fix test. 823 if node.FirstChild == nil { 824 r.cr(w) 825 } 826 } else { 827 r.out(w, tbodyCloseTag) 828 r.cr(w) 829 } 830 case TableRow: 831 if entering { 832 r.cr(w) 833 r.out(w, trTag) 834 } else { 835 r.out(w, trCloseTag) 836 r.cr(w) 837 } 838 default: 839 panic("Unknown node type " + node.Type.String()) 840 } 841 return GoToNext 842 } 843 844 // RenderHeader writes HTML document preamble and TOC if requested. 845 func (r *HTMLRenderer) RenderHeader(w io.Writer, ast *Node) { 846 r.writeDocumentHeader(w) 847 if r.Flags&TOC != 0 { 848 r.writeTOC(w, ast) 849 } 850 } 851 852 // RenderFooter writes HTML document footer. 853 func (r *HTMLRenderer) RenderFooter(w io.Writer, ast *Node) { 854 if r.Flags&CompletePage == 0 { 855 return 856 } 857 io.WriteString(w, "\n</body>\n</html>\n") 858 } 859 860 func (r *HTMLRenderer) writeDocumentHeader(w io.Writer) { 861 if r.Flags&CompletePage == 0 { 862 return 863 } 864 ending := "" 865 if r.Flags&UseXHTML != 0 { 866 io.WriteString(w, "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" ") 867 io.WriteString(w, "\"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">\n") 868 io.WriteString(w, "<html xmlns=\"http://www.w3.org/1999/xhtml\">\n") 869 ending = " /" 870 } else { 871 io.WriteString(w, "<!DOCTYPE html>\n") 872 io.WriteString(w, "<html>\n") 873 } 874 io.WriteString(w, "<head>\n") 875 io.WriteString(w, " <title>") 876 if r.Flags&Smartypants != 0 { 877 r.sr.Process(w, []byte(r.Title)) 878 } else { 879 escapeHTML(w, []byte(r.Title)) 880 } 881 io.WriteString(w, "</title>\n") 882 io.WriteString(w, " <meta name=\"GENERATOR\" content=\"Blackfriday Markdown Processor v") 883 io.WriteString(w, Version) 884 io.WriteString(w, "\"") 885 io.WriteString(w, ending) 886 io.WriteString(w, ">\n") 887 io.WriteString(w, " <meta charset=\"utf-8\"") 888 io.WriteString(w, ending) 889 io.WriteString(w, ">\n") 890 if r.CSS != "" { 891 io.WriteString(w, " <link rel=\"stylesheet\" type=\"text/css\" href=\"") 892 escapeHTML(w, []byte(r.CSS)) 893 io.WriteString(w, "\"") 894 io.WriteString(w, ending) 895 io.WriteString(w, ">\n") 896 } 897 if r.Icon != "" { 898 io.WriteString(w, " <link rel=\"icon\" type=\"image/x-icon\" href=\"") 899 escapeHTML(w, []byte(r.Icon)) 900 io.WriteString(w, "\"") 901 io.WriteString(w, ending) 902 io.WriteString(w, ">\n") 903 } 904 io.WriteString(w, "</head>\n") 905 io.WriteString(w, "<body>\n\n") 906 } 907 908 func (r *HTMLRenderer) writeTOC(w io.Writer, ast *Node) { 909 buf := bytes.Buffer{} 910 911 inHeading := false 912 tocLevel := 0 913 headingCount := 0 914 915 ast.Walk(func(node *Node, entering bool) WalkStatus { 916 if node.Type == Heading && !node.HeadingData.IsTitleblock { 917 inHeading = entering 918 if entering { 919 node.HeadingID = fmt.Sprintf("toc_%d", headingCount) 920 if node.Level == tocLevel { 921 buf.WriteString("</li>\n\n<li>") 922 } else if node.Level < tocLevel { 923 for node.Level < tocLevel { 924 tocLevel-- 925 buf.WriteString("</li>\n</ul>") 926 } 927 buf.WriteString("</li>\n\n<li>") 928 } else { 929 for node.Level > tocLevel { 930 tocLevel++ 931 buf.WriteString("\n<ul>\n<li>") 932 } 933 } 934 935 fmt.Fprintf(&buf, `<a href="#toc_%d">`, headingCount) 936 headingCount++ 937 } else { 938 buf.WriteString("</a>") 939 } 940 return GoToNext 941 } 942 943 if inHeading { 944 return r.RenderNode(&buf, node, entering) 945 } 946 947 return GoToNext 948 }) 949 950 for ; tocLevel > 0; tocLevel-- { 951 buf.WriteString("</li>\n</ul>") 952 } 953 954 if buf.Len() > 0 { 955 io.WriteString(w, "<nav>\n") 956 w.Write(buf.Bytes()) 957 io.WriteString(w, "\n\n</nav>\n") 958 } 959 r.lastOutputLen = buf.Len() 960 }