all repos — grayfriday @ 0e0e3095cb287f44f2aa15e43540194f14b0385a

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