all repos — grayfriday @ 76d8c71d704b5109ae86cc2d8d2d5d4714a70917

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