all repos — grayfriday @ 70124f1ea3199bbac3fd4a5cb61d75002879da4e

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