all repos — grayfriday @ 3d1baecb3d2a8f7db23de78dfd69d2408a6494ac

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	SkipImages                                    // Skip embedded images
 35	SkipLinks                                     // Skip all links
 36	Safelink                                      // Only link to trusted protocols
 37	NofollowLinks                                 // Only link with rel="nofollow"
 38	NoreferrerLinks                               // Only link with rel="noreferrer"
 39	HrefTargetBlank                               // Add a blank target
 40	CompletePage                                  // Generate a complete HTML page
 41	UseXHTML                                      // Generate XHTML output instead of HTML
 42	FootnoteReturnLinks                           // Generate a link at the end of a footnote to return to the source
 43	Smartypants                                   // Enable smart punctuation substitutions
 44	SmartypantsFractions                          // Enable smart fractions (with Smartypants)
 45	SmartypantsDashes                             // Enable smart dashes (with Smartypants)
 46	SmartypantsLatexDashes                        // Enable LaTeX-style dashes (with Smartypants)
 47	SmartypantsAngledQuotes                       // Enable angled double quotes (with Smartypants) for double quotes rendering
 48
 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 Header ID, to ensure
 83	// uniqueness.
 84	HeaderIDPrefix string
 85	// If set, add this text to the back of each Header ID, to ensure uniqueness.
 86	HeaderIDSuffix 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	Extensions Extensions // Extensions give Smartypants and HTML renderer access to Blackfriday's global extensions
 94}
 95
 96// HTMLRenderer is a type that implements the Renderer interface for HTML output.
 97//
 98// Do not create this directly, instead use the NewHTMLRenderer function.
 99type HTMLRenderer struct {
100	HTMLRendererParameters
101
102	closeTag string // how to end singleton tags: either " />" or ">"
103
104	// Track header IDs to prevent ID collision in a single generation.
105	headerIDs map[string]int
106
107	lastOutputLen int
108	disableTags   int
109
110	sr *SPRenderer
111}
112
113const (
114	xhtmlClose = " />"
115	htmlClose  = ">"
116)
117
118// NewHTMLRenderer creates and configures an HTMLRenderer object, which
119// satisfies the Renderer interface.
120func NewHTMLRenderer(params HTMLRendererParameters) *HTMLRenderer {
121	// configure the rendering engine
122	closeTag := htmlClose
123	if params.Flags&UseXHTML != 0 {
124		closeTag = xhtmlClose
125	}
126
127	if params.FootnoteReturnLinkContents == "" {
128		params.FootnoteReturnLinkContents = `<sup>[return]</sup>`
129	}
130
131	return &HTMLRenderer{
132		HTMLRendererParameters: params,
133
134		closeTag:  closeTag,
135		headerIDs: make(map[string]int),
136
137		sr: NewSmartypantsRenderer(params.Flags),
138	}
139}
140
141func isHTMLTag(tag []byte, tagname string) bool {
142	found, _ := findHTMLTagPos(tag, tagname)
143	return found
144}
145
146// Look for a character, but ignore it when it's in any kind of quotes, it
147// might be JavaScript
148func skipUntilCharIgnoreQuotes(html []byte, start int, char byte) int {
149	inSingleQuote := false
150	inDoubleQuote := false
151	inGraveQuote := false
152	i := start
153	for i < len(html) {
154		switch {
155		case html[i] == char && !inSingleQuote && !inDoubleQuote && !inGraveQuote:
156			return i
157		case html[i] == '\'':
158			inSingleQuote = !inSingleQuote
159		case html[i] == '"':
160			inDoubleQuote = !inDoubleQuote
161		case html[i] == '`':
162			inGraveQuote = !inGraveQuote
163		}
164		i++
165	}
166	return start
167}
168
169func findHTMLTagPos(tag []byte, tagname string) (bool, int) {
170	i := 0
171	if i < len(tag) && tag[0] != '<' {
172		return false, -1
173	}
174	i++
175	i = skipSpace(tag, i)
176
177	if i < len(tag) && tag[i] == '/' {
178		i++
179	}
180
181	i = skipSpace(tag, i)
182	j := 0
183	for ; i < len(tag); i, j = i+1, j+1 {
184		if j >= len(tagname) {
185			break
186		}
187
188		if strings.ToLower(string(tag[i]))[0] != tagname[j] {
189			return false, -1
190		}
191	}
192
193	if i == len(tag) {
194		return false, -1
195	}
196
197	rightAngle := skipUntilCharIgnoreQuotes(tag, i, '>')
198	if rightAngle >= i {
199		return true, rightAngle
200	}
201
202	return false, -1
203}
204
205func skipSpace(tag []byte, i int) int {
206	for i < len(tag) && isspace(tag[i]) {
207		i++
208	}
209	return i
210}
211
212func isRelativeLink(link []byte) (yes bool) {
213	// a tag begin with '#'
214	if link[0] == '#' {
215		return true
216	}
217
218	// link begin with '/' but not '//', the second maybe a protocol relative link
219	if len(link) >= 2 && link[0] == '/' && link[1] != '/' {
220		return true
221	}
222
223	// only the root '/'
224	if len(link) == 1 && link[0] == '/' {
225		return true
226	}
227
228	// current directory : begin with "./"
229	if bytes.HasPrefix(link, []byte("./")) {
230		return true
231	}
232
233	// parent directory : begin with "../"
234	if bytes.HasPrefix(link, []byte("../")) {
235		return true
236	}
237
238	return false
239}
240
241func (r *HTMLRenderer) ensureUniqueHeaderID(id string) string {
242	for count, found := r.headerIDs[id]; found; count, found = r.headerIDs[id] {
243		tmp := fmt.Sprintf("%s-%d", id, count+1)
244
245		if _, tmpFound := r.headerIDs[tmp]; !tmpFound {
246			r.headerIDs[id] = count + 1
247			id = tmp
248		} else {
249			id = id + "-1"
250		}
251	}
252
253	if _, found := r.headerIDs[id]; !found {
254		r.headerIDs[id] = 0
255	}
256
257	return id
258}
259
260func (r *HTMLRenderer) addAbsPrefix(link []byte) []byte {
261	if r.AbsolutePrefix != "" && isRelativeLink(link) && link[0] != '.' {
262		newDest := r.AbsolutePrefix
263		if link[0] != '/' {
264			newDest += "/"
265		}
266		newDest += string(link)
267		return []byte(newDest)
268	}
269	return link
270}
271
272func appendLinkAttrs(attrs []string, flags HTMLFlags, link []byte) []string {
273	if isRelativeLink(link) {
274		return attrs
275	}
276	val := []string{}
277	if flags&NofollowLinks != 0 {
278		val = append(val, "nofollow")
279	}
280	if flags&NoreferrerLinks != 0 {
281		val = append(val, "noreferrer")
282	}
283	if flags&HrefTargetBlank != 0 {
284		attrs = append(attrs, "target=\"_blank\"")
285	}
286	if len(val) == 0 {
287		return attrs
288	}
289	attr := fmt.Sprintf("rel=%q", strings.Join(val, " "))
290	return append(attrs, attr)
291}
292
293func isMailto(link []byte) bool {
294	return bytes.HasPrefix(link, []byte("mailto:"))
295}
296
297func needSkipLink(flags HTMLFlags, dest []byte) bool {
298	if flags&SkipLinks != 0 {
299		return true
300	}
301	return flags&Safelink != 0 && !isSafeLink(dest) && !isMailto(dest)
302}
303
304func isSmartypantable(node *Node) bool {
305	pt := node.Parent.Type
306	return pt != Link && pt != CodeBlock && pt != Code
307}
308
309func appendLanguageAttr(attrs []string, info []byte) []string {
310	infoWords := bytes.Split(info, []byte("\t "))
311	if len(infoWords) > 0 && len(infoWords[0]) > 0 {
312		attrs = append(attrs, fmt.Sprintf("class=\"language-%s\"", infoWords[0]))
313	}
314	return attrs
315}
316
317func tag(name string, attrs []string, selfClosing bool) []byte {
318	result := "<" + name
319	if attrs != nil && len(attrs) > 0 {
320		result += " " + strings.Join(attrs, " ")
321	}
322	if selfClosing {
323		result += " /"
324	}
325	return []byte(result + ">")
326}
327
328func footnoteRef(prefix string, node *Node) []byte {
329	urlFrag := prefix + string(slugify(node.Destination))
330	anchor := fmt.Sprintf(`<a rel="footnote" href="#fn:%s">%d</a>`, urlFrag, node.NoteID)
331	return []byte(fmt.Sprintf(`<sup class="footnote-ref" id="fnref:%s">%s</sup>`, urlFrag, anchor))
332}
333
334func footnoteItem(prefix string, slug []byte) []byte {
335	return []byte(fmt.Sprintf(`<li id="fn:%s%s">`, prefix, slug))
336}
337
338func footnoteReturnLink(prefix, returnLink string, slug []byte) []byte {
339	const format = ` <a class="footnote-return" href="#fnref:%s%s">%s</a>`
340	return []byte(fmt.Sprintf(format, prefix, slug, returnLink))
341}
342
343func itemOpenCR(node *Node) bool {
344	if node.Prev == nil {
345		return false
346	}
347	ld := node.Parent.ListData
348	return !ld.Tight && ld.ListFlags&ListTypeDefinition == 0
349}
350
351func skipParagraphTags(node *Node) bool {
352	grandparent := node.Parent.Parent
353	if grandparent == nil || grandparent.Type != List {
354		return false
355	}
356	tightOrTerm := grandparent.Tight || node.Parent.ListFlags&ListTypeTerm != 0
357	return grandparent.Type == List && tightOrTerm
358}
359
360func cellAlignment(align CellAlignFlags) string {
361	switch align {
362	case TableAlignmentLeft:
363		return "left"
364	case TableAlignmentRight:
365		return "right"
366	case TableAlignmentCenter:
367		return "center"
368	default:
369		return ""
370	}
371}
372
373func esc(text []byte) []byte {
374	unesc := []byte(html.UnescapeString(string(text)))
375	return escCode(unesc)
376}
377
378func escCode(text []byte) []byte {
379	e1 := []byte(html.EscapeString(string(text)))
380	e2 := bytes.Replace(e1, []byte("&#34;"), []byte("&quot;"), -1)
381	return bytes.Replace(e2, []byte("&#39;"), []byte{'\''}, -1)
382}
383
384func (r *HTMLRenderer) out(w io.Writer, text []byte) {
385	if r.disableTags > 0 {
386		w.Write(htmlTagRe.ReplaceAll(text, []byte{}))
387	} else {
388		w.Write(text)
389	}
390	r.lastOutputLen = len(text)
391}
392
393func (r *HTMLRenderer) cr(w io.Writer) {
394	if r.lastOutputLen > 0 {
395		r.out(w, []byte{'\n'})
396	}
397}
398
399// RenderNode is a default renderer of a single node of a syntax tree. For
400// block nodes it will be called twice: first time with entering=true, second
401// time with entering=false, so that it could know when it's working on an open
402// tag and when on close. It writes the result to w.
403//
404// The return value is a way to tell the calling walker to adjust its walk
405// pattern: e.g. it can terminate the traversal by returning Terminate. Or it
406// can ask the walker to skip a subtree of this node by returning SkipChildren.
407// The typical behavior is to return GoToNext, which asks for the usual
408// traversal to the next node.
409func (r *HTMLRenderer) RenderNode(w io.Writer, node *Node, entering bool) WalkStatus {
410	attrs := []string{}
411	switch node.Type {
412	case Text:
413		node.Literal = esc(node.Literal)
414		if r.Flags&Smartypants != 0 {
415			node.Literal = r.sr.Process(node.Literal)
416		}
417		r.out(w, node.Literal)
418	case Softbreak:
419		r.out(w, []byte{'\n'})
420		// TODO: make it configurable via out(renderer.softbreak)
421	case Hardbreak:
422		r.out(w, tag("br", nil, true))
423		r.cr(w)
424	case Emph:
425		if entering {
426			r.out(w, tag("em", nil, false))
427		} else {
428			r.out(w, tag("/em", nil, false))
429		}
430	case Strong:
431		if entering {
432			r.out(w, tag("strong", nil, false))
433		} else {
434			r.out(w, tag("/strong", nil, false))
435		}
436	case Del:
437		if entering {
438			r.out(w, tag("del", nil, false))
439		} else {
440			r.out(w, tag("/del", nil, false))
441		}
442	case HTMLSpan:
443		if r.Flags&SkipHTML != 0 {
444			break
445		}
446		r.out(w, node.Literal)
447	case Link:
448		// mark it but don't link it if it is not a safe link: no smartypants
449		dest := node.LinkData.Destination
450		if needSkipLink(r.Flags, dest) {
451			if entering {
452				r.out(w, tag("tt", nil, false))
453			} else {
454				r.out(w, tag("/tt", nil, false))
455			}
456		} else {
457			if entering {
458				dest = r.addAbsPrefix(dest)
459				//if (!(options.safe && potentiallyUnsafe(node.destination))) {
460				attrs = append(attrs, fmt.Sprintf("href=%q", esc(dest)))
461				//}
462				if node.NoteID != 0 {
463					r.out(w, footnoteRef(r.FootnoteAnchorPrefix, node))
464					break
465				}
466				attrs = appendLinkAttrs(attrs, r.Flags, dest)
467				if len(node.LinkData.Title) > 0 {
468					attrs = append(attrs, fmt.Sprintf("title=%q", esc(node.LinkData.Title)))
469				}
470				r.out(w, tag("a", attrs, false))
471			} else {
472				if node.NoteID != 0 {
473					break
474				}
475				r.out(w, tag("/a", nil, false))
476			}
477		}
478	case Image:
479		if r.Flags&SkipImages != 0 {
480			return SkipChildren
481		}
482		if entering {
483			dest := node.LinkData.Destination
484			dest = r.addAbsPrefix(dest)
485			if r.disableTags == 0 {
486				//if options.safe && potentiallyUnsafe(dest) {
487				//out(w, `<img src="" alt="`)
488				//} else {
489				r.out(w, []byte(fmt.Sprintf(`<img src="%s" alt="`, esc(dest))))
490				//}
491			}
492			r.disableTags++
493		} else {
494			r.disableTags--
495			if r.disableTags == 0 {
496				if node.LinkData.Title != nil {
497					r.out(w, []byte(`" title="`))
498					r.out(w, esc(node.LinkData.Title))
499				}
500				r.out(w, []byte(`" />`))
501			}
502		}
503	case Code:
504		r.out(w, tag("code", nil, false))
505		r.out(w, escCode(node.Literal))
506		r.out(w, tag("/code", nil, false))
507	case Document:
508		break
509	case Paragraph:
510		if skipParagraphTags(node) {
511			break
512		}
513		if entering {
514			// TODO: untangle this clusterfuck about when the newlines need
515			// to be added and when not.
516			if node.Prev != nil {
517				switch node.Prev.Type {
518				case HTMLBlock, List, Paragraph, Header, CodeBlock, BlockQuote, HorizontalRule:
519					r.cr(w)
520				}
521			}
522			if node.Parent.Type == BlockQuote && node.Prev == nil {
523				r.cr(w)
524			}
525			r.out(w, tag("p", attrs, false))
526		} else {
527			r.out(w, tag("/p", attrs, false))
528			if !(node.Parent.Type == Item && node.Next == nil) {
529				r.cr(w)
530			}
531		}
532	case BlockQuote:
533		if entering {
534			r.cr(w)
535			r.out(w, tag("blockquote", attrs, false))
536		} else {
537			r.out(w, tag("/blockquote", nil, false))
538			r.cr(w)
539		}
540	case HTMLBlock:
541		if r.Flags&SkipHTML != 0 {
542			break
543		}
544		r.cr(w)
545		r.out(w, node.Literal)
546		r.cr(w)
547	case Header:
548		tagname := fmt.Sprintf("h%d", node.Level)
549		if entering {
550			if node.IsTitleblock {
551				attrs = append(attrs, `class="title"`)
552			}
553			if node.HeaderID != "" {
554				id := r.ensureUniqueHeaderID(node.HeaderID)
555				if r.HeaderIDPrefix != "" {
556					id = r.HeaderIDPrefix + id
557				}
558				if r.HeaderIDSuffix != "" {
559					id = id + r.HeaderIDSuffix
560				}
561				attrs = append(attrs, fmt.Sprintf(`id="%s"`, id))
562			}
563			r.cr(w)
564			r.out(w, tag(tagname, attrs, false))
565		} else {
566			r.out(w, tag("/"+tagname, nil, false))
567			if !(node.Parent.Type == Item && node.Next == nil) {
568				r.cr(w)
569			}
570		}
571	case HorizontalRule:
572		r.cr(w)
573		r.out(w, tag("hr", attrs, r.Flags&UseXHTML != 0))
574		r.cr(w)
575	case List:
576		tagName := "ul"
577		if node.ListFlags&ListTypeOrdered != 0 {
578			tagName = "ol"
579		}
580		if node.ListFlags&ListTypeDefinition != 0 {
581			tagName = "dl"
582		}
583		if entering {
584			if node.IsFootnotesList {
585				r.out(w, []byte("\n<div class=\"footnotes\">\n\n"))
586				r.out(w, tag("hr", attrs, r.Flags&UseXHTML != 0))
587				r.cr(w)
588			}
589			r.cr(w)
590			if node.Parent.Type == Item && node.Parent.Parent.Tight {
591				r.cr(w)
592			}
593			r.out(w, tag(tagName, attrs, false))
594			r.cr(w)
595		} else {
596			r.out(w, tag("/"+tagName, nil, false))
597			//cr(w)
598			//if node.parent.Type != Item {
599			//	cr(w)
600			//}
601			if node.Parent.Type == Item && node.Next != nil {
602				r.cr(w)
603			}
604			if node.Parent.Type == Document || node.Parent.Type == BlockQuote {
605				r.cr(w)
606			}
607			if node.IsFootnotesList {
608				r.out(w, []byte("\n</div>\n"))
609			}
610		}
611	case Item:
612		tagName := "li"
613		if node.ListFlags&ListTypeDefinition != 0 {
614			tagName = "dd"
615		}
616		if node.ListFlags&ListTypeTerm != 0 {
617			tagName = "dt"
618		}
619		if entering {
620			if itemOpenCR(node) {
621				r.cr(w)
622			}
623			if node.ListData.RefLink != nil {
624				slug := slugify(node.ListData.RefLink)
625				r.out(w, footnoteItem(r.FootnoteAnchorPrefix, slug))
626				break
627			}
628			r.out(w, tag(tagName, nil, false))
629		} else {
630			if node.ListData.RefLink != nil {
631				slug := slugify(node.ListData.RefLink)
632				if r.Flags&FootnoteReturnLinks != 0 {
633					r.out(w, footnoteReturnLink(r.FootnoteAnchorPrefix, r.FootnoteReturnLinkContents, slug))
634				}
635			}
636			r.out(w, tag("/"+tagName, nil, false))
637			r.cr(w)
638		}
639	case CodeBlock:
640		attrs = appendLanguageAttr(attrs, node.Info)
641		r.cr(w)
642		r.out(w, tag("pre", nil, false))
643		r.out(w, tag("code", attrs, false))
644		r.out(w, escCode(node.Literal))
645		r.out(w, tag("/code", nil, false))
646		r.out(w, tag("/pre", nil, false))
647		if node.Parent.Type != Item {
648			r.cr(w)
649		}
650	case Table:
651		if entering {
652			r.cr(w)
653			r.out(w, tag("table", nil, false))
654		} else {
655			r.out(w, tag("/table", nil, false))
656			r.cr(w)
657		}
658	case TableCell:
659		tagName := "td"
660		if node.IsHeader {
661			tagName = "th"
662		}
663		if entering {
664			align := cellAlignment(node.Align)
665			if align != "" {
666				attrs = append(attrs, fmt.Sprintf(`align="%s"`, align))
667			}
668			if node.Prev == nil {
669				r.cr(w)
670			}
671			r.out(w, tag(tagName, attrs, false))
672		} else {
673			r.out(w, tag("/"+tagName, nil, false))
674			r.cr(w)
675		}
676	case TableHead:
677		if entering {
678			r.cr(w)
679			r.out(w, tag("thead", nil, false))
680		} else {
681			r.out(w, tag("/thead", nil, false))
682			r.cr(w)
683		}
684	case TableBody:
685		if entering {
686			r.cr(w)
687			r.out(w, tag("tbody", nil, false))
688			// XXX: this is to adhere to a rather silly test. Should fix test.
689			if node.FirstChild == nil {
690				r.cr(w)
691			}
692		} else {
693			r.out(w, tag("/tbody", nil, false))
694			r.cr(w)
695		}
696	case TableRow:
697		if entering {
698			r.cr(w)
699			r.out(w, tag("tr", nil, false))
700		} else {
701			r.out(w, tag("/tr", nil, false))
702			r.cr(w)
703		}
704	default:
705		panic("Unknown node type " + node.Type.String())
706	}
707	return GoToNext
708}
709
710func (r *HTMLRenderer) writeDocumentHeader(w *bytes.Buffer) {
711	if r.Flags&CompletePage == 0 {
712		return
713	}
714	ending := ""
715	if r.Flags&UseXHTML != 0 {
716		w.WriteString("<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" ")
717		w.WriteString("\"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">\n")
718		w.WriteString("<html xmlns=\"http://www.w3.org/1999/xhtml\">\n")
719		ending = " /"
720	} else {
721		w.WriteString("<!DOCTYPE html>\n")
722		w.WriteString("<html>\n")
723	}
724	w.WriteString("<head>\n")
725	w.WriteString("  <title>")
726	if r.Flags&Smartypants != 0 {
727		w.Write(r.sr.Process([]byte(r.Title)))
728	} else {
729		w.Write(esc([]byte(r.Title)))
730	}
731	w.WriteString("</title>\n")
732	w.WriteString("  <meta name=\"GENERATOR\" content=\"Blackfriday Markdown Processor v")
733	w.WriteString(Version)
734	w.WriteString("\"")
735	w.WriteString(ending)
736	w.WriteString(">\n")
737	w.WriteString("  <meta charset=\"utf-8\"")
738	w.WriteString(ending)
739	w.WriteString(">\n")
740	if r.CSS != "" {
741		w.WriteString("  <link rel=\"stylesheet\" type=\"text/css\" href=\"")
742		w.Write(esc([]byte(r.CSS)))
743		w.WriteString("\"")
744		w.WriteString(ending)
745		w.WriteString(">\n")
746	}
747	if r.Icon != "" {
748		w.WriteString("  <link rel=\"icon\" type=\"image/x-icon\" href=\"")
749		w.Write(esc([]byte(r.Icon)))
750		w.WriteString("\"")
751		w.WriteString(ending)
752		w.WriteString(">\n")
753	}
754	w.WriteString("</head>\n")
755	w.WriteString("<body>\n\n")
756}
757
758func (r *HTMLRenderer) writeTOC(w *bytes.Buffer, ast *Node) {
759	buf := bytes.Buffer{}
760
761	inHeader := false
762	tocLevel := 0
763	headerCount := 0
764
765	ast.Walk(func(node *Node, entering bool) WalkStatus {
766		if node.Type == Header && !node.HeaderData.IsTitleblock {
767			inHeader = entering
768			if entering {
769				node.HeaderID = fmt.Sprintf("toc_%d", headerCount)
770				if node.Level == tocLevel {
771					buf.WriteString("</li>\n\n<li>")
772				} else if node.Level < tocLevel {
773					for node.Level < tocLevel {
774						tocLevel--
775						buf.WriteString("</li>\n</ul>")
776					}
777					buf.WriteString("</li>\n\n<li>")
778				} else {
779					for node.Level > tocLevel {
780						tocLevel++
781						buf.WriteString("\n<ul>\n<li>")
782					}
783				}
784
785				fmt.Fprintf(&buf, `<a href="#toc_%d">`, headerCount)
786				headerCount++
787			} else {
788				buf.WriteString("</a>")
789			}
790			return GoToNext
791		}
792
793		if inHeader {
794			return r.RenderNode(&buf, node, entering)
795		}
796
797		return GoToNext
798	})
799
800	for ; tocLevel > 0; tocLevel-- {
801		buf.WriteString("</li>\n</ul>")
802	}
803
804	if buf.Len() > 0 {
805		w.WriteString("<nav>\n")
806		w.Write(buf.Bytes())
807		w.WriteString("\n\n</nav>\n")
808	}
809}
810
811func (r *HTMLRenderer) writeDocumentFooter(w *bytes.Buffer) {
812	if r.Flags&CompletePage == 0 {
813		return
814	}
815	w.WriteString("\n</body>\n</html>\n")
816}
817
818// Render walks the specified syntax (sub)tree and returns a HTML document.
819func (r *HTMLRenderer) Render(ast *Node) []byte {
820	//println("render_Blackfriday")
821	//dump(ast)
822	var buff bytes.Buffer
823	r.writeDocumentHeader(&buff)
824	if r.Extensions&TOC != 0 || r.Extensions&OmitContents != 0 {
825		r.writeTOC(&buff, ast)
826		if r.Extensions&OmitContents != 0 {
827			return buff.Bytes()
828		}
829	}
830	ast.Walk(func(node *Node, entering bool) WalkStatus {
831		return r.RenderNode(&buff, node, entering)
832	})
833	r.writeDocumentFooter(&buff)
834	return buff.Bytes()
835}