renderer.go

   1package html
   2
   3import (
   4	"bytes"
   5	"fmt"
   6	"io"
   7	"regexp"
   8	"sort"
   9	"strconv"
  10	"strings"
  11
  12	"github.com/gomarkdown/markdown/ast"
  13)
  14
  15// Flags control optional behavior of HTML renderer.
  16type Flags int
  17
  18// IDTag is the tag used for tag identification, it defaults to "id", some renderers
  19// may wish to override this and use e.g. "anchor".
  20var IDTag = "id"
  21
  22// HTML renderer configuration options.
  23const (
  24	FlagsNone               Flags = 0
  25	SkipHTML                Flags = 1 << iota // Skip preformatted HTML blocks
  26	SkipImages                                // Skip embedded images
  27	SkipLinks                                 // Skip all links
  28	Safelink                                  // Only link to trusted protocols
  29	NofollowLinks                             // Only link with rel="nofollow"
  30	NoreferrerLinks                           // Only link with rel="noreferrer"
  31	HrefTargetBlank                           // Add a blank target
  32	CompletePage                              // Generate a complete HTML page
  33	UseXHTML                                  // Generate XHTML output instead of HTML
  34	FootnoteReturnLinks                       // Generate a link at the end of a footnote to return to the source
  35	FootnoteNoHRTag                           // Do not output an HR after starting a footnote list.
  36	Smartypants                               // Enable smart punctuation substitutions
  37	SmartypantsFractions                      // Enable smart fractions (with Smartypants)
  38	SmartypantsDashes                         // Enable smart dashes (with Smartypants)
  39	SmartypantsLatexDashes                    // Enable LaTeX-style dashes (with Smartypants)
  40	SmartypantsAngledQuotes                   // Enable angled double quotes (with Smartypants) for double quotes rendering
  41	SmartypantsQuotesNBSP                     // Enable « French guillemets » (with Smartypants)
  42	TOC                                       // Generate a table of contents
  43
  44	CommonFlags Flags = Smartypants | SmartypantsFractions | SmartypantsDashes | SmartypantsLatexDashes
  45)
  46
  47var (
  48	htmlTagRe = regexp.MustCompile("(?i)^" + htmlTag)
  49)
  50
  51const (
  52	htmlTag = "(?:" + openTag + "|" + closeTag + "|" + htmlComment + "|" +
  53		processingInstruction + "|" + declaration + "|" + cdata + ")"
  54	closeTag              = "</" + tagName + "\\s*[>]"
  55	openTag               = "<" + tagName + attribute + "*" + "\\s*/?>"
  56	attribute             = "(?:" + "\\s+" + attributeName + attributeValueSpec + "?)"
  57	attributeValue        = "(?:" + unquotedValue + "|" + singleQuotedValue + "|" + doubleQuotedValue + ")"
  58	attributeValueSpec    = "(?:" + "\\s*=" + "\\s*" + attributeValue + ")"
  59	attributeName         = "[a-zA-Z_:][a-zA-Z0-9:._-]*"
  60	cdata                 = "<!\\[CDATA\\[[\\s\\S]*?\\]\\]>"
  61	declaration           = "<![A-Z]+" + "\\s+[^>]*>"
  62	doubleQuotedValue     = "\"[^\"]*\""
  63	htmlComment           = "<!---->|<!--(?:-?[^>-])(?:-?[^-])*-->"
  64	processingInstruction = "[<][?].*?[?][>]"
  65	singleQuotedValue     = "'[^']*'"
  66	tagName               = "[A-Za-z][A-Za-z0-9-]*"
  67	unquotedValue         = "[^\"'=<>`\\x00-\\x20]+"
  68)
  69
  70// RenderNodeFunc allows reusing most of Renderer logic and replacing
  71// rendering of some nodes. If it returns false, Renderer.RenderNode
  72// will execute its logic. If it returns true, Renderer.RenderNode will
  73// skip rendering this node and will return WalkStatus
  74type RenderNodeFunc func(w io.Writer, node ast.Node, entering bool) (ast.WalkStatus, bool)
  75
  76// RendererOptions is a collection of supplementary parameters tweaking
  77// the behavior of various parts of HTML renderer.
  78type RendererOptions struct {
  79	// Prepend this text to each relative URL.
  80	AbsolutePrefix string
  81	// Add this text to each footnote anchor, to ensure uniqueness.
  82	FootnoteAnchorPrefix string
  83	// Show this text inside the <a> tag for a footnote return link, if the
  84	// FootnoteReturnLinks flag is enabled. If blank, the string
  85	// <sup>[return]</sup> is used.
  86	FootnoteReturnLinkContents string
  87	// CitationFormatString defines how a citation is rendered. If blnck, the string
  88	// <sup>[%s]</sup> is used. Where %s will be substituted with the citation target.
  89	CitationFormatString string
  90	// If set, add this text to the front of each Heading ID, to ensure uniqueness.
  91	HeadingIDPrefix string
  92	// If set, add this text to the back of each Heading ID, to ensure uniqueness.
  93	HeadingIDSuffix string
  94
  95	Title string // Document title (used if CompletePage is set)
  96	CSS   string // Optional CSS file URL (used if CompletePage is set)
  97	Icon  string // Optional icon file URL (used if CompletePage is set)
  98	Head  []byte // Optional head data injected in the <head> section (used if CompletePage is set)
  99
 100	Flags Flags // Flags allow customizing this renderer's behavior
 101
 102	// if set, called at the start of RenderNode(). Allows replacing
 103	// rendering of some nodes
 104	RenderNodeHook RenderNodeFunc
 105
 106	// Comments is a list of comments the renderer should detect when
 107	// parsing code blocks and detecting callouts.
 108	Comments [][]byte
 109
 110	// Generator is a meta tag that is inserted in the generated HTML so show what rendered it. It should not include the closing tag.
 111	// Defaults (note content quote is not closed) to `  <meta name="GENERATOR" content="github.com/gomarkdown/markdown markdown processor for Go`
 112	Generator string
 113}
 114
 115// Renderer implements Renderer interface for HTML output.
 116//
 117// Do not create this directly, instead use the NewRenderer function.
 118type Renderer struct {
 119	opts RendererOptions
 120
 121	closeTag string // how to end singleton tags: either " />" or ">"
 122
 123	// Track heading IDs to prevent ID collision in a single generation.
 124	headingIDs map[string]int
 125
 126	lastOutputLen int
 127	disableTags   int
 128
 129	sr *SPRenderer
 130
 131	documentMatter ast.DocumentMatters // keep track of front/main/back matter.
 132}
 133
 134// NewRenderer creates and configures an Renderer object, which
 135// satisfies the Renderer interface.
 136func NewRenderer(opts RendererOptions) *Renderer {
 137	// configure the rendering engine
 138	closeTag := ">"
 139	if opts.Flags&UseXHTML != 0 {
 140		closeTag = " />"
 141	}
 142
 143	if opts.FootnoteReturnLinkContents == "" {
 144		opts.FootnoteReturnLinkContents = `<sup>[return]</sup>`
 145	}
 146	if opts.CitationFormatString == "" {
 147		opts.CitationFormatString = `<sup>[%s]</sup>`
 148	}
 149	if opts.Generator == "" {
 150		opts.Generator = `  <meta name="GENERATOR" content="github.com/gomarkdown/markdown markdown processor for Go`
 151	}
 152
 153	return &Renderer{
 154		opts: opts,
 155
 156		closeTag:   closeTag,
 157		headingIDs: make(map[string]int),
 158
 159		sr: NewSmartypantsRenderer(opts.Flags),
 160	}
 161}
 162
 163func isHTMLTag(tag []byte, tagname string) bool {
 164	found, _ := findHTMLTagPos(tag, tagname)
 165	return found
 166}
 167
 168// Look for a character, but ignore it when it's in any kind of quotes, it
 169// might be JavaScript
 170func skipUntilCharIgnoreQuotes(html []byte, start int, char byte) int {
 171	inSingleQuote := false
 172	inDoubleQuote := false
 173	inGraveQuote := false
 174	i := start
 175	for i < len(html) {
 176		switch {
 177		case html[i] == char && !inSingleQuote && !inDoubleQuote && !inGraveQuote:
 178			return i
 179		case html[i] == '\'':
 180			inSingleQuote = !inSingleQuote
 181		case html[i] == '"':
 182			inDoubleQuote = !inDoubleQuote
 183		case html[i] == '`':
 184			inGraveQuote = !inGraveQuote
 185		}
 186		i++
 187	}
 188	return start
 189}
 190
 191func findHTMLTagPos(tag []byte, tagname string) (bool, int) {
 192	i := 0
 193	if i < len(tag) && tag[0] != '<' {
 194		return false, -1
 195	}
 196	i++
 197	i = skipSpace(tag, i)
 198
 199	if i < len(tag) && tag[i] == '/' {
 200		i++
 201	}
 202
 203	i = skipSpace(tag, i)
 204	j := 0
 205	for ; i < len(tag); i, j = i+1, j+1 {
 206		if j >= len(tagname) {
 207			break
 208		}
 209
 210		if strings.ToLower(string(tag[i]))[0] != tagname[j] {
 211			return false, -1
 212		}
 213	}
 214
 215	if i == len(tag) {
 216		return false, -1
 217	}
 218
 219	rightAngle := skipUntilCharIgnoreQuotes(tag, i, '>')
 220	if rightAngle >= i {
 221		return true, rightAngle
 222	}
 223
 224	return false, -1
 225}
 226
 227func isRelativeLink(link []byte) (yes bool) {
 228	// a tag begin with '#'
 229	if link[0] == '#' {
 230		return true
 231	}
 232
 233	// link begin with '/' but not '//', the second maybe a protocol relative link
 234	if len(link) >= 2 && link[0] == '/' && link[1] != '/' {
 235		return true
 236	}
 237
 238	// only the root '/'
 239	if len(link) == 1 && link[0] == '/' {
 240		return true
 241	}
 242
 243	// current directory : begin with "./"
 244	if bytes.HasPrefix(link, []byte("./")) {
 245		return true
 246	}
 247
 248	// parent directory : begin with "../"
 249	if bytes.HasPrefix(link, []byte("../")) {
 250		return true
 251	}
 252
 253	return false
 254}
 255
 256func (r *Renderer) ensureUniqueHeadingID(id string) string {
 257	for count, found := r.headingIDs[id]; found; count, found = r.headingIDs[id] {
 258		tmp := fmt.Sprintf("%s-%d", id, count+1)
 259
 260		if _, tmpFound := r.headingIDs[tmp]; !tmpFound {
 261			r.headingIDs[id] = count + 1
 262			id = tmp
 263		} else {
 264			id = id + "-1"
 265		}
 266	}
 267
 268	if _, found := r.headingIDs[id]; !found {
 269		r.headingIDs[id] = 0
 270	}
 271
 272	return id
 273}
 274
 275func (r *Renderer) addAbsPrefix(link []byte) []byte {
 276	if r.opts.AbsolutePrefix != "" && isRelativeLink(link) && link[0] != '.' {
 277		newDest := r.opts.AbsolutePrefix
 278		if link[0] != '/' {
 279			newDest += "/"
 280		}
 281		newDest += string(link)
 282		return []byte(newDest)
 283	}
 284	return link
 285}
 286
 287func appendLinkAttrs(attrs []string, flags Flags, link []byte) []string {
 288	if isRelativeLink(link) {
 289		return attrs
 290	}
 291	var val []string
 292	if flags&NofollowLinks != 0 {
 293		val = append(val, "nofollow")
 294	}
 295	if flags&NoreferrerLinks != 0 {
 296		val = append(val, "noreferrer")
 297	}
 298	if flags&HrefTargetBlank != 0 {
 299		attrs = append(attrs, `target="_blank"`)
 300	}
 301	if len(val) == 0 {
 302		return attrs
 303	}
 304	attr := fmt.Sprintf("rel=%q", strings.Join(val, " "))
 305	return append(attrs, attr)
 306}
 307
 308func isMailto(link []byte) bool {
 309	return bytes.HasPrefix(link, []byte("mailto:"))
 310}
 311
 312func needSkipLink(flags Flags, dest []byte) bool {
 313	if flags&SkipLinks != 0 {
 314		return true
 315	}
 316	return flags&Safelink != 0 && !isSafeLink(dest) && !isMailto(dest)
 317}
 318
 319func isSmartypantable(node ast.Node) bool {
 320	switch node.GetParent().(type) {
 321	case *ast.Link, *ast.CodeBlock, *ast.Code:
 322		return false
 323	}
 324	return true
 325}
 326
 327func appendLanguageAttr(attrs []string, info []byte) []string {
 328	if len(info) == 0 {
 329		return attrs
 330	}
 331	endOfLang := bytes.IndexAny(info, "\t ")
 332	if endOfLang < 0 {
 333		endOfLang = len(info)
 334	}
 335	s := `class="language-` + string(info[:endOfLang]) + `"`
 336	return append(attrs, s)
 337}
 338
 339func (r *Renderer) outTag(w io.Writer, name string, attrs []string) {
 340	s := name
 341	if len(attrs) > 0 {
 342		s += " " + strings.Join(attrs, " ")
 343	}
 344	io.WriteString(w, s+">")
 345	r.lastOutputLen = 1
 346}
 347
 348func footnoteRef(prefix string, node *ast.Link) string {
 349	urlFrag := prefix + string(slugify(node.Destination))
 350	nStr := strconv.Itoa(node.NoteID)
 351	anchor := `<a href="#fn:` + urlFrag + `">` + nStr + `</a>`
 352	return `<sup class="footnote-ref" id="fnref:` + urlFrag + `">` + anchor + `</sup>`
 353}
 354
 355func footnoteItem(prefix string, slug []byte) string {
 356	return `<li id="fn:` + prefix + string(slug) + `">`
 357}
 358
 359func footnoteReturnLink(prefix, returnLink string, slug []byte) string {
 360	return ` <a class="footnote-return" href="#fnref:` + prefix + string(slug) + `">` + returnLink + `</a>`
 361}
 362
 363func listItemOpenCR(listItem *ast.ListItem) bool {
 364	if ast.GetPrevNode(listItem) == nil {
 365		return false
 366	}
 367	ld := listItem.Parent.(*ast.List)
 368	return !ld.Tight && ld.ListFlags&ast.ListTypeDefinition == 0
 369}
 370
 371func skipParagraphTags(para *ast.Paragraph) bool {
 372	parent := para.Parent
 373	grandparent := parent.GetParent()
 374	if grandparent == nil || !isList(grandparent) {
 375		return false
 376	}
 377	isParentTerm := isListItemTerm(parent)
 378	grandparentListData := grandparent.(*ast.List)
 379	tightOrTerm := grandparentListData.Tight || isParentTerm
 380	return tightOrTerm
 381}
 382
 383func (r *Renderer) out(w io.Writer, d []byte) {
 384	r.lastOutputLen = len(d)
 385	if r.disableTags > 0 {
 386		d = htmlTagRe.ReplaceAll(d, []byte{})
 387	}
 388	w.Write(d)
 389}
 390
 391func (r *Renderer) outs(w io.Writer, s string) {
 392	r.lastOutputLen = len(s)
 393	if r.disableTags > 0 {
 394		s = htmlTagRe.ReplaceAllString(s, "")
 395	}
 396	io.WriteString(w, s)
 397}
 398
 399func (r *Renderer) cr(w io.Writer) {
 400	if r.lastOutputLen > 0 {
 401		r.outs(w, "\n")
 402	}
 403}
 404
 405var (
 406	openHTags  = []string{"<h1", "<h2", "<h3", "<h4", "<h5"}
 407	closeHTags = []string{"</h1>", "</h2>", "</h3>", "</h4>", "</h5>"}
 408)
 409
 410func headingOpenTagFromLevel(level int) string {
 411	if level < 1 || level > 5 {
 412		return "<h6"
 413	}
 414	return openHTags[level-1]
 415}
 416
 417func headingCloseTagFromLevel(level int) string {
 418	if level < 1 || level > 5 {
 419		return "</h6>"
 420	}
 421	return closeHTags[level-1]
 422}
 423
 424func (r *Renderer) outHRTag(w io.Writer, attrs []string) {
 425	hr := tagWithAttributes("<hr", attrs)
 426	r.outOneOf(w, r.opts.Flags&UseXHTML == 0, hr, "<hr />")
 427}
 428
 429func (r *Renderer) text(w io.Writer, text *ast.Text) {
 430	if r.opts.Flags&Smartypants != 0 {
 431		var tmp bytes.Buffer
 432		EscapeHTML(&tmp, text.Literal)
 433		r.sr.Process(w, tmp.Bytes())
 434	} else {
 435		_, parentIsLink := text.Parent.(*ast.Link)
 436		if parentIsLink {
 437			escLink(w, text.Literal)
 438		} else {
 439			EscapeHTML(w, text.Literal)
 440		}
 441	}
 442}
 443
 444func (r *Renderer) hardBreak(w io.Writer, node *ast.Hardbreak) {
 445	r.outOneOf(w, r.opts.Flags&UseXHTML == 0, "<br>", "<br />")
 446	r.cr(w)
 447}
 448
 449func (r *Renderer) nonBlockingSpace(w io.Writer, node *ast.NonBlockingSpace) {
 450	r.outs(w, "&nbsp;")
 451}
 452
 453func (r *Renderer) outOneOf(w io.Writer, outFirst bool, first string, second string) {
 454	if outFirst {
 455		r.outs(w, first)
 456	} else {
 457		r.outs(w, second)
 458	}
 459}
 460
 461func (r *Renderer) outOneOfCr(w io.Writer, outFirst bool, first string, second string) {
 462	if outFirst {
 463		r.cr(w)
 464		r.outs(w, first)
 465	} else {
 466		r.outs(w, second)
 467		r.cr(w)
 468	}
 469}
 470
 471func (r *Renderer) htmlSpan(w io.Writer, span *ast.HTMLSpan) {
 472	if r.opts.Flags&SkipHTML == 0 {
 473		r.out(w, span.Literal)
 474	}
 475}
 476
 477func (r *Renderer) linkEnter(w io.Writer, link *ast.Link) {
 478	var attrs []string
 479	dest := link.Destination
 480	dest = r.addAbsPrefix(dest)
 481	var hrefBuf bytes.Buffer
 482	hrefBuf.WriteString("href=\"")
 483	escLink(&hrefBuf, dest)
 484	hrefBuf.WriteByte('"')
 485	attrs = append(attrs, hrefBuf.String())
 486	if link.NoteID != 0 {
 487		r.outs(w, footnoteRef(r.opts.FootnoteAnchorPrefix, link))
 488		return
 489	}
 490
 491	attrs = appendLinkAttrs(attrs, r.opts.Flags, dest)
 492	if len(link.Title) > 0 {
 493		var titleBuff bytes.Buffer
 494		titleBuff.WriteString("title=\"")
 495		EscapeHTML(&titleBuff, link.Title)
 496		titleBuff.WriteByte('"')
 497		attrs = append(attrs, titleBuff.String())
 498	}
 499	r.outTag(w, "<a", attrs)
 500}
 501
 502func (r *Renderer) linkExit(w io.Writer, link *ast.Link) {
 503	if link.NoteID == 0 {
 504		r.outs(w, "</a>")
 505	}
 506}
 507
 508func (r *Renderer) link(w io.Writer, link *ast.Link, entering bool) {
 509	// mark it but don't link it if it is not a safe link: no smartypants
 510	if needSkipLink(r.opts.Flags, link.Destination) {
 511		r.outOneOf(w, entering, "<tt>", "</tt>")
 512		return
 513	}
 514
 515	if entering {
 516		r.linkEnter(w, link)
 517	} else {
 518		r.linkExit(w, link)
 519	}
 520}
 521
 522func (r *Renderer) imageEnter(w io.Writer, image *ast.Image) {
 523	dest := image.Destination
 524	dest = r.addAbsPrefix(dest)
 525	if r.disableTags == 0 {
 526		//if options.safe && potentiallyUnsafe(dest) {
 527		//out(w, `<img src="" alt="`)
 528		//} else {
 529		r.outs(w, `<img src="`)
 530		escLink(w, dest)
 531		r.outs(w, `" alt="`)
 532		//}
 533	}
 534	r.disableTags++
 535}
 536
 537func (r *Renderer) imageExit(w io.Writer, image *ast.Image) {
 538	r.disableTags--
 539	if r.disableTags == 0 {
 540		if image.Title != nil {
 541			r.outs(w, `" title="`)
 542			EscapeHTML(w, image.Title)
 543		}
 544		r.outs(w, `" />`)
 545	}
 546}
 547
 548func (r *Renderer) paragraphEnter(w io.Writer, para *ast.Paragraph) {
 549	// TODO: untangle this clusterfuck about when the newlines need
 550	// to be added and when not.
 551	prev := ast.GetPrevNode(para)
 552	if prev != nil {
 553		switch prev.(type) {
 554		case *ast.HTMLBlock, *ast.List, *ast.Paragraph, *ast.Heading, *ast.CaptionFigure, *ast.CodeBlock, *ast.BlockQuote, *ast.Aside, *ast.HorizontalRule:
 555			r.cr(w)
 556		}
 557	}
 558
 559	if prev == nil {
 560		_, isParentBlockQuote := para.Parent.(*ast.BlockQuote)
 561		if isParentBlockQuote {
 562			r.cr(w)
 563		}
 564		_, isParentAside := para.Parent.(*ast.Aside)
 565		if isParentAside {
 566			r.cr(w)
 567		}
 568	}
 569
 570	tag := tagWithAttributes("<p", BlockAttrs(para))
 571	r.outs(w, tag)
 572}
 573
 574func (r *Renderer) paragraphExit(w io.Writer, para *ast.Paragraph) {
 575	r.outs(w, "</p>")
 576	if !(isListItem(para.Parent) && ast.GetNextNode(para) == nil) {
 577		r.cr(w)
 578	}
 579}
 580
 581func (r *Renderer) paragraph(w io.Writer, para *ast.Paragraph, entering bool) {
 582	if skipParagraphTags(para) {
 583		return
 584	}
 585	if entering {
 586		r.paragraphEnter(w, para)
 587	} else {
 588		r.paragraphExit(w, para)
 589	}
 590}
 591func (r *Renderer) image(w io.Writer, node *ast.Image, entering bool) {
 592	if entering {
 593		r.imageEnter(w, node)
 594	} else {
 595		r.imageExit(w, node)
 596	}
 597}
 598
 599func (r *Renderer) code(w io.Writer, node *ast.Code) {
 600	r.outs(w, "<code>")
 601	EscapeHTML(w, node.Literal)
 602	r.outs(w, "</code>")
 603}
 604
 605func (r *Renderer) htmlBlock(w io.Writer, node *ast.HTMLBlock) {
 606	if r.opts.Flags&SkipHTML != 0 {
 607		return
 608	}
 609	r.cr(w)
 610	r.out(w, node.Literal)
 611	r.cr(w)
 612}
 613
 614func (r *Renderer) headingEnter(w io.Writer, nodeData *ast.Heading) {
 615	var attrs []string
 616	var class string
 617	// TODO(miek): add helper functions for coalescing these classes.
 618	if nodeData.IsTitleblock {
 619		class = "title"
 620	}
 621	if nodeData.IsSpecial {
 622		if class != "" {
 623			class += " special"
 624		} else {
 625			class = "special"
 626		}
 627	}
 628	if class != "" {
 629		attrs = []string{`class="` + class + `"`}
 630	}
 631	if nodeData.HeadingID != "" {
 632		id := r.ensureUniqueHeadingID(nodeData.HeadingID)
 633		if r.opts.HeadingIDPrefix != "" {
 634			id = r.opts.HeadingIDPrefix + id
 635		}
 636		if r.opts.HeadingIDSuffix != "" {
 637			id = id + r.opts.HeadingIDSuffix
 638		}
 639		attrID := `id="` + id + `"`
 640		attrs = append(attrs, attrID)
 641	}
 642	attrs = append(attrs, BlockAttrs(nodeData)...)
 643	r.cr(w)
 644	r.outTag(w, headingOpenTagFromLevel(nodeData.Level), attrs)
 645}
 646
 647func (r *Renderer) headingExit(w io.Writer, heading *ast.Heading) {
 648	r.outs(w, headingCloseTagFromLevel(heading.Level))
 649	if !(isListItem(heading.Parent) && ast.GetNextNode(heading) == nil) {
 650		r.cr(w)
 651	}
 652}
 653
 654func (r *Renderer) heading(w io.Writer, node *ast.Heading, entering bool) {
 655	if entering {
 656		r.headingEnter(w, node)
 657	} else {
 658		r.headingExit(w, node)
 659	}
 660}
 661
 662func (r *Renderer) horizontalRule(w io.Writer, node *ast.HorizontalRule) {
 663	r.cr(w)
 664	r.outHRTag(w, BlockAttrs(node))
 665	r.cr(w)
 666}
 667
 668func (r *Renderer) listEnter(w io.Writer, nodeData *ast.List) {
 669	// TODO: attrs don't seem to be set
 670	var attrs []string
 671
 672	if nodeData.IsFootnotesList {
 673		r.outs(w, "\n<div class=\"footnotes\">\n\n")
 674		if r.opts.Flags&FootnoteNoHRTag == 0 {
 675			r.outHRTag(w, nil)
 676			r.cr(w)
 677		}
 678	}
 679	r.cr(w)
 680	if isListItem(nodeData.Parent) {
 681		grand := nodeData.Parent.GetParent()
 682		if isListTight(grand) {
 683			r.cr(w)
 684		}
 685	}
 686
 687	openTag := "<ul"
 688	if nodeData.ListFlags&ast.ListTypeOrdered != 0 {
 689		if nodeData.Start > 0 {
 690			attrs = append(attrs, fmt.Sprintf(`start="%d"`, nodeData.Start))
 691		}
 692		openTag = "<ol"
 693	}
 694	if nodeData.ListFlags&ast.ListTypeDefinition != 0 {
 695		openTag = "<dl"
 696	}
 697	attrs = append(attrs, BlockAttrs(nodeData)...)
 698	r.outTag(w, openTag, attrs)
 699	r.cr(w)
 700}
 701
 702func (r *Renderer) listExit(w io.Writer, list *ast.List) {
 703	closeTag := "</ul>"
 704	if list.ListFlags&ast.ListTypeOrdered != 0 {
 705		closeTag = "</ol>"
 706	}
 707	if list.ListFlags&ast.ListTypeDefinition != 0 {
 708		closeTag = "</dl>"
 709	}
 710	r.outs(w, closeTag)
 711
 712	//cr(w)
 713	//if node.parent.Type != Item {
 714	//	cr(w)
 715	//}
 716	parent := list.Parent
 717	switch parent.(type) {
 718	case *ast.ListItem:
 719		if ast.GetNextNode(list) != nil {
 720			r.cr(w)
 721		}
 722	case *ast.Document, *ast.BlockQuote, *ast.Aside:
 723		r.cr(w)
 724	}
 725
 726	if list.IsFootnotesList {
 727		r.outs(w, "\n</div>\n")
 728	}
 729}
 730
 731func (r *Renderer) list(w io.Writer, list *ast.List, entering bool) {
 732	if entering {
 733		r.listEnter(w, list)
 734	} else {
 735		r.listExit(w, list)
 736	}
 737}
 738
 739func (r *Renderer) listItemEnter(w io.Writer, listItem *ast.ListItem) {
 740	if listItemOpenCR(listItem) {
 741		r.cr(w)
 742	}
 743	if listItem.RefLink != nil {
 744		slug := slugify(listItem.RefLink)
 745		r.outs(w, footnoteItem(r.opts.FootnoteAnchorPrefix, slug))
 746		return
 747	}
 748
 749	openTag := "<li>"
 750	if listItem.ListFlags&ast.ListTypeDefinition != 0 {
 751		openTag = "<dd>"
 752	}
 753	if listItem.ListFlags&ast.ListTypeTerm != 0 {
 754		openTag = "<dt>"
 755	}
 756	r.outs(w, openTag)
 757}
 758
 759func (r *Renderer) listItemExit(w io.Writer, listItem *ast.ListItem) {
 760	if listItem.RefLink != nil && r.opts.Flags&FootnoteReturnLinks != 0 {
 761		slug := slugify(listItem.RefLink)
 762		prefix := r.opts.FootnoteAnchorPrefix
 763		link := r.opts.FootnoteReturnLinkContents
 764		s := footnoteReturnLink(prefix, link, slug)
 765		r.outs(w, s)
 766	}
 767
 768	closeTag := "</li>"
 769	if listItem.ListFlags&ast.ListTypeDefinition != 0 {
 770		closeTag = "</dd>"
 771	}
 772	if listItem.ListFlags&ast.ListTypeTerm != 0 {
 773		closeTag = "</dt>"
 774	}
 775	r.outs(w, closeTag)
 776	r.cr(w)
 777}
 778
 779func (r *Renderer) listItem(w io.Writer, listItem *ast.ListItem, entering bool) {
 780	if entering {
 781		r.listItemEnter(w, listItem)
 782	} else {
 783		r.listItemExit(w, listItem)
 784	}
 785}
 786
 787func (r *Renderer) codeBlock(w io.Writer, codeBlock *ast.CodeBlock) {
 788	var attrs []string
 789	// TODO(miek): this can add multiple class= attribute, they should be coalesced into one.
 790	// This is probably true for some other elements as well
 791	attrs = appendLanguageAttr(attrs, codeBlock.Info)
 792	attrs = append(attrs, BlockAttrs(codeBlock)...)
 793	r.cr(w)
 794
 795	r.outs(w, "<pre>")
 796	code := tagWithAttributes("<code", attrs)
 797	r.outs(w, code)
 798	if r.opts.Comments != nil {
 799		r.EscapeHTMLCallouts(w, codeBlock.Literal)
 800	} else {
 801		EscapeHTML(w, codeBlock.Literal)
 802	}
 803	r.outs(w, "</code>")
 804	r.outs(w, "</pre>")
 805	if !isListItem(codeBlock.Parent) {
 806		r.cr(w)
 807	}
 808}
 809
 810func (r *Renderer) caption(w io.Writer, caption *ast.Caption, entering bool) {
 811	if entering {
 812		r.outs(w, "<figcaption>")
 813		return
 814	}
 815	r.outs(w, "</figcaption>")
 816}
 817
 818func (r *Renderer) captionFigure(w io.Writer, figure *ast.CaptionFigure, entering bool) {
 819	// TODO(miek): copy more generic ways of mmark over to here.
 820	fig := "<figure"
 821	if figure.HeadingID != "" {
 822		fig += ` id="` + figure.HeadingID + `">`
 823	} else {
 824		fig += ">"
 825	}
 826	r.outOneOf(w, entering, fig, "\n</figure>\n")
 827}
 828
 829func (r *Renderer) tableCell(w io.Writer, tableCell *ast.TableCell, entering bool) {
 830	if !entering {
 831		r.outOneOf(w, tableCell.IsHeader, "</th>", "</td>")
 832		r.cr(w)
 833		return
 834	}
 835
 836	// entering
 837	var attrs []string
 838	openTag := "<td"
 839	if tableCell.IsHeader {
 840		openTag = "<th"
 841	}
 842	align := tableCell.Align.String()
 843	if align != "" {
 844		attrs = append(attrs, fmt.Sprintf(`align="%s"`, align))
 845	}
 846	if ast.GetPrevNode(tableCell) == nil {
 847		r.cr(w)
 848	}
 849	r.outTag(w, openTag, attrs)
 850}
 851
 852func (r *Renderer) tableBody(w io.Writer, node *ast.TableBody, entering bool) {
 853	if entering {
 854		r.cr(w)
 855		r.outs(w, "<tbody>")
 856		// XXX: this is to adhere to a rather silly test. Should fix test.
 857		if ast.GetFirstChild(node) == nil {
 858			r.cr(w)
 859		}
 860	} else {
 861		r.outs(w, "</tbody>")
 862		r.cr(w)
 863	}
 864}
 865
 866func (r *Renderer) matter(w io.Writer, node *ast.DocumentMatter, entering bool) {
 867	if !entering {
 868		return
 869	}
 870	if r.documentMatter != ast.DocumentMatterNone {
 871		r.outs(w, "</section>\n")
 872	}
 873	switch node.Matter {
 874	case ast.DocumentMatterFront:
 875		r.outs(w, `<section data-matter="front">`)
 876	case ast.DocumentMatterMain:
 877		r.outs(w, `<section data-matter="main">`)
 878	case ast.DocumentMatterBack:
 879		r.outs(w, `<section data-matter="back">`)
 880	}
 881	r.documentMatter = node.Matter
 882}
 883
 884func (r *Renderer) citation(w io.Writer, node *ast.Citation) {
 885	for i, c := range node.Destination {
 886		attr := []string{`class="none"`}
 887		switch node.Type[i] {
 888		case ast.CitationTypeNormative:
 889			attr[0] = `class="normative"`
 890		case ast.CitationTypeInformative:
 891			attr[0] = `class="informative"`
 892		case ast.CitationTypeSuppressed:
 893			attr[0] = `class="suppressed"`
 894		}
 895		r.outTag(w, "<cite", attr)
 896		r.outs(w, fmt.Sprintf(`<a href="#%s">`+r.opts.CitationFormatString+`</a>`, c, c))
 897		r.outs(w, "</cite>")
 898	}
 899}
 900
 901func (r *Renderer) callout(w io.Writer, node *ast.Callout) {
 902	attr := []string{`class="callout"`}
 903	r.outTag(w, "<span", attr)
 904	r.out(w, node.ID)
 905	r.outs(w, "</span>")
 906}
 907
 908func (r *Renderer) index(w io.Writer, node *ast.Index) {
 909	// there is no in-text representation.
 910	attr := []string{`class="index"`, fmt.Sprintf(`id="%s"`, node.ID)}
 911	r.outTag(w, "<span", attr)
 912	r.outs(w, "</span>")
 913}
 914
 915// RenderNode renders a markdown node to HTML
 916func (r *Renderer) RenderNode(w io.Writer, node ast.Node, entering bool) ast.WalkStatus {
 917	if r.opts.RenderNodeHook != nil {
 918		status, didHandle := r.opts.RenderNodeHook(w, node, entering)
 919		if didHandle {
 920			return status
 921		}
 922	}
 923	switch node := node.(type) {
 924	case *ast.Text:
 925		r.text(w, node)
 926	case *ast.Softbreak:
 927		r.cr(w)
 928		// TODO: make it configurable via out(renderer.softbreak)
 929	case *ast.Hardbreak:
 930		r.hardBreak(w, node)
 931	case *ast.NonBlockingSpace:
 932		r.nonBlockingSpace(w, node)
 933	case *ast.Emph:
 934		r.outOneOf(w, entering, "<em>", "</em>")
 935	case *ast.Strong:
 936		r.outOneOf(w, entering, "<strong>", "</strong>")
 937	case *ast.Del:
 938		r.outOneOf(w, entering, "<del>", "</del>")
 939	case *ast.BlockQuote:
 940		tag := tagWithAttributes("<blockquote", BlockAttrs(node))
 941		r.outOneOfCr(w, entering, tag, "</blockquote>")
 942	case *ast.Aside:
 943		tag := tagWithAttributes("<aside", BlockAttrs(node))
 944		r.outOneOfCr(w, entering, tag, "</aside>")
 945	case *ast.Link:
 946		r.link(w, node, entering)
 947	case *ast.CrossReference:
 948		link := &ast.Link{Destination: append([]byte("#"), node.Destination...)}
 949		r.link(w, link, entering)
 950	case *ast.Citation:
 951		r.citation(w, node)
 952	case *ast.Image:
 953		if r.opts.Flags&SkipImages != 0 {
 954			return ast.SkipChildren
 955		}
 956		r.image(w, node, entering)
 957	case *ast.Code:
 958		r.code(w, node)
 959	case *ast.CodeBlock:
 960		r.codeBlock(w, node)
 961	case *ast.Caption:
 962		r.caption(w, node, entering)
 963	case *ast.CaptionFigure:
 964		r.captionFigure(w, node, entering)
 965	case *ast.Document:
 966		// do nothing
 967	case *ast.Paragraph:
 968		r.paragraph(w, node, entering)
 969	case *ast.HTMLSpan:
 970		r.htmlSpan(w, node)
 971	case *ast.HTMLBlock:
 972		r.htmlBlock(w, node)
 973	case *ast.Heading:
 974		r.heading(w, node, entering)
 975	case *ast.HorizontalRule:
 976		r.horizontalRule(w, node)
 977	case *ast.List:
 978		r.list(w, node, entering)
 979	case *ast.ListItem:
 980		r.listItem(w, node, entering)
 981	case *ast.Table:
 982		tag := tagWithAttributes("<table", BlockAttrs(node))
 983		r.outOneOfCr(w, entering, tag, "</table>")
 984	case *ast.TableCell:
 985		r.tableCell(w, node, entering)
 986	case *ast.TableHeader:
 987		r.outOneOfCr(w, entering, "<thead>", "</thead>")
 988	case *ast.TableBody:
 989		r.tableBody(w, node, entering)
 990	case *ast.TableRow:
 991		r.outOneOfCr(w, entering, "<tr>", "</tr>")
 992	case *ast.TableFooter:
 993		r.outOneOfCr(w, entering, "<tfoot>", "</tfoot>")
 994	case *ast.Math:
 995		r.outOneOf(w, true, `<span class="math inline">\(`, `\)</span>`)
 996		EscapeHTML(w, node.Literal)
 997		r.outOneOf(w, false, `<span class="math inline">\(`, `\)</span>`)
 998	case *ast.MathBlock:
 999		r.outOneOf(w, entering, `<p><span class="math display">\[`, `\]</span></p>`)
1000		if entering {
1001			EscapeHTML(w, node.Literal)
1002		}
1003	case *ast.DocumentMatter:
1004		r.matter(w, node, entering)
1005	case *ast.Callout:
1006		r.callout(w, node)
1007	case *ast.Index:
1008		r.index(w, node)
1009	case *ast.Subscript:
1010		r.outOneOf(w, true, "<sub>", "</sub>")
1011		if entering {
1012			Escape(w, node.Literal)
1013		}
1014		r.outOneOf(w, false, "<sub>", "</sub>")
1015	case *ast.Superscript:
1016		r.outOneOf(w, true, "<sup>", "</sup>")
1017		if entering {
1018			Escape(w, node.Literal)
1019		}
1020		r.outOneOf(w, false, "<sup>", "</sup>")
1021	case *ast.Footnotes:
1022		// nothing by default; just output the list.
1023	default:
1024		panic(fmt.Sprintf("Unknown node %T", node))
1025	}
1026	return ast.GoToNext
1027}
1028
1029// RenderHeader writes HTML document preamble and TOC if requested.
1030func (r *Renderer) RenderHeader(w io.Writer, ast ast.Node) {
1031	r.writeDocumentHeader(w)
1032	if r.opts.Flags&TOC != 0 {
1033		r.writeTOC(w, ast)
1034	}
1035}
1036
1037// RenderFooter writes HTML document footer.
1038func (r *Renderer) RenderFooter(w io.Writer, _ ast.Node) {
1039	if r.documentMatter != ast.DocumentMatterNone {
1040		r.outs(w, "</section>\n")
1041	}
1042
1043	if r.opts.Flags&CompletePage == 0 {
1044		return
1045	}
1046	io.WriteString(w, "\n</body>\n</html>\n")
1047}
1048
1049func (r *Renderer) writeDocumentHeader(w io.Writer) {
1050	if r.opts.Flags&CompletePage == 0 {
1051		return
1052	}
1053	ending := ""
1054	if r.opts.Flags&UseXHTML != 0 {
1055		io.WriteString(w, "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" ")
1056		io.WriteString(w, "\"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">\n")
1057		io.WriteString(w, "<html xmlns=\"http://www.w3.org/1999/xhtml\">\n")
1058		ending = " /"
1059	} else {
1060		io.WriteString(w, "<!DOCTYPE html>\n")
1061		io.WriteString(w, "<html>\n")
1062	}
1063	io.WriteString(w, "<head>\n")
1064	io.WriteString(w, "  <title>")
1065	if r.opts.Flags&Smartypants != 0 {
1066		r.sr.Process(w, []byte(r.opts.Title))
1067	} else {
1068		EscapeHTML(w, []byte(r.opts.Title))
1069	}
1070	io.WriteString(w, "</title>\n")
1071	io.WriteString(w, r.opts.Generator)
1072	io.WriteString(w, "\"")
1073	io.WriteString(w, ending)
1074	io.WriteString(w, ">\n")
1075	io.WriteString(w, "  <meta charset=\"utf-8\"")
1076	io.WriteString(w, ending)
1077	io.WriteString(w, ">\n")
1078	if r.opts.CSS != "" {
1079		io.WriteString(w, "  <link rel=\"stylesheet\" type=\"text/css\" href=\"")
1080		EscapeHTML(w, []byte(r.opts.CSS))
1081		io.WriteString(w, "\"")
1082		io.WriteString(w, ending)
1083		io.WriteString(w, ">\n")
1084	}
1085	if r.opts.Icon != "" {
1086		io.WriteString(w, "  <link rel=\"icon\" type=\"image/x-icon\" href=\"")
1087		EscapeHTML(w, []byte(r.opts.Icon))
1088		io.WriteString(w, "\"")
1089		io.WriteString(w, ending)
1090		io.WriteString(w, ">\n")
1091	}
1092	if r.opts.Head != nil {
1093		w.Write(r.opts.Head)
1094	}
1095	io.WriteString(w, "</head>\n")
1096	io.WriteString(w, "<body>\n\n")
1097}
1098
1099func (r *Renderer) writeTOC(w io.Writer, doc ast.Node) {
1100	buf := bytes.Buffer{}
1101
1102	inHeading := false
1103	tocLevel := 0
1104	headingCount := 0
1105
1106	ast.WalkFunc(doc, func(node ast.Node, entering bool) ast.WalkStatus {
1107		if nodeData, ok := node.(*ast.Heading); ok && !nodeData.IsTitleblock {
1108			inHeading = entering
1109			if !entering {
1110				buf.WriteString("</a>")
1111				return ast.GoToNext
1112			}
1113			nodeData.HeadingID = fmt.Sprintf("toc_%d", headingCount)
1114			if nodeData.Level == tocLevel {
1115				buf.WriteString("</li>\n\n<li>")
1116			} else if nodeData.Level < tocLevel {
1117				for nodeData.Level < tocLevel {
1118					tocLevel--
1119					buf.WriteString("</li>\n</ul>")
1120				}
1121				buf.WriteString("</li>\n\n<li>")
1122			} else {
1123				for nodeData.Level > tocLevel {
1124					tocLevel++
1125					buf.WriteString("\n<ul>\n<li>")
1126				}
1127			}
1128
1129			fmt.Fprintf(&buf, `<a href="#toc_%d">`, headingCount)
1130			headingCount++
1131			return ast.GoToNext
1132		}
1133
1134		if inHeading {
1135			return r.RenderNode(&buf, node, entering)
1136		}
1137
1138		return ast.GoToNext
1139	})
1140
1141	for ; tocLevel > 0; tocLevel-- {
1142		buf.WriteString("</li>\n</ul>")
1143	}
1144
1145	if buf.Len() > 0 {
1146		io.WriteString(w, "<nav>\n")
1147		w.Write(buf.Bytes())
1148		io.WriteString(w, "\n\n</nav>\n")
1149	}
1150	r.lastOutputLen = buf.Len()
1151}
1152
1153func isList(node ast.Node) bool {
1154	_, ok := node.(*ast.List)
1155	return ok
1156}
1157
1158func isListTight(node ast.Node) bool {
1159	if list, ok := node.(*ast.List); ok {
1160		return list.Tight
1161	}
1162	return false
1163}
1164
1165func isListItem(node ast.Node) bool {
1166	_, ok := node.(*ast.ListItem)
1167	return ok
1168}
1169
1170func isListItemTerm(node ast.Node) bool {
1171	data, ok := node.(*ast.ListItem)
1172	return ok && data.ListFlags&ast.ListTypeTerm != 0
1173}
1174
1175// TODO: move to internal package
1176func skipSpace(data []byte, i int) int {
1177	n := len(data)
1178	for i < n && isSpace(data[i]) {
1179		i++
1180	}
1181	return i
1182}
1183
1184// TODO: move to internal package
1185var validUris = [][]byte{[]byte("http://"), []byte("https://"), []byte("ftp://"), []byte("mailto://")}
1186var validPaths = [][]byte{[]byte("/"), []byte("./"), []byte("../")}
1187
1188func isSafeLink(link []byte) bool {
1189	for _, path := range validPaths {
1190		if len(link) >= len(path) && bytes.Equal(link[:len(path)], path) {
1191			if len(link) == len(path) {
1192				return true
1193			} else if isAlnum(link[len(path)]) {
1194				return true
1195			}
1196		}
1197	}
1198
1199	for _, prefix := range validUris {
1200		// TODO: handle unicode here
1201		// case-insensitive prefix test
1202		if len(link) > len(prefix) && bytes.Equal(bytes.ToLower(link[:len(prefix)]), prefix) && isAlnum(link[len(prefix)]) {
1203			return true
1204		}
1205	}
1206
1207	return false
1208}
1209
1210// TODO: move to internal package
1211// Create a url-safe slug for fragments
1212func slugify(in []byte) []byte {
1213	if len(in) == 0 {
1214		return in
1215	}
1216	out := make([]byte, 0, len(in))
1217	sym := false
1218
1219	for _, ch := range in {
1220		if isAlnum(ch) {
1221			sym = false
1222			out = append(out, ch)
1223		} else if sym {
1224			continue
1225		} else {
1226			out = append(out, '-')
1227			sym = true
1228		}
1229	}
1230	var a, b int
1231	var ch byte
1232	for a, ch = range out {
1233		if ch != '-' {
1234			break
1235		}
1236	}
1237	for b = len(out) - 1; b > 0; b-- {
1238		if out[b] != '-' {
1239			break
1240		}
1241	}
1242	return out[a : b+1]
1243}
1244
1245// TODO: move to internal package
1246// isAlnum returns true if c is a digit or letter
1247// TODO: check when this is looking for ASCII alnum and when it should use unicode
1248func isAlnum(c byte) bool {
1249	return (c >= '0' && c <= '9') || isLetter(c)
1250}
1251
1252// isSpace returns true if c is a white-space charactr
1253func isSpace(c byte) bool {
1254	return c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == '\f' || c == '\v'
1255}
1256
1257// isLetter returns true if c is ascii letter
1258func isLetter(c byte) bool {
1259	return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')
1260}
1261
1262// isPunctuation returns true if c is a punctuation symbol.
1263func isPunctuation(c byte) bool {
1264	for _, r := range []byte("!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~") {
1265		if c == r {
1266			return true
1267		}
1268	}
1269	return false
1270}
1271
1272// BlockAttrs takes a node and checks if it has block level attributes set. If so it
1273// will return a slice each containing a "key=value(s)" string.
1274func BlockAttrs(node ast.Node) []string {
1275	var attr *ast.Attribute
1276	if c := node.AsContainer(); c != nil && c.Attribute != nil {
1277		attr = c.Attribute
1278	}
1279	if l := node.AsLeaf(); l != nil && l.Attribute != nil {
1280		attr = l.Attribute
1281	}
1282	if attr == nil {
1283		return nil
1284	}
1285
1286	var s []string
1287	if attr.ID != nil {
1288		s = append(s, fmt.Sprintf(`%s="%s"`, IDTag, attr.ID))
1289	}
1290
1291	classes := ""
1292	for _, c := range attr.Classes {
1293		classes += " " + string(c)
1294	}
1295	if classes != "" {
1296		s = append(s, fmt.Sprintf(`class="%s"`, classes[1:])) // skip space we added.
1297	}
1298
1299	// sort the attributes so it remain stable between runs
1300	var keys = []string{}
1301	for k, _ := range attr.Attrs {
1302		keys = append(keys, k)
1303	}
1304	sort.Strings(keys)
1305	for _, k := range keys {
1306		s = append(s, fmt.Sprintf(`%s="%s"`, k, attr.Attrs[k]))
1307	}
1308
1309	return s
1310}
1311
1312func tagWithAttributes(name string, attrs []string) string {
1313	s := name
1314	if len(attrs) > 0 {
1315		s += " " + strings.Join(attrs, " ")
1316	}
1317	return s + ">"
1318}