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