all repos — grayfriday @ f90a576a05b8a97dd6e2936e613eadb48c57112a

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