vendor/github.com/russross/blackfriday/v2/html.go
changeset 256 6d9efbef00a9
child 260 445e01aede7e
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/vendor/github.com/russross/blackfriday/v2/html.go	Sun Jul 11 10:35:56 2021 +0200
@@ -0,0 +1,949 @@
+//
+// Blackfriday Markdown Processor
+// Available at http://github.com/russross/blackfriday
+//
+// Copyright © 2011 Russ Ross <russ@russross.com>.
+// Distributed under the Simplified BSD License.
+// See README.md for details.
+//
+
+//
+//
+// HTML rendering backend
+//
+//
+
+package blackfriday
+
+import (
+	"bytes"
+	"fmt"
+	"io"
+	"regexp"
+	"strings"
+)
+
+// HTMLFlags control optional behavior of HTML renderer.
+type HTMLFlags int
+
+// HTML renderer configuration options.
+const (
+	HTMLFlagsNone           HTMLFlags = 0
+	SkipHTML                HTMLFlags = 1 << iota // Skip preformatted HTML blocks
+	SkipImages                                    // Skip embedded images
+	SkipLinks                                     // Skip all links
+	Safelink                                      // Only link to trusted protocols
+	NofollowLinks                                 // Only link with rel="nofollow"
+	NoreferrerLinks                               // Only link with rel="noreferrer"
+	NoopenerLinks                                 // Only link with rel="noopener"
+	HrefTargetBlank                               // Add a blank target
+	CompletePage                                  // Generate a complete HTML page
+	UseXHTML                                      // Generate XHTML output instead of HTML
+	FootnoteReturnLinks                           // Generate a link at the end of a footnote to return to the source
+	Smartypants                                   // Enable smart punctuation substitutions
+	SmartypantsFractions                          // Enable smart fractions (with Smartypants)
+	SmartypantsDashes                             // Enable smart dashes (with Smartypants)
+	SmartypantsLatexDashes                        // Enable LaTeX-style dashes (with Smartypants)
+	SmartypantsAngledQuotes                       // Enable angled double quotes (with Smartypants) for double quotes rendering
+	SmartypantsQuotesNBSP                         // Enable « French guillemets » (with Smartypants)
+	TOC                                           // Generate a table of contents
+)
+
+var (
+	htmlTagRe = regexp.MustCompile("(?i)^" + htmlTag)
+)
+
+const (
+	htmlTag = "(?:" + openTag + "|" + closeTag + "|" + htmlComment + "|" +
+		processingInstruction + "|" + declaration + "|" + cdata + ")"
+	closeTag              = "</" + tagName + "\\s*[>]"
+	openTag               = "<" + tagName + attribute + "*" + "\\s*/?>"
+	attribute             = "(?:" + "\\s+" + attributeName + attributeValueSpec + "?)"
+	attributeValue        = "(?:" + unquotedValue + "|" + singleQuotedValue + "|" + doubleQuotedValue + ")"
+	attributeValueSpec    = "(?:" + "\\s*=" + "\\s*" + attributeValue + ")"
+	attributeName         = "[a-zA-Z_:][a-zA-Z0-9:._-]*"
+	cdata                 = "<!\\[CDATA\\[[\\s\\S]*?\\]\\]>"
+	declaration           = "<![A-Z]+" + "\\s+[^>]*>"
+	doubleQuotedValue     = "\"[^\"]*\""
+	htmlComment           = "<!---->|<!--(?:-?[^>-])(?:-?[^-])*-->"
+	processingInstruction = "[<][?].*?[?][>]"
+	singleQuotedValue     = "'[^']*'"
+	tagName               = "[A-Za-z][A-Za-z0-9-]*"
+	unquotedValue         = "[^\"'=<>`\\x00-\\x20]+"
+)
+
+// HTMLRendererParameters is a collection of supplementary parameters tweaking
+// the behavior of various parts of HTML renderer.
+type HTMLRendererParameters struct {
+	// Prepend this text to each relative URL.
+	AbsolutePrefix string
+	// Add this text to each footnote anchor, to ensure uniqueness.
+	FootnoteAnchorPrefix string
+	// Show this text inside the <a> tag for a footnote return link, if the
+	// HTML_FOOTNOTE_RETURN_LINKS flag is enabled. If blank, the string
+	// <sup>[return]</sup> is used.
+	FootnoteReturnLinkContents string
+	// If set, add this text to the front of each Heading ID, to ensure
+	// uniqueness.
+	HeadingIDPrefix string
+	// If set, add this text to the back of each Heading ID, to ensure uniqueness.
+	HeadingIDSuffix string
+	// Increase heading levels: if the offset is 1, <h1> becomes <h2> etc.
+	// Negative offset is also valid.
+	// Resulting levels are clipped between 1 and 6.
+	HeadingLevelOffset int
+
+	Title string // Document title (used if CompletePage is set)
+	CSS   string // Optional CSS file URL (used if CompletePage is set)
+	Icon  string // Optional icon file URL (used if CompletePage is set)
+
+	Flags HTMLFlags // Flags allow customizing this renderer's behavior
+}
+
+// HTMLRenderer is a type that implements the Renderer interface for HTML output.
+//
+// Do not create this directly, instead use the NewHTMLRenderer function.
+type HTMLRenderer struct {
+	HTMLRendererParameters
+
+	closeTag string // how to end singleton tags: either " />" or ">"
+
+	// Track heading IDs to prevent ID collision in a single generation.
+	headingIDs map[string]int
+
+	lastOutputLen int
+	disableTags   int
+
+	sr *SPRenderer
+}
+
+const (
+	xhtmlClose = " />"
+	htmlClose  = ">"
+)
+
+// NewHTMLRenderer creates and configures an HTMLRenderer object, which
+// satisfies the Renderer interface.
+func NewHTMLRenderer(params HTMLRendererParameters) *HTMLRenderer {
+	// configure the rendering engine
+	closeTag := htmlClose
+	if params.Flags&UseXHTML != 0 {
+		closeTag = xhtmlClose
+	}
+
+	if params.FootnoteReturnLinkContents == "" {
+		params.FootnoteReturnLinkContents = `<sup>[return]</sup>`
+	}
+
+	return &HTMLRenderer{
+		HTMLRendererParameters: params,
+
+		closeTag:   closeTag,
+		headingIDs: make(map[string]int),
+
+		sr: NewSmartypantsRenderer(params.Flags),
+	}
+}
+
+func isHTMLTag(tag []byte, tagname string) bool {
+	found, _ := findHTMLTagPos(tag, tagname)
+	return found
+}
+
+// Look for a character, but ignore it when it's in any kind of quotes, it
+// might be JavaScript
+func skipUntilCharIgnoreQuotes(html []byte, start int, char byte) int {
+	inSingleQuote := false
+	inDoubleQuote := false
+	inGraveQuote := false
+	i := start
+	for i < len(html) {
+		switch {
+		case html[i] == char && !inSingleQuote && !inDoubleQuote && !inGraveQuote:
+			return i
+		case html[i] == '\'':
+			inSingleQuote = !inSingleQuote
+		case html[i] == '"':
+			inDoubleQuote = !inDoubleQuote
+		case html[i] == '`':
+			inGraveQuote = !inGraveQuote
+		}
+		i++
+	}
+	return start
+}
+
+func findHTMLTagPos(tag []byte, tagname string) (bool, int) {
+	i := 0
+	if i < len(tag) && tag[0] != '<' {
+		return false, -1
+	}
+	i++
+	i = skipSpace(tag, i)
+
+	if i < len(tag) && tag[i] == '/' {
+		i++
+	}
+
+	i = skipSpace(tag, i)
+	j := 0
+	for ; i < len(tag); i, j = i+1, j+1 {
+		if j >= len(tagname) {
+			break
+		}
+
+		if strings.ToLower(string(tag[i]))[0] != tagname[j] {
+			return false, -1
+		}
+	}
+
+	if i == len(tag) {
+		return false, -1
+	}
+
+	rightAngle := skipUntilCharIgnoreQuotes(tag, i, '>')
+	if rightAngle >= i {
+		return true, rightAngle
+	}
+
+	return false, -1
+}
+
+func skipSpace(tag []byte, i int) int {
+	for i < len(tag) && isspace(tag[i]) {
+		i++
+	}
+	return i
+}
+
+func isRelativeLink(link []byte) (yes bool) {
+	// a tag begin with '#'
+	if link[0] == '#' {
+		return true
+	}
+
+	// link begin with '/' but not '//', the second maybe a protocol relative link
+	if len(link) >= 2 && link[0] == '/' && link[1] != '/' {
+		return true
+	}
+
+	// only the root '/'
+	if len(link) == 1 && link[0] == '/' {
+		return true
+	}
+
+	// current directory : begin with "./"
+	if bytes.HasPrefix(link, []byte("./")) {
+		return true
+	}
+
+	// parent directory : begin with "../"
+	if bytes.HasPrefix(link, []byte("../")) {
+		return true
+	}
+
+	return false
+}
+
+func (r *HTMLRenderer) ensureUniqueHeadingID(id string) string {
+	for count, found := r.headingIDs[id]; found; count, found = r.headingIDs[id] {
+		tmp := fmt.Sprintf("%s-%d", id, count+1)
+
+		if _, tmpFound := r.headingIDs[tmp]; !tmpFound {
+			r.headingIDs[id] = count + 1
+			id = tmp
+		} else {
+			id = id + "-1"
+		}
+	}
+
+	if _, found := r.headingIDs[id]; !found {
+		r.headingIDs[id] = 0
+	}
+
+	return id
+}
+
+func (r *HTMLRenderer) addAbsPrefix(link []byte) []byte {
+	if r.AbsolutePrefix != "" && isRelativeLink(link) && link[0] != '.' {
+		newDest := r.AbsolutePrefix
+		if link[0] != '/' {
+			newDest += "/"
+		}
+		newDest += string(link)
+		return []byte(newDest)
+	}
+	return link
+}
+
+func appendLinkAttrs(attrs []string, flags HTMLFlags, link []byte) []string {
+	if isRelativeLink(link) {
+		return attrs
+	}
+	val := []string{}
+	if flags&NofollowLinks != 0 {
+		val = append(val, "nofollow")
+	}
+	if flags&NoreferrerLinks != 0 {
+		val = append(val, "noreferrer")
+	}
+	if flags&NoopenerLinks != 0 {
+		val = append(val, "noopener")
+	}
+	if flags&HrefTargetBlank != 0 {
+		attrs = append(attrs, "target=\"_blank\"")
+	}
+	if len(val) == 0 {
+		return attrs
+	}
+	attr := fmt.Sprintf("rel=%q", strings.Join(val, " "))
+	return append(attrs, attr)
+}
+
+func isMailto(link []byte) bool {
+	return bytes.HasPrefix(link, []byte("mailto:"))
+}
+
+func needSkipLink(flags HTMLFlags, dest []byte) bool {
+	if flags&SkipLinks != 0 {
+		return true
+	}
+	return flags&Safelink != 0 && !isSafeLink(dest) && !isMailto(dest)
+}
+
+func isSmartypantable(node *Node) bool {
+	pt := node.Parent.Type
+	return pt != Link && pt != CodeBlock && pt != Code
+}
+
+func appendLanguageAttr(attrs []string, info []byte) []string {
+	if len(info) == 0 {
+		return attrs
+	}
+	endOfLang := bytes.IndexAny(info, "\t ")
+	if endOfLang < 0 {
+		endOfLang = len(info)
+	}
+	return append(attrs, fmt.Sprintf("class=\"language-%s\"", info[:endOfLang]))
+}
+
+func (r *HTMLRenderer) tag(w io.Writer, name []byte, attrs []string) {
+	w.Write(name)
+	if len(attrs) > 0 {
+		w.Write(spaceBytes)
+		w.Write([]byte(strings.Join(attrs, " ")))
+	}
+	w.Write(gtBytes)
+	r.lastOutputLen = 1
+}
+
+func footnoteRef(prefix string, node *Node) []byte {
+	urlFrag := prefix + string(slugify(node.Destination))
+	anchor := fmt.Sprintf(`<a href="#fn:%s">%d</a>`, urlFrag, node.NoteID)
+	return []byte(fmt.Sprintf(`<sup class="footnote-ref" id="fnref:%s">%s</sup>`, urlFrag, anchor))
+}
+
+func footnoteItem(prefix string, slug []byte) []byte {
+	return []byte(fmt.Sprintf(`<li id="fn:%s%s">`, prefix, slug))
+}
+
+func footnoteReturnLink(prefix, returnLink string, slug []byte) []byte {
+	const format = ` <a class="footnote-return" href="#fnref:%s%s">%s</a>`
+	return []byte(fmt.Sprintf(format, prefix, slug, returnLink))
+}
+
+func itemOpenCR(node *Node) bool {
+	if node.Prev == nil {
+		return false
+	}
+	ld := node.Parent.ListData
+	return !ld.Tight && ld.ListFlags&ListTypeDefinition == 0
+}
+
+func skipParagraphTags(node *Node) bool {
+	grandparent := node.Parent.Parent
+	if grandparent == nil || grandparent.Type != List {
+		return false
+	}
+	tightOrTerm := grandparent.Tight || node.Parent.ListFlags&ListTypeTerm != 0
+	return grandparent.Type == List && tightOrTerm
+}
+
+func cellAlignment(align CellAlignFlags) string {
+	switch align {
+	case TableAlignmentLeft:
+		return "left"
+	case TableAlignmentRight:
+		return "right"
+	case TableAlignmentCenter:
+		return "center"
+	default:
+		return ""
+	}
+}
+
+func (r *HTMLRenderer) out(w io.Writer, text []byte) {
+	if r.disableTags > 0 {
+		w.Write(htmlTagRe.ReplaceAll(text, []byte{}))
+	} else {
+		w.Write(text)
+	}
+	r.lastOutputLen = len(text)
+}
+
+func (r *HTMLRenderer) cr(w io.Writer) {
+	if r.lastOutputLen > 0 {
+		r.out(w, nlBytes)
+	}
+}
+
+var (
+	nlBytes    = []byte{'\n'}
+	gtBytes    = []byte{'>'}
+	spaceBytes = []byte{' '}
+)
+
+var (
+	brTag              = []byte("<br>")
+	brXHTMLTag         = []byte("<br />")
+	emTag              = []byte("<em>")
+	emCloseTag         = []byte("</em>")
+	strongTag          = []byte("<strong>")
+	strongCloseTag     = []byte("</strong>")
+	delTag             = []byte("<del>")
+	delCloseTag        = []byte("</del>")
+	ttTag              = []byte("<tt>")
+	ttCloseTag         = []byte("</tt>")
+	aTag               = []byte("<a")
+	aCloseTag          = []byte("</a>")
+	preTag             = []byte("<pre>")
+	preCloseTag        = []byte("</pre>")
+	codeTag            = []byte("<code>")
+	codeCloseTag       = []byte("</code>")
+	pTag               = []byte("<p>")
+	pCloseTag          = []byte("</p>")
+	blockquoteTag      = []byte("<blockquote>")
+	blockquoteCloseTag = []byte("</blockquote>")
+	hrTag              = []byte("<hr>")
+	hrXHTMLTag         = []byte("<hr />")
+	ulTag              = []byte("<ul>")
+	ulCloseTag         = []byte("</ul>")
+	olTag              = []byte("<ol>")
+	olCloseTag         = []byte("</ol>")
+	dlTag              = []byte("<dl>")
+	dlCloseTag         = []byte("</dl>")
+	liTag              = []byte("<li>")
+	liCloseTag         = []byte("</li>")
+	ddTag              = []byte("<dd>")
+	ddCloseTag         = []byte("</dd>")
+	dtTag              = []byte("<dt>")
+	dtCloseTag         = []byte("</dt>")
+	tableTag           = []byte("<table>")
+	tableCloseTag      = []byte("</table>")
+	tdTag              = []byte("<td")
+	tdCloseTag         = []byte("</td>")
+	thTag              = []byte("<th")
+	thCloseTag         = []byte("</th>")
+	theadTag           = []byte("<thead>")
+	theadCloseTag      = []byte("</thead>")
+	tbodyTag           = []byte("<tbody>")
+	tbodyCloseTag      = []byte("</tbody>")
+	trTag              = []byte("<tr>")
+	trCloseTag         = []byte("</tr>")
+	h1Tag              = []byte("<h1")
+	h1CloseTag         = []byte("</h1>")
+	h2Tag              = []byte("<h2")
+	h2CloseTag         = []byte("</h2>")
+	h3Tag              = []byte("<h3")
+	h3CloseTag         = []byte("</h3>")
+	h4Tag              = []byte("<h4")
+	h4CloseTag         = []byte("</h4>")
+	h5Tag              = []byte("<h5")
+	h5CloseTag         = []byte("</h5>")
+	h6Tag              = []byte("<h6")
+	h6CloseTag         = []byte("</h6>")
+
+	footnotesDivBytes      = []byte("\n<div class=\"footnotes\">\n\n")
+	footnotesCloseDivBytes = []byte("\n</div>\n")
+)
+
+func headingTagsFromLevel(level int) ([]byte, []byte) {
+	if level <= 1 {
+		return h1Tag, h1CloseTag
+	}
+	switch level {
+	case 2:
+		return h2Tag, h2CloseTag
+	case 3:
+		return h3Tag, h3CloseTag
+	case 4:
+		return h4Tag, h4CloseTag
+	case 5:
+		return h5Tag, h5CloseTag
+	}
+	return h6Tag, h6CloseTag
+}
+
+func (r *HTMLRenderer) outHRTag(w io.Writer) {
+	if r.Flags&UseXHTML == 0 {
+		r.out(w, hrTag)
+	} else {
+		r.out(w, hrXHTMLTag)
+	}
+}
+
+// RenderNode is a default renderer of a single node of a syntax tree. For
+// block nodes it will be called twice: first time with entering=true, second
+// time with entering=false, so that it could know when it's working on an open
+// tag and when on close. It writes the result to w.
+//
+// The return value is a way to tell the calling walker to adjust its walk
+// pattern: e.g. it can terminate the traversal by returning Terminate. Or it
+// can ask the walker to skip a subtree of this node by returning SkipChildren.
+// The typical behavior is to return GoToNext, which asks for the usual
+// traversal to the next node.
+func (r *HTMLRenderer) RenderNode(w io.Writer, node *Node, entering bool) WalkStatus {
+	attrs := []string{}
+	switch node.Type {
+	case Text:
+		if r.Flags&Smartypants != 0 {
+			var tmp bytes.Buffer
+			escapeHTML(&tmp, node.Literal)
+			r.sr.Process(w, tmp.Bytes())
+		} else {
+			if node.Parent.Type == Link {
+				escLink(w, node.Literal)
+			} else {
+				escapeHTML(w, node.Literal)
+			}
+		}
+	case Softbreak:
+		r.cr(w)
+		// TODO: make it configurable via out(renderer.softbreak)
+	case Hardbreak:
+		if r.Flags&UseXHTML == 0 {
+			r.out(w, brTag)
+		} else {
+			r.out(w, brXHTMLTag)
+		}
+		r.cr(w)
+	case Emph:
+		if entering {
+			r.out(w, emTag)
+		} else {
+			r.out(w, emCloseTag)
+		}
+	case Strong:
+		if entering {
+			r.out(w, strongTag)
+		} else {
+			r.out(w, strongCloseTag)
+		}
+	case Del:
+		if entering {
+			r.out(w, delTag)
+		} else {
+			r.out(w, delCloseTag)
+		}
+	case HTMLSpan:
+		if r.Flags&SkipHTML != 0 {
+			break
+		}
+		r.out(w, node.Literal)
+	case Link:
+		// mark it but don't link it if it is not a safe link: no smartypants
+		dest := node.LinkData.Destination
+		if needSkipLink(r.Flags, dest) {
+			if entering {
+				r.out(w, ttTag)
+			} else {
+				r.out(w, ttCloseTag)
+			}
+		} else {
+			if entering {
+				dest = r.addAbsPrefix(dest)
+				var hrefBuf bytes.Buffer
+				hrefBuf.WriteString("href=\"")
+				escLink(&hrefBuf, dest)
+				hrefBuf.WriteByte('"')
+				attrs = append(attrs, hrefBuf.String())
+				if node.NoteID != 0 {
+					r.out(w, footnoteRef(r.FootnoteAnchorPrefix, node))
+					break
+				}
+				attrs = appendLinkAttrs(attrs, r.Flags, dest)
+				if len(node.LinkData.Title) > 0 {
+					var titleBuff bytes.Buffer
+					titleBuff.WriteString("title=\"")
+					escapeHTML(&titleBuff, node.LinkData.Title)
+					titleBuff.WriteByte('"')
+					attrs = append(attrs, titleBuff.String())
+				}
+				r.tag(w, aTag, attrs)
+			} else {
+				if node.NoteID != 0 {
+					break
+				}
+				r.out(w, aCloseTag)
+			}
+		}
+	case Image:
+		if r.Flags&SkipImages != 0 {
+			return SkipChildren
+		}
+		if entering {
+			dest := node.LinkData.Destination
+			dest = r.addAbsPrefix(dest)
+			if r.disableTags == 0 {
+				//if options.safe && potentiallyUnsafe(dest) {
+				//out(w, `<img src="" alt="`)
+				//} else {
+				r.out(w, []byte(`<img src="`))
+				escLink(w, dest)
+				r.out(w, []byte(`" alt="`))
+				//}
+			}
+			r.disableTags++
+		} else {
+			r.disableTags--
+			if r.disableTags == 0 {
+				if node.LinkData.Title != nil {
+					r.out(w, []byte(`" title="`))
+					escapeHTML(w, node.LinkData.Title)
+				}
+				r.out(w, []byte(`" />`))
+			}
+		}
+	case Code:
+		r.out(w, codeTag)
+		escapeHTML(w, node.Literal)
+		r.out(w, codeCloseTag)
+	case Document:
+		break
+	case Paragraph:
+		if skipParagraphTags(node) {
+			break
+		}
+		if entering {
+			// TODO: untangle this clusterfuck about when the newlines need
+			// to be added and when not.
+			if node.Prev != nil {
+				switch node.Prev.Type {
+				case HTMLBlock, List, Paragraph, Heading, CodeBlock, BlockQuote, HorizontalRule:
+					r.cr(w)
+				}
+			}
+			if node.Parent.Type == BlockQuote && node.Prev == nil {
+				r.cr(w)
+			}
+			r.out(w, pTag)
+		} else {
+			r.out(w, pCloseTag)
+			if !(node.Parent.Type == Item && node.Next == nil) {
+				r.cr(w)
+			}
+		}
+	case BlockQuote:
+		if entering {
+			r.cr(w)
+			r.out(w, blockquoteTag)
+		} else {
+			r.out(w, blockquoteCloseTag)
+			r.cr(w)
+		}
+	case HTMLBlock:
+		if r.Flags&SkipHTML != 0 {
+			break
+		}
+		r.cr(w)
+		r.out(w, node.Literal)
+		r.cr(w)
+	case Heading:
+		headingLevel := r.HTMLRendererParameters.HeadingLevelOffset + node.Level
+		openTag, closeTag := headingTagsFromLevel(headingLevel)
+		if entering {
+			if node.IsTitleblock {
+				attrs = append(attrs, `class="title"`)
+			}
+			if node.HeadingID != "" {
+				id := r.ensureUniqueHeadingID(node.HeadingID)
+				if r.HeadingIDPrefix != "" {
+					id = r.HeadingIDPrefix + id
+				}
+				if r.HeadingIDSuffix != "" {
+					id = id + r.HeadingIDSuffix
+				}
+				attrs = append(attrs, fmt.Sprintf(`id="%s"`, id))
+			}
+			r.cr(w)
+			r.tag(w, openTag, attrs)
+		} else {
+			r.out(w, closeTag)
+			if !(node.Parent.Type == Item && node.Next == nil) {
+				r.cr(w)
+			}
+		}
+	case HorizontalRule:
+		r.cr(w)
+		r.outHRTag(w)
+		r.cr(w)
+	case List:
+		openTag := ulTag
+		closeTag := ulCloseTag
+		if node.ListFlags&ListTypeOrdered != 0 {
+			openTag = olTag
+			closeTag = olCloseTag
+		}
+		if node.ListFlags&ListTypeDefinition != 0 {
+			openTag = dlTag
+			closeTag = dlCloseTag
+		}
+		if entering {
+			if node.IsFootnotesList {
+				r.out(w, footnotesDivBytes)
+				r.outHRTag(w)
+				r.cr(w)
+			}
+			r.cr(w)
+			if node.Parent.Type == Item && node.Parent.Parent.Tight {
+				r.cr(w)
+			}
+			r.tag(w, openTag[:len(openTag)-1], attrs)
+			r.cr(w)
+		} else {
+			r.out(w, closeTag)
+			//cr(w)
+			//if node.parent.Type != Item {
+			//	cr(w)
+			//}
+			if node.Parent.Type == Item && node.Next != nil {
+				r.cr(w)
+			}
+			if node.Parent.Type == Document || node.Parent.Type == BlockQuote {
+				r.cr(w)
+			}
+			if node.IsFootnotesList {
+				r.out(w, footnotesCloseDivBytes)
+			}
+		}
+	case Item:
+		openTag := liTag
+		closeTag := liCloseTag
+		if node.ListFlags&ListTypeDefinition != 0 {
+			openTag = ddTag
+			closeTag = ddCloseTag
+		}
+		if node.ListFlags&ListTypeTerm != 0 {
+			openTag = dtTag
+			closeTag = dtCloseTag
+		}
+		if entering {
+			if itemOpenCR(node) {
+				r.cr(w)
+			}
+			if node.ListData.RefLink != nil {
+				slug := slugify(node.ListData.RefLink)
+				r.out(w, footnoteItem(r.FootnoteAnchorPrefix, slug))
+				break
+			}
+			r.out(w, openTag)
+		} else {
+			if node.ListData.RefLink != nil {
+				slug := slugify(node.ListData.RefLink)
+				if r.Flags&FootnoteReturnLinks != 0 {
+					r.out(w, footnoteReturnLink(r.FootnoteAnchorPrefix, r.FootnoteReturnLinkContents, slug))
+				}
+			}
+			r.out(w, closeTag)
+			r.cr(w)
+		}
+	case CodeBlock:
+		attrs = appendLanguageAttr(attrs, node.Info)
+		r.cr(w)
+		r.out(w, preTag)
+		r.tag(w, codeTag[:len(codeTag)-1], attrs)
+		escapeHTML(w, node.Literal)
+		r.out(w, codeCloseTag)
+		r.out(w, preCloseTag)
+		if node.Parent.Type != Item {
+			r.cr(w)
+		}
+	case Table:
+		if entering {
+			r.cr(w)
+			r.out(w, tableTag)
+		} else {
+			r.out(w, tableCloseTag)
+			r.cr(w)
+		}
+	case TableCell:
+		openTag := tdTag
+		closeTag := tdCloseTag
+		if node.IsHeader {
+			openTag = thTag
+			closeTag = thCloseTag
+		}
+		if entering {
+			align := cellAlignment(node.Align)
+			if align != "" {
+				attrs = append(attrs, fmt.Sprintf(`align="%s"`, align))
+			}
+			if node.Prev == nil {
+				r.cr(w)
+			}
+			r.tag(w, openTag, attrs)
+		} else {
+			r.out(w, closeTag)
+			r.cr(w)
+		}
+	case TableHead:
+		if entering {
+			r.cr(w)
+			r.out(w, theadTag)
+		} else {
+			r.out(w, theadCloseTag)
+			r.cr(w)
+		}
+	case TableBody:
+		if entering {
+			r.cr(w)
+			r.out(w, tbodyTag)
+			// XXX: this is to adhere to a rather silly test. Should fix test.
+			if node.FirstChild == nil {
+				r.cr(w)
+			}
+		} else {
+			r.out(w, tbodyCloseTag)
+			r.cr(w)
+		}
+	case TableRow:
+		if entering {
+			r.cr(w)
+			r.out(w, trTag)
+		} else {
+			r.out(w, trCloseTag)
+			r.cr(w)
+		}
+	default:
+		panic("Unknown node type " + node.Type.String())
+	}
+	return GoToNext
+}
+
+// RenderHeader writes HTML document preamble and TOC if requested.
+func (r *HTMLRenderer) RenderHeader(w io.Writer, ast *Node) {
+	r.writeDocumentHeader(w)
+	if r.Flags&TOC != 0 {
+		r.writeTOC(w, ast)
+	}
+}
+
+// RenderFooter writes HTML document footer.
+func (r *HTMLRenderer) RenderFooter(w io.Writer, ast *Node) {
+	if r.Flags&CompletePage == 0 {
+		return
+	}
+	io.WriteString(w, "\n</body>\n</html>\n")
+}
+
+func (r *HTMLRenderer) writeDocumentHeader(w io.Writer) {
+	if r.Flags&CompletePage == 0 {
+		return
+	}
+	ending := ""
+	if r.Flags&UseXHTML != 0 {
+		io.WriteString(w, "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" ")
+		io.WriteString(w, "\"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">\n")
+		io.WriteString(w, "<html xmlns=\"http://www.w3.org/1999/xhtml\">\n")
+		ending = " /"
+	} else {
+		io.WriteString(w, "<!DOCTYPE html>\n")
+		io.WriteString(w, "<html>\n")
+	}
+	io.WriteString(w, "<head>\n")
+	io.WriteString(w, "  <title>")
+	if r.Flags&Smartypants != 0 {
+		r.sr.Process(w, []byte(r.Title))
+	} else {
+		escapeHTML(w, []byte(r.Title))
+	}
+	io.WriteString(w, "</title>\n")
+	io.WriteString(w, "  <meta name=\"GENERATOR\" content=\"Blackfriday Markdown Processor v")
+	io.WriteString(w, Version)
+	io.WriteString(w, "\"")
+	io.WriteString(w, ending)
+	io.WriteString(w, ">\n")
+	io.WriteString(w, "  <meta charset=\"utf-8\"")
+	io.WriteString(w, ending)
+	io.WriteString(w, ">\n")
+	if r.CSS != "" {
+		io.WriteString(w, "  <link rel=\"stylesheet\" type=\"text/css\" href=\"")
+		escapeHTML(w, []byte(r.CSS))
+		io.WriteString(w, "\"")
+		io.WriteString(w, ending)
+		io.WriteString(w, ">\n")
+	}
+	if r.Icon != "" {
+		io.WriteString(w, "  <link rel=\"icon\" type=\"image/x-icon\" href=\"")
+		escapeHTML(w, []byte(r.Icon))
+		io.WriteString(w, "\"")
+		io.WriteString(w, ending)
+		io.WriteString(w, ">\n")
+	}
+	io.WriteString(w, "</head>\n")
+	io.WriteString(w, "<body>\n\n")
+}
+
+func (r *HTMLRenderer) writeTOC(w io.Writer, ast *Node) {
+	buf := bytes.Buffer{}
+
+	inHeading := false
+	tocLevel := 0
+	headingCount := 0
+
+	ast.Walk(func(node *Node, entering bool) WalkStatus {
+		if node.Type == Heading && !node.HeadingData.IsTitleblock {
+			inHeading = entering
+			if entering {
+				node.HeadingID = fmt.Sprintf("toc_%d", headingCount)
+				if node.Level == tocLevel {
+					buf.WriteString("</li>\n\n<li>")
+				} else if node.Level < tocLevel {
+					for node.Level < tocLevel {
+						tocLevel--
+						buf.WriteString("</li>\n</ul>")
+					}
+					buf.WriteString("</li>\n\n<li>")
+				} else {
+					for node.Level > tocLevel {
+						tocLevel++
+						buf.WriteString("\n<ul>\n<li>")
+					}
+				}
+
+				fmt.Fprintf(&buf, `<a href="#toc_%d">`, headingCount)
+				headingCount++
+			} else {
+				buf.WriteString("</a>")
+			}
+			return GoToNext
+		}
+
+		if inHeading {
+			return r.RenderNode(&buf, node, entering)
+		}
+
+		return GoToNext
+	})
+
+	for ; tocLevel > 0; tocLevel-- {
+		buf.WriteString("</li>\n</ul>")
+	}
+
+	if buf.Len() > 0 {
+		io.WriteString(w, "<nav>\n")
+		w.Write(buf.Bytes())
+		io.WriteString(w, "\n\n</nav>\n")
+	}
+	r.lastOutputLen = buf.Len()
+}