all repos — grayfriday @ e0df70211230f75ef418655c3bfb721c37010345

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