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 "regexp" 22 "strconv" 23 "strings" 24) 25 26// Html renderer configuration options. 27const ( 28 HTML_SKIP_HTML = 1 << iota // skip preformatted HTML blocks 29 HTML_SKIP_STYLE // skip embedded <style> elements 30 HTML_SKIP_IMAGES // skip embedded images 31 HTML_SKIP_LINKS // skip all links 32 HTML_SAFELINK // only link to trusted protocols 33 HTML_NOFOLLOW_LINKS // only link with rel="nofollow" 34 HTML_NOREFERRER_LINKS // only link with rel="noreferrer" 35 HTML_NOOPENER_LINKS // only link with rel="noopener" 36 HTML_HREF_TARGET_BLANK // add a blank target 37 HTML_TOC // generate a table of contents 38 HTML_OMIT_CONTENTS // skip the main contents (for a standalone table of contents) 39 HTML_COMPLETE_PAGE // generate a complete HTML page 40 HTML_USE_XHTML // generate XHTML output instead of HTML 41 HTML_USE_SMARTYPANTS // enable smart punctuation substitutions 42 HTML_SMARTYPANTS_FRACTIONS // enable smart fractions (with HTML_USE_SMARTYPANTS) 43 HTML_SMARTYPANTS_DASHES // enable smart dashes (with HTML_USE_SMARTYPANTS) 44 HTML_SMARTYPANTS_LATEX_DASHES // enable LaTeX-style dashes (with HTML_USE_SMARTYPANTS and HTML_SMARTYPANTS_DASHES) 45 HTML_SMARTYPANTS_ANGLED_QUOTES // enable angled double quotes (with HTML_USE_SMARTYPANTS) for double quotes rendering 46 HTML_SMARTYPANTS_QUOTES_NBSP // enable "French guillemets" (with HTML_USE_SMARTYPANTS) 47 HTML_FOOTNOTE_RETURN_LINKS // generate a link at the end of a footnote to return to the source 48) 49 50var ( 51 alignments = []string{ 52 "left", 53 "right", 54 "center", 55 } 56 57 // TODO: improve this regexp to catch all possible entities: 58 htmlEntity = regexp.MustCompile(`&[a-z]{2,5};`) 59) 60 61type HtmlRendererParameters struct { 62 // Prepend this text to each relative URL. 63 AbsolutePrefix string 64 // Add this text to each footnote anchor, to ensure uniqueness. 65 FootnoteAnchorPrefix string 66 // Show this text inside the <a> tag for a footnote return link, if the 67 // HTML_FOOTNOTE_RETURN_LINKS flag is enabled. If blank, the string 68 // <sup>[return]</sup> is used. 69 FootnoteReturnLinkContents string 70 // If set, add this text to the front of each Header ID, to ensure 71 // uniqueness. 72 HeaderIDPrefix string 73 // If set, add this text to the back of each Header ID, to ensure uniqueness. 74 HeaderIDSuffix string 75} 76 77// Html is a type that implements the Renderer interface for HTML output. 78// 79// Do not create this directly, instead use the HtmlRenderer function. 80type Html struct { 81 flags int // HTML_* options 82 closeTag string // how to end singleton tags: either " />" or ">" 83 title string // document title 84 css string // optional css file url (used with HTML_COMPLETE_PAGE) 85 86 parameters HtmlRendererParameters 87 88 // table of contents data 89 tocMarker int 90 headerCount int 91 currentLevel int 92 toc *bytes.Buffer 93 94 // Track header IDs to prevent ID collision in a single generation. 95 headerIDs map[string]int 96 97 smartypants *smartypantsRenderer 98} 99 100const ( 101 xhtmlClose = " />" 102 htmlClose = ">" 103) 104 105// HtmlRenderer creates and configures an Html object, which 106// satisfies the Renderer interface. 107// 108// flags is a set of HTML_* options ORed together. 109// title is the title of the document, and css is a URL for the document's 110// stylesheet. 111// title and css are only used when HTML_COMPLETE_PAGE is selected. 112func HtmlRenderer(flags int, title string, css string) Renderer { 113 return HtmlRendererWithParameters(flags, title, css, HtmlRendererParameters{}) 114} 115 116func HtmlRendererWithParameters(flags int, title string, 117 css string, renderParameters HtmlRendererParameters) Renderer { 118 // configure the rendering engine 119 closeTag := htmlClose 120 if flags&HTML_USE_XHTML != 0 { 121 closeTag = xhtmlClose 122 } 123 124 if renderParameters.FootnoteReturnLinkContents == "" { 125 renderParameters.FootnoteReturnLinkContents = `<sup>[return]</sup>` 126 } 127 128 return &Html{ 129 flags: flags, 130 closeTag: closeTag, 131 title: title, 132 css: css, 133 parameters: renderParameters, 134 135 headerCount: 0, 136 currentLevel: 0, 137 toc: new(bytes.Buffer), 138 139 headerIDs: make(map[string]int), 140 141 smartypants: smartypants(flags), 142 } 143} 144 145// Using if statements is a bit faster than a switch statement. As the compiler 146// improves, this should be unnecessary this is only worthwhile because 147// attrEscape is the single largest CPU user in normal use. 148// Also tried using map, but that gave a ~3x slowdown. 149func escapeSingleChar(char byte) (string, bool) { 150 if char == '"' { 151 return """, true 152 } 153 if char == '&' { 154 return "&", true 155 } 156 if char == '<' { 157 return "<", true 158 } 159 if char == '>' { 160 return ">", true 161 } 162 return "", false 163} 164 165func attrEscape(out *bytes.Buffer, src []byte) { 166 org := 0 167 for i, ch := range src { 168 if entity, ok := escapeSingleChar(ch); ok { 169 if i > org { 170 // copy all the normal characters since the last escape 171 out.Write(src[org:i]) 172 } 173 org = i + 1 174 out.WriteString(entity) 175 } 176 } 177 if org < len(src) { 178 out.Write(src[org:]) 179 } 180} 181 182func entityEscapeWithSkip(out *bytes.Buffer, src []byte, skipRanges [][]int) { 183 end := 0 184 for _, rang := range skipRanges { 185 attrEscape(out, src[end:rang[0]]) 186 out.Write(src[rang[0]:rang[1]]) 187 end = rang[1] 188 } 189 attrEscape(out, src[end:]) 190} 191 192func (options *Html) GetFlags() int { 193 return options.flags 194} 195 196func (options *Html) TitleBlock(out *bytes.Buffer, text []byte) { 197 text = bytes.TrimPrefix(text, []byte("% ")) 198 text = bytes.Replace(text, []byte("\n% "), []byte("\n"), -1) 199 out.WriteString("<h1 class=\"title\">") 200 out.Write(text) 201 out.WriteString("\n</h1>") 202} 203 204func (options *Html) Header(out *bytes.Buffer, text func() bool, level int, id string) { 205 marker := out.Len() 206 doubleSpace(out) 207 208 if id == "" && options.flags&HTML_TOC != 0 { 209 id = fmt.Sprintf("toc_%d", options.headerCount) 210 } 211 212 if id != "" { 213 id = options.ensureUniqueHeaderID(id) 214 215 if options.parameters.HeaderIDPrefix != "" { 216 id = options.parameters.HeaderIDPrefix + id 217 } 218 219 if options.parameters.HeaderIDSuffix != "" { 220 id = id + options.parameters.HeaderIDSuffix 221 } 222 223 out.WriteString(fmt.Sprintf("<h%d id=\"%s\">", level, id)) 224 } else { 225 out.WriteString(fmt.Sprintf("<h%d>", level)) 226 } 227 228 tocMarker := out.Len() 229 if !text() { 230 out.Truncate(marker) 231 return 232 } 233 234 // are we building a table of contents? 235 if options.flags&HTML_TOC != 0 { 236 options.TocHeaderWithAnchor(out.Bytes()[tocMarker:], level, id) 237 } 238 239 out.WriteString(fmt.Sprintf("</h%d>\n", level)) 240} 241 242func (options *Html) BlockHtml(out *bytes.Buffer, text []byte) { 243 if options.flags&HTML_SKIP_HTML != 0 { 244 return 245 } 246 247 doubleSpace(out) 248 out.Write(text) 249 out.WriteByte('\n') 250} 251 252func (options *Html) HRule(out *bytes.Buffer) { 253 doubleSpace(out) 254 out.WriteString("<hr") 255 out.WriteString(options.closeTag) 256 out.WriteByte('\n') 257} 258 259func (options *Html) BlockCode(out *bytes.Buffer, text []byte, info string) { 260 doubleSpace(out) 261 262 endOfLang := strings.IndexAny(info, "\t ") 263 if endOfLang < 0 { 264 endOfLang = len(info) 265 } 266 lang := info[:endOfLang] 267 if len(lang) == 0 || lang == "." { 268 out.WriteString("<pre><code>") 269 } else { 270 out.WriteString("<pre><code class=\"language-") 271 attrEscape(out, []byte(lang)) 272 out.WriteString("\">") 273 } 274 attrEscape(out, text) 275 out.WriteString("</code></pre>\n") 276} 277 278func (options *Html) BlockQuote(out *bytes.Buffer, text []byte) { 279 doubleSpace(out) 280 out.WriteString("<blockquote>\n") 281 out.Write(text) 282 out.WriteString("</blockquote>\n") 283} 284 285func (options *Html) Table(out *bytes.Buffer, header []byte, body []byte, columnData []int) { 286 doubleSpace(out) 287 out.WriteString("<table>\n<thead>\n") 288 out.Write(header) 289 out.WriteString("</thead>\n\n<tbody>\n") 290 out.Write(body) 291 out.WriteString("</tbody>\n</table>\n") 292} 293 294func (options *Html) TableRow(out *bytes.Buffer, text []byte) { 295 doubleSpace(out) 296 out.WriteString("<tr>\n") 297 out.Write(text) 298 out.WriteString("\n</tr>\n") 299} 300 301func (options *Html) TableHeaderCell(out *bytes.Buffer, text []byte, align int) { 302 doubleSpace(out) 303 switch align { 304 case TABLE_ALIGNMENT_LEFT: 305 out.WriteString("<th align=\"left\">") 306 case TABLE_ALIGNMENT_RIGHT: 307 out.WriteString("<th align=\"right\">") 308 case TABLE_ALIGNMENT_CENTER: 309 out.WriteString("<th align=\"center\">") 310 default: 311 out.WriteString("<th>") 312 } 313 314 out.Write(text) 315 out.WriteString("</th>") 316} 317 318func (options *Html) TableCell(out *bytes.Buffer, text []byte, align int) { 319 doubleSpace(out) 320 switch align { 321 case TABLE_ALIGNMENT_LEFT: 322 out.WriteString("<td align=\"left\">") 323 case TABLE_ALIGNMENT_RIGHT: 324 out.WriteString("<td align=\"right\">") 325 case TABLE_ALIGNMENT_CENTER: 326 out.WriteString("<td align=\"center\">") 327 default: 328 out.WriteString("<td>") 329 } 330 331 out.Write(text) 332 out.WriteString("</td>") 333} 334 335func (options *Html) Footnotes(out *bytes.Buffer, text func() bool) { 336 out.WriteString("<div class=\"footnotes\">\n") 337 options.HRule(out) 338 options.List(out, text, LIST_TYPE_ORDERED) 339 out.WriteString("</div>\n") 340} 341 342func (options *Html) FootnoteItem(out *bytes.Buffer, name, text []byte, flags int) { 343 if flags&LIST_ITEM_CONTAINS_BLOCK != 0 || flags&LIST_ITEM_BEGINNING_OF_LIST != 0 { 344 doubleSpace(out) 345 } 346 slug := slugify(name) 347 out.WriteString(`<li id="`) 348 out.WriteString(`fn:`) 349 out.WriteString(options.parameters.FootnoteAnchorPrefix) 350 out.Write(slug) 351 out.WriteString(`">`) 352 out.Write(text) 353 if options.flags&HTML_FOOTNOTE_RETURN_LINKS != 0 { 354 out.WriteString(` <a class="footnote-return" href="#`) 355 out.WriteString(`fnref:`) 356 out.WriteString(options.parameters.FootnoteAnchorPrefix) 357 out.Write(slug) 358 out.WriteString(`">`) 359 out.WriteString(options.parameters.FootnoteReturnLinkContents) 360 out.WriteString(`</a>`) 361 } 362 out.WriteString("</li>\n") 363} 364 365func (options *Html) List(out *bytes.Buffer, text func() bool, flags int) { 366 marker := out.Len() 367 doubleSpace(out) 368 369 if flags&LIST_TYPE_DEFINITION != 0 { 370 out.WriteString("<dl>") 371 } else if flags&LIST_TYPE_ORDERED != 0 { 372 out.WriteString("<ol>") 373 } else { 374 out.WriteString("<ul>") 375 } 376 if !text() { 377 out.Truncate(marker) 378 return 379 } 380 if flags&LIST_TYPE_DEFINITION != 0 { 381 out.WriteString("</dl>\n") 382 } else if flags&LIST_TYPE_ORDERED != 0 { 383 out.WriteString("</ol>\n") 384 } else { 385 out.WriteString("</ul>\n") 386 } 387} 388 389func (options *Html) ListItem(out *bytes.Buffer, text []byte, flags int) { 390 if (flags&LIST_ITEM_CONTAINS_BLOCK != 0 && flags&LIST_TYPE_DEFINITION == 0) || 391 flags&LIST_ITEM_BEGINNING_OF_LIST != 0 { 392 doubleSpace(out) 393 } 394 if flags&LIST_TYPE_TERM != 0 { 395 out.WriteString("<dt>") 396 } else if flags&LIST_TYPE_DEFINITION != 0 { 397 out.WriteString("<dd>") 398 } else { 399 out.WriteString("<li>") 400 } 401 out.Write(text) 402 if flags&LIST_TYPE_TERM != 0 { 403 out.WriteString("</dt>\n") 404 } else if flags&LIST_TYPE_DEFINITION != 0 { 405 out.WriteString("</dd>\n") 406 } else { 407 out.WriteString("</li>\n") 408 } 409} 410 411func (options *Html) Paragraph(out *bytes.Buffer, text func() bool) { 412 marker := out.Len() 413 doubleSpace(out) 414 415 out.WriteString("<p>") 416 if !text() { 417 out.Truncate(marker) 418 return 419 } 420 out.WriteString("</p>\n") 421} 422 423func (options *Html) AutoLink(out *bytes.Buffer, link []byte, kind int) { 424 skipRanges := htmlEntity.FindAllIndex(link, -1) 425 if options.flags&HTML_SAFELINK != 0 && !isSafeLink(link) && kind != LINK_TYPE_EMAIL { 426 // mark it but don't link it if it is not a safe link: no smartypants 427 out.WriteString("<tt>") 428 entityEscapeWithSkip(out, link, skipRanges) 429 out.WriteString("</tt>") 430 return 431 } 432 433 out.WriteString("<a href=\"") 434 if kind == LINK_TYPE_EMAIL { 435 out.WriteString("mailto:") 436 } else { 437 options.maybeWriteAbsolutePrefix(out, link) 438 } 439 440 entityEscapeWithSkip(out, link, skipRanges) 441 442 var relAttrs []string 443 if options.flags&HTML_NOFOLLOW_LINKS != 0 && !isRelativeLink(link) { 444 relAttrs = append(relAttrs, "nofollow") 445 } 446 if options.flags&HTML_NOREFERRER_LINKS != 0 && !isRelativeLink(link) { 447 relAttrs = append(relAttrs, "noreferrer") 448 } 449 if options.flags&HTML_NOOPENER_LINKS != 0 && !isRelativeLink(link) { 450 relAttrs = append(relAttrs, "noopener") 451 } 452 if len(relAttrs) > 0 { 453 out.WriteString(fmt.Sprintf("\" rel=\"%s", strings.Join(relAttrs, " "))) 454 } 455 456 // blank target only add to external link 457 if options.flags&HTML_HREF_TARGET_BLANK != 0 && !isRelativeLink(link) { 458 out.WriteString("\" target=\"_blank") 459 } 460 461 out.WriteString("\">") 462 463 // Pretty print: if we get an email address as 464 // an actual URI, e.g. `mailto:foo@bar.com`, we don't 465 // want to print the `mailto:` prefix 466 switch { 467 case bytes.HasPrefix(link, []byte("mailto://")): 468 attrEscape(out, link[len("mailto://"):]) 469 case bytes.HasPrefix(link, []byte("mailto:")): 470 attrEscape(out, link[len("mailto:"):]) 471 default: 472 entityEscapeWithSkip(out, link, skipRanges) 473 } 474 475 out.WriteString("</a>") 476} 477 478func (options *Html) CodeSpan(out *bytes.Buffer, text []byte) { 479 out.WriteString("<code>") 480 attrEscape(out, text) 481 out.WriteString("</code>") 482} 483 484func (options *Html) DoubleEmphasis(out *bytes.Buffer, text []byte) { 485 out.WriteString("<strong>") 486 out.Write(text) 487 out.WriteString("</strong>") 488} 489 490func (options *Html) Emphasis(out *bytes.Buffer, text []byte) { 491 if len(text) == 0 { 492 return 493 } 494 out.WriteString("<em>") 495 out.Write(text) 496 out.WriteString("</em>") 497} 498 499func (options *Html) maybeWriteAbsolutePrefix(out *bytes.Buffer, link []byte) { 500 if options.parameters.AbsolutePrefix != "" && isRelativeLink(link) && link[0] != '.' { 501 out.WriteString(options.parameters.AbsolutePrefix) 502 if link[0] != '/' { 503 out.WriteByte('/') 504 } 505 } 506} 507 508func (options *Html) Image(out *bytes.Buffer, link []byte, title []byte, alt []byte) { 509 if options.flags&HTML_SKIP_IMAGES != 0 { 510 return 511 } 512 513 out.WriteString("<img src=\"") 514 options.maybeWriteAbsolutePrefix(out, link) 515 attrEscape(out, link) 516 out.WriteString("\" alt=\"") 517 if len(alt) > 0 { 518 attrEscape(out, alt) 519 } 520 if len(title) > 0 { 521 out.WriteString("\" title=\"") 522 attrEscape(out, title) 523 } 524 525 out.WriteByte('"') 526 out.WriteString(options.closeTag) 527} 528 529func (options *Html) LineBreak(out *bytes.Buffer) { 530 out.WriteString("<br") 531 out.WriteString(options.closeTag) 532 out.WriteByte('\n') 533} 534 535func (options *Html) Link(out *bytes.Buffer, link []byte, title []byte, content []byte) { 536 if options.flags&HTML_SKIP_LINKS != 0 { 537 // write the link text out but don't link it, just mark it with typewriter font 538 out.WriteString("<tt>") 539 attrEscape(out, content) 540 out.WriteString("</tt>") 541 return 542 } 543 544 if options.flags&HTML_SAFELINK != 0 && !isSafeLink(link) { 545 // write the link text out but don't link it, just mark it with typewriter font 546 out.WriteString("<tt>") 547 attrEscape(out, content) 548 out.WriteString("</tt>") 549 return 550 } 551 552 out.WriteString("<a href=\"") 553 options.maybeWriteAbsolutePrefix(out, link) 554 attrEscape(out, link) 555 if len(title) > 0 { 556 out.WriteString("\" title=\"") 557 attrEscape(out, title) 558 } 559 var relAttrs []string 560 if options.flags&HTML_NOFOLLOW_LINKS != 0 && !isRelativeLink(link) { 561 relAttrs = append(relAttrs, "nofollow") 562 } 563 if options.flags&HTML_NOREFERRER_LINKS != 0 && !isRelativeLink(link) { 564 relAttrs = append(relAttrs, "noreferrer") 565 } 566 if options.flags&HTML_NOOPENER_LINKS != 0 && !isRelativeLink(link) { 567 relAttrs = append(relAttrs, "noopener") 568 } 569 if len(relAttrs) > 0 { 570 out.WriteString(fmt.Sprintf("\" rel=\"%s", strings.Join(relAttrs, " "))) 571 } 572 573 // blank target only add to external link 574 if options.flags&HTML_HREF_TARGET_BLANK != 0 && !isRelativeLink(link) { 575 out.WriteString("\" target=\"_blank") 576 } 577 578 out.WriteString("\">") 579 out.Write(content) 580 out.WriteString("</a>") 581 return 582} 583 584func (options *Html) RawHtmlTag(out *bytes.Buffer, text []byte) { 585 if options.flags&HTML_SKIP_HTML != 0 { 586 return 587 } 588 if options.flags&HTML_SKIP_STYLE != 0 && isHtmlTag(text, "style") { 589 return 590 } 591 if options.flags&HTML_SKIP_LINKS != 0 && isHtmlTag(text, "a") { 592 return 593 } 594 if options.flags&HTML_SKIP_IMAGES != 0 && isHtmlTag(text, "img") { 595 return 596 } 597 out.Write(text) 598} 599 600func (options *Html) TripleEmphasis(out *bytes.Buffer, text []byte) { 601 out.WriteString("<strong><em>") 602 out.Write(text) 603 out.WriteString("</em></strong>") 604} 605 606func (options *Html) StrikeThrough(out *bytes.Buffer, text []byte) { 607 out.WriteString("<del>") 608 out.Write(text) 609 out.WriteString("</del>") 610} 611 612func (options *Html) FootnoteRef(out *bytes.Buffer, ref []byte, id int) { 613 slug := slugify(ref) 614 out.WriteString(`<sup class="footnote-ref" id="`) 615 out.WriteString(`fnref:`) 616 out.WriteString(options.parameters.FootnoteAnchorPrefix) 617 out.Write(slug) 618 out.WriteString(`"><a href="#`) 619 out.WriteString(`fn:`) 620 out.WriteString(options.parameters.FootnoteAnchorPrefix) 621 out.Write(slug) 622 out.WriteString(`">`) 623 out.WriteString(strconv.Itoa(id)) 624 out.WriteString(`</a></sup>`) 625} 626 627func (options *Html) Entity(out *bytes.Buffer, entity []byte) { 628 out.Write(entity) 629} 630 631func (options *Html) NormalText(out *bytes.Buffer, text []byte) { 632 if options.flags&HTML_USE_SMARTYPANTS != 0 { 633 options.Smartypants(out, text) 634 } else { 635 attrEscape(out, text) 636 } 637} 638 639func (options *Html) Smartypants(out *bytes.Buffer, text []byte) { 640 smrt := smartypantsData{false, false} 641 642 // first do normal entity escaping 643 var escaped bytes.Buffer 644 attrEscape(&escaped, text) 645 text = escaped.Bytes() 646 647 mark := 0 648 for i := 0; i < len(text); i++ { 649 if action := options.smartypants[text[i]]; action != nil { 650 if i > mark { 651 out.Write(text[mark:i]) 652 } 653 654 previousChar := byte(0) 655 if i > 0 { 656 previousChar = text[i-1] 657 } 658 i += action(out, &smrt, previousChar, text[i:]) 659 mark = i + 1 660 } 661 } 662 663 if mark < len(text) { 664 out.Write(text[mark:]) 665 } 666} 667 668func (options *Html) DocumentHeader(out *bytes.Buffer) { 669 if options.flags&HTML_COMPLETE_PAGE == 0 { 670 return 671 } 672 673 ending := "" 674 if options.flags&HTML_USE_XHTML != 0 { 675 out.WriteString("<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" ") 676 out.WriteString("\"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">\n") 677 out.WriteString("<html xmlns=\"http://www.w3.org/1999/xhtml\">\n") 678 ending = " /" 679 } else { 680 out.WriteString("<!DOCTYPE html>\n") 681 out.WriteString("<html>\n") 682 } 683 out.WriteString("<head>\n") 684 out.WriteString(" <title>") 685 options.NormalText(out, []byte(options.title)) 686 out.WriteString("</title>\n") 687 out.WriteString(" <meta name=\"GENERATOR\" content=\"Blackfriday Markdown Processor v") 688 out.WriteString(VERSION) 689 out.WriteString("\"") 690 out.WriteString(ending) 691 out.WriteString(">\n") 692 out.WriteString(" <meta charset=\"utf-8\"") 693 out.WriteString(ending) 694 out.WriteString(">\n") 695 if options.css != "" { 696 out.WriteString(" <link rel=\"stylesheet\" type=\"text/css\" href=\"") 697 attrEscape(out, []byte(options.css)) 698 out.WriteString("\"") 699 out.WriteString(ending) 700 out.WriteString(">\n") 701 } 702 out.WriteString("</head>\n") 703 out.WriteString("<body>\n") 704 705 options.tocMarker = out.Len() 706} 707 708func (options *Html) DocumentFooter(out *bytes.Buffer) { 709 // finalize and insert the table of contents 710 if options.flags&HTML_TOC != 0 { 711 options.TocFinalize() 712 713 // now we have to insert the table of contents into the document 714 var temp bytes.Buffer 715 716 // start by making a copy of everything after the document header 717 temp.Write(out.Bytes()[options.tocMarker:]) 718 719 // now clear the copied material from the main output buffer 720 out.Truncate(options.tocMarker) 721 722 // corner case spacing issue 723 if options.flags&HTML_COMPLETE_PAGE != 0 { 724 out.WriteByte('\n') 725 } 726 727 // insert the table of contents 728 out.WriteString("<nav>\n") 729 out.Write(options.toc.Bytes()) 730 out.WriteString("</nav>\n") 731 732 // corner case spacing issue 733 if options.flags&HTML_COMPLETE_PAGE == 0 && options.flags&HTML_OMIT_CONTENTS == 0 { 734 out.WriteByte('\n') 735 } 736 737 // write out everything that came after it 738 if options.flags&HTML_OMIT_CONTENTS == 0 { 739 out.Write(temp.Bytes()) 740 } 741 } 742 743 if options.flags&HTML_COMPLETE_PAGE != 0 { 744 out.WriteString("\n</body>\n") 745 out.WriteString("</html>\n") 746 } 747 748} 749 750func (options *Html) TocHeaderWithAnchor(text []byte, level int, anchor string) { 751 for level > options.currentLevel { 752 switch { 753 case bytes.HasSuffix(options.toc.Bytes(), []byte("</li>\n")): 754 // this sublist can nest underneath a header 755 size := options.toc.Len() 756 options.toc.Truncate(size - len("</li>\n")) 757 758 case options.currentLevel > 0: 759 options.toc.WriteString("<li>") 760 } 761 if options.toc.Len() > 0 { 762 options.toc.WriteByte('\n') 763 } 764 options.toc.WriteString("<ul>\n") 765 options.currentLevel++ 766 } 767 768 for level < options.currentLevel { 769 options.toc.WriteString("</ul>") 770 if options.currentLevel > 1 { 771 options.toc.WriteString("</li>\n") 772 } 773 options.currentLevel-- 774 } 775 776 options.toc.WriteString("<li><a href=\"#") 777 if anchor != "" { 778 options.toc.WriteString(anchor) 779 } else { 780 options.toc.WriteString("toc_") 781 options.toc.WriteString(strconv.Itoa(options.headerCount)) 782 } 783 options.toc.WriteString("\">") 784 options.headerCount++ 785 786 options.toc.Write(text) 787 788 options.toc.WriteString("</a></li>\n") 789} 790 791func (options *Html) TocHeader(text []byte, level int) { 792 options.TocHeaderWithAnchor(text, level, "") 793} 794 795func (options *Html) TocFinalize() { 796 for options.currentLevel > 1 { 797 options.toc.WriteString("</ul></li>\n") 798 options.currentLevel-- 799 } 800 801 if options.currentLevel > 0 { 802 options.toc.WriteString("</ul>\n") 803 } 804} 805 806func isHtmlTag(tag []byte, tagname string) bool { 807 found, _ := findHtmlTagPos(tag, tagname) 808 return found 809} 810 811// Look for a character, but ignore it when it's in any kind of quotes, it 812// might be JavaScript 813func skipUntilCharIgnoreQuotes(html []byte, start int, char byte) int { 814 inSingleQuote := false 815 inDoubleQuote := false 816 inGraveQuote := false 817 i := start 818 for i < len(html) { 819 switch { 820 case html[i] == char && !inSingleQuote && !inDoubleQuote && !inGraveQuote: 821 return i 822 case html[i] == '\'': 823 inSingleQuote = !inSingleQuote 824 case html[i] == '"': 825 inDoubleQuote = !inDoubleQuote 826 case html[i] == '`': 827 inGraveQuote = !inGraveQuote 828 } 829 i++ 830 } 831 return start 832} 833 834func findHtmlTagPos(tag []byte, tagname string) (bool, int) { 835 i := 0 836 if i < len(tag) && tag[0] != '<' { 837 return false, -1 838 } 839 i++ 840 i = skipSpace(tag, i) 841 842 if i < len(tag) && tag[i] == '/' { 843 i++ 844 } 845 846 i = skipSpace(tag, i) 847 j := 0 848 for ; i < len(tag); i, j = i+1, j+1 { 849 if j >= len(tagname) { 850 break 851 } 852 853 if strings.ToLower(string(tag[i]))[0] != tagname[j] { 854 return false, -1 855 } 856 } 857 858 if i == len(tag) { 859 return false, -1 860 } 861 862 rightAngle := skipUntilCharIgnoreQuotes(tag, i, '>') 863 if rightAngle > i { 864 return true, rightAngle 865 } 866 867 return false, -1 868} 869 870func skipUntilChar(text []byte, start int, char byte) int { 871 i := start 872 for i < len(text) && text[i] != char { 873 i++ 874 } 875 return i 876} 877 878func skipSpace(tag []byte, i int) int { 879 for i < len(tag) && isspace(tag[i]) { 880 i++ 881 } 882 return i 883} 884 885func skipChar(data []byte, start int, char byte) int { 886 i := start 887 for i < len(data) && data[i] == char { 888 i++ 889 } 890 return i 891} 892 893func doubleSpace(out *bytes.Buffer) { 894 if out.Len() > 0 { 895 out.WriteByte('\n') 896 } 897} 898 899func isRelativeLink(link []byte) (yes bool) { 900 // a tag begin with '#' 901 if link[0] == '#' { 902 return true 903 } 904 905 // link begin with '/' but not '//', the second maybe a protocol relative link 906 if len(link) >= 2 && link[0] == '/' && link[1] != '/' { 907 return true 908 } 909 910 // only the root '/' 911 if len(link) == 1 && link[0] == '/' { 912 return true 913 } 914 915 // current directory : begin with "./" 916 if bytes.HasPrefix(link, []byte("./")) { 917 return true 918 } 919 920 // parent directory : begin with "../" 921 if bytes.HasPrefix(link, []byte("../")) { 922 return true 923 } 924 925 return false 926} 927 928func (options *Html) ensureUniqueHeaderID(id string) string { 929 for count, found := options.headerIDs[id]; found; count, found = options.headerIDs[id] { 930 tmp := fmt.Sprintf("%s-%d", id, count+1) 931 932 if _, tmpFound := options.headerIDs[tmp]; !tmpFound { 933 options.headerIDs[id] = count + 1 934 id = tmp 935 } else { 936 id = id + "-1" 937 } 938 } 939 940 if _, found := options.headerIDs[id]; !found { 941 options.headerIDs[id] = 0 942 } 943 944 return id 945} 946