all repos — grayfriday @ 52676fb0053ff19b59451b2aed2a1b8de7d89592

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	"html"
 22	"io"
 23	"regexp"
 24	"strings"
 25)
 26
 27// HTMLFlags control optional behavior of HTML renderer.
 28type HTMLFlags int
 29
 30// HTML renderer configuration options.
 31const (
 32	HTMLFlagsNone           HTMLFlags = 0
 33	SkipHTML                HTMLFlags = 1 << iota // Skip preformatted HTML blocks
 34	SkipStyle                                     // Skip embedded <style> elements
 35	SkipImages                                    // Skip embedded images
 36	SkipLinks                                     // Skip all links
 37	Safelink                                      // Only link to trusted protocols
 38	NofollowLinks                                 // Only link with rel="nofollow"
 39	NoreferrerLinks                               // Only link with rel="noreferrer"
 40	HrefTargetBlank                               // Add a blank target
 41	CompletePage                                  // Generate a complete HTML page
 42	UseXHTML                                      // Generate XHTML output instead of HTML
 43	FootnoteReturnLinks                           // Generate a link at the end of a footnote to return to the source
 44	Smartypants                                   // Enable smart punctuation substitutions
 45	SmartypantsFractions                          // Enable smart fractions (with Smartypants)
 46	SmartypantsDashes                             // Enable smart dashes (with Smartypants)
 47	SmartypantsLatexDashes                        // Enable LaTeX-style dashes (with Smartypants)
 48	SmartypantsAngledQuotes                       // Enable angled double quotes (with Smartypants) for double quotes rendering
 49
 50	TagName               = "[A-Za-z][A-Za-z0-9-]*"
 51	AttributeName         = "[a-zA-Z_:][a-zA-Z0-9:._-]*"
 52	UnquotedValue         = "[^\"'=<>`\\x00-\\x20]+"
 53	SingleQuotedValue     = "'[^']*'"
 54	DoubleQuotedValue     = "\"[^\"]*\""
 55	AttributeValue        = "(?:" + UnquotedValue + "|" + SingleQuotedValue + "|" + DoubleQuotedValue + ")"
 56	AttributeValueSpec    = "(?:" + "\\s*=" + "\\s*" + AttributeValue + ")"
 57	Attribute             = "(?:" + "\\s+" + AttributeName + AttributeValueSpec + "?)"
 58	OpenTag               = "<" + TagName + Attribute + "*" + "\\s*/?>"
 59	CloseTag              = "</" + TagName + "\\s*[>]"
 60	HTMLComment           = "<!---->|<!--(?:-?[^>-])(?:-?[^-])*-->"
 61	ProcessingInstruction = "[<][?].*?[?][>]"
 62	Declaration           = "<![A-Z]+" + "\\s+[^>]*>"
 63	CDATA                 = "<!\\[CDATA\\[[\\s\\S]*?\\]\\]>"
 64	HTMLTag               = "(?:" + OpenTag + "|" + CloseTag + "|" + HTMLComment + "|" +
 65		ProcessingInstruction + "|" + Declaration + "|" + CDATA + ")"
 66)
 67
 68var (
 69	htmlTagRe = regexp.MustCompile("(?i)^" + HTMLTag)
 70)
 71
 72// HTMLRendererParameters is a collection of supplementary parameters tweaking
 73// the behavior of various parts of HTML renderer.
 74type HTMLRendererParameters struct {
 75	// Prepend this text to each relative URL.
 76	AbsolutePrefix string
 77	// Add this text to each footnote anchor, to ensure uniqueness.
 78	FootnoteAnchorPrefix string
 79	// Show this text inside the <a> tag for a footnote return link, if the
 80	// HTML_FOOTNOTE_RETURN_LINKS flag is enabled. If blank, the string
 81	// <sup>[return]</sup> is used.
 82	FootnoteReturnLinkContents string
 83	// If set, add this text to the front of each Header ID, to ensure
 84	// uniqueness.
 85	HeaderIDPrefix string
 86	// If set, add this text to the back of each Header ID, to ensure uniqueness.
 87	HeaderIDSuffix string
 88
 89	Title string // Document title (used if CompletePage is set)
 90	CSS   string // Optional CSS file URL (used if CompletePage is set)
 91	Icon  string // Optional icon file URL (used if CompletePage is set)
 92
 93	Flags      HTMLFlags  // Flags allow customizing this renderer's behavior
 94	Extensions Extensions // Extensions give Smartypants and HTML renderer access to Blackfriday's global extensions
 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 header IDs to prevent ID collision in a single generation.
106	headerIDs 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		headerIDs: 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) ensureUniqueHeaderID(id string) string {
243	for count, found := r.headerIDs[id]; found; count, found = r.headerIDs[id] {
244		tmp := fmt.Sprintf("%s-%d", id, count+1)
245
246		if _, tmpFound := r.headerIDs[tmp]; !tmpFound {
247			r.headerIDs[id] = count + 1
248			id = tmp
249		} else {
250			id = id + "-1"
251		}
252	}
253
254	if _, found := r.headerIDs[id]; !found {
255		r.headerIDs[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	infoWords := bytes.Split(info, []byte("\t "))
312	if len(infoWords) > 0 && len(infoWords[0]) > 0 {
313		attrs = append(attrs, fmt.Sprintf("class=\"language-%s\"", infoWords[0]))
314	}
315	return attrs
316}
317
318func tag(name string, attrs []string, selfClosing bool) []byte {
319	result := "<" + name
320	if attrs != nil && len(attrs) > 0 {
321		result += " " + strings.Join(attrs, " ")
322	}
323	if selfClosing {
324		result += " /"
325	}
326	return []byte(result + ">")
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 esc(text []byte) []byte {
375	unesc := []byte(html.UnescapeString(string(text)))
376	return escCode(unesc)
377}
378
379func escCode(text []byte) []byte {
380	e1 := []byte(html.EscapeString(string(text)))
381	e2 := bytes.Replace(e1, []byte("&#34;"), []byte("&quot;"), -1)
382	return bytes.Replace(e2, []byte("&#39;"), []byte{'\''}, -1)
383}
384
385func (r *HTMLRenderer) out(w io.Writer, text []byte) {
386	if r.disableTags > 0 {
387		w.Write(htmlTagRe.ReplaceAll(text, []byte{}))
388	} else {
389		w.Write(text)
390	}
391	r.lastOutputLen = len(text)
392}
393
394func (r *HTMLRenderer) cr(w io.Writer) {
395	if r.lastOutputLen > 0 {
396		r.out(w, []byte{'\n'})
397	}
398}
399
400// RenderNode is a default renderer of a single node of a syntax tree. For
401// block nodes it will be called twice: first time with entering=true, second
402// time with entering=false, so that it could know when it's working on an open
403// tag and when on close. It writes the result to w.
404//
405// The return value is a way to tell the calling walker to adjust its walk
406// pattern: e.g. it can terminate the traversal by returning Terminate. Or it
407// can ask the walker to skip a subtree of this node by returning SkipChildren.
408// The typical behavior is to return GoToNext, which asks for the usual
409// traversal to the next node.
410func (r *HTMLRenderer) RenderNode(w io.Writer, node *Node, entering bool) WalkStatus {
411	attrs := []string{}
412	switch node.Type {
413	case Text:
414		node.Literal = esc(node.Literal)
415		if r.Flags&Smartypants != 0 {
416			node.Literal = r.sr.Process(node.Literal)
417		}
418		r.out(w, node.Literal)
419	case Softbreak:
420		r.out(w, []byte{'\n'})
421		// TODO: make it configurable via out(renderer.softbreak)
422	case Hardbreak:
423		r.out(w, tag("br", nil, true))
424		r.cr(w)
425	case Emph:
426		if entering {
427			r.out(w, tag("em", nil, false))
428		} else {
429			r.out(w, tag("/em", nil, false))
430		}
431	case Strong:
432		if entering {
433			r.out(w, tag("strong", nil, false))
434		} else {
435			r.out(w, tag("/strong", nil, false))
436		}
437	case Del:
438		if entering {
439			r.out(w, tag("del", nil, false))
440		} else {
441			r.out(w, tag("/del", nil, false))
442		}
443	case HTMLSpan:
444		if r.Flags&SkipHTML != 0 {
445			break
446		}
447		if r.Flags&SkipStyle != 0 && isHTMLTag(node.Literal, "style") {
448			break
449		}
450		//if options.safe {
451		//	out(w, "<!-- raw HTML omitted -->")
452		//} else {
453		r.out(w, node.Literal)
454		//}
455	case Link:
456		// mark it but don't link it if it is not a safe link: no smartypants
457		dest := node.LinkData.Destination
458		if needSkipLink(r.Flags, dest) {
459			if entering {
460				r.out(w, tag("tt", nil, false))
461			} else {
462				r.out(w, tag("/tt", nil, false))
463			}
464		} else {
465			if entering {
466				dest = r.addAbsPrefix(dest)
467				//if (!(options.safe && potentiallyUnsafe(node.destination))) {
468				attrs = append(attrs, fmt.Sprintf("href=%q", esc(dest)))
469				//}
470				if node.NoteID != 0 {
471					r.out(w, footnoteRef(r.FootnoteAnchorPrefix, node))
472					break
473				}
474				attrs = appendLinkAttrs(attrs, r.Flags, dest)
475				if len(node.LinkData.Title) > 0 {
476					attrs = append(attrs, fmt.Sprintf("title=%q", esc(node.LinkData.Title)))
477				}
478				r.out(w, tag("a", attrs, false))
479			} else {
480				if node.NoteID != 0 {
481					break
482				}
483				r.out(w, tag("/a", nil, false))
484			}
485		}
486	case Image:
487		if r.Flags&SkipImages != 0 {
488			return SkipChildren
489		}
490		if entering {
491			dest := node.LinkData.Destination
492			dest = r.addAbsPrefix(dest)
493			if r.disableTags == 0 {
494				//if options.safe && potentiallyUnsafe(dest) {
495				//out(w, `<img src="" alt="`)
496				//} else {
497				r.out(w, []byte(fmt.Sprintf(`<img src="%s" alt="`, esc(dest))))
498				//}
499			}
500			r.disableTags++
501		} else {
502			r.disableTags--
503			if r.disableTags == 0 {
504				if node.LinkData.Title != nil {
505					r.out(w, []byte(`" title="`))
506					r.out(w, esc(node.LinkData.Title))
507				}
508				r.out(w, []byte(`" />`))
509			}
510		}
511	case Code:
512		r.out(w, tag("code", nil, false))
513		r.out(w, escCode(node.Literal))
514		r.out(w, tag("/code", nil, false))
515	case Document:
516		break
517	case Paragraph:
518		if skipParagraphTags(node) {
519			break
520		}
521		if entering {
522			// TODO: untangle this clusterfuck about when the newlines need
523			// to be added and when not.
524			if node.Prev != nil {
525				switch node.Prev.Type {
526				case HTMLBlock, List, Paragraph, Header, CodeBlock, BlockQuote, HorizontalRule:
527					r.cr(w)
528				}
529			}
530			if node.Parent.Type == BlockQuote && node.Prev == nil {
531				r.cr(w)
532			}
533			r.out(w, tag("p", attrs, false))
534		} else {
535			r.out(w, tag("/p", attrs, false))
536			if !(node.Parent.Type == Item && node.Next == nil) {
537				r.cr(w)
538			}
539		}
540	case BlockQuote:
541		if entering {
542			r.cr(w)
543			r.out(w, tag("blockquote", attrs, false))
544		} else {
545			r.out(w, tag("/blockquote", nil, false))
546			r.cr(w)
547		}
548	case HTMLBlock:
549		if r.Flags&SkipHTML != 0 {
550			break
551		}
552		r.cr(w)
553		r.out(w, node.Literal)
554		r.cr(w)
555	case Header:
556		tagname := fmt.Sprintf("h%d", node.Level)
557		if entering {
558			if node.IsTitleblock {
559				attrs = append(attrs, `class="title"`)
560			}
561			if node.HeaderID != "" {
562				id := r.ensureUniqueHeaderID(node.HeaderID)
563				if r.HeaderIDPrefix != "" {
564					id = r.HeaderIDPrefix + id
565				}
566				if r.HeaderIDSuffix != "" {
567					id = id + r.HeaderIDSuffix
568				}
569				attrs = append(attrs, fmt.Sprintf(`id="%s"`, id))
570			}
571			r.cr(w)
572			r.out(w, tag(tagname, attrs, false))
573		} else {
574			r.out(w, tag("/"+tagname, nil, false))
575			if !(node.Parent.Type == Item && node.Next == nil) {
576				r.cr(w)
577			}
578		}
579	case HorizontalRule:
580		r.cr(w)
581		r.out(w, tag("hr", attrs, r.Flags&UseXHTML != 0))
582		r.cr(w)
583	case List:
584		tagName := "ul"
585		if node.ListFlags&ListTypeOrdered != 0 {
586			tagName = "ol"
587		}
588		if node.ListFlags&ListTypeDefinition != 0 {
589			tagName = "dl"
590		}
591		if entering {
592			if node.IsFootnotesList {
593				r.out(w, []byte("\n<div class=\"footnotes\">\n\n"))
594				r.out(w, tag("hr", attrs, r.Flags&UseXHTML != 0))
595				r.cr(w)
596			}
597			r.cr(w)
598			if node.Parent.Type == Item && node.Parent.Parent.Tight {
599				r.cr(w)
600			}
601			r.out(w, tag(tagName, attrs, false))
602			r.cr(w)
603		} else {
604			r.out(w, tag("/"+tagName, nil, false))
605			//cr(w)
606			//if node.parent.Type != Item {
607			//	cr(w)
608			//}
609			if node.Parent.Type == Item && node.Next != nil {
610				r.cr(w)
611			}
612			if node.Parent.Type == Document || node.Parent.Type == BlockQuote {
613				r.cr(w)
614			}
615			if node.IsFootnotesList {
616				r.out(w, []byte("\n</div>\n"))
617			}
618		}
619	case Item:
620		tagName := "li"
621		if node.ListFlags&ListTypeDefinition != 0 {
622			tagName = "dd"
623		}
624		if node.ListFlags&ListTypeTerm != 0 {
625			tagName = "dt"
626		}
627		if entering {
628			if itemOpenCR(node) {
629				r.cr(w)
630			}
631			if node.ListData.RefLink != nil {
632				slug := slugify(node.ListData.RefLink)
633				r.out(w, footnoteItem(r.FootnoteAnchorPrefix, slug))
634				break
635			}
636			r.out(w, tag(tagName, nil, false))
637		} else {
638			if node.ListData.RefLink != nil {
639				slug := slugify(node.ListData.RefLink)
640				if r.Flags&FootnoteReturnLinks != 0 {
641					r.out(w, footnoteReturnLink(r.FootnoteAnchorPrefix, r.FootnoteReturnLinkContents, slug))
642				}
643			}
644			r.out(w, tag("/"+tagName, nil, false))
645			r.cr(w)
646		}
647	case CodeBlock:
648		attrs = appendLanguageAttr(attrs, node.Info)
649		r.cr(w)
650		r.out(w, tag("pre", nil, false))
651		r.out(w, tag("code", attrs, false))
652		r.out(w, escCode(node.Literal))
653		r.out(w, tag("/code", nil, false))
654		r.out(w, tag("/pre", nil, false))
655		if node.Parent.Type != Item {
656			r.cr(w)
657		}
658	case Table:
659		if entering {
660			r.cr(w)
661			r.out(w, tag("table", nil, false))
662		} else {
663			r.out(w, tag("/table", nil, false))
664			r.cr(w)
665		}
666	case TableCell:
667		tagName := "td"
668		if node.IsHeader {
669			tagName = "th"
670		}
671		if entering {
672			align := cellAlignment(node.Align)
673			if align != "" {
674				attrs = append(attrs, fmt.Sprintf(`align="%s"`, align))
675			}
676			if node.Prev == nil {
677				r.cr(w)
678			}
679			r.out(w, tag(tagName, attrs, false))
680		} else {
681			r.out(w, tag("/"+tagName, nil, false))
682			r.cr(w)
683		}
684	case TableHead:
685		if entering {
686			r.cr(w)
687			r.out(w, tag("thead", nil, false))
688		} else {
689			r.out(w, tag("/thead", nil, false))
690			r.cr(w)
691		}
692	case TableBody:
693		if entering {
694			r.cr(w)
695			r.out(w, tag("tbody", nil, false))
696			// XXX: this is to adhere to a rather silly test. Should fix test.
697			if node.FirstChild == nil {
698				r.cr(w)
699			}
700		} else {
701			r.out(w, tag("/tbody", nil, false))
702			r.cr(w)
703		}
704	case TableRow:
705		if entering {
706			r.cr(w)
707			r.out(w, tag("tr", nil, false))
708		} else {
709			r.out(w, tag("/tr", nil, false))
710			r.cr(w)
711		}
712	default:
713		panic("Unknown node type " + node.Type.String())
714	}
715	return GoToNext
716}
717
718func (r *HTMLRenderer) writeDocumentHeader(w *bytes.Buffer) {
719	if r.Flags&CompletePage == 0 {
720		return
721	}
722	ending := ""
723	if r.Flags&UseXHTML != 0 {
724		w.WriteString("<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" ")
725		w.WriteString("\"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">\n")
726		w.WriteString("<html xmlns=\"http://www.w3.org/1999/xhtml\">\n")
727		ending = " /"
728	} else {
729		w.WriteString("<!DOCTYPE html>\n")
730		w.WriteString("<html>\n")
731	}
732	w.WriteString("<head>\n")
733	w.WriteString("  <title>")
734	if r.Flags&Smartypants != 0 {
735		w.Write(r.sr.Process([]byte(r.Title)))
736	} else {
737		w.Write(esc([]byte(r.Title)))
738	}
739	w.WriteString("</title>\n")
740	w.WriteString("  <meta name=\"GENERATOR\" content=\"Blackfriday Markdown Processor v")
741	w.WriteString(Version)
742	w.WriteString("\"")
743	w.WriteString(ending)
744	w.WriteString(">\n")
745	w.WriteString("  <meta charset=\"utf-8\"")
746	w.WriteString(ending)
747	w.WriteString(">\n")
748	if r.CSS != "" {
749		w.WriteString("  <link rel=\"stylesheet\" type=\"text/css\" href=\"")
750		w.Write(esc([]byte(r.CSS)))
751		w.WriteString("\"")
752		w.WriteString(ending)
753		w.WriteString(">\n")
754	}
755	if r.Icon != "" {
756		w.WriteString("  <link rel=\"icon\" type=\"image/x-icon\" href=\"")
757		w.Write(esc([]byte(r.Icon)))
758		w.WriteString("\"")
759		w.WriteString(ending)
760		w.WriteString(">\n")
761	}
762	w.WriteString("</head>\n")
763	w.WriteString("<body>\n\n")
764}
765
766func (r *HTMLRenderer) writeTOC(w *bytes.Buffer, ast *Node) {
767	buf := bytes.Buffer{}
768
769	inHeader := false
770	tocLevel := 0
771	headerCount := 0
772
773	ast.Walk(func(node *Node, entering bool) WalkStatus {
774		if node.Type == Header && !node.HeaderData.IsTitleblock {
775			inHeader = entering
776			if entering {
777				node.HeaderID = fmt.Sprintf("toc_%d", headerCount)
778				if node.Level == tocLevel {
779					buf.WriteString("</li>\n\n<li>")
780				} else if node.Level < tocLevel {
781					for node.Level < tocLevel {
782						tocLevel--
783						buf.WriteString("</li>\n</ul>")
784					}
785					buf.WriteString("</li>\n\n<li>")
786				} else {
787					for node.Level > tocLevel {
788						tocLevel++
789						buf.WriteString("\n<ul>\n<li>")
790					}
791				}
792
793				fmt.Fprintf(&buf, `<a href="#toc_%d">`, headerCount)
794				headerCount++
795			} else {
796				buf.WriteString("</a>")
797			}
798			return GoToNext
799		}
800
801		if inHeader {
802			return r.RenderNode(&buf, node, entering)
803		}
804
805		return GoToNext
806	})
807
808	for ; tocLevel > 0; tocLevel-- {
809		buf.WriteString("</li>\n</ul>")
810	}
811
812	if buf.Len() > 0 {
813		w.WriteString("<nav>\n")
814		w.Write(buf.Bytes())
815		w.WriteString("\n\n</nav>\n")
816	}
817}
818
819func (r *HTMLRenderer) writeDocumentFooter(w *bytes.Buffer) {
820	if r.Flags&CompletePage == 0 {
821		return
822	}
823	w.WriteString("\n</body>\n</html>\n")
824}
825
826// Render walks the specified syntax (sub)tree and returns a HTML document.
827func (r *HTMLRenderer) Render(ast *Node) []byte {
828	//println("render_Blackfriday")
829	//dump(ast)
830	var buff bytes.Buffer
831	r.writeDocumentHeader(&buff)
832	if r.Extensions&TOC != 0 || r.Extensions&OmitContents != 0 {
833		r.writeTOC(&buff, ast)
834		if r.Extensions&OmitContents != 0 {
835			return buff.Bytes()
836		}
837	}
838	ast.Walk(func(node *Node, entering bool) WalkStatus {
839		return r.RenderNode(&buff, node, entering)
840	})
841	r.writeDocumentFooter(&buff)
842	return buff.Bytes()
843}