all repos — grayfriday @ 45820516bb308c5fdc7a7efba124057164a5a071

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