all repos — grayfriday @ 2560c5f148246ac49c77d97fb1bb963f3ba3e1d4

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