all repos — grayfriday @ 187c33ff049bddf24fc5c5b82facfdd62813a552

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