all repos — grayfriday @ a5270b6f564501a64972e7d7fb907e9937102c35

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