all repos — grayfriday @ c9ea588e6f2982de480eaea6129d00ee1aa47fbb

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