all repos — grayfriday @ c1011c8ab22a8f6d401f5b6a3f67aed5f6b1362b

blackfriday fork with a few changes

html.go (view raw)

   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	"html"
  22	"regexp"
  23	"strconv"
  24	"strings"
  25)
  26
  27type HtmlFlags int
  28
  29// Html renderer configuration options.
  30const (
  31	HtmlFlagsNone           HtmlFlags = 0
  32	SkipHTML                HtmlFlags = 1 << iota // Skip preformatted HTML blocks
  33	SkipStyle                                     // Skip embedded <style> elements
  34	SkipImages                                    // Skip embedded images
  35	SkipLinks                                     // Skip all links
  36	Safelink                                      // Only link to trusted protocols
  37	NofollowLinks                                 // Only link with rel="nofollow"
  38	NoreferrerLinks                               // Only link with rel="noreferrer"
  39	HrefTargetBlank                               // Add a blank target
  40	Toc                                           // Generate a table of contents
  41	OmitContents                                  // Skip the main contents (for a standalone table of contents)
  42	CompletePage                                  // Generate a complete HTML page
  43	UseXHTML                                      // Generate XHTML output instead of HTML
  44	UseSmartypants                                // Enable smart punctuation substitutions
  45	SmartypantsFractions                          // Enable smart fractions (with UseSmartypants)
  46	SmartypantsDashes                             // Enable smart dashes (with UseSmartypants)
  47	SmartypantsLatexDashes                        // Enable LaTeX-style dashes (with UseSmartypants)
  48	SmartypantsAngledQuotes                       // Enable angled double quotes (with UseSmartypants) for double quotes rendering
  49	FootnoteReturnLinks                           // Generate a link at the end of a footnote to return to the source
  50
  51	TagName               = "[A-Za-z][A-Za-z0-9-]*"
  52	AttributeName         = "[a-zA-Z_:][a-zA-Z0-9:._-]*"
  53	UnquotedValue         = "[^\"'=<>`\\x00-\\x20]+"
  54	SingleQuotedValue     = "'[^']*'"
  55	DoubleQuotedValue     = "\"[^\"]*\""
  56	AttributeValue        = "(?:" + UnquotedValue + "|" + SingleQuotedValue + "|" + DoubleQuotedValue + ")"
  57	AttributeValueSpec    = "(?:" + "\\s*=" + "\\s*" + AttributeValue + ")"
  58	Attribute             = "(?:" + "\\s+" + AttributeName + AttributeValueSpec + "?)"
  59	OpenTag               = "<" + TagName + Attribute + "*" + "\\s*/?>"
  60	CloseTag              = "</" + TagName + "\\s*[>]"
  61	HTMLComment           = "<!---->|<!--(?:-?[^>-])(?:-?[^-])*-->"
  62	ProcessingInstruction = "[<][?].*?[?][>]"
  63	Declaration           = "<![A-Z]+" + "\\s+[^>]*>"
  64	CDATA                 = "<!\\[CDATA\\[[\\s\\S]*?\\]\\]>"
  65	HTMLTag               = "(?:" + OpenTag + "|" + CloseTag + "|" + HTMLComment + "|" +
  66		ProcessingInstruction + "|" + Declaration + "|" + CDATA + ")"
  67)
  68
  69var (
  70	// TODO: improve this regexp to catch all possible entities:
  71	htmlEntity = regexp.MustCompile(`&[a-z]{2,5};`)
  72	reHtmlTag  = regexp.MustCompile("(?i)^" + HTMLTag)
  73)
  74
  75type HtmlRendererParameters struct {
  76	// Prepend this text to each relative URL.
  77	AbsolutePrefix string
  78	// Add this text to each footnote anchor, to ensure uniqueness.
  79	FootnoteAnchorPrefix string
  80	// Show this text inside the <a> tag for a footnote return link, if the
  81	// HTML_FOOTNOTE_RETURN_LINKS flag is enabled. If blank, the string
  82	// <sup>[return]</sup> is used.
  83	FootnoteReturnLinkContents string
  84	// If set, add this text to the front of each Header ID, to ensure
  85	// uniqueness.
  86	HeaderIDPrefix string
  87	// If set, add this text to the back of each Header ID, to ensure uniqueness.
  88	HeaderIDSuffix string
  89}
  90
  91// Html is a type that implements the Renderer interface for HTML output.
  92//
  93// Do not create this directly, instead use the HtmlRenderer function.
  94type Html struct {
  95	flags    HtmlFlags
  96	closeTag string // how to end singleton tags: either " />" or ">"
  97	title    string // document title
  98	css      string // optional css file url (used with HTML_COMPLETE_PAGE)
  99
 100	parameters HtmlRendererParameters
 101
 102	// table of contents data
 103	tocMarker    int
 104	headerCount  int
 105	currentLevel int
 106	toc          *bytes.Buffer
 107
 108	// Track header IDs to prevent ID collision in a single generation.
 109	headerIDs map[string]int
 110
 111	smartypants   *smartypantsRenderer
 112	w             HtmlWriter
 113	lastOutputLen int
 114	disableTags   int
 115	renderBuffer  bytes.Buffer
 116}
 117
 118const (
 119	xhtmlClose = " />"
 120	htmlClose  = ">"
 121)
 122
 123// HtmlRenderer creates and configures an Html object, which
 124// satisfies the Renderer interface.
 125//
 126// flags is a set of HtmlFlags ORed together.
 127// title is the title of the document, and css is a URL for the document's
 128// stylesheet.
 129// title and css are only used when HTML_COMPLETE_PAGE is selected.
 130func HtmlRenderer(flags HtmlFlags, title string, css string) Renderer {
 131	return HtmlRendererWithParameters(flags, title, css, HtmlRendererParameters{})
 132}
 133
 134type HtmlWriter struct {
 135	output bytes.Buffer
 136}
 137
 138func (w *HtmlWriter) Write(p []byte) (n int, err error) {
 139	return w.output.Write(p)
 140}
 141
 142func (w *HtmlWriter) WriteString(s string) (n int, err error) {
 143	return w.output.WriteString(s)
 144}
 145
 146func (w *HtmlWriter) WriteByte(b byte) error {
 147	return w.output.WriteByte(b)
 148}
 149
 150// Writes out a newline if the output is not pristine. Used at the beginning of
 151// every rendering func
 152func (w *HtmlWriter) Newline() {
 153	w.WriteByte('\n')
 154}
 155
 156func (r *Html) Write(b []byte) (int, error) {
 157	return r.w.Write(b)
 158}
 159
 160func HtmlRendererWithParameters(flags HtmlFlags, title string,
 161	css string, renderParameters HtmlRendererParameters) Renderer {
 162	// configure the rendering engine
 163	closeTag := htmlClose
 164	if flags&UseXHTML != 0 {
 165		closeTag = xhtmlClose
 166	}
 167
 168	if renderParameters.FootnoteReturnLinkContents == "" {
 169		renderParameters.FootnoteReturnLinkContents = `<sup>[return]</sup>`
 170	}
 171
 172	var writer HtmlWriter
 173	return &Html{
 174		flags:      flags,
 175		closeTag:   closeTag,
 176		title:      title,
 177		css:        css,
 178		parameters: renderParameters,
 179
 180		headerCount:  0,
 181		currentLevel: 0,
 182		toc:          new(bytes.Buffer),
 183
 184		headerIDs: make(map[string]int),
 185
 186		smartypants: smartypants(flags),
 187		w:           writer,
 188	}
 189}
 190
 191// Using if statements is a bit faster than a switch statement. As the compiler
 192// improves, this should be unnecessary this is only worthwhile because
 193// attrEscape is the single largest CPU user in normal use.
 194// Also tried using map, but that gave a ~3x slowdown.
 195func escapeSingleChar(char byte) (string, bool) {
 196	if char == '"' {
 197		return "&quot;", true
 198	}
 199	if char == '&' {
 200		return "&amp;", true
 201	}
 202	if char == '<' {
 203		return "&lt;", true
 204	}
 205	if char == '>' {
 206		return "&gt;", true
 207	}
 208	return "", false
 209}
 210
 211func (r *Html) attrEscape(src []byte) {
 212	org := 0
 213	for i, ch := range src {
 214		if entity, ok := escapeSingleChar(ch); ok {
 215			if i > org {
 216				// copy all the normal characters since the last escape
 217				r.w.Write(src[org:i])
 218			}
 219			org = i + 1
 220			r.w.WriteString(entity)
 221		}
 222	}
 223	if org < len(src) {
 224		r.w.Write(src[org:])
 225	}
 226}
 227
 228func attrEscape2(src []byte) []byte {
 229	unesc := []byte(html.UnescapeString(string(src)))
 230	esc1 := []byte(html.EscapeString(string(unesc)))
 231	esc2 := bytes.Replace(esc1, []byte("&#34;"), []byte("&quot;"), -1)
 232	return bytes.Replace(esc2, []byte("&#39;"), []byte{'\''}, -1)
 233}
 234
 235func (r *Html) entityEscapeWithSkip(src []byte, skipRanges [][]int) {
 236	end := 0
 237	for _, rang := range skipRanges {
 238		r.attrEscape(src[end:rang[0]])
 239		r.w.Write(src[rang[0]:rang[1]])
 240		end = rang[1]
 241	}
 242	r.attrEscape(src[end:])
 243}
 244
 245func (r *Html) GetFlags() HtmlFlags {
 246	return r.flags
 247}
 248
 249func (r *Html) TitleBlock(text []byte) {
 250	text = bytes.TrimPrefix(text, []byte("% "))
 251	text = bytes.Replace(text, []byte("\n% "), []byte("\n"), -1)
 252	r.w.WriteString("<h1 class=\"title\">")
 253	r.w.Write(text)
 254	r.w.WriteString("\n</h1>")
 255}
 256
 257func (r *Html) BeginHeader(level int, id string) {
 258	r.w.Newline()
 259
 260	if id == "" && r.flags&Toc != 0 {
 261		id = fmt.Sprintf("toc_%d", r.headerCount)
 262	}
 263
 264	if id != "" {
 265		id = r.ensureUniqueHeaderID(id)
 266
 267		if r.parameters.HeaderIDPrefix != "" {
 268			id = r.parameters.HeaderIDPrefix + id
 269		}
 270
 271		if r.parameters.HeaderIDSuffix != "" {
 272			id = id + r.parameters.HeaderIDSuffix
 273		}
 274
 275		r.w.WriteString(fmt.Sprintf("<h%d id=\"%s\">", level, id))
 276	} else {
 277		r.w.WriteString(fmt.Sprintf("<h%d>", level))
 278	}
 279}
 280
 281func (r *Html) EndHeader(level int, id string, header []byte) {
 282	// are we building a table of contents?
 283	if r.flags&Toc != 0 {
 284		r.TocHeaderWithAnchor(header, level, id)
 285	}
 286
 287	r.w.WriteString(fmt.Sprintf("</h%d>\n", level))
 288}
 289
 290func (r *Html) BlockHtml(text []byte) {
 291	if r.flags&SkipHTML != 0 {
 292		return
 293	}
 294
 295	r.w.Newline()
 296	r.w.Write(text)
 297	r.w.WriteByte('\n')
 298}
 299
 300func (r *Html) HRule() {
 301	r.w.Newline()
 302	r.w.WriteString("<hr")
 303	r.w.WriteString(r.closeTag)
 304	r.w.WriteByte('\n')
 305}
 306
 307func (r *Html) BlockCode(text []byte, lang string) {
 308	r.w.Newline()
 309
 310	// parse out the language names/classes
 311	count := 0
 312	for _, elt := range strings.Fields(lang) {
 313		if elt[0] == '.' {
 314			elt = elt[1:]
 315		}
 316		if len(elt) == 0 {
 317			continue
 318		}
 319		if count == 0 {
 320			r.w.WriteString("<pre><code class=\"language-")
 321		} else {
 322			r.w.WriteByte(' ')
 323		}
 324		r.attrEscape([]byte(elt))
 325		count++
 326	}
 327
 328	if count == 0 {
 329		r.w.WriteString("<pre><code>")
 330	} else {
 331		r.w.WriteString("\">")
 332	}
 333
 334	r.attrEscape(text)
 335	r.w.WriteString("</code></pre>\n")
 336}
 337
 338func (r *Html) BlockQuote(text []byte) {
 339	r.w.Newline()
 340	r.w.WriteString("<blockquote>\n")
 341	r.w.Write(text)
 342	r.w.WriteString("</blockquote>\n")
 343}
 344
 345func (r *Html) Table(header []byte, body []byte, columnData []int) {
 346	r.w.Newline()
 347	r.w.WriteString("<table>\n<thead>\n")
 348	r.w.Write(header)
 349	r.w.WriteString("</thead>\n\n<tbody>\n")
 350	r.w.Write(body)
 351	r.w.WriteString("</tbody>\n</table>\n")
 352}
 353
 354func (r *Html) TableRow(text []byte) {
 355	r.w.Newline()
 356	r.w.WriteString("<tr>\n")
 357	r.w.Write(text)
 358	r.w.WriteString("\n</tr>\n")
 359}
 360
 361func leadingNewline(out *bytes.Buffer) {
 362	if out.Len() > 0 {
 363		out.WriteByte('\n')
 364	}
 365}
 366
 367func (r *Html) TableHeaderCell(out *bytes.Buffer, text []byte, align int) {
 368	leadingNewline(out)
 369	switch align {
 370	case TableAlignmentLeft:
 371		out.WriteString("<th align=\"left\">")
 372	case TableAlignmentRight:
 373		out.WriteString("<th align=\"right\">")
 374	case TableAlignmentCenter:
 375		out.WriteString("<th align=\"center\">")
 376	default:
 377		out.WriteString("<th>")
 378	}
 379
 380	out.Write(text)
 381	out.WriteString("</th>")
 382}
 383
 384func (r *Html) TableCell(out *bytes.Buffer, text []byte, align int) {
 385	leadingNewline(out)
 386	switch align {
 387	case TableAlignmentLeft:
 388		out.WriteString("<td align=\"left\">")
 389	case TableAlignmentRight:
 390		out.WriteString("<td align=\"right\">")
 391	case TableAlignmentCenter:
 392		out.WriteString("<td align=\"center\">")
 393	default:
 394		out.WriteString("<td>")
 395	}
 396
 397	out.Write(text)
 398	out.WriteString("</td>")
 399}
 400
 401func (r *Html) BeginFootnotes() {
 402	r.w.WriteString("<div class=\"footnotes\">\n")
 403	r.HRule()
 404	r.BeginList(ListTypeOrdered)
 405}
 406
 407func (r *Html) EndFootnotes() {
 408	r.EndList(ListTypeOrdered)
 409	r.w.WriteString("</div>\n")
 410}
 411
 412func (r *Html) FootnoteItem(name, text []byte, flags ListType) {
 413	if flags&ListItemContainsBlock != 0 || flags&ListItemBeginningOfList != 0 {
 414		r.w.Newline()
 415	}
 416	slug := slugify(name)
 417	r.w.WriteString(`<li id="`)
 418	r.w.WriteString(`fn:`)
 419	r.w.WriteString(r.parameters.FootnoteAnchorPrefix)
 420	r.w.Write(slug)
 421	r.w.WriteString(`">`)
 422	r.w.Write(text)
 423	if r.flags&FootnoteReturnLinks != 0 {
 424		r.w.WriteString(` <a class="footnote-return" href="#`)
 425		r.w.WriteString(`fnref:`)
 426		r.w.WriteString(r.parameters.FootnoteAnchorPrefix)
 427		r.w.Write(slug)
 428		r.w.WriteString(`">`)
 429		r.w.WriteString(r.parameters.FootnoteReturnLinkContents)
 430		r.w.WriteString(`</a>`)
 431	}
 432	r.w.WriteString("</li>\n")
 433}
 434
 435func (r *Html) BeginList(flags ListType) {
 436	r.w.Newline()
 437
 438	if flags&ListTypeDefinition != 0 {
 439		r.w.WriteString("<dl>")
 440	} else if flags&ListTypeOrdered != 0 {
 441		r.w.WriteString("<ol>")
 442	} else {
 443		r.w.WriteString("<ul>")
 444	}
 445}
 446
 447func (r *Html) EndList(flags ListType) {
 448	if flags&ListTypeDefinition != 0 {
 449		r.w.WriteString("</dl>\n")
 450	} else if flags&ListTypeOrdered != 0 {
 451		r.w.WriteString("</ol>\n")
 452	} else {
 453		r.w.WriteString("</ul>\n")
 454	}
 455}
 456
 457func (r *Html) ListItem(text []byte, flags ListType) {
 458	if (flags&ListItemContainsBlock != 0 && flags&ListTypeDefinition == 0) ||
 459		flags&ListItemBeginningOfList != 0 {
 460		r.w.Newline()
 461	}
 462	if flags&ListTypeTerm != 0 {
 463		r.w.WriteString("<dt>")
 464	} else if flags&ListTypeDefinition != 0 {
 465		r.w.WriteString("<dd>")
 466	} else {
 467		r.w.WriteString("<li>")
 468	}
 469	r.w.Write(text)
 470	if flags&ListTypeTerm != 0 {
 471		r.w.WriteString("</dt>\n")
 472	} else if flags&ListTypeDefinition != 0 {
 473		r.w.WriteString("</dd>\n")
 474	} else {
 475		r.w.WriteString("</li>\n")
 476	}
 477}
 478
 479func (r *Html) BeginParagraph() {
 480	r.w.Newline()
 481	r.w.WriteString("<p>")
 482}
 483
 484func (r *Html) EndParagraph() {
 485	r.w.WriteString("</p>\n")
 486}
 487
 488func (r *Html) AutoLink(link []byte, kind LinkType) {
 489	skipRanges := htmlEntity.FindAllIndex(link, -1)
 490	if r.flags&Safelink != 0 && !isSafeLink(link) && kind != LinkTypeEmail {
 491		// mark it but don't link it if it is not a safe link: no smartypants
 492		r.w.WriteString("<tt>")
 493		r.entityEscapeWithSkip(link, skipRanges)
 494		r.w.WriteString("</tt>")
 495		return
 496	}
 497
 498	r.w.WriteString("<a href=\"")
 499	if kind == LinkTypeEmail {
 500		r.w.WriteString("mailto:")
 501	} else {
 502		r.maybeWriteAbsolutePrefix(link)
 503	}
 504
 505	r.entityEscapeWithSkip(link, skipRanges)
 506
 507	var relAttrs []string
 508	if r.flags&NofollowLinks != 0 && !isRelativeLink(link) {
 509		relAttrs = append(relAttrs, "nofollow")
 510	}
 511	if r.flags&NoreferrerLinks != 0 && !isRelativeLink(link) {
 512		relAttrs = append(relAttrs, "noreferrer")
 513	}
 514	if len(relAttrs) > 0 {
 515		r.w.WriteString(fmt.Sprintf("\" rel=\"%s", strings.Join(relAttrs, " ")))
 516	}
 517
 518	// blank target only add to external link
 519	if r.flags&HrefTargetBlank != 0 && !isRelativeLink(link) {
 520		r.w.WriteString("\" target=\"_blank")
 521	}
 522
 523	r.w.WriteString("\">")
 524
 525	// Pretty print: if we get an email address as
 526	// an actual URI, e.g. `mailto:foo@bar.com`, we don't
 527	// want to print the `mailto:` prefix
 528	switch {
 529	case bytes.HasPrefix(link, []byte("mailto://")):
 530		r.attrEscape(link[len("mailto://"):])
 531	case bytes.HasPrefix(link, []byte("mailto:")):
 532		r.attrEscape(link[len("mailto:"):])
 533	default:
 534		r.entityEscapeWithSkip(link, skipRanges)
 535	}
 536
 537	r.w.WriteString("</a>")
 538}
 539
 540func (r *Html) CodeSpan(text []byte) {
 541	r.w.WriteString("<code>")
 542	r.attrEscape(text)
 543	r.w.WriteString("</code>")
 544}
 545
 546func (r *Html) DoubleEmphasis(text []byte) {
 547	r.w.WriteString("<strong>")
 548	r.w.Write(text)
 549	r.w.WriteString("</strong>")
 550}
 551
 552func (r *Html) Emphasis(text []byte) {
 553	if len(text) == 0 {
 554		return
 555	}
 556	r.w.WriteString("<em>")
 557	r.w.Write(text)
 558	r.w.WriteString("</em>")
 559}
 560
 561func (r *Html) maybeWriteAbsolutePrefix(link []byte) {
 562	if r.parameters.AbsolutePrefix != "" && isRelativeLink(link) && link[0] != '.' {
 563		r.w.WriteString(r.parameters.AbsolutePrefix)
 564		if link[0] != '/' {
 565			r.w.WriteByte('/')
 566		}
 567	}
 568}
 569
 570func (r *Html) Image(link []byte, title []byte, alt []byte) {
 571	if r.flags&SkipImages != 0 {
 572		return
 573	}
 574
 575	r.w.WriteString("<img src=\"")
 576	r.maybeWriteAbsolutePrefix(link)
 577	r.attrEscape(link)
 578	r.w.WriteString("\" alt=\"")
 579	if len(alt) > 0 {
 580		r.attrEscape(alt)
 581	}
 582	if len(title) > 0 {
 583		r.w.WriteString("\" title=\"")
 584		r.attrEscape(title)
 585	}
 586
 587	r.w.WriteByte('"')
 588	r.w.WriteString(r.closeTag)
 589}
 590
 591func (r *Html) LineBreak() {
 592	r.w.WriteString("<br")
 593	r.w.WriteString(r.closeTag)
 594	r.w.WriteByte('\n')
 595}
 596
 597func (r *Html) Link(link []byte, title []byte, content []byte) {
 598	if r.flags&SkipLinks != 0 {
 599		// write the link text out but don't link it, just mark it with typewriter font
 600		r.w.WriteString("<tt>")
 601		r.attrEscape(content)
 602		r.w.WriteString("</tt>")
 603		return
 604	}
 605
 606	if r.flags&Safelink != 0 && !isSafeLink(link) {
 607		// write the link text out but don't link it, just mark it with typewriter font
 608		r.w.WriteString("<tt>")
 609		r.attrEscape(content)
 610		r.w.WriteString("</tt>")
 611		return
 612	}
 613
 614	r.w.WriteString("<a href=\"")
 615	r.maybeWriteAbsolutePrefix(link)
 616	r.attrEscape(link)
 617	if len(title) > 0 {
 618		r.w.WriteString("\" title=\"")
 619		r.attrEscape(title)
 620	}
 621	var relAttrs []string
 622	if r.flags&NofollowLinks != 0 && !isRelativeLink(link) {
 623		relAttrs = append(relAttrs, "nofollow")
 624	}
 625	if r.flags&NoreferrerLinks != 0 && !isRelativeLink(link) {
 626		relAttrs = append(relAttrs, "noreferrer")
 627	}
 628	if len(relAttrs) > 0 {
 629		r.w.WriteString(fmt.Sprintf("\" rel=\"%s", strings.Join(relAttrs, " ")))
 630	}
 631
 632	// blank target only add to external link
 633	if r.flags&HrefTargetBlank != 0 && !isRelativeLink(link) {
 634		r.w.WriteString("\" target=\"_blank")
 635	}
 636
 637	r.w.WriteString("\">")
 638	r.w.Write(content)
 639	r.w.WriteString("</a>")
 640	return
 641}
 642
 643func (r *Html) RawHtmlTag(text []byte) {
 644	if r.flags&SkipHTML != 0 {
 645		return
 646	}
 647	if r.flags&SkipStyle != 0 && isHtmlTag(text, "style") {
 648		return
 649	}
 650	if r.flags&SkipLinks != 0 && isHtmlTag(text, "a") {
 651		return
 652	}
 653	if r.flags&SkipImages != 0 && isHtmlTag(text, "img") {
 654		return
 655	}
 656	r.w.Write(text)
 657}
 658
 659func (r *Html) TripleEmphasis(text []byte) {
 660	r.w.WriteString("<strong><em>")
 661	r.w.Write(text)
 662	r.w.WriteString("</em></strong>")
 663}
 664
 665func (r *Html) StrikeThrough(text []byte) {
 666	r.w.WriteString("<del>")
 667	r.w.Write(text)
 668	r.w.WriteString("</del>")
 669}
 670
 671func (r *Html) FootnoteRef(ref []byte, id int) {
 672	slug := slugify(ref)
 673	r.w.WriteString(`<sup class="footnote-ref" id="`)
 674	r.w.WriteString(`fnref:`)
 675	r.w.WriteString(r.parameters.FootnoteAnchorPrefix)
 676	r.w.Write(slug)
 677	r.w.WriteString(`"><a rel="footnote" href="#`)
 678	r.w.WriteString(`fn:`)
 679	r.w.WriteString(r.parameters.FootnoteAnchorPrefix)
 680	r.w.Write(slug)
 681	r.w.WriteString(`">`)
 682	r.w.WriteString(strconv.Itoa(id))
 683	r.w.WriteString(`</a></sup>`)
 684}
 685
 686func (r *Html) Entity(entity []byte) {
 687	r.w.Write(entity)
 688}
 689
 690func (r *Html) NormalText(text []byte) {
 691	if r.flags&UseSmartypants != 0 {
 692		r.Smartypants(text)
 693	} else {
 694		r.attrEscape(text)
 695	}
 696}
 697
 698func (r *Html) Smartypants2(text []byte) []byte {
 699	smrt := smartypantsData{false, false}
 700	var buff bytes.Buffer
 701	// first do normal entity escaping
 702	text = attrEscape2(text)
 703	mark := 0
 704	for i := 0; i < len(text); i++ {
 705		if action := r.smartypants[text[i]]; action != nil {
 706			if i > mark {
 707				buff.Write(text[mark:i])
 708			}
 709			previousChar := byte(0)
 710			if i > 0 {
 711				previousChar = text[i-1]
 712			}
 713			var tmp bytes.Buffer
 714			i += action(&tmp, &smrt, previousChar, text[i:])
 715			buff.Write(tmp.Bytes())
 716			mark = i + 1
 717		}
 718	}
 719	if mark < len(text) {
 720		buff.Write(text[mark:])
 721	}
 722	return buff.Bytes()
 723}
 724
 725func (r *Html) Smartypants(text []byte) {
 726	smrt := smartypantsData{false, false}
 727
 728	// first do normal entity escaping
 729	r.attrEscape(text)
 730
 731	mark := 0
 732	for i := 0; i < len(text); i++ {
 733		if action := r.smartypants[text[i]]; action != nil {
 734			if i > mark {
 735				r.w.Write(text[mark:i])
 736			}
 737
 738			previousChar := byte(0)
 739			if i > 0 {
 740				previousChar = text[i-1]
 741			}
 742			var tmp bytes.Buffer
 743			i += action(&tmp, &smrt, previousChar, text[i:])
 744			r.w.Write(tmp.Bytes())
 745			mark = i + 1
 746		}
 747	}
 748
 749	if mark < len(text) {
 750		r.w.Write(text[mark:])
 751	}
 752}
 753
 754func (r *Html) DocumentHeader() {
 755	if r.flags&CompletePage == 0 {
 756		return
 757	}
 758
 759	ending := ""
 760	if r.flags&UseXHTML != 0 {
 761		r.w.WriteString("<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" ")
 762		r.w.WriteString("\"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">\n")
 763		r.w.WriteString("<html xmlns=\"http://www.w3.org/1999/xhtml\">\n")
 764		ending = " /"
 765	} else {
 766		r.w.WriteString("<!DOCTYPE html>\n")
 767		r.w.WriteString("<html>\n")
 768	}
 769	r.w.WriteString("<head>\n")
 770	r.w.WriteString("  <title>")
 771	r.NormalText([]byte(r.title))
 772	r.w.WriteString("</title>\n")
 773	r.w.WriteString("  <meta name=\"GENERATOR\" content=\"Blackfriday Markdown Processor v")
 774	r.w.WriteString(VERSION)
 775	r.w.WriteString("\"")
 776	r.w.WriteString(ending)
 777	r.w.WriteString(">\n")
 778	r.w.WriteString("  <meta charset=\"utf-8\"")
 779	r.w.WriteString(ending)
 780	r.w.WriteString(">\n")
 781	if r.css != "" {
 782		r.w.WriteString("  <link rel=\"stylesheet\" type=\"text/css\" href=\"")
 783		r.attrEscape([]byte(r.css))
 784		r.w.WriteString("\"")
 785		r.w.WriteString(ending)
 786		r.w.WriteString(">\n")
 787	}
 788	r.w.WriteString("</head>\n")
 789	r.w.WriteString("<body>\n")
 790
 791	r.tocMarker = r.w.output.Len() // XXX
 792}
 793
 794func (r *Html) DocumentFooter() {
 795	// finalize and insert the table of contents
 796	if r.flags&Toc != 0 {
 797		r.TocFinalize()
 798
 799		// now we have to insert the table of contents into the document
 800		var temp bytes.Buffer
 801
 802		// start by making a copy of everything after the document header
 803		temp.Write(r.w.output.Bytes()[r.tocMarker:])
 804
 805		// now clear the copied material from the main output buffer
 806		r.w.output.Truncate(r.tocMarker)
 807
 808		// corner case spacing issue
 809		if r.flags&CompletePage != 0 {
 810			r.w.WriteByte('\n')
 811		}
 812
 813		// insert the table of contents
 814		r.w.WriteString("<nav>\n")
 815		r.w.Write(r.toc.Bytes())
 816		r.w.WriteString("</nav>\n")
 817
 818		// corner case spacing issue
 819		if r.flags&CompletePage == 0 && r.flags&OmitContents == 0 {
 820			r.w.WriteByte('\n')
 821		}
 822
 823		// write out everything that came after it
 824		if r.flags&OmitContents == 0 {
 825			r.w.Write(temp.Bytes())
 826		}
 827	}
 828
 829	if r.flags&CompletePage != 0 {
 830		r.w.WriteString("\n</body>\n")
 831		r.w.WriteString("</html>\n")
 832	}
 833
 834}
 835
 836func (r *Html) TocHeaderWithAnchor(text []byte, level int, anchor string) {
 837	for level > r.currentLevel {
 838		switch {
 839		case bytes.HasSuffix(r.toc.Bytes(), []byte("</li>\n")):
 840			// this sublist can nest underneath a header
 841			size := r.toc.Len()
 842			r.toc.Truncate(size - len("</li>\n"))
 843
 844		case r.currentLevel > 0:
 845			r.toc.WriteString("<li>")
 846		}
 847		if r.toc.Len() > 0 {
 848			r.toc.WriteByte('\n')
 849		}
 850		r.toc.WriteString("<ul>\n")
 851		r.currentLevel++
 852	}
 853
 854	for level < r.currentLevel {
 855		r.toc.WriteString("</ul>")
 856		if r.currentLevel > 1 {
 857			r.toc.WriteString("</li>\n")
 858		}
 859		r.currentLevel--
 860	}
 861
 862	r.toc.WriteString("<li><a href=\"#")
 863	if anchor != "" {
 864		r.toc.WriteString(anchor)
 865	} else {
 866		r.toc.WriteString("toc_")
 867		r.toc.WriteString(strconv.Itoa(r.headerCount))
 868	}
 869	r.toc.WriteString("\">")
 870	r.headerCount++
 871
 872	r.toc.Write(text)
 873
 874	r.toc.WriteString("</a></li>\n")
 875}
 876
 877func (r *Html) TocHeader(text []byte, level int) {
 878	r.TocHeaderWithAnchor(text, level, "")
 879}
 880
 881func (r *Html) TocFinalize() {
 882	for r.currentLevel > 1 {
 883		r.toc.WriteString("</ul></li>\n")
 884		r.currentLevel--
 885	}
 886
 887	if r.currentLevel > 0 {
 888		r.toc.WriteString("</ul>\n")
 889	}
 890}
 891
 892func isHtmlTag(tag []byte, tagname string) bool {
 893	found, _ := findHtmlTagPos(tag, tagname)
 894	return found
 895}
 896
 897// Look for a character, but ignore it when it's in any kind of quotes, it
 898// might be JavaScript
 899func skipUntilCharIgnoreQuotes(html []byte, start int, char byte) int {
 900	inSingleQuote := false
 901	inDoubleQuote := false
 902	inGraveQuote := false
 903	i := start
 904	for i < len(html) {
 905		switch {
 906		case html[i] == char && !inSingleQuote && !inDoubleQuote && !inGraveQuote:
 907			return i
 908		case html[i] == '\'':
 909			inSingleQuote = !inSingleQuote
 910		case html[i] == '"':
 911			inDoubleQuote = !inDoubleQuote
 912		case html[i] == '`':
 913			inGraveQuote = !inGraveQuote
 914		}
 915		i++
 916	}
 917	return start
 918}
 919
 920func findHtmlTagPos(tag []byte, tagname string) (bool, int) {
 921	i := 0
 922	if i < len(tag) && tag[0] != '<' {
 923		return false, -1
 924	}
 925	i++
 926	i = skipSpace(tag, i)
 927
 928	if i < len(tag) && tag[i] == '/' {
 929		i++
 930	}
 931
 932	i = skipSpace(tag, i)
 933	j := 0
 934	for ; i < len(tag); i, j = i+1, j+1 {
 935		if j >= len(tagname) {
 936			break
 937		}
 938
 939		if strings.ToLower(string(tag[i]))[0] != tagname[j] {
 940			return false, -1
 941		}
 942	}
 943
 944	if i == len(tag) {
 945		return false, -1
 946	}
 947
 948	rightAngle := skipUntilCharIgnoreQuotes(tag, i, '>')
 949	if rightAngle > i {
 950		return true, rightAngle
 951	}
 952
 953	return false, -1
 954}
 955
 956func skipUntilChar(text []byte, start int, char byte) int {
 957	i := start
 958	for i < len(text) && text[i] != char {
 959		i++
 960	}
 961	return i
 962}
 963
 964func skipSpace(tag []byte, i int) int {
 965	for i < len(tag) && isspace(tag[i]) {
 966		i++
 967	}
 968	return i
 969}
 970
 971func skipChar(data []byte, start int, char byte) int {
 972	i := start
 973	for i < len(data) && data[i] == char {
 974		i++
 975	}
 976	return i
 977}
 978
 979func isRelativeLink(link []byte) (yes bool) {
 980	// a tag begin with '#'
 981	if link[0] == '#' {
 982		return true
 983	}
 984
 985	// link begin with '/' but not '//', the second maybe a protocol relative link
 986	if len(link) >= 2 && link[0] == '/' && link[1] != '/' {
 987		return true
 988	}
 989
 990	// only the root '/'
 991	if len(link) == 1 && link[0] == '/' {
 992		return true
 993	}
 994
 995	// current directory : begin with "./"
 996	if bytes.HasPrefix(link, []byte("./")) {
 997		return true
 998	}
 999
1000	// parent directory : begin with "../"
1001	if bytes.HasPrefix(link, []byte("../")) {
1002		return true
1003	}
1004
1005	return false
1006}
1007
1008func (r *Html) ensureUniqueHeaderID(id string) string {
1009	for count, found := r.headerIDs[id]; found; count, found = r.headerIDs[id] {
1010		tmp := fmt.Sprintf("%s-%d", id, count+1)
1011
1012		if _, tmpFound := r.headerIDs[tmp]; !tmpFound {
1013			r.headerIDs[id] = count + 1
1014			id = tmp
1015		} else {
1016			id = id + "-1"
1017		}
1018	}
1019
1020	if _, found := r.headerIDs[id]; !found {
1021		r.headerIDs[id] = 0
1022	}
1023
1024	return id
1025}
1026
1027func (r *Html) addAbsPrefix(link []byte) []byte {
1028	if r.parameters.AbsolutePrefix != "" && isRelativeLink(link) && link[0] != '.' {
1029		newDest := r.parameters.AbsolutePrefix
1030		if link[0] != '/' {
1031			newDest += "/"
1032		}
1033		newDest += string(link)
1034		return []byte(newDest)
1035	}
1036	return link
1037}
1038
1039func appendLinkAttrs(attrs []string, flags HtmlFlags, link []byte) []string {
1040	if isRelativeLink(link) {
1041		return attrs
1042	}
1043	val := []string{}
1044	if flags&NofollowLinks != 0 {
1045		val = append(val, "nofollow")
1046	}
1047	if flags&NoreferrerLinks != 0 {
1048		val = append(val, "noreferrer")
1049	}
1050	if flags&HrefTargetBlank != 0 {
1051		attrs = append(attrs, "target=\"_blank\"")
1052	}
1053	if len(val) == 0 {
1054		return attrs
1055	}
1056	attr := fmt.Sprintf("rel=%q", strings.Join(val, " "))
1057	return append(attrs, attr)
1058}
1059
1060func isMailto(link []byte) bool {
1061	return bytes.HasPrefix(link, []byte("mailto:"))
1062}
1063
1064func isSmartypantable(node *Node) bool {
1065	pt := node.Parent.Type
1066	return pt != Link && pt != CodeBlock && pt != Code
1067}
1068
1069func appendLanguageAttr(attrs []string, info []byte) []string {
1070	infoWords := bytes.Split(info, []byte("\t "))
1071	if len(infoWords) > 0 && len(infoWords[0]) > 0 {
1072		attrs = append(attrs, fmt.Sprintf("class=\"language-%s\"", infoWords[0]))
1073	}
1074	return attrs
1075}
1076
1077func tag(name string, attrs []string, selfClosing bool) []byte {
1078	result := "<" + name
1079	if attrs != nil && len(attrs) > 0 {
1080		result += " " + strings.Join(attrs, " ")
1081	}
1082	if selfClosing {
1083		result += " /"
1084	}
1085	return []byte(result + ">")
1086}
1087
1088func footnoteRef(prefix string, node *Node) []byte {
1089	urlFrag := prefix + string(slugify(node.Destination))
1090	anchor := fmt.Sprintf(`<a rel="footnote" href="#fn:%s">%d</a>`, urlFrag, node.NoteID)
1091	return []byte(fmt.Sprintf(`<sup class="footnote-ref" id="fnref:%s">%s</sup>`, urlFrag, anchor))
1092}
1093
1094func footnoteItem(prefix string, slug []byte) []byte {
1095	return []byte(fmt.Sprintf(`<li id="fn:%s%s">`, prefix, slug))
1096}
1097
1098func footnoteReturnLink(prefix, returnLink string, slug []byte) []byte {
1099	const format = ` <a class="footnote-return" href="#fnref:%s%s">%s</a>`
1100	return []byte(fmt.Sprintf(format, prefix, slug, returnLink))
1101}
1102
1103func itemOpenCR(node *Node) bool {
1104	if node.Prev == nil {
1105		return false
1106	}
1107	ld := node.Parent.ListData
1108	return !ld.Tight && ld.Flags&ListTypeDefinition == 0
1109}
1110
1111func skipParagraphTags(node *Node) bool {
1112	grandparent := node.Parent.Parent
1113	if grandparent == nil || grandparent.ListData == nil {
1114		return false
1115	}
1116	tightOrTerm := grandparent.ListData.Tight || node.Parent.ListData.Flags&ListTypeTerm != 0
1117	return grandparent.Type == List && tightOrTerm
1118}
1119
1120func cellAlignment(align int) string {
1121	switch align {
1122	case TableAlignmentLeft:
1123		return "left"
1124	case TableAlignmentRight:
1125		return "right"
1126	case TableAlignmentCenter:
1127		return "center"
1128	default:
1129		return ""
1130	}
1131}
1132
1133func esc(text []byte, preserveEntities bool) []byte {
1134	return attrEscape2(text)
1135}
1136
1137func escCode(text []byte, preserveEntities bool) []byte {
1138	e1 := []byte(html.EscapeString(string(text)))
1139	e2 := bytes.Replace(e1, []byte("&#34;"), []byte("&quot;"), -1)
1140	return bytes.Replace(e2, []byte("&#39;"), []byte{'\''}, -1)
1141}
1142
1143func (r *Html) out(text []byte) {
1144	if r.disableTags > 0 {
1145		r.renderBuffer.Write(reHtmlTag.ReplaceAll(text, []byte{}))
1146	} else {
1147		r.renderBuffer.Write(text)
1148	}
1149	r.lastOutputLen = len(text)
1150}
1151
1152func (r *Html) cr() {
1153	if r.lastOutputLen > 0 {
1154		r.out([]byte{'\n'})
1155	}
1156}
1157
1158func (r *Html) RenderNode(node *Node, entering bool) {
1159	attrs := []string{}
1160	switch node.Type {
1161	case Text:
1162		if r.flags&UseSmartypants != 0 && isSmartypantable(node) {
1163			// TODO: don't do that in renderer, do that at parse time!
1164			r.out(r.Smartypants2(node.Literal))
1165		} else {
1166			r.out(esc(node.Literal, false))
1167		}
1168		break
1169	case Softbreak:
1170		r.out([]byte("\n"))
1171		// TODO: make it configurable via out(renderer.softbreak)
1172	case Hardbreak:
1173		r.out(tag("br", nil, true))
1174		r.cr()
1175	case Emph:
1176		if entering {
1177			r.out(tag("em", nil, false))
1178		} else {
1179			r.out(tag("/em", nil, false))
1180		}
1181		break
1182	case Strong:
1183		if entering {
1184			r.out(tag("strong", nil, false))
1185		} else {
1186			r.out(tag("/strong", nil, false))
1187		}
1188		break
1189	case Del:
1190		if entering {
1191			r.out(tag("del", nil, false))
1192		} else {
1193			r.out(tag("/del", nil, false))
1194		}
1195	case HtmlSpan:
1196		//if options.safe {
1197		//	out("<!-- raw HTML omitted -->")
1198		//} else {
1199		r.out(node.Literal)
1200		//}
1201	case Link:
1202		// mark it but don't link it if it is not a safe link: no smartypants
1203		dest := node.LinkData.Destination
1204		if r.flags&Safelink != 0 && !isSafeLink(dest) && !isMailto(dest) {
1205			if entering {
1206				r.out(tag("tt", nil, false))
1207			} else {
1208				r.out(tag("/tt", nil, false))
1209			}
1210		} else {
1211			if entering {
1212				dest = r.addAbsPrefix(dest)
1213				//if (!(options.safe && potentiallyUnsafe(node.destination))) {
1214				attrs = append(attrs, fmt.Sprintf("href=%q", esc(dest, true)))
1215				//}
1216				if node.NoteID != 0 {
1217					r.out(footnoteRef(r.parameters.FootnoteAnchorPrefix, node))
1218					break
1219				}
1220				attrs = appendLinkAttrs(attrs, r.flags, dest)
1221				if len(node.LinkData.Title) > 0 {
1222					attrs = append(attrs, fmt.Sprintf("title=%q", esc(node.LinkData.Title, true)))
1223				}
1224				r.out(tag("a", attrs, false))
1225			} else {
1226				if node.NoteID != 0 {
1227					break
1228				}
1229				r.out(tag("/a", nil, false))
1230			}
1231		}
1232	case Image:
1233		if entering {
1234			dest := node.LinkData.Destination
1235			dest = r.addAbsPrefix(dest)
1236			if r.disableTags == 0 {
1237				//if options.safe && potentiallyUnsafe(dest) {
1238				//out(`<img src="" alt="`)
1239				//} else {
1240				r.out([]byte(fmt.Sprintf(`<img src="%s" alt="`, esc(dest, true))))
1241				//}
1242			}
1243			r.disableTags++
1244		} else {
1245			r.disableTags--
1246			if r.disableTags == 0 {
1247				if node.LinkData.Title != nil {
1248					r.out([]byte(`" title="`))
1249					r.out(esc(node.LinkData.Title, true))
1250				}
1251				r.out([]byte(`" />`))
1252			}
1253		}
1254	case Code:
1255		r.out(tag("code", nil, false))
1256		r.out(escCode(node.Literal, false))
1257		r.out(tag("/code", nil, false))
1258	case Document:
1259		break
1260	case Paragraph:
1261		if skipParagraphTags(node) {
1262			break
1263		}
1264		if entering {
1265			// TODO: untangle this clusterfuck about when the newlines need
1266			// to be added and when not.
1267			if node.Prev != nil {
1268				t := node.Prev.Type
1269				if t == HtmlBlock || t == List || t == Paragraph || t == Header || t == CodeBlock || t == BlockQuote || t == HorizontalRule {
1270					r.cr()
1271				}
1272			}
1273			if node.Parent.Type == BlockQuote && node.Prev == nil {
1274				r.cr()
1275			}
1276			r.out(tag("p", attrs, false))
1277		} else {
1278			r.out(tag("/p", attrs, false))
1279			if !(node.Parent.Type == Item && node.Next == nil) {
1280				r.cr()
1281			}
1282		}
1283		break
1284	case BlockQuote:
1285		if entering {
1286			r.cr()
1287			r.out(tag("blockquote", attrs, false))
1288		} else {
1289			r.out(tag("/blockquote", nil, false))
1290			r.cr()
1291		}
1292		break
1293	case HtmlBlock:
1294		r.cr()
1295		r.out(node.Literal)
1296		r.cr()
1297	case Header:
1298		tagname := fmt.Sprintf("h%d", node.Level)
1299		if entering {
1300			if node.IsTitleblock {
1301				attrs = append(attrs, `class="title"`)
1302			}
1303			if node.HeaderID != "" {
1304				id := r.ensureUniqueHeaderID(node.HeaderID)
1305				if r.parameters.HeaderIDPrefix != "" {
1306					id = r.parameters.HeaderIDPrefix + id
1307				}
1308				if r.parameters.HeaderIDSuffix != "" {
1309					id = id + r.parameters.HeaderIDSuffix
1310				}
1311				attrs = append(attrs, fmt.Sprintf(`id="%s"`, id))
1312			}
1313			r.cr()
1314			r.out(tag(tagname, attrs, false))
1315		} else {
1316			r.out(tag("/"+tagname, nil, false))
1317			if !(node.Parent.Type == Item && node.Next == nil) {
1318				r.cr()
1319			}
1320		}
1321		break
1322	case HorizontalRule:
1323		r.cr()
1324		r.out(tag("hr", attrs, r.flags&UseXHTML != 0))
1325		r.cr()
1326		break
1327	case List:
1328		tagName := "ul"
1329		if node.ListData.Flags&ListTypeOrdered != 0 {
1330			tagName = "ol"
1331		}
1332		if node.ListData.Flags&ListTypeDefinition != 0 {
1333			tagName = "dl"
1334		}
1335		if entering {
1336			// var start = node.listStart;
1337			// if (start !== null && start !== 1) {
1338			//     attrs.push(['start', start.toString()]);
1339			// }
1340			r.cr()
1341			if node.Parent.Type == Item && node.Parent.Parent.ListData.Tight {
1342				r.cr()
1343			}
1344			r.out(tag(tagName, attrs, false))
1345			r.cr()
1346		} else {
1347			r.out(tag("/"+tagName, nil, false))
1348			//cr()
1349			//if node.parent.Type != Item {
1350			//	cr()
1351			//}
1352			if node.Parent.Type == Item && node.Next != nil {
1353				r.cr()
1354			}
1355			if node.Parent.Type == Document || node.Parent.Type == BlockQuote {
1356				r.cr()
1357			}
1358		}
1359	case Item:
1360		tagName := "li"
1361		if node.ListData.Flags&ListTypeDefinition != 0 {
1362			tagName = "dd"
1363		}
1364		if node.ListData.Flags&ListTypeTerm != 0 {
1365			tagName = "dt"
1366		}
1367		if entering {
1368			if itemOpenCR(node) {
1369				r.cr()
1370			}
1371			if node.ListData.RefLink != nil {
1372				slug := slugify(node.ListData.RefLink)
1373				r.out(footnoteItem(r.parameters.FootnoteAnchorPrefix, slug))
1374				break
1375			}
1376			r.out(tag(tagName, nil, false))
1377		} else {
1378			if node.ListData.RefLink != nil {
1379				slug := slugify(node.ListData.RefLink)
1380				if r.flags&FootnoteReturnLinks != 0 {
1381					r.out(footnoteReturnLink(r.parameters.FootnoteAnchorPrefix, r.parameters.FootnoteReturnLinkContents, slug))
1382				}
1383			}
1384			r.out(tag("/"+tagName, nil, false))
1385			r.cr()
1386		}
1387	case CodeBlock:
1388		attrs = appendLanguageAttr(attrs, node.Info)
1389		r.cr()
1390		r.out(tag("pre", nil, false))
1391		r.out(tag("code", attrs, false))
1392		r.out(escCode(node.Literal, false))
1393		r.out(tag("/code", nil, false))
1394		r.out(tag("/pre", nil, false))
1395		if node.Parent.Type != Item {
1396			r.cr()
1397		}
1398	case Table:
1399		if entering {
1400			r.cr()
1401			r.out(tag("table", nil, false))
1402		} else {
1403			r.out(tag("/table", nil, false))
1404			r.cr()
1405		}
1406	case TableCell:
1407		tagName := "td"
1408		if node.IsHeader {
1409			tagName = "th"
1410		}
1411		if entering {
1412			align := cellAlignment(node.Align)
1413			if align != "" {
1414				attrs = append(attrs, fmt.Sprintf(`align="%s"`, align))
1415			}
1416			if node.Prev == nil {
1417				r.cr()
1418			}
1419			r.out(tag(tagName, attrs, false))
1420		} else {
1421			r.out(tag("/"+tagName, nil, false))
1422			r.cr()
1423		}
1424	case TableHead:
1425		if entering {
1426			r.cr()
1427			r.out(tag("thead", nil, false))
1428		} else {
1429			r.out(tag("/thead", nil, false))
1430			r.cr()
1431		}
1432	case TableBody:
1433		if entering {
1434			r.cr()
1435			r.out(tag("tbody", nil, false))
1436			// XXX: this is to adhere to a rather silly test. Should fix test.
1437			if node.FirstChild == nil {
1438				r.cr()
1439			}
1440		} else {
1441			r.out(tag("/tbody", nil, false))
1442			r.cr()
1443		}
1444	case TableRow:
1445		if entering {
1446			r.cr()
1447			r.out(tag("tr", nil, false))
1448		} else {
1449			r.out(tag("/tr", nil, false))
1450			r.cr()
1451		}
1452	default:
1453		panic("Unknown node type " + node.Type.String())
1454	}
1455}
1456
1457func (r *Html) Render(ast *Node) []byte {
1458	//println("render_Blackfriday")
1459	//dump(ast)
1460	ForEachNode(ast, r.RenderNode)
1461	return r.renderBuffer.Bytes()
1462}