vendor/github.com/russross/blackfriday/html.go
changeset 251 1c52a0eeb952
equal deleted inserted replaced
250:c040f992052f 251:1c52a0eeb952
       
     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 	"regexp"
       
    22 	"strconv"
       
    23 	"strings"
       
    24 )
       
    25 
       
    26 // Html renderer configuration options.
       
    27 const (
       
    28 	HTML_SKIP_HTML                 = 1 << iota // skip preformatted HTML blocks
       
    29 	HTML_SKIP_STYLE                            // skip embedded <style> elements
       
    30 	HTML_SKIP_IMAGES                           // skip embedded images
       
    31 	HTML_SKIP_LINKS                            // skip all links
       
    32 	HTML_SAFELINK                              // only link to trusted protocols
       
    33 	HTML_NOFOLLOW_LINKS                        // only link with rel="nofollow"
       
    34 	HTML_NOREFERRER_LINKS                      // only link with rel="noreferrer"
       
    35 	HTML_HREF_TARGET_BLANK                     // add a blank target
       
    36 	HTML_TOC                                   // generate a table of contents
       
    37 	HTML_OMIT_CONTENTS                         // skip the main contents (for a standalone table of contents)
       
    38 	HTML_COMPLETE_PAGE                         // generate a complete HTML page
       
    39 	HTML_USE_XHTML                             // generate XHTML output instead of HTML
       
    40 	HTML_USE_SMARTYPANTS                       // enable smart punctuation substitutions
       
    41 	HTML_SMARTYPANTS_FRACTIONS                 // enable smart fractions (with HTML_USE_SMARTYPANTS)
       
    42 	HTML_SMARTYPANTS_DASHES                    // enable smart dashes (with HTML_USE_SMARTYPANTS)
       
    43 	HTML_SMARTYPANTS_LATEX_DASHES              // enable LaTeX-style dashes (with HTML_USE_SMARTYPANTS and HTML_SMARTYPANTS_DASHES)
       
    44 	HTML_SMARTYPANTS_ANGLED_QUOTES             // enable angled double quotes (with HTML_USE_SMARTYPANTS) for double quotes rendering
       
    45 	HTML_SMARTYPANTS_QUOTES_NBSP               // enable "French guillemets" (with HTML_USE_SMARTYPANTS)
       
    46 	HTML_FOOTNOTE_RETURN_LINKS                 // generate a link at the end of a footnote to return to the source
       
    47 )
       
    48 
       
    49 var (
       
    50 	alignments = []string{
       
    51 		"left",
       
    52 		"right",
       
    53 		"center",
       
    54 	}
       
    55 
       
    56 	// TODO: improve this regexp to catch all possible entities:
       
    57 	htmlEntity = regexp.MustCompile(`&[a-z]{2,5};`)
       
    58 )
       
    59 
       
    60 type HtmlRendererParameters struct {
       
    61 	// Prepend this text to each relative URL.
       
    62 	AbsolutePrefix string
       
    63 	// Add this text to each footnote anchor, to ensure uniqueness.
       
    64 	FootnoteAnchorPrefix string
       
    65 	// Show this text inside the <a> tag for a footnote return link, if the
       
    66 	// HTML_FOOTNOTE_RETURN_LINKS flag is enabled. If blank, the string
       
    67 	// <sup>[return]</sup> is used.
       
    68 	FootnoteReturnLinkContents string
       
    69 	// If set, add this text to the front of each Header ID, to ensure
       
    70 	// uniqueness.
       
    71 	HeaderIDPrefix string
       
    72 	// If set, add this text to the back of each Header ID, to ensure uniqueness.
       
    73 	HeaderIDSuffix string
       
    74 }
       
    75 
       
    76 // Html is a type that implements the Renderer interface for HTML output.
       
    77 //
       
    78 // Do not create this directly, instead use the HtmlRenderer function.
       
    79 type Html struct {
       
    80 	flags    int    // HTML_* options
       
    81 	closeTag string // how to end singleton tags: either " />" or ">"
       
    82 	title    string // document title
       
    83 	css      string // optional css file url (used with HTML_COMPLETE_PAGE)
       
    84 
       
    85 	parameters HtmlRendererParameters
       
    86 
       
    87 	// table of contents data
       
    88 	tocMarker    int
       
    89 	headerCount  int
       
    90 	currentLevel int
       
    91 	toc          *bytes.Buffer
       
    92 
       
    93 	// Track header IDs to prevent ID collision in a single generation.
       
    94 	headerIDs map[string]int
       
    95 
       
    96 	smartypants *smartypantsRenderer
       
    97 }
       
    98 
       
    99 const (
       
   100 	xhtmlClose = " />"
       
   101 	htmlClose  = ">"
       
   102 )
       
   103 
       
   104 // HtmlRenderer creates and configures an Html object, which
       
   105 // satisfies the Renderer interface.
       
   106 //
       
   107 // flags is a set of HTML_* options ORed together.
       
   108 // title is the title of the document, and css is a URL for the document's
       
   109 // stylesheet.
       
   110 // title and css are only used when HTML_COMPLETE_PAGE is selected.
       
   111 func HtmlRenderer(flags int, title string, css string) Renderer {
       
   112 	return HtmlRendererWithParameters(flags, title, css, HtmlRendererParameters{})
       
   113 }
       
   114 
       
   115 func HtmlRendererWithParameters(flags int, title string,
       
   116 	css string, renderParameters HtmlRendererParameters) Renderer {
       
   117 	// configure the rendering engine
       
   118 	closeTag := htmlClose
       
   119 	if flags&HTML_USE_XHTML != 0 {
       
   120 		closeTag = xhtmlClose
       
   121 	}
       
   122 
       
   123 	if renderParameters.FootnoteReturnLinkContents == "" {
       
   124 		renderParameters.FootnoteReturnLinkContents = `<sup>[return]</sup>`
       
   125 	}
       
   126 
       
   127 	return &Html{
       
   128 		flags:      flags,
       
   129 		closeTag:   closeTag,
       
   130 		title:      title,
       
   131 		css:        css,
       
   132 		parameters: renderParameters,
       
   133 
       
   134 		headerCount:  0,
       
   135 		currentLevel: 0,
       
   136 		toc:          new(bytes.Buffer),
       
   137 
       
   138 		headerIDs: make(map[string]int),
       
   139 
       
   140 		smartypants: smartypants(flags),
       
   141 	}
       
   142 }
       
   143 
       
   144 // Using if statements is a bit faster than a switch statement. As the compiler
       
   145 // improves, this should be unnecessary this is only worthwhile because
       
   146 // attrEscape is the single largest CPU user in normal use.
       
   147 // Also tried using map, but that gave a ~3x slowdown.
       
   148 func escapeSingleChar(char byte) (string, bool) {
       
   149 	if char == '"' {
       
   150 		return "&quot;", true
       
   151 	}
       
   152 	if char == '&' {
       
   153 		return "&amp;", true
       
   154 	}
       
   155 	if char == '<' {
       
   156 		return "&lt;", true
       
   157 	}
       
   158 	if char == '>' {
       
   159 		return "&gt;", true
       
   160 	}
       
   161 	return "", false
       
   162 }
       
   163 
       
   164 func attrEscape(out *bytes.Buffer, src []byte) {
       
   165 	org := 0
       
   166 	for i, ch := range src {
       
   167 		if entity, ok := escapeSingleChar(ch); ok {
       
   168 			if i > org {
       
   169 				// copy all the normal characters since the last escape
       
   170 				out.Write(src[org:i])
       
   171 			}
       
   172 			org = i + 1
       
   173 			out.WriteString(entity)
       
   174 		}
       
   175 	}
       
   176 	if org < len(src) {
       
   177 		out.Write(src[org:])
       
   178 	}
       
   179 }
       
   180 
       
   181 func entityEscapeWithSkip(out *bytes.Buffer, src []byte, skipRanges [][]int) {
       
   182 	end := 0
       
   183 	for _, rang := range skipRanges {
       
   184 		attrEscape(out, src[end:rang[0]])
       
   185 		out.Write(src[rang[0]:rang[1]])
       
   186 		end = rang[1]
       
   187 	}
       
   188 	attrEscape(out, src[end:])
       
   189 }
       
   190 
       
   191 func (options *Html) GetFlags() int {
       
   192 	return options.flags
       
   193 }
       
   194 
       
   195 func (options *Html) TitleBlock(out *bytes.Buffer, text []byte) {
       
   196 	text = bytes.TrimPrefix(text, []byte("% "))
       
   197 	text = bytes.Replace(text, []byte("\n% "), []byte("\n"), -1)
       
   198 	out.WriteString("<h1 class=\"title\">")
       
   199 	out.Write(text)
       
   200 	out.WriteString("\n</h1>")
       
   201 }
       
   202 
       
   203 func (options *Html) Header(out *bytes.Buffer, text func() bool, level int, id string) {
       
   204 	marker := out.Len()
       
   205 	doubleSpace(out)
       
   206 
       
   207 	if id == "" && options.flags&HTML_TOC != 0 {
       
   208 		id = fmt.Sprintf("toc_%d", options.headerCount)
       
   209 	}
       
   210 
       
   211 	if id != "" {
       
   212 		id = options.ensureUniqueHeaderID(id)
       
   213 
       
   214 		if options.parameters.HeaderIDPrefix != "" {
       
   215 			id = options.parameters.HeaderIDPrefix + id
       
   216 		}
       
   217 
       
   218 		if options.parameters.HeaderIDSuffix != "" {
       
   219 			id = id + options.parameters.HeaderIDSuffix
       
   220 		}
       
   221 
       
   222 		out.WriteString(fmt.Sprintf("<h%d id=\"%s\">", level, id))
       
   223 	} else {
       
   224 		out.WriteString(fmt.Sprintf("<h%d>", level))
       
   225 	}
       
   226 
       
   227 	tocMarker := out.Len()
       
   228 	if !text() {
       
   229 		out.Truncate(marker)
       
   230 		return
       
   231 	}
       
   232 
       
   233 	// are we building a table of contents?
       
   234 	if options.flags&HTML_TOC != 0 {
       
   235 		options.TocHeaderWithAnchor(out.Bytes()[tocMarker:], level, id)
       
   236 	}
       
   237 
       
   238 	out.WriteString(fmt.Sprintf("</h%d>\n", level))
       
   239 }
       
   240 
       
   241 func (options *Html) BlockHtml(out *bytes.Buffer, text []byte) {
       
   242 	if options.flags&HTML_SKIP_HTML != 0 {
       
   243 		return
       
   244 	}
       
   245 
       
   246 	doubleSpace(out)
       
   247 	out.Write(text)
       
   248 	out.WriteByte('\n')
       
   249 }
       
   250 
       
   251 func (options *Html) HRule(out *bytes.Buffer) {
       
   252 	doubleSpace(out)
       
   253 	out.WriteString("<hr")
       
   254 	out.WriteString(options.closeTag)
       
   255 	out.WriteByte('\n')
       
   256 }
       
   257 
       
   258 func (options *Html) BlockCode(out *bytes.Buffer, text []byte, info string) {
       
   259 	doubleSpace(out)
       
   260 
       
   261 	endOfLang := strings.IndexAny(info, "\t ")
       
   262 	if endOfLang < 0 {
       
   263 		endOfLang = len(info)
       
   264 	}
       
   265 	lang := info[:endOfLang]
       
   266 	if len(lang) == 0 || lang == "." {
       
   267 		out.WriteString("<pre><code>")
       
   268 	} else {
       
   269 		out.WriteString("<pre><code class=\"language-")
       
   270 		attrEscape(out, []byte(lang))
       
   271 		out.WriteString("\">")
       
   272 	}
       
   273 	attrEscape(out, text)
       
   274 	out.WriteString("</code></pre>\n")
       
   275 }
       
   276 
       
   277 func (options *Html) BlockQuote(out *bytes.Buffer, text []byte) {
       
   278 	doubleSpace(out)
       
   279 	out.WriteString("<blockquote>\n")
       
   280 	out.Write(text)
       
   281 	out.WriteString("</blockquote>\n")
       
   282 }
       
   283 
       
   284 func (options *Html) Table(out *bytes.Buffer, header []byte, body []byte, columnData []int) {
       
   285 	doubleSpace(out)
       
   286 	out.WriteString("<table>\n<thead>\n")
       
   287 	out.Write(header)
       
   288 	out.WriteString("</thead>\n\n<tbody>\n")
       
   289 	out.Write(body)
       
   290 	out.WriteString("</tbody>\n</table>\n")
       
   291 }
       
   292 
       
   293 func (options *Html) TableRow(out *bytes.Buffer, text []byte) {
       
   294 	doubleSpace(out)
       
   295 	out.WriteString("<tr>\n")
       
   296 	out.Write(text)
       
   297 	out.WriteString("\n</tr>\n")
       
   298 }
       
   299 
       
   300 func (options *Html) TableHeaderCell(out *bytes.Buffer, text []byte, align int) {
       
   301 	doubleSpace(out)
       
   302 	switch align {
       
   303 	case TABLE_ALIGNMENT_LEFT:
       
   304 		out.WriteString("<th align=\"left\">")
       
   305 	case TABLE_ALIGNMENT_RIGHT:
       
   306 		out.WriteString("<th align=\"right\">")
       
   307 	case TABLE_ALIGNMENT_CENTER:
       
   308 		out.WriteString("<th align=\"center\">")
       
   309 	default:
       
   310 		out.WriteString("<th>")
       
   311 	}
       
   312 
       
   313 	out.Write(text)
       
   314 	out.WriteString("</th>")
       
   315 }
       
   316 
       
   317 func (options *Html) TableCell(out *bytes.Buffer, text []byte, align int) {
       
   318 	doubleSpace(out)
       
   319 	switch align {
       
   320 	case TABLE_ALIGNMENT_LEFT:
       
   321 		out.WriteString("<td align=\"left\">")
       
   322 	case TABLE_ALIGNMENT_RIGHT:
       
   323 		out.WriteString("<td align=\"right\">")
       
   324 	case TABLE_ALIGNMENT_CENTER:
       
   325 		out.WriteString("<td align=\"center\">")
       
   326 	default:
       
   327 		out.WriteString("<td>")
       
   328 	}
       
   329 
       
   330 	out.Write(text)
       
   331 	out.WriteString("</td>")
       
   332 }
       
   333 
       
   334 func (options *Html) Footnotes(out *bytes.Buffer, text func() bool) {
       
   335 	out.WriteString("<div class=\"footnotes\">\n")
       
   336 	options.HRule(out)
       
   337 	options.List(out, text, LIST_TYPE_ORDERED)
       
   338 	out.WriteString("</div>\n")
       
   339 }
       
   340 
       
   341 func (options *Html) FootnoteItem(out *bytes.Buffer, name, text []byte, flags int) {
       
   342 	if flags&LIST_ITEM_CONTAINS_BLOCK != 0 || flags&LIST_ITEM_BEGINNING_OF_LIST != 0 {
       
   343 		doubleSpace(out)
       
   344 	}
       
   345 	slug := slugify(name)
       
   346 	out.WriteString(`<li id="`)
       
   347 	out.WriteString(`fn:`)
       
   348 	out.WriteString(options.parameters.FootnoteAnchorPrefix)
       
   349 	out.Write(slug)
       
   350 	out.WriteString(`">`)
       
   351 	out.Write(text)
       
   352 	if options.flags&HTML_FOOTNOTE_RETURN_LINKS != 0 {
       
   353 		out.WriteString(` <a class="footnote-return" href="#`)
       
   354 		out.WriteString(`fnref:`)
       
   355 		out.WriteString(options.parameters.FootnoteAnchorPrefix)
       
   356 		out.Write(slug)
       
   357 		out.WriteString(`">`)
       
   358 		out.WriteString(options.parameters.FootnoteReturnLinkContents)
       
   359 		out.WriteString(`</a>`)
       
   360 	}
       
   361 	out.WriteString("</li>\n")
       
   362 }
       
   363 
       
   364 func (options *Html) List(out *bytes.Buffer, text func() bool, flags int) {
       
   365 	marker := out.Len()
       
   366 	doubleSpace(out)
       
   367 
       
   368 	if flags&LIST_TYPE_DEFINITION != 0 {
       
   369 		out.WriteString("<dl>")
       
   370 	} else if flags&LIST_TYPE_ORDERED != 0 {
       
   371 		out.WriteString("<ol>")
       
   372 	} else {
       
   373 		out.WriteString("<ul>")
       
   374 	}
       
   375 	if !text() {
       
   376 		out.Truncate(marker)
       
   377 		return
       
   378 	}
       
   379 	if flags&LIST_TYPE_DEFINITION != 0 {
       
   380 		out.WriteString("</dl>\n")
       
   381 	} else if flags&LIST_TYPE_ORDERED != 0 {
       
   382 		out.WriteString("</ol>\n")
       
   383 	} else {
       
   384 		out.WriteString("</ul>\n")
       
   385 	}
       
   386 }
       
   387 
       
   388 func (options *Html) ListItem(out *bytes.Buffer, text []byte, flags int) {
       
   389 	if (flags&LIST_ITEM_CONTAINS_BLOCK != 0 && flags&LIST_TYPE_DEFINITION == 0) ||
       
   390 		flags&LIST_ITEM_BEGINNING_OF_LIST != 0 {
       
   391 		doubleSpace(out)
       
   392 	}
       
   393 	if flags&LIST_TYPE_TERM != 0 {
       
   394 		out.WriteString("<dt>")
       
   395 	} else if flags&LIST_TYPE_DEFINITION != 0 {
       
   396 		out.WriteString("<dd>")
       
   397 	} else {
       
   398 		out.WriteString("<li>")
       
   399 	}
       
   400 	out.Write(text)
       
   401 	if flags&LIST_TYPE_TERM != 0 {
       
   402 		out.WriteString("</dt>\n")
       
   403 	} else if flags&LIST_TYPE_DEFINITION != 0 {
       
   404 		out.WriteString("</dd>\n")
       
   405 	} else {
       
   406 		out.WriteString("</li>\n")
       
   407 	}
       
   408 }
       
   409 
       
   410 func (options *Html) Paragraph(out *bytes.Buffer, text func() bool) {
       
   411 	marker := out.Len()
       
   412 	doubleSpace(out)
       
   413 
       
   414 	out.WriteString("<p>")
       
   415 	if !text() {
       
   416 		out.Truncate(marker)
       
   417 		return
       
   418 	}
       
   419 	out.WriteString("</p>\n")
       
   420 }
       
   421 
       
   422 func (options *Html) AutoLink(out *bytes.Buffer, link []byte, kind int) {
       
   423 	skipRanges := htmlEntity.FindAllIndex(link, -1)
       
   424 	if options.flags&HTML_SAFELINK != 0 && !isSafeLink(link) && kind != LINK_TYPE_EMAIL {
       
   425 		// mark it but don't link it if it is not a safe link: no smartypants
       
   426 		out.WriteString("<tt>")
       
   427 		entityEscapeWithSkip(out, link, skipRanges)
       
   428 		out.WriteString("</tt>")
       
   429 		return
       
   430 	}
       
   431 
       
   432 	out.WriteString("<a href=\"")
       
   433 	if kind == LINK_TYPE_EMAIL {
       
   434 		out.WriteString("mailto:")
       
   435 	} else {
       
   436 		options.maybeWriteAbsolutePrefix(out, link)
       
   437 	}
       
   438 
       
   439 	entityEscapeWithSkip(out, link, skipRanges)
       
   440 
       
   441 	var relAttrs []string
       
   442 	if options.flags&HTML_NOFOLLOW_LINKS != 0 && !isRelativeLink(link) {
       
   443 		relAttrs = append(relAttrs, "nofollow")
       
   444 	}
       
   445 	if options.flags&HTML_NOREFERRER_LINKS != 0 && !isRelativeLink(link) {
       
   446 		relAttrs = append(relAttrs, "noreferrer")
       
   447 	}
       
   448 	if len(relAttrs) > 0 {
       
   449 		out.WriteString(fmt.Sprintf("\" rel=\"%s", strings.Join(relAttrs, " ")))
       
   450 	}
       
   451 
       
   452 	// blank target only add to external link
       
   453 	if options.flags&HTML_HREF_TARGET_BLANK != 0 && !isRelativeLink(link) {
       
   454 		out.WriteString("\" target=\"_blank")
       
   455 	}
       
   456 
       
   457 	out.WriteString("\">")
       
   458 
       
   459 	// Pretty print: if we get an email address as
       
   460 	// an actual URI, e.g. `mailto:foo@bar.com`, we don't
       
   461 	// want to print the `mailto:` prefix
       
   462 	switch {
       
   463 	case bytes.HasPrefix(link, []byte("mailto://")):
       
   464 		attrEscape(out, link[len("mailto://"):])
       
   465 	case bytes.HasPrefix(link, []byte("mailto:")):
       
   466 		attrEscape(out, link[len("mailto:"):])
       
   467 	default:
       
   468 		entityEscapeWithSkip(out, link, skipRanges)
       
   469 	}
       
   470 
       
   471 	out.WriteString("</a>")
       
   472 }
       
   473 
       
   474 func (options *Html) CodeSpan(out *bytes.Buffer, text []byte) {
       
   475 	out.WriteString("<code>")
       
   476 	attrEscape(out, text)
       
   477 	out.WriteString("</code>")
       
   478 }
       
   479 
       
   480 func (options *Html) DoubleEmphasis(out *bytes.Buffer, text []byte) {
       
   481 	out.WriteString("<strong>")
       
   482 	out.Write(text)
       
   483 	out.WriteString("</strong>")
       
   484 }
       
   485 
       
   486 func (options *Html) Emphasis(out *bytes.Buffer, text []byte) {
       
   487 	if len(text) == 0 {
       
   488 		return
       
   489 	}
       
   490 	out.WriteString("<em>")
       
   491 	out.Write(text)
       
   492 	out.WriteString("</em>")
       
   493 }
       
   494 
       
   495 func (options *Html) maybeWriteAbsolutePrefix(out *bytes.Buffer, link []byte) {
       
   496 	if options.parameters.AbsolutePrefix != "" && isRelativeLink(link) && link[0] != '.' {
       
   497 		out.WriteString(options.parameters.AbsolutePrefix)
       
   498 		if link[0] != '/' {
       
   499 			out.WriteByte('/')
       
   500 		}
       
   501 	}
       
   502 }
       
   503 
       
   504 func (options *Html) Image(out *bytes.Buffer, link []byte, title []byte, alt []byte) {
       
   505 	if options.flags&HTML_SKIP_IMAGES != 0 {
       
   506 		return
       
   507 	}
       
   508 
       
   509 	out.WriteString("<img src=\"")
       
   510 	options.maybeWriteAbsolutePrefix(out, link)
       
   511 	attrEscape(out, link)
       
   512 	out.WriteString("\" alt=\"")
       
   513 	if len(alt) > 0 {
       
   514 		attrEscape(out, alt)
       
   515 	}
       
   516 	if len(title) > 0 {
       
   517 		out.WriteString("\" title=\"")
       
   518 		attrEscape(out, title)
       
   519 	}
       
   520 
       
   521 	out.WriteByte('"')
       
   522 	out.WriteString(options.closeTag)
       
   523 }
       
   524 
       
   525 func (options *Html) LineBreak(out *bytes.Buffer) {
       
   526 	out.WriteString("<br")
       
   527 	out.WriteString(options.closeTag)
       
   528 	out.WriteByte('\n')
       
   529 }
       
   530 
       
   531 func (options *Html) Link(out *bytes.Buffer, link []byte, title []byte, content []byte) {
       
   532 	if options.flags&HTML_SKIP_LINKS != 0 {
       
   533 		// write the link text out but don't link it, just mark it with typewriter font
       
   534 		out.WriteString("<tt>")
       
   535 		attrEscape(out, content)
       
   536 		out.WriteString("</tt>")
       
   537 		return
       
   538 	}
       
   539 
       
   540 	if options.flags&HTML_SAFELINK != 0 && !isSafeLink(link) {
       
   541 		// write the link text out but don't link it, just mark it with typewriter font
       
   542 		out.WriteString("<tt>")
       
   543 		attrEscape(out, content)
       
   544 		out.WriteString("</tt>")
       
   545 		return
       
   546 	}
       
   547 
       
   548 	out.WriteString("<a href=\"")
       
   549 	options.maybeWriteAbsolutePrefix(out, link)
       
   550 	attrEscape(out, link)
       
   551 	if len(title) > 0 {
       
   552 		out.WriteString("\" title=\"")
       
   553 		attrEscape(out, title)
       
   554 	}
       
   555 	var relAttrs []string
       
   556 	if options.flags&HTML_NOFOLLOW_LINKS != 0 && !isRelativeLink(link) {
       
   557 		relAttrs = append(relAttrs, "nofollow")
       
   558 	}
       
   559 	if options.flags&HTML_NOREFERRER_LINKS != 0 && !isRelativeLink(link) {
       
   560 		relAttrs = append(relAttrs, "noreferrer")
       
   561 	}
       
   562 	if len(relAttrs) > 0 {
       
   563 		out.WriteString(fmt.Sprintf("\" rel=\"%s", strings.Join(relAttrs, " ")))
       
   564 	}
       
   565 
       
   566 	// blank target only add to external link
       
   567 	if options.flags&HTML_HREF_TARGET_BLANK != 0 && !isRelativeLink(link) {
       
   568 		out.WriteString("\" target=\"_blank")
       
   569 	}
       
   570 
       
   571 	out.WriteString("\">")
       
   572 	out.Write(content)
       
   573 	out.WriteString("</a>")
       
   574 	return
       
   575 }
       
   576 
       
   577 func (options *Html) RawHtmlTag(out *bytes.Buffer, text []byte) {
       
   578 	if options.flags&HTML_SKIP_HTML != 0 {
       
   579 		return
       
   580 	}
       
   581 	if options.flags&HTML_SKIP_STYLE != 0 && isHtmlTag(text, "style") {
       
   582 		return
       
   583 	}
       
   584 	if options.flags&HTML_SKIP_LINKS != 0 && isHtmlTag(text, "a") {
       
   585 		return
       
   586 	}
       
   587 	if options.flags&HTML_SKIP_IMAGES != 0 && isHtmlTag(text, "img") {
       
   588 		return
       
   589 	}
       
   590 	out.Write(text)
       
   591 }
       
   592 
       
   593 func (options *Html) TripleEmphasis(out *bytes.Buffer, text []byte) {
       
   594 	out.WriteString("<strong><em>")
       
   595 	out.Write(text)
       
   596 	out.WriteString("</em></strong>")
       
   597 }
       
   598 
       
   599 func (options *Html) StrikeThrough(out *bytes.Buffer, text []byte) {
       
   600 	out.WriteString("<del>")
       
   601 	out.Write(text)
       
   602 	out.WriteString("</del>")
       
   603 }
       
   604 
       
   605 func (options *Html) FootnoteRef(out *bytes.Buffer, ref []byte, id int) {
       
   606 	slug := slugify(ref)
       
   607 	out.WriteString(`<sup class="footnote-ref" id="`)
       
   608 	out.WriteString(`fnref:`)
       
   609 	out.WriteString(options.parameters.FootnoteAnchorPrefix)
       
   610 	out.Write(slug)
       
   611 	out.WriteString(`"><a href="#`)
       
   612 	out.WriteString(`fn:`)
       
   613 	out.WriteString(options.parameters.FootnoteAnchorPrefix)
       
   614 	out.Write(slug)
       
   615 	out.WriteString(`">`)
       
   616 	out.WriteString(strconv.Itoa(id))
       
   617 	out.WriteString(`</a></sup>`)
       
   618 }
       
   619 
       
   620 func (options *Html) Entity(out *bytes.Buffer, entity []byte) {
       
   621 	out.Write(entity)
       
   622 }
       
   623 
       
   624 func (options *Html) NormalText(out *bytes.Buffer, text []byte) {
       
   625 	if options.flags&HTML_USE_SMARTYPANTS != 0 {
       
   626 		options.Smartypants(out, text)
       
   627 	} else {
       
   628 		attrEscape(out, text)
       
   629 	}
       
   630 }
       
   631 
       
   632 func (options *Html) Smartypants(out *bytes.Buffer, text []byte) {
       
   633 	smrt := smartypantsData{false, false}
       
   634 
       
   635 	// first do normal entity escaping
       
   636 	var escaped bytes.Buffer
       
   637 	attrEscape(&escaped, text)
       
   638 	text = escaped.Bytes()
       
   639 
       
   640 	mark := 0
       
   641 	for i := 0; i < len(text); i++ {
       
   642 		if action := options.smartypants[text[i]]; action != nil {
       
   643 			if i > mark {
       
   644 				out.Write(text[mark:i])
       
   645 			}
       
   646 
       
   647 			previousChar := byte(0)
       
   648 			if i > 0 {
       
   649 				previousChar = text[i-1]
       
   650 			}
       
   651 			i += action(out, &smrt, previousChar, text[i:])
       
   652 			mark = i + 1
       
   653 		}
       
   654 	}
       
   655 
       
   656 	if mark < len(text) {
       
   657 		out.Write(text[mark:])
       
   658 	}
       
   659 }
       
   660 
       
   661 func (options *Html) DocumentHeader(out *bytes.Buffer) {
       
   662 	if options.flags&HTML_COMPLETE_PAGE == 0 {
       
   663 		return
       
   664 	}
       
   665 
       
   666 	ending := ""
       
   667 	if options.flags&HTML_USE_XHTML != 0 {
       
   668 		out.WriteString("<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" ")
       
   669 		out.WriteString("\"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">\n")
       
   670 		out.WriteString("<html xmlns=\"http://www.w3.org/1999/xhtml\">\n")
       
   671 		ending = " /"
       
   672 	} else {
       
   673 		out.WriteString("<!DOCTYPE html>\n")
       
   674 		out.WriteString("<html>\n")
       
   675 	}
       
   676 	out.WriteString("<head>\n")
       
   677 	out.WriteString("  <title>")
       
   678 	options.NormalText(out, []byte(options.title))
       
   679 	out.WriteString("</title>\n")
       
   680 	out.WriteString("  <meta name=\"GENERATOR\" content=\"Blackfriday Markdown Processor v")
       
   681 	out.WriteString(VERSION)
       
   682 	out.WriteString("\"")
       
   683 	out.WriteString(ending)
       
   684 	out.WriteString(">\n")
       
   685 	out.WriteString("  <meta charset=\"utf-8\"")
       
   686 	out.WriteString(ending)
       
   687 	out.WriteString(">\n")
       
   688 	if options.css != "" {
       
   689 		out.WriteString("  <link rel=\"stylesheet\" type=\"text/css\" href=\"")
       
   690 		attrEscape(out, []byte(options.css))
       
   691 		out.WriteString("\"")
       
   692 		out.WriteString(ending)
       
   693 		out.WriteString(">\n")
       
   694 	}
       
   695 	out.WriteString("</head>\n")
       
   696 	out.WriteString("<body>\n")
       
   697 
       
   698 	options.tocMarker = out.Len()
       
   699 }
       
   700 
       
   701 func (options *Html) DocumentFooter(out *bytes.Buffer) {
       
   702 	// finalize and insert the table of contents
       
   703 	if options.flags&HTML_TOC != 0 {
       
   704 		options.TocFinalize()
       
   705 
       
   706 		// now we have to insert the table of contents into the document
       
   707 		var temp bytes.Buffer
       
   708 
       
   709 		// start by making a copy of everything after the document header
       
   710 		temp.Write(out.Bytes()[options.tocMarker:])
       
   711 
       
   712 		// now clear the copied material from the main output buffer
       
   713 		out.Truncate(options.tocMarker)
       
   714 
       
   715 		// corner case spacing issue
       
   716 		if options.flags&HTML_COMPLETE_PAGE != 0 {
       
   717 			out.WriteByte('\n')
       
   718 		}
       
   719 
       
   720 		// insert the table of contents
       
   721 		out.WriteString("<nav>\n")
       
   722 		out.Write(options.toc.Bytes())
       
   723 		out.WriteString("</nav>\n")
       
   724 
       
   725 		// corner case spacing issue
       
   726 		if options.flags&HTML_COMPLETE_PAGE == 0 && options.flags&HTML_OMIT_CONTENTS == 0 {
       
   727 			out.WriteByte('\n')
       
   728 		}
       
   729 
       
   730 		// write out everything that came after it
       
   731 		if options.flags&HTML_OMIT_CONTENTS == 0 {
       
   732 			out.Write(temp.Bytes())
       
   733 		}
       
   734 	}
       
   735 
       
   736 	if options.flags&HTML_COMPLETE_PAGE != 0 {
       
   737 		out.WriteString("\n</body>\n")
       
   738 		out.WriteString("</html>\n")
       
   739 	}
       
   740 
       
   741 }
       
   742 
       
   743 func (options *Html) TocHeaderWithAnchor(text []byte, level int, anchor string) {
       
   744 	for level > options.currentLevel {
       
   745 		switch {
       
   746 		case bytes.HasSuffix(options.toc.Bytes(), []byte("</li>\n")):
       
   747 			// this sublist can nest underneath a header
       
   748 			size := options.toc.Len()
       
   749 			options.toc.Truncate(size - len("</li>\n"))
       
   750 
       
   751 		case options.currentLevel > 0:
       
   752 			options.toc.WriteString("<li>")
       
   753 		}
       
   754 		if options.toc.Len() > 0 {
       
   755 			options.toc.WriteByte('\n')
       
   756 		}
       
   757 		options.toc.WriteString("<ul>\n")
       
   758 		options.currentLevel++
       
   759 	}
       
   760 
       
   761 	for level < options.currentLevel {
       
   762 		options.toc.WriteString("</ul>")
       
   763 		if options.currentLevel > 1 {
       
   764 			options.toc.WriteString("</li>\n")
       
   765 		}
       
   766 		options.currentLevel--
       
   767 	}
       
   768 
       
   769 	options.toc.WriteString("<li><a href=\"#")
       
   770 	if anchor != "" {
       
   771 		options.toc.WriteString(anchor)
       
   772 	} else {
       
   773 		options.toc.WriteString("toc_")
       
   774 		options.toc.WriteString(strconv.Itoa(options.headerCount))
       
   775 	}
       
   776 	options.toc.WriteString("\">")
       
   777 	options.headerCount++
       
   778 
       
   779 	options.toc.Write(text)
       
   780 
       
   781 	options.toc.WriteString("</a></li>\n")
       
   782 }
       
   783 
       
   784 func (options *Html) TocHeader(text []byte, level int) {
       
   785 	options.TocHeaderWithAnchor(text, level, "")
       
   786 }
       
   787 
       
   788 func (options *Html) TocFinalize() {
       
   789 	for options.currentLevel > 1 {
       
   790 		options.toc.WriteString("</ul></li>\n")
       
   791 		options.currentLevel--
       
   792 	}
       
   793 
       
   794 	if options.currentLevel > 0 {
       
   795 		options.toc.WriteString("</ul>\n")
       
   796 	}
       
   797 }
       
   798 
       
   799 func isHtmlTag(tag []byte, tagname string) bool {
       
   800 	found, _ := findHtmlTagPos(tag, tagname)
       
   801 	return found
       
   802 }
       
   803 
       
   804 // Look for a character, but ignore it when it's in any kind of quotes, it
       
   805 // might be JavaScript
       
   806 func skipUntilCharIgnoreQuotes(html []byte, start int, char byte) int {
       
   807 	inSingleQuote := false
       
   808 	inDoubleQuote := false
       
   809 	inGraveQuote := false
       
   810 	i := start
       
   811 	for i < len(html) {
       
   812 		switch {
       
   813 		case html[i] == char && !inSingleQuote && !inDoubleQuote && !inGraveQuote:
       
   814 			return i
       
   815 		case html[i] == '\'':
       
   816 			inSingleQuote = !inSingleQuote
       
   817 		case html[i] == '"':
       
   818 			inDoubleQuote = !inDoubleQuote
       
   819 		case html[i] == '`':
       
   820 			inGraveQuote = !inGraveQuote
       
   821 		}
       
   822 		i++
       
   823 	}
       
   824 	return start
       
   825 }
       
   826 
       
   827 func findHtmlTagPos(tag []byte, tagname string) (bool, int) {
       
   828 	i := 0
       
   829 	if i < len(tag) && tag[0] != '<' {
       
   830 		return false, -1
       
   831 	}
       
   832 	i++
       
   833 	i = skipSpace(tag, i)
       
   834 
       
   835 	if i < len(tag) && tag[i] == '/' {
       
   836 		i++
       
   837 	}
       
   838 
       
   839 	i = skipSpace(tag, i)
       
   840 	j := 0
       
   841 	for ; i < len(tag); i, j = i+1, j+1 {
       
   842 		if j >= len(tagname) {
       
   843 			break
       
   844 		}
       
   845 
       
   846 		if strings.ToLower(string(tag[i]))[0] != tagname[j] {
       
   847 			return false, -1
       
   848 		}
       
   849 	}
       
   850 
       
   851 	if i == len(tag) {
       
   852 		return false, -1
       
   853 	}
       
   854 
       
   855 	rightAngle := skipUntilCharIgnoreQuotes(tag, i, '>')
       
   856 	if rightAngle > i {
       
   857 		return true, rightAngle
       
   858 	}
       
   859 
       
   860 	return false, -1
       
   861 }
       
   862 
       
   863 func skipUntilChar(text []byte, start int, char byte) int {
       
   864 	i := start
       
   865 	for i < len(text) && text[i] != char {
       
   866 		i++
       
   867 	}
       
   868 	return i
       
   869 }
       
   870 
       
   871 func skipSpace(tag []byte, i int) int {
       
   872 	for i < len(tag) && isspace(tag[i]) {
       
   873 		i++
       
   874 	}
       
   875 	return i
       
   876 }
       
   877 
       
   878 func skipChar(data []byte, start int, char byte) int {
       
   879 	i := start
       
   880 	for i < len(data) && data[i] == char {
       
   881 		i++
       
   882 	}
       
   883 	return i
       
   884 }
       
   885 
       
   886 func doubleSpace(out *bytes.Buffer) {
       
   887 	if out.Len() > 0 {
       
   888 		out.WriteByte('\n')
       
   889 	}
       
   890 }
       
   891 
       
   892 func isRelativeLink(link []byte) (yes bool) {
       
   893 	// a tag begin with '#'
       
   894 	if link[0] == '#' {
       
   895 		return true
       
   896 	}
       
   897 
       
   898 	// link begin with '/' but not '//', the second maybe a protocol relative link
       
   899 	if len(link) >= 2 && link[0] == '/' && link[1] != '/' {
       
   900 		return true
       
   901 	}
       
   902 
       
   903 	// only the root '/'
       
   904 	if len(link) == 1 && link[0] == '/' {
       
   905 		return true
       
   906 	}
       
   907 
       
   908 	// current directory : begin with "./"
       
   909 	if bytes.HasPrefix(link, []byte("./")) {
       
   910 		return true
       
   911 	}
       
   912 
       
   913 	// parent directory : begin with "../"
       
   914 	if bytes.HasPrefix(link, []byte("../")) {
       
   915 		return true
       
   916 	}
       
   917 
       
   918 	return false
       
   919 }
       
   920 
       
   921 func (options *Html) ensureUniqueHeaderID(id string) string {
       
   922 	for count, found := options.headerIDs[id]; found; count, found = options.headerIDs[id] {
       
   923 		tmp := fmt.Sprintf("%s-%d", id, count+1)
       
   924 
       
   925 		if _, tmpFound := options.headerIDs[tmp]; !tmpFound {
       
   926 			options.headerIDs[id] = count + 1
       
   927 			id = tmp
       
   928 		} else {
       
   929 			id = id + "-1"
       
   930 		}
       
   931 	}
       
   932 
       
   933 	if _, found := options.headerIDs[id]; !found {
       
   934 		options.headerIDs[id] = 0
       
   935 	}
       
   936 
       
   937 	return id
       
   938 }