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 16package blackfriday 17 18import ( 19 "bytes" 20 "fmt" 21 "io" 22 "regexp" 23 "strings" 24) 25 26// HTMLFlags control optional behavior of HTML renderer. 27type HTMLFlags int 28 29// HTML renderer configuration options. 30const ( 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 52var ( 53 htmlTagRe = regexp.MustCompile("(?i)^" + htmlTag) 54) 55 56const ( 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. 77type 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. 106type 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 120const ( 121 xhtmlClose = " />" 122 htmlClose = ">" 123) 124 125// NewHTMLRenderer creates and configures an HTMLRenderer object, which 126// satisfies the Renderer interface. 127func 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 151func 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 158func 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 179func 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 215func skipSpace(tag []byte, i int) int { 216 for i < len(tag) && isspace(tag[i]) { 217 i++ 218 } 219 return i 220} 221 222func 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 251func (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 270func (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 282func 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 306func isMailto(link []byte) bool { 307 return bytes.HasPrefix(link, []byte("mailto:")) 308} 309 310func 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 317func isSmartypantable(node *Node) bool { 318 pt := node.Parent.Type 319 return pt != Link && pt != CodeBlock && pt != Code 320} 321 322func 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 333func (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 343func 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 349func footnoteItem(prefix string, slug []byte) []byte { 350 return []byte(fmt.Sprintf(`<li id="fn:%s%s">`, prefix, slug)) 351} 352 353func 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 358func 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 366func 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 375func 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 388func (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 397func (r *HTMLRenderer) cr(w io.Writer) { 398 if r.lastOutputLen > 0 { 399 r.out(w, nlBytes) 400 } 401} 402 403var ( 404 nlBytes = []byte{'\n'} 405 gtBytes = []byte{'>'} 406 spaceBytes = []byte{' '} 407) 408 409var ( 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 473func 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 490func (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. 508func (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. 837func (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. 845func (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 852func (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 900func (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} 953