all repos — grayfriday @ 413328d30b1d3038f1ef0c1794003997639907d2

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