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 params.FootnoteReturnLinkContents = `<sup>[return]</sup>` 136 } 137 138 return &HTMLRenderer{ 139 HTMLRendererParameters: params, 140 141 closeTag: closeTag, 142 headingIDs: make(map[string]int), 143 144 sr: NewSmartypantsRenderer(params.Flags), 145 } 146} 147 148func isHTMLTag(tag []byte, tagname string) bool { 149 found, _ := findHTMLTagPos(tag, tagname) 150 return found 151} 152 153// Look for a character, but ignore it when it's in any kind of quotes, it 154// might be JavaScript 155func skipUntilCharIgnoreQuotes(html []byte, start int, char byte) int { 156 inSingleQuote := false 157 inDoubleQuote := false 158 inGraveQuote := false 159 i := start 160 for i < len(html) { 161 switch { 162 case html[i] == char && !inSingleQuote && !inDoubleQuote && !inGraveQuote: 163 return i 164 case html[i] == '\'': 165 inSingleQuote = !inSingleQuote 166 case html[i] == '"': 167 inDoubleQuote = !inDoubleQuote 168 case html[i] == '`': 169 inGraveQuote = !inGraveQuote 170 } 171 i++ 172 } 173 return start 174} 175 176func findHTMLTagPos(tag []byte, tagname string) (bool, int) { 177 i := 0 178 if i < len(tag) && tag[0] != '<' { 179 return false, -1 180 } 181 i++ 182 i = skipSpace(tag, i) 183 184 if i < len(tag) && tag[i] == '/' { 185 i++ 186 } 187 188 i = skipSpace(tag, i) 189 j := 0 190 for ; i < len(tag); i, j = i+1, j+1 { 191 if j >= len(tagname) { 192 break 193 } 194 195 if strings.ToLower(string(tag[i]))[0] != tagname[j] { 196 return false, -1 197 } 198 } 199 200 if i == len(tag) { 201 return false, -1 202 } 203 204 rightAngle := skipUntilCharIgnoreQuotes(tag, i, '>') 205 if rightAngle >= i { 206 return true, rightAngle 207 } 208 209 return false, -1 210} 211 212func skipSpace(tag []byte, i int) int { 213 for i < len(tag) && isspace(tag[i]) { 214 i++ 215 } 216 return i 217} 218 219func isRelativeLink(link []byte) (yes bool) { 220 // a tag begin with '#' 221 if link[0] == '#' { 222 return true 223 } 224 225 // link begin with '/' but not '//', the second maybe a protocol relative link 226 if len(link) >= 2 && link[0] == '/' && link[1] != '/' { 227 return true 228 } 229 230 // only the root '/' 231 if len(link) == 1 && link[0] == '/' { 232 return true 233 } 234 235 // current directory : begin with "./" 236 if bytes.HasPrefix(link, []byte("./")) { 237 return true 238 } 239 240 // parent directory : begin with "../" 241 if bytes.HasPrefix(link, []byte("../")) { 242 return true 243 } 244 245 return false 246} 247 248func (r *HTMLRenderer) ensureUniqueHeadingID(id string) string { 249 for count, found := r.headingIDs[id]; found; count, found = r.headingIDs[id] { 250 tmp := fmt.Sprintf("%s-%d", id, count+1) 251 252 if _, tmpFound := r.headingIDs[tmp]; !tmpFound { 253 r.headingIDs[id] = count + 1 254 id = tmp 255 } else { 256 id = id + "-1" 257 } 258 } 259 260 if _, found := r.headingIDs[id]; !found { 261 r.headingIDs[id] = 0 262 } 263 264 return id 265} 266 267func (r *HTMLRenderer) addAbsPrefix(link []byte) []byte { 268 if r.AbsolutePrefix != "" && isRelativeLink(link) && link[0] != '.' { 269 newDest := r.AbsolutePrefix 270 if link[0] != '/' { 271 newDest += "/" 272 } 273 newDest += string(link) 274 return []byte(newDest) 275 } 276 return link 277} 278 279func appendLinkAttrs(attrs []string, flags HTMLFlags, link []byte) []string { 280 if isRelativeLink(link) { 281 return attrs 282 } 283 val := []string{} 284 if flags&NofollowLinks != 0 { 285 val = append(val, "nofollow") 286 } 287 if flags&NoreferrerLinks != 0 { 288 val = append(val, "noreferrer") 289 } 290 if flags&NoopenerLinks != 0 { 291 val = append(val, "noopener") 292 } 293 if flags&HrefTargetBlank != 0 { 294 attrs = append(attrs, "target=\"_blank\"") 295 } 296 if len(val) == 0 { 297 return attrs 298 } 299 attr := fmt.Sprintf("rel=%q", strings.Join(val, " ")) 300 return append(attrs, attr) 301} 302 303func isMailto(link []byte) bool { 304 return bytes.HasPrefix(link, []byte("mailto:")) 305} 306 307func needSkipLink(flags HTMLFlags, dest []byte) bool { 308 if flags&SkipLinks != 0 { 309 return true 310 } 311 return flags&Safelink != 0 && !isSafeLink(dest) && !isMailto(dest) 312} 313 314func isSmartypantable(node *Node) bool { 315 pt := node.Parent.Type 316 return pt != Link && pt != CodeBlock && pt != Code 317} 318 319func appendLanguageAttr(attrs []string, info []byte) []string { 320 if len(info) == 0 { 321 return attrs 322 } 323 endOfLang := bytes.IndexAny(info, "\t ") 324 if endOfLang < 0 { 325 endOfLang = len(info) 326 } 327 return append(attrs, fmt.Sprintf("class=\"language-%s\"", info[:endOfLang])) 328} 329 330func (r *HTMLRenderer) tag(w io.Writer, name []byte, attrs []string) { 331 w.Write(name) 332 if len(attrs) > 0 { 333 w.Write(spaceBytes) 334 w.Write([]byte(strings.Join(attrs, " "))) 335 } 336 w.Write(gtBytes) 337 r.lastOutputLen = 1 338} 339 340func footnoteRef(prefix string, node *Node) []byte { 341 urlFrag := prefix + string(slugify(node.Destination)) 342 anchor := fmt.Sprintf(`<a href="#fn:%s">%d</a>`, urlFrag, node.NoteID) 343 return []byte(fmt.Sprintf(`<sup class="footnote-ref" id="fnref:%s">%s</sup>`, urlFrag, anchor)) 344} 345 346func footnoteItem(prefix string, slug []byte) []byte { 347 return []byte(fmt.Sprintf(`<li id="fn:%s%s">`, prefix, slug)) 348} 349 350func footnoteReturnLink(prefix, returnLink string, slug []byte) []byte { 351 const format = ` <a class="footnote-return" href="#fnref:%s%s">%s</a>` 352 return []byte(fmt.Sprintf(format, prefix, slug, returnLink)) 353} 354 355func itemOpenCR(node *Node) bool { 356 if node.Prev == nil { 357 return false 358 } 359 ld := node.Parent.ListData 360 return !ld.Tight && ld.ListFlags&ListTypeDefinition == 0 361} 362 363func skipParagraphTags(node *Node) bool { 364 grandparent := node.Parent.Parent 365 if grandparent == nil || grandparent.Type != List { 366 return false 367 } 368 tightOrTerm := grandparent.Tight || node.Parent.ListFlags&ListTypeTerm != 0 369 return grandparent.Type == List && tightOrTerm 370} 371 372func cellAlignment(align CellAlignFlags) string { 373 switch align { 374 case TableAlignmentLeft: 375 return "left" 376 case TableAlignmentRight: 377 return "right" 378 case TableAlignmentCenter: 379 return "center" 380 default: 381 return "" 382 } 383} 384 385func (r *HTMLRenderer) out(w io.Writer, text []byte) { 386 if r.disableTags > 0 { 387 w.Write(htmlTagRe.ReplaceAll(text, []byte{})) 388 } else { 389 w.Write(text) 390 } 391 r.lastOutputLen = len(text) 392} 393 394func (r *HTMLRenderer) cr(w io.Writer) { 395 if r.lastOutputLen > 0 { 396 r.out(w, nlBytes) 397 } 398} 399 400var ( 401 nlBytes = []byte{'\n'} 402 gtBytes = []byte{'>'} 403 spaceBytes = []byte{' '} 404) 405 406var ( 407 brTag = []byte("<br>") 408 brXHTMLTag = []byte("<br />") 409 emTag = []byte("<em>") 410 emCloseTag = []byte("</em>") 411 strongTag = []byte("<strong>") 412 strongCloseTag = []byte("</strong>") 413 delTag = []byte("<del>") 414 delCloseTag = []byte("</del>") 415 ttTag = []byte("<tt>") 416 ttCloseTag = []byte("</tt>") 417 aTag = []byte("<a") 418 aCloseTag = []byte("</a>") 419 preTag = []byte("<pre>") 420 preCloseTag = []byte("</pre>") 421 codeTag = []byte("<code>") 422 codeCloseTag = []byte("</code>") 423 pTag = []byte("<p>") 424 pCloseTag = []byte("</p>") 425 blockquoteTag = []byte("<blockquote>") 426 blockquoteCloseTag = []byte("</blockquote>") 427 hrTag = []byte("<hr>") 428 hrXHTMLTag = []byte("<hr />") 429 ulTag = []byte("<ul>") 430 ulCloseTag = []byte("</ul>") 431 olTag = []byte("<ol>") 432 olCloseTag = []byte("</ol>") 433 dlTag = []byte("<dl>") 434 dlCloseTag = []byte("</dl>") 435 liTag = []byte("<li>") 436 liCloseTag = []byte("</li>") 437 ddTag = []byte("<dd>") 438 ddCloseTag = []byte("</dd>") 439 dtTag = []byte("<dt>") 440 dtCloseTag = []byte("</dt>") 441 tableTag = []byte("<table>") 442 tableCloseTag = []byte("</table>") 443 tdTag = []byte("<td") 444 tdCloseTag = []byte("</td>") 445 thTag = []byte("<th") 446 thCloseTag = []byte("</th>") 447 theadTag = []byte("<thead>") 448 theadCloseTag = []byte("</thead>") 449 tbodyTag = []byte("<tbody>") 450 tbodyCloseTag = []byte("</tbody>") 451 trTag = []byte("<tr>") 452 trCloseTag = []byte("</tr>") 453 h1Tag = []byte("<h1") 454 h1CloseTag = []byte("</h1>") 455 h2Tag = []byte("<h2") 456 h2CloseTag = []byte("</h2>") 457 h3Tag = []byte("<h3") 458 h3CloseTag = []byte("</h3>") 459 h4Tag = []byte("<h4") 460 h4CloseTag = []byte("</h4>") 461 h5Tag = []byte("<h5") 462 h5CloseTag = []byte("</h5>") 463 h6Tag = []byte("<h6") 464 h6CloseTag = []byte("</h6>") 465 466 footnotesDivBytes = []byte("\n<div class=\"footnotes\">\n\n") 467 footnotesCloseDivBytes = []byte("\n</div>\n") 468) 469 470func headingTagsFromLevel(level int) ([]byte, []byte) { 471 if level <= 1 { 472 return h1Tag, h1CloseTag 473 } 474 switch level { 475 case 2: 476 return h2Tag, h2CloseTag 477 case 3: 478 return h3Tag, h3CloseTag 479 case 4: 480 return h4Tag, h4CloseTag 481 case 5: 482 return h5Tag, h5CloseTag 483 } 484 return h6Tag, h6CloseTag 485} 486 487func (r *HTMLRenderer) outHRTag(w io.Writer) { 488 if r.Flags&UseXHTML == 0 { 489 r.out(w, hrTag) 490 } else { 491 r.out(w, hrXHTMLTag) 492 } 493} 494 495// RenderNode is a default renderer of a single node of a syntax tree. For 496// block nodes it will be called twice: first time with entering=true, second 497// time with entering=false, so that it could know when it's working on an open 498// tag and when on close. It writes the result to w. 499// 500// The return value is a way to tell the calling walker to adjust its walk 501// pattern: e.g. it can terminate the traversal by returning Terminate. Or it 502// can ask the walker to skip a subtree of this node by returning SkipChildren. 503// The typical behavior is to return GoToNext, which asks for the usual 504// traversal to the next node. 505func (r *HTMLRenderer) RenderNode(w io.Writer, node *Node, entering bool) WalkStatus { 506 attrs := []string{} 507 switch node.Type { 508 case Text: 509 if r.Flags&Smartypants != 0 { 510 var tmp bytes.Buffer 511 escapeHTML(&tmp, node.Literal) 512 r.sr.Process(w, tmp.Bytes()) 513 } else { 514 if node.Parent.Type == Link { 515 escLink(w, node.Literal) 516 } else { 517 escapeHTML(w, node.Literal) 518 } 519 } 520 case Softbreak: 521 r.cr(w) 522 // TODO: make it configurable via out(renderer.softbreak) 523 case Hardbreak: 524 if r.Flags&UseXHTML == 0 { 525 r.out(w, brTag) 526 } else { 527 r.out(w, brXHTMLTag) 528 } 529 r.cr(w) 530 case Emph: 531 if entering { 532 r.out(w, emTag) 533 } else { 534 r.out(w, emCloseTag) 535 } 536 case Strong: 537 if entering { 538 r.out(w, strongTag) 539 } else { 540 r.out(w, strongCloseTag) 541 } 542 case Del: 543 if entering { 544 r.out(w, delTag) 545 } else { 546 r.out(w, delCloseTag) 547 } 548 case HTMLSpan: 549 if r.Flags&SkipHTML != 0 { 550 break 551 } 552 r.out(w, node.Literal) 553 case Link: 554 // mark it but don't link it if it is not a safe link: no smartypants 555 dest := node.LinkData.Destination 556 if needSkipLink(r.Flags, dest) { 557 if entering { 558 r.out(w, ttTag) 559 } else { 560 r.out(w, ttCloseTag) 561 } 562 } else { 563 if entering { 564 dest = r.addAbsPrefix(dest) 565 var hrefBuf bytes.Buffer 566 hrefBuf.WriteString("href=\"") 567 escLink(&hrefBuf, dest) 568 hrefBuf.WriteByte('"') 569 attrs = append(attrs, hrefBuf.String()) 570 if node.NoteID != 0 { 571 r.out(w, footnoteRef(r.FootnoteAnchorPrefix, node)) 572 break 573 } 574 attrs = appendLinkAttrs(attrs, r.Flags, dest) 575 if len(node.LinkData.Title) > 0 { 576 var titleBuff bytes.Buffer 577 titleBuff.WriteString("title=\"") 578 escapeHTML(&titleBuff, node.LinkData.Title) 579 titleBuff.WriteByte('"') 580 attrs = append(attrs, titleBuff.String()) 581 } 582 r.tag(w, aTag, attrs) 583 } else { 584 if node.NoteID != 0 { 585 break 586 } 587 r.out(w, aCloseTag) 588 } 589 } 590 case Image: 591 if r.Flags&SkipImages != 0 { 592 return SkipChildren 593 } 594 if entering { 595 dest := node.LinkData.Destination 596 dest = r.addAbsPrefix(dest) 597 if r.disableTags == 0 { 598 //if options.safe && potentiallyUnsafe(dest) { 599 //out(w, `<img src="" alt="`) 600 //} else { 601 r.out(w, []byte(`<img src="`)) 602 escLink(w, dest) 603 r.out(w, []byte(`" alt="`)) 604 //} 605 } 606 r.disableTags++ 607 } else { 608 r.disableTags-- 609 if r.disableTags == 0 { 610 if node.LinkData.Title != nil { 611 r.out(w, []byte(`" title="`)) 612 escapeHTML(w, node.LinkData.Title) 613 } 614 r.out(w, []byte(`" />`)) 615 } 616 } 617 case Code: 618 r.out(w, codeTag) 619 escapeHTML(w, node.Literal) 620 r.out(w, codeCloseTag) 621 case Document: 622 break 623 case Paragraph: 624 if skipParagraphTags(node) { 625 break 626 } 627 if entering { 628 // TODO: untangle this clusterfuck about when the newlines need 629 // to be added and when not. 630 if node.Prev != nil { 631 switch node.Prev.Type { 632 case HTMLBlock, List, Paragraph, Heading, CodeBlock, BlockQuote, HorizontalRule: 633 r.cr(w) 634 } 635 } 636 if node.Parent.Type == BlockQuote && node.Prev == nil { 637 r.cr(w) 638 } 639 r.out(w, pTag) 640 } else { 641 r.out(w, pCloseTag) 642 if !(node.Parent.Type == Item && node.Next == nil) { 643 r.cr(w) 644 } 645 } 646 case BlockQuote: 647 if entering { 648 r.cr(w) 649 r.out(w, blockquoteTag) 650 } else { 651 r.out(w, blockquoteCloseTag) 652 r.cr(w) 653 } 654 case HTMLBlock: 655 if r.Flags&SkipHTML != 0 { 656 break 657 } 658 r.cr(w) 659 r.out(w, node.Literal) 660 r.cr(w) 661 case Heading: 662 headingLevel := r.HTMLRendererParameters.HeadingLevelOffset + node.Level 663 openTag, closeTag := headingTagsFromLevel(headingLevel) 664 if entering { 665 if node.IsTitleblock { 666 attrs = append(attrs, `class="title"`) 667 } 668 if node.HeadingID != "" { 669 id := r.ensureUniqueHeadingID(node.HeadingID) 670 if r.HeadingIDPrefix != "" { 671 id = r.HeadingIDPrefix + id 672 } 673 if r.HeadingIDSuffix != "" { 674 id = id + r.HeadingIDSuffix 675 } 676 attrs = append(attrs, fmt.Sprintf(`id="%s"`, id)) 677 } 678 r.cr(w) 679 r.tag(w, openTag, attrs) 680 } else { 681 r.out(w, closeTag) 682 if !(node.Parent.Type == Item && node.Next == nil) { 683 r.cr(w) 684 } 685 } 686 case HorizontalRule: 687 r.cr(w) 688 r.outHRTag(w) 689 r.cr(w) 690 case List: 691 openTag := ulTag 692 closeTag := ulCloseTag 693 if node.ListFlags&ListTypeOrdered != 0 { 694 openTag = olTag 695 closeTag = olCloseTag 696 } 697 if node.ListFlags&ListTypeDefinition != 0 { 698 openTag = dlTag 699 closeTag = dlCloseTag 700 } 701 if entering { 702 if node.IsFootnotesList { 703 r.out(w, footnotesDivBytes) 704 r.outHRTag(w) 705 r.cr(w) 706 } 707 r.cr(w) 708 if node.Parent.Type == Item && node.Parent.Parent.Tight { 709 r.cr(w) 710 } 711 r.tag(w, openTag[:len(openTag)-1], attrs) 712 r.cr(w) 713 } else { 714 r.out(w, closeTag) 715 //cr(w) 716 //if node.parent.Type != Item { 717 // cr(w) 718 //} 719 if node.Parent.Type == Item && node.Next != nil { 720 r.cr(w) 721 } 722 if node.Parent.Type == Document || node.Parent.Type == BlockQuote { 723 r.cr(w) 724 } 725 if node.IsFootnotesList { 726 r.out(w, footnotesCloseDivBytes) 727 } 728 } 729 case Item: 730 openTag := liTag 731 closeTag := liCloseTag 732 if node.ListFlags&ListTypeDefinition != 0 { 733 openTag = ddTag 734 closeTag = ddCloseTag 735 } 736 if node.ListFlags&ListTypeTerm != 0 { 737 openTag = dtTag 738 closeTag = dtCloseTag 739 } 740 if entering { 741 if itemOpenCR(node) { 742 r.cr(w) 743 } 744 if node.ListData.RefLink != nil { 745 slug := slugify(node.ListData.RefLink) 746 r.out(w, footnoteItem(r.FootnoteAnchorPrefix, slug)) 747 break 748 } 749 r.out(w, openTag) 750 } else { 751 if node.ListData.RefLink != nil { 752 slug := slugify(node.ListData.RefLink) 753 if r.Flags&FootnoteReturnLinks != 0 { 754 r.out(w, footnoteReturnLink(r.FootnoteAnchorPrefix, r.FootnoteReturnLinkContents, slug)) 755 } 756 } 757 r.out(w, closeTag) 758 r.cr(w) 759 } 760 case CodeBlock: 761 attrs = appendLanguageAttr(attrs, node.Info) 762 r.cr(w) 763 r.out(w, preTag) 764 r.tag(w, codeTag[:len(codeTag)-1], attrs) 765 escapeHTML(w, node.Literal) 766 r.out(w, codeCloseTag) 767 r.out(w, preCloseTag) 768 if node.Parent.Type != Item { 769 r.cr(w) 770 } 771 case Table: 772 if entering { 773 r.cr(w) 774 r.out(w, tableTag) 775 } else { 776 r.out(w, tableCloseTag) 777 r.cr(w) 778 } 779 case TableCell: 780 openTag := tdTag 781 closeTag := tdCloseTag 782 if node.IsHeader { 783 openTag = thTag 784 closeTag = thCloseTag 785 } 786 if entering { 787 align := cellAlignment(node.Align) 788 if align != "" { 789 attrs = append(attrs, fmt.Sprintf(`align="%s"`, align)) 790 } 791 if node.Prev == nil { 792 r.cr(w) 793 } 794 r.tag(w, openTag, attrs) 795 } else { 796 r.out(w, closeTag) 797 r.cr(w) 798 } 799 case TableHead: 800 if entering { 801 r.cr(w) 802 r.out(w, theadTag) 803 } else { 804 r.out(w, theadCloseTag) 805 r.cr(w) 806 } 807 case TableBody: 808 if entering { 809 r.cr(w) 810 r.out(w, tbodyTag) 811 // XXX: this is to adhere to a rather silly test. Should fix test. 812 if node.FirstChild == nil { 813 r.cr(w) 814 } 815 } else { 816 r.out(w, tbodyCloseTag) 817 r.cr(w) 818 } 819 case TableRow: 820 if entering { 821 r.cr(w) 822 r.out(w, trTag) 823 } else { 824 r.out(w, trCloseTag) 825 r.cr(w) 826 } 827 default: 828 panic("Unknown node type " + node.Type.String()) 829 } 830 return GoToNext 831} 832 833// RenderHeader writes HTML document preamble and TOC if requested. 834func (r *HTMLRenderer) RenderHeader(w io.Writer, ast *Node) { 835 r.writeDocumentHeader(w) 836 if r.Flags&TOC != 0 { 837 r.writeTOC(w, ast) 838 } 839} 840 841// RenderFooter writes HTML document footer. 842func (r *HTMLRenderer) RenderFooter(w io.Writer, ast *Node) { 843 if r.Flags&CompletePage == 0 { 844 return 845 } 846 io.WriteString(w, "\n</body>\n</html>\n") 847} 848 849func (r *HTMLRenderer) writeDocumentHeader(w io.Writer) { 850 if r.Flags&CompletePage == 0 { 851 return 852 } 853 ending := "" 854 if r.Flags&UseXHTML != 0 { 855 io.WriteString(w, "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" ") 856 io.WriteString(w, "\"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">\n") 857 io.WriteString(w, "<html xmlns=\"http://www.w3.org/1999/xhtml\">\n") 858 ending = " /" 859 } else { 860 io.WriteString(w, "<!DOCTYPE html>\n") 861 io.WriteString(w, "<html>\n") 862 } 863 io.WriteString(w, "<head>\n") 864 io.WriteString(w, " <title>") 865 if r.Flags&Smartypants != 0 { 866 r.sr.Process(w, []byte(r.Title)) 867 } else { 868 escapeHTML(w, []byte(r.Title)) 869 } 870 io.WriteString(w, "</title>\n") 871 io.WriteString(w, " <meta name=\"GENERATOR\" content=\"Blackfriday Markdown Processor v") 872 io.WriteString(w, Version) 873 io.WriteString(w, "\"") 874 io.WriteString(w, ending) 875 io.WriteString(w, ">\n") 876 io.WriteString(w, " <meta charset=\"utf-8\"") 877 io.WriteString(w, ending) 878 io.WriteString(w, ">\n") 879 if r.CSS != "" { 880 io.WriteString(w, " <link rel=\"stylesheet\" type=\"text/css\" href=\"") 881 escapeHTML(w, []byte(r.CSS)) 882 io.WriteString(w, "\"") 883 io.WriteString(w, ending) 884 io.WriteString(w, ">\n") 885 } 886 if r.Icon != "" { 887 io.WriteString(w, " <link rel=\"icon\" type=\"image/x-icon\" href=\"") 888 escapeHTML(w, []byte(r.Icon)) 889 io.WriteString(w, "\"") 890 io.WriteString(w, ending) 891 io.WriteString(w, ">\n") 892 } 893 io.WriteString(w, "</head>\n") 894 io.WriteString(w, "<body>\n\n") 895} 896 897func (r *HTMLRenderer) writeTOC(w io.Writer, ast *Node) { 898 buf := bytes.Buffer{} 899 900 inHeading := false 901 tocLevel := 0 902 headingCount := 0 903 904 ast.Walk(func(node *Node, entering bool) WalkStatus { 905 if node.Type == Heading && !node.HeadingData.IsTitleblock { 906 inHeading = entering 907 if entering { 908 node.HeadingID = fmt.Sprintf("toc_%d", headingCount) 909 if node.Level == tocLevel { 910 buf.WriteString("</li>\n\n<li>") 911 } else if node.Level < tocLevel { 912 for node.Level < tocLevel { 913 tocLevel-- 914 buf.WriteString("</li>\n</ul>") 915 } 916 buf.WriteString("</li>\n\n<li>") 917 } else { 918 for node.Level > tocLevel { 919 tocLevel++ 920 buf.WriteString("\n<ul>\n<li>") 921 } 922 } 923 924 fmt.Fprintf(&buf, `<a href="#toc_%d">`, headingCount) 925 headingCount++ 926 } else { 927 buf.WriteString("</a>") 928 } 929 return GoToNext 930 } 931 932 if inHeading { 933 return r.RenderNode(&buf, node, entering) 934 } 935 936 return GoToNext 937 }) 938 939 for ; tocLevel > 0; tocLevel-- { 940 buf.WriteString("</li>\n</ul>") 941 } 942 943 if buf.Len() > 0 { 944 io.WriteString(w, "<nav>\n") 945 w.Write(buf.Bytes()) 946 io.WriteString(w, "\n\n</nav>\n") 947 } 948 r.lastOutputLen = buf.Len() 949} 950