dkforest

A forum and chat platform (onion)
git clone https://git.dasho.dev/n0tr1v/dkforest.git
Log | Files | Refs | LICENSE

html.go (24872B)


      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 
     16 package blackfriday
     17 
     18 import (
     19 	"bytes"
     20 	"fmt"
     21 	"io"
     22 	"regexp"
     23 	"strings"
     24 )
     25 
     26 // HTMLFlags control optional behavior of HTML renderer.
     27 type HTMLFlags int
     28 
     29 // HTML renderer configuration options.
     30 const (
     31 	HTMLFlagsNone           HTMLFlags = 0
     32 	SkipHTML                HTMLFlags = 1 << iota // Skip preformatted HTML blocks
     33 	SkipImages                                    // Skip embedded images
     34 	SkipLinks                                     // Skip all links
     35 	Safelink                                      // Only link to trusted protocols
     36 	NofollowLinks                                 // Only link with rel="nofollow"
     37 	NoreferrerLinks                               // Only link with rel="noreferrer"
     38 	NoopenerLinks                                 // Only link with rel="noopener"
     39 	HrefTargetBlank                               // Add a blank target
     40 	CompletePage                                  // Generate a complete HTML page
     41 	UseXHTML                                      // Generate XHTML output instead of HTML
     42 	FootnoteReturnLinks                           // Generate a link at the end of a footnote to return to the source
     43 	Smartypants                                   // Enable smart punctuation substitutions
     44 	SmartypantsFractions                          // Enable smart fractions (with Smartypants)
     45 	SmartypantsDashes                             // Enable smart dashes (with Smartypants)
     46 	SmartypantsLatexDashes                        // Enable LaTeX-style dashes (with Smartypants)
     47 	SmartypantsAngledQuotes                       // Enable angled double quotes (with Smartypants) for double quotes rendering
     48 	SmartypantsQuotesNBSP                         // Enable « French guillemets » (with Smartypants)
     49 	TOC                                           // Generate a table of contents
     50 )
     51 
     52 var (
     53 	htmlTagRe = regexp.MustCompile("(?i)^" + htmlTag)
     54 )
     55 
     56 const (
     57 	htmlTag = "(?:" + openTag + "|" + closeTag + "|" + htmlComment + "|" +
     58 		processingInstruction + "|" + declaration + "|" + cdata + ")"
     59 	closeTag              = "</" + tagName + "\\s*[>]"
     60 	openTag               = "<" + tagName + attribute + "*" + "\\s*/?>"
     61 	attribute             = "(?:" + "\\s+" + attributeName + attributeValueSpec + "?)"
     62 	attributeValue        = "(?:" + unquotedValue + "|" + singleQuotedValue + "|" + doubleQuotedValue + ")"
     63 	attributeValueSpec    = "(?:" + "\\s*=" + "\\s*" + attributeValue + ")"
     64 	attributeName         = "[a-zA-Z_:][a-zA-Z0-9:._-]*"
     65 	cdata                 = "<!\\[CDATA\\[[\\s\\S]*?\\]\\]>"
     66 	declaration           = "<![A-Z]+" + "\\s+[^>]*>"
     67 	doubleQuotedValue     = "\"[^\"]*\""
     68 	htmlComment           = "<!---->|<!--(?:-?[^>-])(?:-?[^-])*-->"
     69 	processingInstruction = "[<][?].*?[?][>]"
     70 	singleQuotedValue     = "'[^']*'"
     71 	tagName               = "[A-Za-z][A-Za-z0-9-]*"
     72 	unquotedValue         = "[^\"'=<>`\\x00-\\x20]+"
     73 )
     74 
     75 // HTMLRendererParameters is a collection of supplementary parameters tweaking
     76 // the behavior of various parts of HTML renderer.
     77 type HTMLRendererParameters struct {
     78 	// Prepend this text to each relative URL.
     79 	AbsolutePrefix string
     80 	// Add this text to each footnote anchor, to ensure uniqueness.
     81 	FootnoteAnchorPrefix string
     82 	// Show this text inside the <a> tag for a footnote return link, if the
     83 	// HTML_FOOTNOTE_RETURN_LINKS flag is enabled. If blank, the string
     84 	// <sup>[return]</sup> is used.
     85 	FootnoteReturnLinkContents string
     86 	// If set, add this text to the front of each Heading ID, to ensure
     87 	// uniqueness.
     88 	HeadingIDPrefix string
     89 	// If set, add this text to the back of each Heading ID, to ensure uniqueness.
     90 	HeadingIDSuffix string
     91 	// Increase heading levels: if the offset is 1, <h1> becomes <h2> etc.
     92 	// Negative offset is also valid.
     93 	// Resulting levels are clipped between 1 and 6.
     94 	HeadingLevelOffset int
     95 
     96 	Title string // Document title (used if CompletePage is set)
     97 	CSS   string // Optional CSS file URL (used if CompletePage is set)
     98 	Icon  string // Optional icon file URL (used if CompletePage is set)
     99 
    100 	Flags HTMLFlags // Flags allow customizing this renderer's behavior
    101 }
    102 
    103 // HTMLRenderer is a type that implements the Renderer interface for HTML output.
    104 //
    105 // Do not create this directly, instead use the NewHTMLRenderer function.
    106 type HTMLRenderer struct {
    107 	HTMLRendererParameters
    108 
    109 	closeTag string // how to end singleton tags: either " />" or ">"
    110 
    111 	// Track heading IDs to prevent ID collision in a single generation.
    112 	headingIDs map[string]int
    113 
    114 	lastOutputLen int
    115 	disableTags   int
    116 
    117 	sr *SPRenderer
    118 }
    119 
    120 const (
    121 	xhtmlClose = " />"
    122 	htmlClose  = ">"
    123 )
    124 
    125 // NewHTMLRenderer creates and configures an HTMLRenderer object, which
    126 // satisfies the Renderer interface.
    127 func NewHTMLRenderer(params HTMLRendererParameters) *HTMLRenderer {
    128 	// configure the rendering engine
    129 	closeTag := htmlClose
    130 	if params.Flags&UseXHTML != 0 {
    131 		closeTag = xhtmlClose
    132 	}
    133 
    134 	if params.FootnoteReturnLinkContents == "" {
    135 		// U+FE0E is VARIATION SELECTOR-15.
    136 		// It suppresses automatic emoji presentation of the preceding
    137 		// U+21A9 LEFTWARDS ARROW WITH HOOK on iOS and iPadOS.
    138 		params.FootnoteReturnLinkContents = "<span aria-label='Return'>↩\ufe0e</span>"
    139 	}
    140 
    141 	return &HTMLRenderer{
    142 		HTMLRendererParameters: params,
    143 
    144 		closeTag:   closeTag,
    145 		headingIDs: make(map[string]int),
    146 
    147 		sr: NewSmartypantsRenderer(params.Flags),
    148 	}
    149 }
    150 
    151 func isHTMLTag(tag []byte, tagname string) bool {
    152 	found, _ := findHTMLTagPos(tag, tagname)
    153 	return found
    154 }
    155 
    156 // Look for a character, but ignore it when it's in any kind of quotes, it
    157 // might be JavaScript
    158 func skipUntilCharIgnoreQuotes(html []byte, start int, char byte) int {
    159 	inSingleQuote := false
    160 	inDoubleQuote := false
    161 	inGraveQuote := false
    162 	i := start
    163 	for i < len(html) {
    164 		switch {
    165 		case html[i] == char && !inSingleQuote && !inDoubleQuote && !inGraveQuote:
    166 			return i
    167 		case html[i] == '\'':
    168 			inSingleQuote = !inSingleQuote
    169 		case html[i] == '"':
    170 			inDoubleQuote = !inDoubleQuote
    171 		case html[i] == '`':
    172 			inGraveQuote = !inGraveQuote
    173 		}
    174 		i++
    175 	}
    176 	return start
    177 }
    178 
    179 func findHTMLTagPos(tag []byte, tagname string) (bool, int) {
    180 	i := 0
    181 	if i < len(tag) && tag[0] != '<' {
    182 		return false, -1
    183 	}
    184 	i++
    185 	i = skipSpace(tag, i)
    186 
    187 	if i < len(tag) && tag[i] == '/' {
    188 		i++
    189 	}
    190 
    191 	i = skipSpace(tag, i)
    192 	j := 0
    193 	for ; i < len(tag); i, j = i+1, j+1 {
    194 		if j >= len(tagname) {
    195 			break
    196 		}
    197 
    198 		if strings.ToLower(string(tag[i]))[0] != tagname[j] {
    199 			return false, -1
    200 		}
    201 	}
    202 
    203 	if i == len(tag) {
    204 		return false, -1
    205 	}
    206 
    207 	rightAngle := skipUntilCharIgnoreQuotes(tag, i, '>')
    208 	if rightAngle >= i {
    209 		return true, rightAngle
    210 	}
    211 
    212 	return false, -1
    213 }
    214 
    215 func skipSpace(tag []byte, i int) int {
    216 	for i < len(tag) && isspace(tag[i]) {
    217 		i++
    218 	}
    219 	return i
    220 }
    221 
    222 func isRelativeLink(link []byte) (yes bool) {
    223 	// a tag begin with '#'
    224 	if link[0] == '#' {
    225 		return true
    226 	}
    227 
    228 	// link begin with '/' but not '//', the second maybe a protocol relative link
    229 	if len(link) >= 2 && link[0] == '/' && link[1] != '/' {
    230 		return true
    231 	}
    232 
    233 	// only the root '/'
    234 	if len(link) == 1 && link[0] == '/' {
    235 		return true
    236 	}
    237 
    238 	// current directory : begin with "./"
    239 	if bytes.HasPrefix(link, []byte("./")) {
    240 		return true
    241 	}
    242 
    243 	// parent directory : begin with "../"
    244 	if bytes.HasPrefix(link, []byte("../")) {
    245 		return true
    246 	}
    247 
    248 	return false
    249 }
    250 
    251 func (r *HTMLRenderer) ensureUniqueHeadingID(id string) string {
    252 	for count, found := r.headingIDs[id]; found; count, found = r.headingIDs[id] {
    253 		tmp := fmt.Sprintf("%s-%d", id, count+1)
    254 
    255 		if _, tmpFound := r.headingIDs[tmp]; !tmpFound {
    256 			r.headingIDs[id] = count + 1
    257 			id = tmp
    258 		} else {
    259 			id = id + "-1"
    260 		}
    261 	}
    262 
    263 	if _, found := r.headingIDs[id]; !found {
    264 		r.headingIDs[id] = 0
    265 	}
    266 
    267 	return id
    268 }
    269 
    270 func (r *HTMLRenderer) addAbsPrefix(link []byte) []byte {
    271 	if r.AbsolutePrefix != "" && isRelativeLink(link) && link[0] != '.' {
    272 		newDest := r.AbsolutePrefix
    273 		if link[0] != '/' {
    274 			newDest += "/"
    275 		}
    276 		newDest += string(link)
    277 		return []byte(newDest)
    278 	}
    279 	return link
    280 }
    281 
    282 func appendLinkAttrs(attrs []string, flags HTMLFlags, link []byte) []string {
    283 	if isRelativeLink(link) {
    284 		return attrs
    285 	}
    286 	val := []string{}
    287 	if flags&NofollowLinks != 0 {
    288 		val = append(val, "nofollow")
    289 	}
    290 	if flags&NoreferrerLinks != 0 {
    291 		val = append(val, "noreferrer")
    292 	}
    293 	if flags&NoopenerLinks != 0 {
    294 		val = append(val, "noopener")
    295 	}
    296 	if flags&HrefTargetBlank != 0 {
    297 		attrs = append(attrs, "target=\"_blank\"")
    298 	}
    299 	if len(val) == 0 {
    300 		return attrs
    301 	}
    302 	attr := fmt.Sprintf("rel=%q", strings.Join(val, " "))
    303 	return append(attrs, attr)
    304 }
    305 
    306 func isMailto(link []byte) bool {
    307 	return bytes.HasPrefix(link, []byte("mailto:"))
    308 }
    309 
    310 func needSkipLink(flags HTMLFlags, dest []byte) bool {
    311 	if flags&SkipLinks != 0 {
    312 		return true
    313 	}
    314 	return flags&Safelink != 0 && !isSafeLink(dest) && !isMailto(dest)
    315 }
    316 
    317 func isSmartypantable(node *Node) bool {
    318 	pt := node.Parent.Type
    319 	return pt != Link && pt != CodeBlock && pt != Code
    320 }
    321 
    322 func appendLanguageAttr(attrs []string, info []byte) []string {
    323 	if len(info) == 0 {
    324 		return attrs
    325 	}
    326 	endOfLang := bytes.IndexAny(info, "\t ")
    327 	if endOfLang < 0 {
    328 		endOfLang = len(info)
    329 	}
    330 	return append(attrs, fmt.Sprintf("class=\"language-%s\"", info[:endOfLang]))
    331 }
    332 
    333 func (r *HTMLRenderer) tag(w io.Writer, name []byte, attrs []string) {
    334 	w.Write(name)
    335 	if len(attrs) > 0 {
    336 		w.Write(spaceBytes)
    337 		w.Write([]byte(strings.Join(attrs, " ")))
    338 	}
    339 	w.Write(gtBytes)
    340 	r.lastOutputLen = 1
    341 }
    342 
    343 func footnoteRef(prefix string, node *Node) []byte {
    344 	urlFrag := prefix + string(slugify(node.Destination))
    345 	anchor := fmt.Sprintf(`<a href="#fn:%s">%d</a>`, urlFrag, node.NoteID)
    346 	return []byte(fmt.Sprintf(`<sup class="footnote-ref" id="fnref:%s">%s</sup>`, urlFrag, anchor))
    347 }
    348 
    349 func footnoteItem(prefix string, slug []byte) []byte {
    350 	return []byte(fmt.Sprintf(`<li id="fn:%s%s">`, prefix, slug))
    351 }
    352 
    353 func footnoteReturnLink(prefix, returnLink string, slug []byte) []byte {
    354 	const format = ` <a class="footnote-return" href="#fnref:%s%s">%s</a>`
    355 	return []byte(fmt.Sprintf(format, prefix, slug, returnLink))
    356 }
    357 
    358 func itemOpenCR(node *Node) bool {
    359 	if node.Prev == nil {
    360 		return false
    361 	}
    362 	ld := node.Parent.ListData
    363 	return !ld.Tight && ld.ListFlags&ListTypeDefinition == 0
    364 }
    365 
    366 func skipParagraphTags(node *Node) bool {
    367 	grandparent := node.Parent.Parent
    368 	if grandparent == nil || grandparent.Type != List {
    369 		return false
    370 	}
    371 	tightOrTerm := grandparent.Tight || node.Parent.ListFlags&ListTypeTerm != 0
    372 	return grandparent.Type == List && tightOrTerm
    373 }
    374 
    375 func cellAlignment(align CellAlignFlags) string {
    376 	switch align {
    377 	case TableAlignmentLeft:
    378 		return "left"
    379 	case TableAlignmentRight:
    380 		return "right"
    381 	case TableAlignmentCenter:
    382 		return "center"
    383 	default:
    384 		return ""
    385 	}
    386 }
    387 
    388 func (r *HTMLRenderer) out(w io.Writer, text []byte) {
    389 	if r.disableTags > 0 {
    390 		w.Write(htmlTagRe.ReplaceAll(text, []byte{}))
    391 	} else {
    392 		w.Write(text)
    393 	}
    394 	r.lastOutputLen = len(text)
    395 }
    396 
    397 func (r *HTMLRenderer) cr(w io.Writer) {
    398 	if r.lastOutputLen > 0 {
    399 		r.out(w, nlBytes)
    400 	}
    401 }
    402 
    403 var (
    404 	nlBytes    = []byte{'\n'}
    405 	gtBytes    = []byte{'>'}
    406 	spaceBytes = []byte{' '}
    407 )
    408 
    409 var (
    410 	brTag              = []byte("<br>")
    411 	brXHTMLTag         = []byte("<br />")
    412 	emTag              = []byte("<em>")
    413 	emCloseTag         = []byte("</em>")
    414 	censoredTag        = []byte(`<span class="censored">`)
    415 	censoredCloseTag   = []byte("</span>")
    416 	strongTag          = []byte("<strong>")
    417 	strongCloseTag     = []byte("</strong>")
    418 	delTag             = []byte("<del>")
    419 	delCloseTag        = []byte("</del>")
    420 	ttTag              = []byte("<tt>")
    421 	ttCloseTag         = []byte("</tt>")
    422 	aTag               = []byte("<a")
    423 	aCloseTag          = []byte("</a>")
    424 	preTag             = []byte("<pre>")
    425 	preCloseTag        = []byte("</pre>")
    426 	codeTag            = []byte("<code>")
    427 	codeCloseTag       = []byte("</code>")
    428 	pTag               = []byte("<p>")
    429 	pCloseTag          = []byte("</p>")
    430 	blockquoteTag      = []byte("<blockquote>")
    431 	blockquoteCloseTag = []byte("</blockquote>")
    432 	hrTag              = []byte("<hr>")
    433 	hrXHTMLTag         = []byte("<hr />")
    434 	ulTag              = []byte("<ul>")
    435 	ulCloseTag         = []byte("</ul>")
    436 	olTag              = []byte("<ol>")
    437 	olCloseTag         = []byte("</ol>")
    438 	dlTag              = []byte("<dl>")
    439 	dlCloseTag         = []byte("</dl>")
    440 	liTag              = []byte("<li>")
    441 	liCloseTag         = []byte("</li>")
    442 	ddTag              = []byte("<dd>")
    443 	ddCloseTag         = []byte("</dd>")
    444 	dtTag              = []byte("<dt>")
    445 	dtCloseTag         = []byte("</dt>")
    446 	tableTag           = []byte("<table>")
    447 	tableCloseTag      = []byte("</table>")
    448 	tdTag              = []byte("<td")
    449 	tdCloseTag         = []byte("</td>")
    450 	thTag              = []byte("<th")
    451 	thCloseTag         = []byte("</th>")
    452 	theadTag           = []byte("<thead>")
    453 	theadCloseTag      = []byte("</thead>")
    454 	tbodyTag           = []byte("<tbody>")
    455 	tbodyCloseTag      = []byte("</tbody>")
    456 	trTag              = []byte("<tr>")
    457 	trCloseTag         = []byte("</tr>")
    458 	h1Tag              = []byte("<h1")
    459 	h1CloseTag         = []byte("</h1>")
    460 	h2Tag              = []byte("<h2")
    461 	h2CloseTag         = []byte("</h2>")
    462 	h3Tag              = []byte("<h3")
    463 	h3CloseTag         = []byte("</h3>")
    464 	h4Tag              = []byte("<h4")
    465 	h4CloseTag         = []byte("</h4>")
    466 	h5Tag              = []byte("<h5")
    467 	h5CloseTag         = []byte("</h5>")
    468 	h6Tag              = []byte("<h6")
    469 	h6CloseTag         = []byte("</h6>")
    470 
    471 	footnotesDivBytes      = []byte("\n<div class=\"footnotes\">\n\n")
    472 	footnotesCloseDivBytes = []byte("\n</div>\n")
    473 )
    474 
    475 func headingTagsFromLevel(level int) ([]byte, []byte) {
    476 	if level <= 1 {
    477 		return h1Tag, h1CloseTag
    478 	}
    479 	switch level {
    480 	case 2:
    481 		return h2Tag, h2CloseTag
    482 	case 3:
    483 		return h3Tag, h3CloseTag
    484 	case 4:
    485 		return h4Tag, h4CloseTag
    486 	case 5:
    487 		return h5Tag, h5CloseTag
    488 	}
    489 	return h6Tag, h6CloseTag
    490 }
    491 
    492 func (r *HTMLRenderer) outHRTag(w io.Writer) {
    493 	if r.Flags&UseXHTML == 0 {
    494 		r.out(w, hrTag)
    495 	} else {
    496 		r.out(w, hrXHTMLTag)
    497 	}
    498 }
    499 
    500 // RenderNode is a default renderer of a single node of a syntax tree. For
    501 // block nodes it will be called twice: first time with entering=true, second
    502 // time with entering=false, so that it could know when it's working on an open
    503 // tag and when on close. It writes the result to w.
    504 //
    505 // The return value is a way to tell the calling walker to adjust its walk
    506 // pattern: e.g. it can terminate the traversal by returning Terminate. Or it
    507 // can ask the walker to skip a subtree of this node by returning SkipChildren.
    508 // The typical behavior is to return GoToNext, which asks for the usual
    509 // traversal to the next node.
    510 func (r *HTMLRenderer) RenderNode(w io.Writer, node *Node, entering bool) WalkStatus {
    511 	attrs := []string{}
    512 	switch node.Type {
    513 	case Text:
    514 		if r.Flags&Smartypants != 0 {
    515 			var tmp bytes.Buffer
    516 			escapeHTML(&tmp, node.Literal)
    517 			r.sr.Process(w, tmp.Bytes())
    518 		} else {
    519 			if node.Parent.Type == Link {
    520 				escLink(w, node.Literal)
    521 			} else {
    522 				escapeHTML(w, node.Literal)
    523 			}
    524 		}
    525 	case Softbreak:
    526 		r.cr(w)
    527 		// TODO: make it configurable via out(renderer.softbreak)
    528 	case Hardbreak:
    529 		if r.Flags&UseXHTML == 0 {
    530 			r.out(w, brTag)
    531 		} else {
    532 			r.out(w, brXHTMLTag)
    533 		}
    534 		r.cr(w)
    535 	case Emph:
    536 		if entering {
    537 			r.out(w, emTag)
    538 		} else {
    539 			r.out(w, emCloseTag)
    540 		}
    541 	case Censored:
    542 		if entering {
    543 			r.out(w, censoredTag)
    544 		} else {
    545 			r.out(w, censoredCloseTag)
    546 		}
    547 	case Strong:
    548 		if entering {
    549 			r.out(w, strongTag)
    550 		} else {
    551 			r.out(w, strongCloseTag)
    552 		}
    553 	case Del:
    554 		if entering {
    555 			r.out(w, delTag)
    556 		} else {
    557 			r.out(w, delCloseTag)
    558 		}
    559 	case HTMLSpan:
    560 		if r.Flags&SkipHTML != 0 {
    561 			break
    562 		}
    563 		r.out(w, node.Literal)
    564 	case Link:
    565 		// mark it but don't link it if it is not a safe link: no smartypants
    566 		dest := node.LinkData.Destination
    567 		if needSkipLink(r.Flags, dest) {
    568 			if entering {
    569 				r.out(w, ttTag)
    570 			} else {
    571 				r.out(w, ttCloseTag)
    572 			}
    573 		} else {
    574 			if entering {
    575 				dest = r.addAbsPrefix(dest)
    576 				var hrefBuf bytes.Buffer
    577 				hrefBuf.WriteString("href=\"")
    578 				escLink(&hrefBuf, dest)
    579 				hrefBuf.WriteByte('"')
    580 				attrs = append(attrs, hrefBuf.String())
    581 				if node.NoteID != 0 {
    582 					r.out(w, footnoteRef(r.FootnoteAnchorPrefix, node))
    583 					break
    584 				}
    585 				attrs = appendLinkAttrs(attrs, r.Flags, dest)
    586 				if len(node.LinkData.Title) > 0 {
    587 					var titleBuff bytes.Buffer
    588 					titleBuff.WriteString("title=\"")
    589 					escapeHTML(&titleBuff, node.LinkData.Title)
    590 					titleBuff.WriteByte('"')
    591 					attrs = append(attrs, titleBuff.String())
    592 				}
    593 				r.tag(w, aTag, attrs)
    594 			} else {
    595 				if node.NoteID != 0 {
    596 					break
    597 				}
    598 				r.out(w, aCloseTag)
    599 			}
    600 		}
    601 	case Image:
    602 		if r.Flags&SkipImages != 0 {
    603 			return SkipChildren
    604 		}
    605 		if entering {
    606 			dest := node.LinkData.Destination
    607 			dest = r.addAbsPrefix(dest)
    608 			if r.disableTags == 0 {
    609 				//if options.safe && potentiallyUnsafe(dest) {
    610 				//out(w, `<img src="" alt="`)
    611 				//} else {
    612 				r.out(w, []byte(`<img src="`))
    613 				escLink(w, dest)
    614 				r.out(w, []byte(`" alt="`))
    615 				//}
    616 			}
    617 			r.disableTags++
    618 		} else {
    619 			r.disableTags--
    620 			if r.disableTags == 0 {
    621 				if node.LinkData.Title != nil {
    622 					r.out(w, []byte(`" title="`))
    623 					escapeHTML(w, node.LinkData.Title)
    624 				}
    625 				r.out(w, []byte(`" />`))
    626 			}
    627 		}
    628 	case Code:
    629 		r.out(w, codeTag)
    630 		escapeAllHTML(w, node.Literal)
    631 		r.out(w, codeCloseTag)
    632 	case Document:
    633 		break
    634 	case Paragraph:
    635 		if skipParagraphTags(node) {
    636 			break
    637 		}
    638 		if entering {
    639 			// TODO: untangle this clusterfuck about when the newlines need
    640 			// to be added and when not.
    641 			if node.Prev != nil {
    642 				switch node.Prev.Type {
    643 				case HTMLBlock, List, Paragraph, Heading, CodeBlock, BlockQuote, HorizontalRule:
    644 					r.cr(w)
    645 				}
    646 			}
    647 			if node.Parent.Type == BlockQuote && node.Prev == nil {
    648 				r.cr(w)
    649 			}
    650 			r.out(w, pTag)
    651 		} else {
    652 			r.out(w, pCloseTag)
    653 			if !(node.Parent.Type == Item && node.Next == nil) {
    654 				r.cr(w)
    655 			}
    656 		}
    657 	case BlockQuote:
    658 		if entering {
    659 			r.cr(w)
    660 			r.out(w, blockquoteTag)
    661 		} else {
    662 			r.out(w, blockquoteCloseTag)
    663 			r.cr(w)
    664 		}
    665 	case HTMLBlock:
    666 		if r.Flags&SkipHTML != 0 {
    667 			break
    668 		}
    669 		r.cr(w)
    670 		r.out(w, node.Literal)
    671 		r.cr(w)
    672 	case Heading:
    673 		headingLevel := r.HTMLRendererParameters.HeadingLevelOffset + node.Level
    674 		openTag, closeTag := headingTagsFromLevel(headingLevel)
    675 		if entering {
    676 			if node.IsTitleblock {
    677 				attrs = append(attrs, `class="title"`)
    678 			}
    679 			if node.HeadingID != "" {
    680 				id := r.ensureUniqueHeadingID(node.HeadingID)
    681 				if r.HeadingIDPrefix != "" {
    682 					id = r.HeadingIDPrefix + id
    683 				}
    684 				if r.HeadingIDSuffix != "" {
    685 					id = id + r.HeadingIDSuffix
    686 				}
    687 				attrs = append(attrs, fmt.Sprintf(`id="%s"`, id))
    688 			}
    689 			r.cr(w)
    690 			r.tag(w, openTag, attrs)
    691 		} else {
    692 			r.out(w, closeTag)
    693 			if !(node.Parent.Type == Item && node.Next == nil) {
    694 				r.cr(w)
    695 			}
    696 		}
    697 	case HorizontalRule:
    698 		r.cr(w)
    699 		r.outHRTag(w)
    700 		r.cr(w)
    701 	case List:
    702 		openTag := ulTag
    703 		closeTag := ulCloseTag
    704 		if node.ListFlags&ListTypeOrdered != 0 {
    705 			openTag = olTag
    706 			closeTag = olCloseTag
    707 		}
    708 		if node.ListFlags&ListTypeDefinition != 0 {
    709 			openTag = dlTag
    710 			closeTag = dlCloseTag
    711 		}
    712 		if entering {
    713 			if node.IsFootnotesList {
    714 				r.out(w, footnotesDivBytes)
    715 				r.outHRTag(w)
    716 				r.cr(w)
    717 			}
    718 			r.cr(w)
    719 			if node.Parent.Type == Item && node.Parent.Parent.Tight {
    720 				r.cr(w)
    721 			}
    722 			r.tag(w, openTag[:len(openTag)-1], attrs)
    723 			r.cr(w)
    724 		} else {
    725 			r.out(w, closeTag)
    726 			//cr(w)
    727 			//if node.parent.Type != Item {
    728 			//	cr(w)
    729 			//}
    730 			if node.Parent.Type == Item && node.Next != nil {
    731 				r.cr(w)
    732 			}
    733 			if node.Parent.Type == Document || node.Parent.Type == BlockQuote {
    734 				r.cr(w)
    735 			}
    736 			if node.IsFootnotesList {
    737 				r.out(w, footnotesCloseDivBytes)
    738 			}
    739 		}
    740 	case Item:
    741 		openTag := liTag
    742 		closeTag := liCloseTag
    743 		if node.ListFlags&ListTypeDefinition != 0 {
    744 			openTag = ddTag
    745 			closeTag = ddCloseTag
    746 		}
    747 		if node.ListFlags&ListTypeTerm != 0 {
    748 			openTag = dtTag
    749 			closeTag = dtCloseTag
    750 		}
    751 		if entering {
    752 			if itemOpenCR(node) {
    753 				r.cr(w)
    754 			}
    755 			if node.ListData.RefLink != nil {
    756 				slug := slugify(node.ListData.RefLink)
    757 				r.out(w, footnoteItem(r.FootnoteAnchorPrefix, slug))
    758 				break
    759 			}
    760 			r.out(w, openTag)
    761 		} else {
    762 			if node.ListData.RefLink != nil {
    763 				slug := slugify(node.ListData.RefLink)
    764 				if r.Flags&FootnoteReturnLinks != 0 {
    765 					r.out(w, footnoteReturnLink(r.FootnoteAnchorPrefix, r.FootnoteReturnLinkContents, slug))
    766 				}
    767 			}
    768 			r.out(w, closeTag)
    769 			r.cr(w)
    770 		}
    771 	case CodeBlock:
    772 		attrs = appendLanguageAttr(attrs, node.Info)
    773 		r.cr(w)
    774 		r.out(w, preTag)
    775 		r.tag(w, codeTag[:len(codeTag)-1], attrs)
    776 		escapeAllHTML(w, node.Literal)
    777 		r.out(w, codeCloseTag)
    778 		r.out(w, preCloseTag)
    779 		if node.Parent.Type != Item {
    780 			r.cr(w)
    781 		}
    782 	case Table:
    783 		if entering {
    784 			r.cr(w)
    785 			r.out(w, tableTag)
    786 		} else {
    787 			r.out(w, tableCloseTag)
    788 			r.cr(w)
    789 		}
    790 	case TableCell:
    791 		openTag := tdTag
    792 		closeTag := tdCloseTag
    793 		if node.IsHeader {
    794 			openTag = thTag
    795 			closeTag = thCloseTag
    796 		}
    797 		if entering {
    798 			align := cellAlignment(node.Align)
    799 			if align != "" {
    800 				attrs = append(attrs, fmt.Sprintf(`align="%s"`, align))
    801 			}
    802 			if node.Prev == nil {
    803 				r.cr(w)
    804 			}
    805 			r.tag(w, openTag, attrs)
    806 		} else {
    807 			r.out(w, closeTag)
    808 			r.cr(w)
    809 		}
    810 	case TableHead:
    811 		if entering {
    812 			r.cr(w)
    813 			r.out(w, theadTag)
    814 		} else {
    815 			r.out(w, theadCloseTag)
    816 			r.cr(w)
    817 		}
    818 	case TableBody:
    819 		if entering {
    820 			r.cr(w)
    821 			r.out(w, tbodyTag)
    822 			// XXX: this is to adhere to a rather silly test. Should fix test.
    823 			if node.FirstChild == nil {
    824 				r.cr(w)
    825 			}
    826 		} else {
    827 			r.out(w, tbodyCloseTag)
    828 			r.cr(w)
    829 		}
    830 	case TableRow:
    831 		if entering {
    832 			r.cr(w)
    833 			r.out(w, trTag)
    834 		} else {
    835 			r.out(w, trCloseTag)
    836 			r.cr(w)
    837 		}
    838 	default:
    839 		panic("Unknown node type " + node.Type.String())
    840 	}
    841 	return GoToNext
    842 }
    843 
    844 // RenderHeader writes HTML document preamble and TOC if requested.
    845 func (r *HTMLRenderer) RenderHeader(w io.Writer, ast *Node) {
    846 	r.writeDocumentHeader(w)
    847 	if r.Flags&TOC != 0 {
    848 		r.writeTOC(w, ast)
    849 	}
    850 }
    851 
    852 // RenderFooter writes HTML document footer.
    853 func (r *HTMLRenderer) RenderFooter(w io.Writer, ast *Node) {
    854 	if r.Flags&CompletePage == 0 {
    855 		return
    856 	}
    857 	io.WriteString(w, "\n</body>\n</html>\n")
    858 }
    859 
    860 func (r *HTMLRenderer) writeDocumentHeader(w io.Writer) {
    861 	if r.Flags&CompletePage == 0 {
    862 		return
    863 	}
    864 	ending := ""
    865 	if r.Flags&UseXHTML != 0 {
    866 		io.WriteString(w, "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" ")
    867 		io.WriteString(w, "\"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">\n")
    868 		io.WriteString(w, "<html xmlns=\"http://www.w3.org/1999/xhtml\">\n")
    869 		ending = " /"
    870 	} else {
    871 		io.WriteString(w, "<!DOCTYPE html>\n")
    872 		io.WriteString(w, "<html>\n")
    873 	}
    874 	io.WriteString(w, "<head>\n")
    875 	io.WriteString(w, "  <title>")
    876 	if r.Flags&Smartypants != 0 {
    877 		r.sr.Process(w, []byte(r.Title))
    878 	} else {
    879 		escapeHTML(w, []byte(r.Title))
    880 	}
    881 	io.WriteString(w, "</title>\n")
    882 	io.WriteString(w, "  <meta name=\"GENERATOR\" content=\"Blackfriday Markdown Processor v")
    883 	io.WriteString(w, Version)
    884 	io.WriteString(w, "\"")
    885 	io.WriteString(w, ending)
    886 	io.WriteString(w, ">\n")
    887 	io.WriteString(w, "  <meta charset=\"utf-8\"")
    888 	io.WriteString(w, ending)
    889 	io.WriteString(w, ">\n")
    890 	if r.CSS != "" {
    891 		io.WriteString(w, "  <link rel=\"stylesheet\" type=\"text/css\" href=\"")
    892 		escapeHTML(w, []byte(r.CSS))
    893 		io.WriteString(w, "\"")
    894 		io.WriteString(w, ending)
    895 		io.WriteString(w, ">\n")
    896 	}
    897 	if r.Icon != "" {
    898 		io.WriteString(w, "  <link rel=\"icon\" type=\"image/x-icon\" href=\"")
    899 		escapeHTML(w, []byte(r.Icon))
    900 		io.WriteString(w, "\"")
    901 		io.WriteString(w, ending)
    902 		io.WriteString(w, ">\n")
    903 	}
    904 	io.WriteString(w, "</head>\n")
    905 	io.WriteString(w, "<body>\n\n")
    906 }
    907 
    908 func (r *HTMLRenderer) writeTOC(w io.Writer, ast *Node) {
    909 	buf := bytes.Buffer{}
    910 
    911 	inHeading := false
    912 	tocLevel := 0
    913 	headingCount := 0
    914 
    915 	ast.Walk(func(node *Node, entering bool) WalkStatus {
    916 		if node.Type == Heading && !node.HeadingData.IsTitleblock {
    917 			inHeading = entering
    918 			if entering {
    919 				node.HeadingID = fmt.Sprintf("toc_%d", headingCount)
    920 				if node.Level == tocLevel {
    921 					buf.WriteString("</li>\n\n<li>")
    922 				} else if node.Level < tocLevel {
    923 					for node.Level < tocLevel {
    924 						tocLevel--
    925 						buf.WriteString("</li>\n</ul>")
    926 					}
    927 					buf.WriteString("</li>\n\n<li>")
    928 				} else {
    929 					for node.Level > tocLevel {
    930 						tocLevel++
    931 						buf.WriteString("\n<ul>\n<li>")
    932 					}
    933 				}
    934 
    935 				fmt.Fprintf(&buf, `<a href="#toc_%d">`, headingCount)
    936 				headingCount++
    937 			} else {
    938 				buf.WriteString("</a>")
    939 			}
    940 			return GoToNext
    941 		}
    942 
    943 		if inHeading {
    944 			return r.RenderNode(&buf, node, entering)
    945 		}
    946 
    947 		return GoToNext
    948 	})
    949 
    950 	for ; tocLevel > 0; tocLevel-- {
    951 		buf.WriteString("</li>\n</ul>")
    952 	}
    953 
    954 	if buf.Len() > 0 {
    955 		io.WriteString(w, "<nav>\n")
    956 		w.Write(buf.Bytes())
    957 		io.WriteString(w, "\n\n</nav>\n")
    958 	}
    959 	r.lastOutputLen = buf.Len()
    960 }