all repos — grayfriday @ 886a1405c025ae7cdbd5b28a3c354543bfa3dab9

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