all repos — grayfriday @ a110088781ed1fabba4a59d6355a9058dc4fdf6a

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		// 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
151func 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
158func 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
179func 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
215func skipSpace(tag []byte, i int) int {
216	for i < len(tag) && isspace(tag[i]) {
217		i++
218	}
219	return i
220}
221
222func 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
251func (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
270func (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
282func 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
306func isMailto(link []byte) bool {
307	return bytes.HasPrefix(link, []byte("mailto:"))
308}
309
310func 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
317func isSmartypantable(node *Node) bool {
318	pt := node.Parent.Type
319	return pt != Link && pt != CodeBlock && pt != Code
320}
321
322func 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
333func (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
343func 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
349func footnoteItem(prefix string, slug []byte) []byte {
350	return []byte(fmt.Sprintf(`<li id="fn:%s%s">`, prefix, slug))
351}
352
353func 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
358func 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
366func 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
375func 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
388func (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
397func (r *HTMLRenderer) cr(w io.Writer) {
398	if r.lastOutputLen > 0 {
399		r.out(w, nlBytes)
400	}
401}
402
403var (
404	nlBytes    = []byte{'\n'}
405	gtBytes    = []byte{'>'}
406	spaceBytes = []byte{' '}
407)
408
409var (
410	brTag              = []byte("<br>")
411	brXHTMLTag         = []byte("<br />")
412	emTag              = []byte("<em>")
413	emCloseTag         = []byte("</em>")
414	strongTag          = []byte("<strong>")
415	strongCloseTag     = []byte("</strong>")
416	delTag             = []byte("<del>")
417	delCloseTag        = []byte("</del>")
418	ttTag              = []byte("<tt>")
419	ttCloseTag         = []byte("</tt>")
420	aTag               = []byte("<a")
421	aCloseTag          = []byte("</a>")
422	preTag             = []byte("<pre>")
423	preCloseTag        = []byte("</pre>")
424	codeTag            = []byte("<code>")
425	codeCloseTag       = []byte("</code>")
426	pTag               = []byte("<p>")
427	pCloseTag          = []byte("</p>")
428	blockquoteTag      = []byte("<blockquote>")
429	blockquoteCloseTag = []byte("</blockquote>")
430	hrTag              = []byte("<hr>")
431	hrXHTMLTag         = []byte("<hr />")
432	ulTag              = []byte("<ul>")
433	ulCloseTag         = []byte("</ul>")
434	olTag              = []byte("<ol>")
435	olCloseTag         = []byte("</ol>")
436	dlTag              = []byte("<dl>")
437	dlCloseTag         = []byte("</dl>")
438	liTag              = []byte("<li>")
439	liCloseTag         = []byte("</li>")
440	ddTag              = []byte("<dd>")
441	ddCloseTag         = []byte("</dd>")
442	dtTag              = []byte("<dt>")
443	dtCloseTag         = []byte("</dt>")
444	tableTag           = []byte("<table>")
445	tableCloseTag      = []byte("</table>")
446	tdTag              = []byte("<td")
447	tdCloseTag         = []byte("</td>")
448	thTag              = []byte("<th")
449	thCloseTag         = []byte("</th>")
450	theadTag           = []byte("<thead>")
451	theadCloseTag      = []byte("</thead>")
452	tbodyTag           = []byte("<tbody>")
453	tbodyCloseTag      = []byte("</tbody>")
454	trTag              = []byte("<tr>")
455	trCloseTag         = []byte("</tr>")
456	h1Tag              = []byte("<h1")
457	h1CloseTag         = []byte("</h1>")
458	h2Tag              = []byte("<h2")
459	h2CloseTag         = []byte("</h2>")
460	h3Tag              = []byte("<h3")
461	h3CloseTag         = []byte("</h3>")
462	h4Tag              = []byte("<h4")
463	h4CloseTag         = []byte("</h4>")
464	h5Tag              = []byte("<h5")
465	h5CloseTag         = []byte("</h5>")
466	h6Tag              = []byte("<h6")
467	h6CloseTag         = []byte("</h6>")
468
469	footnotesDivBytes      = []byte("\n<div class=\"footnotes\">\n\n")
470	footnotesCloseDivBytes = []byte("\n</div>\n")
471)
472
473func headingTagsFromLevel(level int) ([]byte, []byte) {
474	if level <= 1 {
475		return h1Tag, h1CloseTag
476	}
477	switch level {
478	case 2:
479		return h2Tag, h2CloseTag
480	case 3:
481		return h3Tag, h3CloseTag
482	case 4:
483		return h4Tag, h4CloseTag
484	case 5:
485		return h5Tag, h5CloseTag
486	}
487	return h6Tag, h6CloseTag
488}
489
490func (r *HTMLRenderer) outHRTag(w io.Writer) {
491	if r.Flags&UseXHTML == 0 {
492		r.out(w, hrTag)
493	} else {
494		r.out(w, hrXHTMLTag)
495	}
496}
497
498// RenderNode is a default renderer of a single node of a syntax tree. For
499// block nodes it will be called twice: first time with entering=true, second
500// time with entering=false, so that it could know when it's working on an open
501// tag and when on close. It writes the result to w.
502//
503// The return value is a way to tell the calling walker to adjust its walk
504// pattern: e.g. it can terminate the traversal by returning Terminate. Or it
505// can ask the walker to skip a subtree of this node by returning SkipChildren.
506// The typical behavior is to return GoToNext, which asks for the usual
507// traversal to the next node.
508func (r *HTMLRenderer) RenderNode(w io.Writer, node *Node, entering bool) WalkStatus {
509	attrs := []string{}
510	switch node.Type {
511	case Text:
512		if r.Flags&Smartypants != 0 {
513			var tmp bytes.Buffer
514			escapeHTML(&tmp, node.Literal)
515			r.sr.Process(w, tmp.Bytes())
516		} else {
517			if node.Parent.Type == Link {
518				escLink(w, node.Literal)
519			} else {
520				escapeHTML(w, node.Literal)
521			}
522		}
523	case Softbreak:
524		r.cr(w)
525		// TODO: make it configurable via out(renderer.softbreak)
526	case Hardbreak:
527		if r.Flags&UseXHTML == 0 {
528			r.out(w, brTag)
529		} else {
530			r.out(w, brXHTMLTag)
531		}
532		r.cr(w)
533	case Emph:
534		if entering {
535			r.out(w, emTag)
536		} else {
537			r.out(w, emCloseTag)
538		}
539	case Strong:
540		if entering {
541			r.out(w, strongTag)
542		} else {
543			r.out(w, strongCloseTag)
544		}
545	case Del:
546		if entering {
547			r.out(w, delTag)
548		} else {
549			r.out(w, delCloseTag)
550		}
551	case HTMLSpan:
552		if r.Flags&SkipHTML != 0 {
553			break
554		}
555		r.out(w, node.Literal)
556	case Link:
557		// mark it but don't link it if it is not a safe link: no smartypants
558		dest := node.LinkData.Destination
559		if needSkipLink(r.Flags, dest) {
560			if entering {
561				r.out(w, ttTag)
562			} else {
563				r.out(w, ttCloseTag)
564			}
565		} else {
566			if entering {
567				dest = r.addAbsPrefix(dest)
568				var hrefBuf bytes.Buffer
569				hrefBuf.WriteString("href=\"")
570				escLink(&hrefBuf, dest)
571				hrefBuf.WriteByte('"')
572				attrs = append(attrs, hrefBuf.String())
573				if node.NoteID != 0 {
574					r.out(w, footnoteRef(r.FootnoteAnchorPrefix, node))
575					break
576				}
577				attrs = appendLinkAttrs(attrs, r.Flags, dest)
578				if len(node.LinkData.Title) > 0 {
579					var titleBuff bytes.Buffer
580					titleBuff.WriteString("title=\"")
581					escapeHTML(&titleBuff, node.LinkData.Title)
582					titleBuff.WriteByte('"')
583					attrs = append(attrs, titleBuff.String())
584				}
585				r.tag(w, aTag, attrs)
586			} else {
587				if node.NoteID != 0 {
588					break
589				}
590				r.out(w, aCloseTag)
591			}
592		}
593	case Image:
594		if r.Flags&SkipImages != 0 {
595			return SkipChildren
596		}
597		if entering {
598			dest := node.LinkData.Destination
599			dest = r.addAbsPrefix(dest)
600			if r.disableTags == 0 {
601				//if options.safe && potentiallyUnsafe(dest) {
602				//out(w, `<img src="" alt="`)
603				//} else {
604				r.out(w, []byte(`<img src="`))
605				escLink(w, dest)
606				r.out(w, []byte(`" alt="`))
607				//}
608			}
609			r.disableTags++
610		} else {
611			r.disableTags--
612			if r.disableTags == 0 {
613				if node.LinkData.Title != nil {
614					r.out(w, []byte(`" title="`))
615					escapeHTML(w, node.LinkData.Title)
616				}
617				if node.LinkData.Width != 0 {
618					r.out(w, []byte(fmt.Sprintf(`" width="%d" height="%d`,
619						node.LinkData.Width, node.LinkData.Height)))
620				}
621				r.out(w, []byte(`" />`))
622			}
623		}
624	case Code:
625		r.out(w, codeTag)
626		escapeAllHTML(w, node.Literal)
627		r.out(w, codeCloseTag)
628	case Document:
629		break
630	case Paragraph:
631		if skipParagraphTags(node) {
632			break
633		}
634		if entering {
635			// TODO: untangle this clusterfuck about when the newlines need
636			// to be added and when not.
637			if node.Prev != nil {
638				switch node.Prev.Type {
639				case HTMLBlock, List, Paragraph, Heading, CodeBlock, BlockQuote, HorizontalRule:
640					r.cr(w)
641				}
642			}
643			if node.Parent.Type == BlockQuote && node.Prev == nil {
644				r.cr(w)
645			}
646			r.out(w, pTag)
647		} else {
648			r.out(w, pCloseTag)
649			if !(node.Parent.Type == Item && node.Next == nil) {
650				r.cr(w)
651			}
652		}
653	case BlockQuote:
654		if entering {
655			r.cr(w)
656			r.out(w, blockquoteTag)
657		} else {
658			r.out(w, blockquoteCloseTag)
659			r.cr(w)
660		}
661	case HTMLBlock:
662		if r.Flags&SkipHTML != 0 {
663			break
664		}
665		r.cr(w)
666		r.out(w, node.Literal)
667		r.cr(w)
668	case Heading:
669		headingLevel := r.HTMLRendererParameters.HeadingLevelOffset + node.Level
670		openTag, closeTag := headingTagsFromLevel(headingLevel)
671		if entering {
672			if node.IsTitleblock {
673				attrs = append(attrs, `class="title"`)
674			}
675			if node.HeadingID != "" {
676				id := r.ensureUniqueHeadingID(node.HeadingID)
677				if r.HeadingIDPrefix != "" {
678					id = r.HeadingIDPrefix + id
679				}
680				if r.HeadingIDSuffix != "" {
681					id = id + r.HeadingIDSuffix
682				}
683				attrs = append(attrs, fmt.Sprintf(`id="%s"`, id))
684			}
685			r.cr(w)
686			r.tag(w, openTag, attrs)
687		} else {
688			r.out(w, closeTag)
689			if !(node.Parent.Type == Item && node.Next == nil) {
690				r.cr(w)
691			}
692		}
693	case HorizontalRule:
694		r.cr(w)
695		r.outHRTag(w)
696		r.cr(w)
697	case List:
698		openTag := ulTag
699		closeTag := ulCloseTag
700		if node.ListFlags&ListTypeOrdered != 0 {
701			openTag = olTag
702			closeTag = olCloseTag
703		}
704		if node.ListFlags&ListTypeDefinition != 0 {
705			openTag = dlTag
706			closeTag = dlCloseTag
707		}
708		if entering {
709			if node.IsFootnotesList {
710				r.out(w, footnotesDivBytes)
711				r.outHRTag(w)
712				r.cr(w)
713			}
714			r.cr(w)
715			if node.Parent.Type == Item && node.Parent.Parent.Tight {
716				r.cr(w)
717			}
718			r.tag(w, openTag[:len(openTag)-1], attrs)
719			r.cr(w)
720		} else {
721			r.out(w, closeTag)
722			//cr(w)
723			//if node.parent.Type != Item {
724			//	cr(w)
725			//}
726			if node.Parent.Type == Item && node.Next != nil {
727				r.cr(w)
728			}
729			if node.Parent.Type == Document || node.Parent.Type == BlockQuote {
730				r.cr(w)
731			}
732			if node.IsFootnotesList {
733				r.out(w, footnotesCloseDivBytes)
734			}
735		}
736	case Item:
737		openTag := liTag
738		closeTag := liCloseTag
739		if node.ListFlags&ListTypeDefinition != 0 {
740			openTag = ddTag
741			closeTag = ddCloseTag
742		}
743		if node.ListFlags&ListTypeTerm != 0 {
744			openTag = dtTag
745			closeTag = dtCloseTag
746		}
747		if entering {
748			if itemOpenCR(node) {
749				r.cr(w)
750			}
751			if node.ListData.RefLink != nil {
752				slug := slugify(node.ListData.RefLink)
753				r.out(w, footnoteItem(r.FootnoteAnchorPrefix, slug))
754				break
755			}
756			r.out(w, openTag)
757		} else {
758			if node.ListData.RefLink != nil {
759				slug := slugify(node.ListData.RefLink)
760				if r.Flags&FootnoteReturnLinks != 0 {
761					r.out(w, footnoteReturnLink(r.FootnoteAnchorPrefix, r.FootnoteReturnLinkContents, slug))
762				}
763			}
764			r.out(w, closeTag)
765			r.cr(w)
766		}
767	case CodeBlock:
768		attrs = appendLanguageAttr(attrs, node.Info)
769		r.cr(w)
770		r.out(w, preTag)
771		r.tag(w, codeTag[:len(codeTag)-1], attrs)
772		escapeAllHTML(w, node.Literal)
773		r.out(w, codeCloseTag)
774		r.out(w, preCloseTag)
775		if node.Parent.Type != Item {
776			r.cr(w)
777		}
778	case Table:
779		if entering {
780			r.cr(w)
781			r.out(w, tableTag)
782		} else {
783			r.out(w, tableCloseTag)
784			r.cr(w)
785		}
786	case TableCell:
787		openTag := tdTag
788		closeTag := tdCloseTag
789		if node.IsHeader {
790			openTag = thTag
791			closeTag = thCloseTag
792		}
793		if entering {
794			align := cellAlignment(node.Align)
795			if align != "" {
796				attrs = append(attrs, fmt.Sprintf(`align="%s"`, align))
797			}
798			if node.Prev == nil {
799				r.cr(w)
800			}
801			r.tag(w, openTag, attrs)
802		} else {
803			r.out(w, closeTag)
804			r.cr(w)
805		}
806	case TableHead:
807		if entering {
808			r.cr(w)
809			r.out(w, theadTag)
810		} else {
811			r.out(w, theadCloseTag)
812			r.cr(w)
813		}
814	case TableBody:
815		if entering {
816			r.cr(w)
817			r.out(w, tbodyTag)
818			// XXX: this is to adhere to a rather silly test. Should fix test.
819			if node.FirstChild == nil {
820				r.cr(w)
821			}
822		} else {
823			r.out(w, tbodyCloseTag)
824			r.cr(w)
825		}
826	case TableRow:
827		if entering {
828			r.cr(w)
829			r.out(w, trTag)
830		} else {
831			r.out(w, trCloseTag)
832			r.cr(w)
833		}
834	default:
835		panic("Unknown node type " + node.Type.String())
836	}
837	return GoToNext
838}
839
840// RenderHeader writes HTML document preamble and TOC if requested.
841func (r *HTMLRenderer) RenderHeader(w io.Writer, ast *Node) {
842	r.writeDocumentHeader(w)
843	if r.Flags&TOC != 0 {
844		r.writeTOC(w, ast)
845	}
846}
847
848// RenderFooter writes HTML document footer.
849func (r *HTMLRenderer) RenderFooter(w io.Writer, ast *Node) {
850	if r.Flags&CompletePage == 0 {
851		return
852	}
853	io.WriteString(w, "\n</body>\n</html>\n")
854}
855
856func (r *HTMLRenderer) writeDocumentHeader(w io.Writer) {
857	if r.Flags&CompletePage == 0 {
858		return
859	}
860	ending := ""
861	if r.Flags&UseXHTML != 0 {
862		io.WriteString(w, "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" ")
863		io.WriteString(w, "\"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">\n")
864		io.WriteString(w, "<html xmlns=\"http://www.w3.org/1999/xhtml\">\n")
865		ending = " /"
866	} else {
867		io.WriteString(w, "<!DOCTYPE html>\n")
868		io.WriteString(w, "<html>\n")
869	}
870	io.WriteString(w, "<head>\n")
871	io.WriteString(w, "  <title>")
872	if r.Flags&Smartypants != 0 {
873		r.sr.Process(w, []byte(r.Title))
874	} else {
875		escapeHTML(w, []byte(r.Title))
876	}
877	io.WriteString(w, "</title>\n")
878	io.WriteString(w, "  <meta name=\"GENERATOR\" content=\"Blackfriday Markdown Processor v")
879	io.WriteString(w, Version)
880	io.WriteString(w, "\"")
881	io.WriteString(w, ending)
882	io.WriteString(w, ">\n")
883	io.WriteString(w, "  <meta charset=\"utf-8\"")
884	io.WriteString(w, ending)
885	io.WriteString(w, ">\n")
886	if r.CSS != "" {
887		io.WriteString(w, "  <link rel=\"stylesheet\" type=\"text/css\" href=\"")
888		escapeHTML(w, []byte(r.CSS))
889		io.WriteString(w, "\"")
890		io.WriteString(w, ending)
891		io.WriteString(w, ">\n")
892	}
893	if r.Icon != "" {
894		io.WriteString(w, "  <link rel=\"icon\" type=\"image/x-icon\" href=\"")
895		escapeHTML(w, []byte(r.Icon))
896		io.WriteString(w, "\"")
897		io.WriteString(w, ending)
898		io.WriteString(w, ">\n")
899	}
900	io.WriteString(w, "</head>\n")
901	io.WriteString(w, "<body>\n\n")
902}
903
904func (r *HTMLRenderer) writeTOC(w io.Writer, ast *Node) {
905	buf := bytes.Buffer{}
906
907	inHeading := false
908	tocLevel := 0
909	headingCount := 0
910
911	ast.Walk(func(node *Node, entering bool) WalkStatus {
912		if node.Type == Heading && !node.HeadingData.IsTitleblock {
913			inHeading = entering
914			if entering {
915				node.HeadingID = fmt.Sprintf("toc_%d", headingCount)
916				if node.Level == tocLevel {
917					buf.WriteString("</li>\n\n<li>")
918				} else if node.Level < tocLevel {
919					for node.Level < tocLevel {
920						tocLevel--
921						buf.WriteString("</li>\n</ul>")
922					}
923					buf.WriteString("</li>\n\n<li>")
924				} else {
925					for node.Level > tocLevel {
926						tocLevel++
927						buf.WriteString("\n<ul>\n<li>")
928					}
929				}
930
931				fmt.Fprintf(&buf, `<a href="#toc_%d">`, headingCount)
932				headingCount++
933			} else {
934				buf.WriteString("</a>")
935			}
936			return GoToNext
937		}
938
939		if inHeading {
940			return r.RenderNode(&buf, node, entering)
941		}
942
943		return GoToNext
944	})
945
946	for ; tocLevel > 0; tocLevel-- {
947		buf.WriteString("</li>\n</ul>")
948	}
949
950	if buf.Len() > 0 {
951		io.WriteString(w, "<nav>\n")
952		w.Write(buf.Bytes())
953		io.WriteString(w, "\n\n</nav>\n")
954	}
955	r.lastOutputLen = buf.Len()
956}