all repos — grayfriday @ c207eca993c2bdbe6130ac017352ea5614eb2bbe

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