...

Source file src/github.com/russross/blackfriday/v2/html.go

Documentation: github.com/russross/blackfriday/v2

     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  
    16  package blackfriday
    17  
    18  import (
    19  	"bytes"
    20  	"fmt"
    21  	"io"
    22  	"regexp"
    23  	"strings"
    24  )
    25  
    26  // HTMLFlags control optional behavior of HTML renderer.
    27  type HTMLFlags int
    28  
    29  // HTML renderer configuration options.
    30  const (
    31  	HTMLFlagsNone           HTMLFlags = 0
    32  	SkipHTML                HTMLFlags = 1 << iota // Skip preformatted HTML blocks
    33  	SkipImages                                    // Skip embedded images
    34  	SkipLinks                                     // Skip all links
    35  	Safelink                                      // Only link to trusted protocols
    36  	NofollowLinks                                 // Only link with rel="nofollow"
    37  	NoreferrerLinks                               // Only link with rel="noreferrer"
    38  	NoopenerLinks                                 // Only link with rel="noopener"
    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  	Smartypants                                   // Enable smart punctuation substitutions
    44  	SmartypantsFractions                          // Enable smart fractions (with Smartypants)
    45  	SmartypantsDashes                             // Enable smart dashes (with Smartypants)
    46  	SmartypantsLatexDashes                        // Enable LaTeX-style dashes (with Smartypants)
    47  	SmartypantsAngledQuotes                       // Enable angled double quotes (with Smartypants) for double quotes rendering
    48  	SmartypantsQuotesNBSP                         // Enable « French guillemets » (with Smartypants)
    49  	TOC                                           // Generate a table of contents
    50  )
    51  
    52  var (
    53  	htmlTagRe = regexp.MustCompile("(?i)^" + htmlTag)
    54  )
    55  
    56  const (
    57  	htmlTag = "(?:" + openTag + "|" + closeTag + "|" + htmlComment + "|" +
    58  		processingInstruction + "|" + declaration + "|" + cdata + ")"
    59  	closeTag              = "</" + tagName + "\\s*[>]"
    60  	openTag               = "<" + tagName + attribute + "*" + "\\s*/?>"
    61  	attribute             = "(?:" + "\\s+" + attributeName + attributeValueSpec + "?)"
    62  	attributeValue        = "(?:" + unquotedValue + "|" + singleQuotedValue + "|" + doubleQuotedValue + ")"
    63  	attributeValueSpec    = "(?:" + "\\s*=" + "\\s*" + attributeValue + ")"
    64  	attributeName         = "[a-zA-Z_:][a-zA-Z0-9:._-]*"
    65  	cdata                 = "<!\\[CDATA\\[[\\s\\S]*?\\]\\]>"
    66  	declaration           = "<![A-Z]+" + "\\s+[^>]*>"
    67  	doubleQuotedValue     = "\"[^\"]*\""
    68  	htmlComment           = "<!---->|<!--(?:-?[^>-])(?:-?[^-])*-->"
    69  	processingInstruction = "[<][?].*?[?][>]"
    70  	singleQuotedValue     = "'[^']*'"
    71  	tagName               = "[A-Za-z][A-Za-z0-9-]*"
    72  	unquotedValue         = "[^\"'=<>`\\x00-\\x20]+"
    73  )
    74  
    75  // HTMLRendererParameters is a collection of supplementary parameters tweaking
    76  // the behavior of various parts of HTML renderer.
    77  type HTMLRendererParameters struct {
    78  	// Prepend this text to each relative URL.
    79  	AbsolutePrefix string
    80  	// Add this text to each footnote anchor, to ensure uniqueness.
    81  	FootnoteAnchorPrefix string
    82  	// Show this text inside the <a> tag for a footnote return link, if the
    83  	// HTML_FOOTNOTE_RETURN_LINKS flag is enabled. If blank, the string
    84  	// <sup>[return]</sup> is used.
    85  	FootnoteReturnLinkContents string
    86  	// If set, add this text to the front of each Heading ID, to ensure
    87  	// uniqueness.
    88  	HeadingIDPrefix string
    89  	// If set, add this text to the back of each Heading ID, to ensure uniqueness.
    90  	HeadingIDSuffix string
    91  	// Increase heading levels: if the offset is 1, <h1> becomes <h2> etc.
    92  	// Negative offset is also valid.
    93  	// Resulting levels are clipped between 1 and 6.
    94  	HeadingLevelOffset int
    95  
    96  	Title string // Document title (used if CompletePage is set)
    97  	CSS   string // Optional CSS file URL (used if CompletePage is set)
    98  	Icon  string // Optional icon file URL (used if CompletePage is set)
    99  
   100  	Flags HTMLFlags // Flags allow customizing this renderer's behavior
   101  }
   102  
   103  // HTMLRenderer is a type that implements the Renderer interface for HTML output.
   104  //
   105  // Do not create this directly, instead use the NewHTMLRenderer function.
   106  type HTMLRenderer struct {
   107  	HTMLRendererParameters
   108  
   109  	closeTag string // how to end singleton tags: either " />" or ">"
   110  
   111  	// Track heading IDs to prevent ID collision in a single generation.
   112  	headingIDs map[string]int
   113  
   114  	lastOutputLen int
   115  	disableTags   int
   116  
   117  	sr *SPRenderer
   118  }
   119  
   120  const (
   121  	xhtmlClose = " />"
   122  	htmlClose  = ">"
   123  )
   124  
   125  // NewHTMLRenderer creates and configures an HTMLRenderer object, which
   126  // satisfies the Renderer interface.
   127  func NewHTMLRenderer(params HTMLRendererParameters) *HTMLRenderer {
   128  	// configure the rendering engine
   129  	closeTag := htmlClose
   130  	if params.Flags&UseXHTML != 0 {
   131  		closeTag = xhtmlClose
   132  	}
   133  
   134  	if params.FootnoteReturnLinkContents == "" {
   135  		// U+FE0E is VARIATION SELECTOR-15.
   136  		// It suppresses automatic emoji presentation of the preceding
   137  		// U+21A9 LEFTWARDS ARROW WITH HOOK on iOS and iPadOS.
   138  		params.FootnoteReturnLinkContents = "<span aria-label='Return'>↩\ufe0e</span>"
   139  	}
   140  
   141  	return &HTMLRenderer{
   142  		HTMLRendererParameters: params,
   143  
   144  		closeTag:   closeTag,
   145  		headingIDs: make(map[string]int),
   146  
   147  		sr: NewSmartypantsRenderer(params.Flags),
   148  	}
   149  }
   150  
   151  func isHTMLTag(tag []byte, tagname string) bool {
   152  	found, _ := findHTMLTagPos(tag, tagname)
   153  	return found
   154  }
   155  
   156  // Look for a character, but ignore it when it's in any kind of quotes, it
   157  // might be JavaScript
   158  func skipUntilCharIgnoreQuotes(html []byte, start int, char byte) int {
   159  	inSingleQuote := false
   160  	inDoubleQuote := false
   161  	inGraveQuote := false
   162  	i := start
   163  	for i < len(html) {
   164  		switch {
   165  		case html[i] == char && !inSingleQuote && !inDoubleQuote && !inGraveQuote:
   166  			return i
   167  		case html[i] == '\'':
   168  			inSingleQuote = !inSingleQuote
   169  		case html[i] == '"':
   170  			inDoubleQuote = !inDoubleQuote
   171  		case html[i] == '`':
   172  			inGraveQuote = !inGraveQuote
   173  		}
   174  		i++
   175  	}
   176  	return start
   177  }
   178  
   179  func findHTMLTagPos(tag []byte, tagname string) (bool, int) {
   180  	i := 0
   181  	if i < len(tag) && tag[0] != '<' {
   182  		return false, -1
   183  	}
   184  	i++
   185  	i = skipSpace(tag, i)
   186  
   187  	if i < len(tag) && tag[i] == '/' {
   188  		i++
   189  	}
   190  
   191  	i = skipSpace(tag, i)
   192  	j := 0
   193  	for ; i < len(tag); i, j = i+1, j+1 {
   194  		if j >= len(tagname) {
   195  			break
   196  		}
   197  
   198  		if strings.ToLower(string(tag[i]))[0] != tagname[j] {
   199  			return false, -1
   200  		}
   201  	}
   202  
   203  	if i == len(tag) {
   204  		return false, -1
   205  	}
   206  
   207  	rightAngle := skipUntilCharIgnoreQuotes(tag, i, '>')
   208  	if rightAngle >= i {
   209  		return true, rightAngle
   210  	}
   211  
   212  	return false, -1
   213  }
   214  
   215  func skipSpace(tag []byte, i int) int {
   216  	for i < len(tag) && isspace(tag[i]) {
   217  		i++
   218  	}
   219  	return i
   220  }
   221  
   222  func isRelativeLink(link []byte) (yes bool) {
   223  	// a tag begin with '#'
   224  	if link[0] == '#' {
   225  		return true
   226  	}
   227  
   228  	// link begin with '/' but not '//', the second maybe a protocol relative link
   229  	if len(link) >= 2 && link[0] == '/' && link[1] != '/' {
   230  		return true
   231  	}
   232  
   233  	// only the root '/'
   234  	if len(link) == 1 && link[0] == '/' {
   235  		return true
   236  	}
   237  
   238  	// current directory : begin with "./"
   239  	if bytes.HasPrefix(link, []byte("./")) {
   240  		return true
   241  	}
   242  
   243  	// parent directory : begin with "../"
   244  	if bytes.HasPrefix(link, []byte("../")) {
   245  		return true
   246  	}
   247  
   248  	return false
   249  }
   250  
   251  func (r *HTMLRenderer) ensureUniqueHeadingID(id string) string {
   252  	for count, found := r.headingIDs[id]; found; count, found = r.headingIDs[id] {
   253  		tmp := fmt.Sprintf("%s-%d", id, count+1)
   254  
   255  		if _, tmpFound := r.headingIDs[tmp]; !tmpFound {
   256  			r.headingIDs[id] = count + 1
   257  			id = tmp
   258  		} else {
   259  			id = id + "-1"
   260  		}
   261  	}
   262  
   263  	if _, found := r.headingIDs[id]; !found {
   264  		r.headingIDs[id] = 0
   265  	}
   266  
   267  	return id
   268  }
   269  
   270  func (r *HTMLRenderer) addAbsPrefix(link []byte) []byte {
   271  	if r.AbsolutePrefix != "" && isRelativeLink(link) && link[0] != '.' {
   272  		newDest := r.AbsolutePrefix
   273  		if link[0] != '/' {
   274  			newDest += "/"
   275  		}
   276  		newDest += string(link)
   277  		return []byte(newDest)
   278  	}
   279  	return link
   280  }
   281  
   282  func appendLinkAttrs(attrs []string, flags HTMLFlags, link []byte) []string {
   283  	if isRelativeLink(link) {
   284  		return attrs
   285  	}
   286  	val := []string{}
   287  	if flags&NofollowLinks != 0 {
   288  		val = append(val, "nofollow")
   289  	}
   290  	if flags&NoreferrerLinks != 0 {
   291  		val = append(val, "noreferrer")
   292  	}
   293  	if flags&NoopenerLinks != 0 {
   294  		val = append(val, "noopener")
   295  	}
   296  	if flags&HrefTargetBlank != 0 {
   297  		attrs = append(attrs, "target=\"_blank\"")
   298  	}
   299  	if len(val) == 0 {
   300  		return attrs
   301  	}
   302  	attr := fmt.Sprintf("rel=%q", strings.Join(val, " "))
   303  	return append(attrs, attr)
   304  }
   305  
   306  func isMailto(link []byte) bool {
   307  	return bytes.HasPrefix(link, []byte("mailto:"))
   308  }
   309  
   310  func needSkipLink(flags HTMLFlags, dest []byte) bool {
   311  	if flags&SkipLinks != 0 {
   312  		return true
   313  	}
   314  	return flags&Safelink != 0 && !isSafeLink(dest) && !isMailto(dest)
   315  }
   316  
   317  func isSmartypantable(node *Node) bool {
   318  	pt := node.Parent.Type
   319  	return pt != Link && pt != CodeBlock && pt != Code
   320  }
   321  
   322  func appendLanguageAttr(attrs []string, info []byte) []string {
   323  	if len(info) == 0 {
   324  		return attrs
   325  	}
   326  	endOfLang := bytes.IndexAny(info, "\t ")
   327  	if endOfLang < 0 {
   328  		endOfLang = len(info)
   329  	}
   330  	return append(attrs, fmt.Sprintf("class=\"language-%s\"", info[:endOfLang]))
   331  }
   332  
   333  func (r *HTMLRenderer) tag(w io.Writer, name []byte, attrs []string) {
   334  	w.Write(name)
   335  	if len(attrs) > 0 {
   336  		w.Write(spaceBytes)
   337  		w.Write([]byte(strings.Join(attrs, " ")))
   338  	}
   339  	w.Write(gtBytes)
   340  	r.lastOutputLen = 1
   341  }
   342  
   343  func footnoteRef(prefix string, node *Node) []byte {
   344  	urlFrag := prefix + string(slugify(node.Destination))
   345  	anchor := fmt.Sprintf(`<a href="#fn:%s">%d</a>`, urlFrag, node.NoteID)
   346  	return []byte(fmt.Sprintf(`<sup class="footnote-ref" id="fnref:%s">%s</sup>`, urlFrag, anchor))
   347  }
   348  
   349  func footnoteItem(prefix string, slug []byte) []byte {
   350  	return []byte(fmt.Sprintf(`<li id="fn:%s%s">`, prefix, slug))
   351  }
   352  
   353  func footnoteReturnLink(prefix, returnLink string, slug []byte) []byte {
   354  	const format = ` <a class="footnote-return" href="#fnref:%s%s">%s</a>`
   355  	return []byte(fmt.Sprintf(format, prefix, slug, returnLink))
   356  }
   357  
   358  func itemOpenCR(node *Node) bool {
   359  	if node.Prev == nil {
   360  		return false
   361  	}
   362  	ld := node.Parent.ListData
   363  	return !ld.Tight && ld.ListFlags&ListTypeDefinition == 0
   364  }
   365  
   366  func skipParagraphTags(node *Node) bool {
   367  	grandparent := node.Parent.Parent
   368  	if grandparent == nil || grandparent.Type != List {
   369  		return false
   370  	}
   371  	tightOrTerm := grandparent.Tight || node.Parent.ListFlags&ListTypeTerm != 0
   372  	return grandparent.Type == List && tightOrTerm
   373  }
   374  
   375  func cellAlignment(align CellAlignFlags) string {
   376  	switch align {
   377  	case TableAlignmentLeft:
   378  		return "left"
   379  	case TableAlignmentRight:
   380  		return "right"
   381  	case TableAlignmentCenter:
   382  		return "center"
   383  	default:
   384  		return ""
   385  	}
   386  }
   387  
   388  func (r *HTMLRenderer) out(w io.Writer, text []byte) {
   389  	if r.disableTags > 0 {
   390  		w.Write(htmlTagRe.ReplaceAll(text, []byte{}))
   391  	} else {
   392  		w.Write(text)
   393  	}
   394  	r.lastOutputLen = len(text)
   395  }
   396  
   397  func (r *HTMLRenderer) cr(w io.Writer) {
   398  	if r.lastOutputLen > 0 {
   399  		r.out(w, nlBytes)
   400  	}
   401  }
   402  
   403  var (
   404  	nlBytes    = []byte{'\n'}
   405  	gtBytes    = []byte{'>'}
   406  	spaceBytes = []byte{' '}
   407  )
   408  
   409  var (
   410  	brTag              = []byte("<br>")
   411  	brXHTMLTag         = []byte("<br />")
   412  	emTag              = []byte("<em>")
   413  	emCloseTag         = []byte("</em>")
   414  	strongTag          = []byte("<strong>")
   415  	strongCloseTag     = []byte("</strong>")
   416  	delTag             = []byte("<del>")
   417  	delCloseTag        = []byte("</del>")
   418  	ttTag              = []byte("<tt>")
   419  	ttCloseTag         = []byte("</tt>")
   420  	aTag               = []byte("<a")
   421  	aCloseTag          = []byte("</a>")
   422  	preTag             = []byte("<pre>")
   423  	preCloseTag        = []byte("</pre>")
   424  	codeTag            = []byte("<code>")
   425  	codeCloseTag       = []byte("</code>")
   426  	pTag               = []byte("<p>")
   427  	pCloseTag          = []byte("</p>")
   428  	blockquoteTag      = []byte("<blockquote>")
   429  	blockquoteCloseTag = []byte("</blockquote>")
   430  	hrTag              = []byte("<hr>")
   431  	hrXHTMLTag         = []byte("<hr />")
   432  	ulTag              = []byte("<ul>")
   433  	ulCloseTag         = []byte("</ul>")
   434  	olTag              = []byte("<ol>")
   435  	olCloseTag         = []byte("</ol>")
   436  	dlTag              = []byte("<dl>")
   437  	dlCloseTag         = []byte("</dl>")
   438  	liTag              = []byte("<li>")
   439  	liCloseTag         = []byte("</li>")
   440  	ddTag              = []byte("<dd>")
   441  	ddCloseTag         = []byte("</dd>")
   442  	dtTag              = []byte("<dt>")
   443  	dtCloseTag         = []byte("</dt>")
   444  	tableTag           = []byte("<table>")
   445  	tableCloseTag      = []byte("</table>")
   446  	tdTag              = []byte("<td")
   447  	tdCloseTag         = []byte("</td>")
   448  	thTag              = []byte("<th")
   449  	thCloseTag         = []byte("</th>")
   450  	theadTag           = []byte("<thead>")
   451  	theadCloseTag      = []byte("</thead>")
   452  	tbodyTag           = []byte("<tbody>")
   453  	tbodyCloseTag      = []byte("</tbody>")
   454  	trTag              = []byte("<tr>")
   455  	trCloseTag         = []byte("</tr>")
   456  	h1Tag              = []byte("<h1")
   457  	h1CloseTag         = []byte("</h1>")
   458  	h2Tag              = []byte("<h2")
   459  	h2CloseTag         = []byte("</h2>")
   460  	h3Tag              = []byte("<h3")
   461  	h3CloseTag         = []byte("</h3>")
   462  	h4Tag              = []byte("<h4")
   463  	h4CloseTag         = []byte("</h4>")
   464  	h5Tag              = []byte("<h5")
   465  	h5CloseTag         = []byte("</h5>")
   466  	h6Tag              = []byte("<h6")
   467  	h6CloseTag         = []byte("</h6>")
   468  
   469  	footnotesDivBytes      = []byte("\n<div class=\"footnotes\">\n\n")
   470  	footnotesCloseDivBytes = []byte("\n</div>\n")
   471  )
   472  
   473  func headingTagsFromLevel(level int) ([]byte, []byte) {
   474  	if level <= 1 {
   475  		return h1Tag, h1CloseTag
   476  	}
   477  	switch level {
   478  	case 2:
   479  		return h2Tag, h2CloseTag
   480  	case 3:
   481  		return h3Tag, h3CloseTag
   482  	case 4:
   483  		return h4Tag, h4CloseTag
   484  	case 5:
   485  		return h5Tag, h5CloseTag
   486  	}
   487  	return h6Tag, h6CloseTag
   488  }
   489  
   490  func (r *HTMLRenderer) outHRTag(w io.Writer) {
   491  	if r.Flags&UseXHTML == 0 {
   492  		r.out(w, hrTag)
   493  	} else {
   494  		r.out(w, hrXHTMLTag)
   495  	}
   496  }
   497  
   498  // RenderNode is a default renderer of a single node of a syntax tree. For
   499  // block nodes it will be called twice: first time with entering=true, second
   500  // time with entering=false, so that it could know when it's working on an open
   501  // tag and when on close. It writes the result to w.
   502  //
   503  // The return value is a way to tell the calling walker to adjust its walk
   504  // pattern: e.g. it can terminate the traversal by returning Terminate. Or it
   505  // can ask the walker to skip a subtree of this node by returning SkipChildren.
   506  // The typical behavior is to return GoToNext, which asks for the usual
   507  // traversal to the next node.
   508  func (r *HTMLRenderer) RenderNode(w io.Writer, node *Node, entering bool) WalkStatus {
   509  	attrs := []string{}
   510  	switch node.Type {
   511  	case Text:
   512  		if r.Flags&Smartypants != 0 {
   513  			var tmp bytes.Buffer
   514  			escapeHTML(&tmp, node.Literal)
   515  			r.sr.Process(w, tmp.Bytes())
   516  		} else {
   517  			if node.Parent.Type == Link {
   518  				escLink(w, node.Literal)
   519  			} else {
   520  				escapeHTML(w, node.Literal)
   521  			}
   522  		}
   523  	case Softbreak:
   524  		r.cr(w)
   525  		// TODO: make it configurable via out(renderer.softbreak)
   526  	case Hardbreak:
   527  		if r.Flags&UseXHTML == 0 {
   528  			r.out(w, brTag)
   529  		} else {
   530  			r.out(w, brXHTMLTag)
   531  		}
   532  		r.cr(w)
   533  	case Emph:
   534  		if entering {
   535  			r.out(w, emTag)
   536  		} else {
   537  			r.out(w, emCloseTag)
   538  		}
   539  	case Strong:
   540  		if entering {
   541  			r.out(w, strongTag)
   542  		} else {
   543  			r.out(w, strongCloseTag)
   544  		}
   545  	case Del:
   546  		if entering {
   547  			r.out(w, delTag)
   548  		} else {
   549  			r.out(w, delCloseTag)
   550  		}
   551  	case HTMLSpan:
   552  		if r.Flags&SkipHTML != 0 {
   553  			break
   554  		}
   555  		r.out(w, node.Literal)
   556  	case Link:
   557  		// mark it but don't link it if it is not a safe link: no smartypants
   558  		dest := node.LinkData.Destination
   559  		if needSkipLink(r.Flags, dest) {
   560  			if entering {
   561  				r.out(w, ttTag)
   562  			} else {
   563  				r.out(w, ttCloseTag)
   564  			}
   565  		} else {
   566  			if entering {
   567  				dest = r.addAbsPrefix(dest)
   568  				var hrefBuf bytes.Buffer
   569  				hrefBuf.WriteString("href=\"")
   570  				escLink(&hrefBuf, dest)
   571  				hrefBuf.WriteByte('"')
   572  				attrs = append(attrs, hrefBuf.String())
   573  				if node.NoteID != 0 {
   574  					r.out(w, footnoteRef(r.FootnoteAnchorPrefix, node))
   575  					break
   576  				}
   577  				attrs = appendLinkAttrs(attrs, r.Flags, dest)
   578  				if len(node.LinkData.Title) > 0 {
   579  					var titleBuff bytes.Buffer
   580  					titleBuff.WriteString("title=\"")
   581  					escapeHTML(&titleBuff, node.LinkData.Title)
   582  					titleBuff.WriteByte('"')
   583  					attrs = append(attrs, titleBuff.String())
   584  				}
   585  				r.tag(w, aTag, attrs)
   586  			} else {
   587  				if node.NoteID != 0 {
   588  					break
   589  				}
   590  				r.out(w, aCloseTag)
   591  			}
   592  		}
   593  	case Image:
   594  		if r.Flags&SkipImages != 0 {
   595  			return SkipChildren
   596  		}
   597  		if entering {
   598  			dest := node.LinkData.Destination
   599  			dest = r.addAbsPrefix(dest)
   600  			if r.disableTags == 0 {
   601  				//if options.safe && potentiallyUnsafe(dest) {
   602  				//out(w, `<img src="" alt="`)
   603  				//} else {
   604  				r.out(w, []byte(`<img src="`))
   605  				escLink(w, dest)
   606  				r.out(w, []byte(`" alt="`))
   607  				//}
   608  			}
   609  			r.disableTags++
   610  		} else {
   611  			r.disableTags--
   612  			if r.disableTags == 0 {
   613  				if node.LinkData.Title != nil {
   614  					r.out(w, []byte(`" title="`))
   615  					escapeHTML(w, node.LinkData.Title)
   616  				}
   617  				r.out(w, []byte(`" />`))
   618  			}
   619  		}
   620  	case Code:
   621  		r.out(w, codeTag)
   622  		escapeAllHTML(w, node.Literal)
   623  		r.out(w, codeCloseTag)
   624  	case Document:
   625  		break
   626  	case Paragraph:
   627  		if skipParagraphTags(node) {
   628  			break
   629  		}
   630  		if entering {
   631  			// TODO: untangle this clusterfuck about when the newlines need
   632  			// to be added and when not.
   633  			if node.Prev != nil {
   634  				switch node.Prev.Type {
   635  				case HTMLBlock, List, Paragraph, Heading, CodeBlock, BlockQuote, HorizontalRule:
   636  					r.cr(w)
   637  				}
   638  			}
   639  			if node.Parent.Type == BlockQuote && node.Prev == nil {
   640  				r.cr(w)
   641  			}
   642  			r.out(w, pTag)
   643  		} else {
   644  			r.out(w, pCloseTag)
   645  			if !(node.Parent.Type == Item && node.Next == nil) {
   646  				r.cr(w)
   647  			}
   648  		}
   649  	case BlockQuote:
   650  		if entering {
   651  			r.cr(w)
   652  			r.out(w, blockquoteTag)
   653  		} else {
   654  			r.out(w, blockquoteCloseTag)
   655  			r.cr(w)
   656  		}
   657  	case HTMLBlock:
   658  		if r.Flags&SkipHTML != 0 {
   659  			break
   660  		}
   661  		r.cr(w)
   662  		r.out(w, node.Literal)
   663  		r.cr(w)
   664  	case Heading:
   665  		headingLevel := r.HTMLRendererParameters.HeadingLevelOffset + node.Level
   666  		openTag, closeTag := headingTagsFromLevel(headingLevel)
   667  		if entering {
   668  			if node.IsTitleblock {
   669  				attrs = append(attrs, `class="title"`)
   670  			}
   671  			if node.HeadingID != "" {
   672  				id := r.ensureUniqueHeadingID(node.HeadingID)
   673  				if r.HeadingIDPrefix != "" {
   674  					id = r.HeadingIDPrefix + id
   675  				}
   676  				if r.HeadingIDSuffix != "" {
   677  					id = id + r.HeadingIDSuffix
   678  				}
   679  				attrs = append(attrs, fmt.Sprintf(`id="%s"`, id))
   680  			}
   681  			r.cr(w)
   682  			r.tag(w, openTag, attrs)
   683  		} else {
   684  			r.out(w, closeTag)
   685  			if !(node.Parent.Type == Item && node.Next == nil) {
   686  				r.cr(w)
   687  			}
   688  		}
   689  	case HorizontalRule:
   690  		r.cr(w)
   691  		r.outHRTag(w)
   692  		r.cr(w)
   693  	case List:
   694  		openTag := ulTag
   695  		closeTag := ulCloseTag
   696  		if node.ListFlags&ListTypeOrdered != 0 {
   697  			openTag = olTag
   698  			closeTag = olCloseTag
   699  		}
   700  		if node.ListFlags&ListTypeDefinition != 0 {
   701  			openTag = dlTag
   702  			closeTag = dlCloseTag
   703  		}
   704  		if entering {
   705  			if node.IsFootnotesList {
   706  				r.out(w, footnotesDivBytes)
   707  				r.outHRTag(w)
   708  				r.cr(w)
   709  			}
   710  			r.cr(w)
   711  			if node.Parent.Type == Item && node.Parent.Parent.Tight {
   712  				r.cr(w)
   713  			}
   714  			r.tag(w, openTag[:len(openTag)-1], attrs)
   715  			r.cr(w)
   716  		} else {
   717  			r.out(w, closeTag)
   718  			//cr(w)
   719  			//if node.parent.Type != Item {
   720  			//	cr(w)
   721  			//}
   722  			if node.Parent.Type == Item && node.Next != nil {
   723  				r.cr(w)
   724  			}
   725  			if node.Parent.Type == Document || node.Parent.Type == BlockQuote {
   726  				r.cr(w)
   727  			}
   728  			if node.IsFootnotesList {
   729  				r.out(w, footnotesCloseDivBytes)
   730  			}
   731  		}
   732  	case Item:
   733  		openTag := liTag
   734  		closeTag := liCloseTag
   735  		if node.ListFlags&ListTypeDefinition != 0 {
   736  			openTag = ddTag
   737  			closeTag = ddCloseTag
   738  		}
   739  		if node.ListFlags&ListTypeTerm != 0 {
   740  			openTag = dtTag
   741  			closeTag = dtCloseTag
   742  		}
   743  		if entering {
   744  			if itemOpenCR(node) {
   745  				r.cr(w)
   746  			}
   747  			if node.ListData.RefLink != nil {
   748  				slug := slugify(node.ListData.RefLink)
   749  				r.out(w, footnoteItem(r.FootnoteAnchorPrefix, slug))
   750  				break
   751  			}
   752  			r.out(w, openTag)
   753  		} else {
   754  			if node.ListData.RefLink != nil {
   755  				slug := slugify(node.ListData.RefLink)
   756  				if r.Flags&FootnoteReturnLinks != 0 {
   757  					r.out(w, footnoteReturnLink(r.FootnoteAnchorPrefix, r.FootnoteReturnLinkContents, slug))
   758  				}
   759  			}
   760  			r.out(w, closeTag)
   761  			r.cr(w)
   762  		}
   763  	case CodeBlock:
   764  		attrs = appendLanguageAttr(attrs, node.Info)
   765  		r.cr(w)
   766  		r.out(w, preTag)
   767  		r.tag(w, codeTag[:len(codeTag)-1], attrs)
   768  		escapeAllHTML(w, node.Literal)
   769  		r.out(w, codeCloseTag)
   770  		r.out(w, preCloseTag)
   771  		if node.Parent.Type != Item {
   772  			r.cr(w)
   773  		}
   774  	case Table:
   775  		if entering {
   776  			r.cr(w)
   777  			r.out(w, tableTag)
   778  		} else {
   779  			r.out(w, tableCloseTag)
   780  			r.cr(w)
   781  		}
   782  	case TableCell:
   783  		openTag := tdTag
   784  		closeTag := tdCloseTag
   785  		if node.IsHeader {
   786  			openTag = thTag
   787  			closeTag = thCloseTag
   788  		}
   789  		if entering {
   790  			align := cellAlignment(node.Align)
   791  			if align != "" {
   792  				attrs = append(attrs, fmt.Sprintf(`align="%s"`, align))
   793  			}
   794  			if node.Prev == nil {
   795  				r.cr(w)
   796  			}
   797  			r.tag(w, openTag, attrs)
   798  		} else {
   799  			r.out(w, closeTag)
   800  			r.cr(w)
   801  		}
   802  	case TableHead:
   803  		if entering {
   804  			r.cr(w)
   805  			r.out(w, theadTag)
   806  		} else {
   807  			r.out(w, theadCloseTag)
   808  			r.cr(w)
   809  		}
   810  	case TableBody:
   811  		if entering {
   812  			r.cr(w)
   813  			r.out(w, tbodyTag)
   814  			// XXX: this is to adhere to a rather silly test. Should fix test.
   815  			if node.FirstChild == nil {
   816  				r.cr(w)
   817  			}
   818  		} else {
   819  			r.out(w, tbodyCloseTag)
   820  			r.cr(w)
   821  		}
   822  	case TableRow:
   823  		if entering {
   824  			r.cr(w)
   825  			r.out(w, trTag)
   826  		} else {
   827  			r.out(w, trCloseTag)
   828  			r.cr(w)
   829  		}
   830  	default:
   831  		panic("Unknown node type " + node.Type.String())
   832  	}
   833  	return GoToNext
   834  }
   835  
   836  // RenderHeader writes HTML document preamble and TOC if requested.
   837  func (r *HTMLRenderer) RenderHeader(w io.Writer, ast *Node) {
   838  	r.writeDocumentHeader(w)
   839  	if r.Flags&TOC != 0 {
   840  		r.writeTOC(w, ast)
   841  	}
   842  }
   843  
   844  // RenderFooter writes HTML document footer.
   845  func (r *HTMLRenderer) RenderFooter(w io.Writer, ast *Node) {
   846  	if r.Flags&CompletePage == 0 {
   847  		return
   848  	}
   849  	io.WriteString(w, "\n</body>\n</html>\n")
   850  }
   851  
   852  func (r *HTMLRenderer) writeDocumentHeader(w io.Writer) {
   853  	if r.Flags&CompletePage == 0 {
   854  		return
   855  	}
   856  	ending := ""
   857  	if r.Flags&UseXHTML != 0 {
   858  		io.WriteString(w, "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" ")
   859  		io.WriteString(w, "\"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">\n")
   860  		io.WriteString(w, "<html xmlns=\"http://www.w3.org/1999/xhtml\">\n")
   861  		ending = " /"
   862  	} else {
   863  		io.WriteString(w, "<!DOCTYPE html>\n")
   864  		io.WriteString(w, "<html>\n")
   865  	}
   866  	io.WriteString(w, "<head>\n")
   867  	io.WriteString(w, "  <title>")
   868  	if r.Flags&Smartypants != 0 {
   869  		r.sr.Process(w, []byte(r.Title))
   870  	} else {
   871  		escapeHTML(w, []byte(r.Title))
   872  	}
   873  	io.WriteString(w, "</title>\n")
   874  	io.WriteString(w, "  <meta name=\"GENERATOR\" content=\"Blackfriday Markdown Processor v")
   875  	io.WriteString(w, Version)
   876  	io.WriteString(w, "\"")
   877  	io.WriteString(w, ending)
   878  	io.WriteString(w, ">\n")
   879  	io.WriteString(w, "  <meta charset=\"utf-8\"")
   880  	io.WriteString(w, ending)
   881  	io.WriteString(w, ">\n")
   882  	if r.CSS != "" {
   883  		io.WriteString(w, "  <link rel=\"stylesheet\" type=\"text/css\" href=\"")
   884  		escapeHTML(w, []byte(r.CSS))
   885  		io.WriteString(w, "\"")
   886  		io.WriteString(w, ending)
   887  		io.WriteString(w, ">\n")
   888  	}
   889  	if r.Icon != "" {
   890  		io.WriteString(w, "  <link rel=\"icon\" type=\"image/x-icon\" href=\"")
   891  		escapeHTML(w, []byte(r.Icon))
   892  		io.WriteString(w, "\"")
   893  		io.WriteString(w, ending)
   894  		io.WriteString(w, ">\n")
   895  	}
   896  	io.WriteString(w, "</head>\n")
   897  	io.WriteString(w, "<body>\n\n")
   898  }
   899  
   900  func (r *HTMLRenderer) writeTOC(w io.Writer, ast *Node) {
   901  	buf := bytes.Buffer{}
   902  
   903  	inHeading := false
   904  	tocLevel := 0
   905  	headingCount := 0
   906  
   907  	ast.Walk(func(node *Node, entering bool) WalkStatus {
   908  		if node.Type == Heading && !node.HeadingData.IsTitleblock {
   909  			inHeading = entering
   910  			if entering {
   911  				node.HeadingID = fmt.Sprintf("toc_%d", headingCount)
   912  				if node.Level == tocLevel {
   913  					buf.WriteString("</li>\n\n<li>")
   914  				} else if node.Level < tocLevel {
   915  					for node.Level < tocLevel {
   916  						tocLevel--
   917  						buf.WriteString("</li>\n</ul>")
   918  					}
   919  					buf.WriteString("</li>\n\n<li>")
   920  				} else {
   921  					for node.Level > tocLevel {
   922  						tocLevel++
   923  						buf.WriteString("\n<ul>\n<li>")
   924  					}
   925  				}
   926  
   927  				fmt.Fprintf(&buf, `<a href="#toc_%d">`, headingCount)
   928  				headingCount++
   929  			} else {
   930  				buf.WriteString("</a>")
   931  			}
   932  			return GoToNext
   933  		}
   934  
   935  		if inHeading {
   936  			return r.RenderNode(&buf, node, entering)
   937  		}
   938  
   939  		return GoToNext
   940  	})
   941  
   942  	for ; tocLevel > 0; tocLevel-- {
   943  		buf.WriteString("</li>\n</ul>")
   944  	}
   945  
   946  	if buf.Len() > 0 {
   947  		io.WriteString(w, "<nav>\n")
   948  		w.Write(buf.Bytes())
   949  		io.WriteString(w, "\n\n</nav>\n")
   950  	}
   951  	r.lastOutputLen = buf.Len()
   952  }
   953  

View as plain text