2011-05-29 05:17:53 +02:00
|
|
|
//
|
2011-06-28 04:11:32 +02:00
|
|
|
// Blackfriday Markdown Processor
|
|
|
|
// Available at http://github.com/russross/blackfriday
|
|
|
|
//
|
|
|
|
// Copyright © 2011 Russ Ross <russ@russross.com>.
|
2011-06-28 19:30:10 +02:00
|
|
|
// Distributed under the Simplified BSD License.
|
2011-06-28 04:11:32 +02:00
|
|
|
// See README.md for details.
|
2011-05-29 05:17:53 +02:00
|
|
|
//
|
|
|
|
|
|
|
|
//
|
|
|
|
//
|
|
|
|
// HTML rendering backend
|
|
|
|
//
|
|
|
|
//
|
|
|
|
|
|
|
|
package blackfriday
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bytes"
|
|
|
|
"fmt"
|
2016-03-30 10:57:02 +02:00
|
|
|
"html"
|
2016-03-30 20:13:02 +02:00
|
|
|
"io"
|
2014-01-22 00:14:35 +01:00
|
|
|
"regexp"
|
2011-05-29 05:17:53 +02:00
|
|
|
"strconv"
|
2011-06-29 23:38:35 +02:00
|
|
|
"strings"
|
2011-05-29 05:17:53 +02:00
|
|
|
)
|
|
|
|
|
2016-04-01 09:49:23 +02:00
|
|
|
type HTMLFlags int
|
2015-10-26 16:29:52 +01:00
|
|
|
|
2016-03-31 12:54:09 +02:00
|
|
|
// HTML renderer configuration options.
|
2011-05-29 05:17:53 +02:00
|
|
|
const (
|
2016-04-01 09:49:23 +02:00
|
|
|
HTMLFlagsNone HTMLFlags = 0
|
|
|
|
SkipHTML HTMLFlags = 1 << iota // Skip preformatted HTML blocks
|
2016-04-01 09:44:22 +02:00
|
|
|
SkipStyle // Skip embedded <style> elements
|
|
|
|
SkipImages // Skip embedded images
|
|
|
|
SkipLinks // Skip all links
|
|
|
|
Safelink // Only link to trusted protocols
|
|
|
|
NofollowLinks // Only link with rel="nofollow"
|
|
|
|
NoreferrerLinks // Only link with rel="noreferrer"
|
|
|
|
HrefTargetBlank // Add a blank target
|
|
|
|
CompletePage // Generate a complete HTML page
|
|
|
|
UseXHTML // Generate XHTML output instead of HTML
|
|
|
|
FootnoteReturnLinks // Generate a link at the end of a footnote to return to the source
|
2016-03-30 10:57:02 +02:00
|
|
|
|
|
|
|
TagName = "[A-Za-z][A-Za-z0-9-]*"
|
|
|
|
AttributeName = "[a-zA-Z_:][a-zA-Z0-9:._-]*"
|
|
|
|
UnquotedValue = "[^\"'=<>`\\x00-\\x20]+"
|
|
|
|
SingleQuotedValue = "'[^']*'"
|
|
|
|
DoubleQuotedValue = "\"[^\"]*\""
|
|
|
|
AttributeValue = "(?:" + UnquotedValue + "|" + SingleQuotedValue + "|" + DoubleQuotedValue + ")"
|
|
|
|
AttributeValueSpec = "(?:" + "\\s*=" + "\\s*" + AttributeValue + ")"
|
|
|
|
Attribute = "(?:" + "\\s+" + AttributeName + AttributeValueSpec + "?)"
|
|
|
|
OpenTag = "<" + TagName + Attribute + "*" + "\\s*/?>"
|
|
|
|
CloseTag = "</" + TagName + "\\s*[>]"
|
|
|
|
HTMLComment = "<!---->|<!--(?:-?[^>-])(?:-?[^-])*-->"
|
|
|
|
ProcessingInstruction = "[<][?].*?[?][>]"
|
|
|
|
Declaration = "<![A-Z]+" + "\\s+[^>]*>"
|
|
|
|
CDATA = "<!\\[CDATA\\[[\\s\\S]*?\\]\\]>"
|
|
|
|
HTMLTag = "(?:" + OpenTag + "|" + CloseTag + "|" + HTMLComment + "|" +
|
|
|
|
ProcessingInstruction + "|" + Declaration + "|" + CDATA + ")"
|
2011-05-29 05:17:53 +02:00
|
|
|
)
|
|
|
|
|
2014-01-22 00:14:35 +01:00
|
|
|
var (
|
2014-01-26 20:39:38 +01:00
|
|
|
// TODO: improve this regexp to catch all possible entities:
|
|
|
|
htmlEntity = regexp.MustCompile(`&[a-z]{2,5};`)
|
2016-03-30 10:57:02 +02:00
|
|
|
reHtmlTag = regexp.MustCompile("(?i)^" + HTMLTag)
|
2014-01-22 00:14:35 +01:00
|
|
|
)
|
|
|
|
|
2016-04-01 14:37:21 +02:00
|
|
|
type HTMLRendererParameters struct {
|
2014-05-29 16:17:20 +02:00
|
|
|
// Prepend this text to each relative URL.
|
2014-05-24 09:55:13 +02:00
|
|
|
AbsolutePrefix string
|
2014-05-29 06:52:45 +02:00
|
|
|
// Add this text to each footnote anchor, to ensure uniqueness.
|
2014-05-24 20:29:39 +02:00
|
|
|
FootnoteAnchorPrefix string
|
|
|
|
// Show this text inside the <a> tag for a footnote return link, if the
|
|
|
|
// HTML_FOOTNOTE_RETURN_LINKS flag is enabled. If blank, the string
|
|
|
|
// <sup>[return]</sup> is used.
|
|
|
|
FootnoteReturnLinkContents string
|
Allow configurable header ID prefix/suffixes.
This is specifically driven by the Hugo usecase where multiple documents
are often rendered into the same ultimate HTML page.
When a header ID is written to the output HTML format (either through
`HTML_TOC`, `EXTENSION_HEADER_IDS`, or `EXTENSION_AUTO_HEADER_IDS`), it
is possible that multiple documents will hvae identical header IDs. To
permit validation to pass, it is useful to have a per-document prefix or
suffix (in our case, an MD5 of the content filename, and we will be
using it as a suffix).
That is, two documents (`A` and `B`) that have the same header ID (`#
Reason {#reason}`), will end up having an actual header ID of the form
`#reason-DOCID` (e.g., `#reason-A`, `#reason-B`) with these HTML
parameters.
This is built on top of #126 (more intelligent collision detection for
`EXTENSION_AUTO_HEADER_IDS`).
2014-11-24 02:37:27 +01:00
|
|
|
// If set, add this text to the front of each Header ID, to ensure
|
|
|
|
// uniqueness.
|
|
|
|
HeaderIDPrefix string
|
|
|
|
// If set, add this text to the back of each Header ID, to ensure uniqueness.
|
|
|
|
HeaderIDSuffix string
|
2014-05-24 09:55:13 +02:00
|
|
|
}
|
|
|
|
|
2016-03-31 12:54:09 +02:00
|
|
|
// HTML is a type that implements the Renderer interface for HTML output.
|
2011-07-07 19:56:45 +02:00
|
|
|
//
|
2016-04-01 14:37:21 +02:00
|
|
|
// Do not create this directly, instead use the HTMLRenderer function.
|
2016-03-31 12:54:09 +02:00
|
|
|
type HTML struct {
|
2016-04-01 09:49:23 +02:00
|
|
|
flags HTMLFlags
|
2015-05-25 21:41:06 +02:00
|
|
|
closeTag string // how to end singleton tags: either " />" or ">"
|
2014-05-24 09:55:13 +02:00
|
|
|
title string // document title
|
|
|
|
css string // optional css file url (used with HTML_COMPLETE_PAGE)
|
|
|
|
|
2016-04-01 14:37:21 +02:00
|
|
|
parameters HTMLRendererParameters
|
2011-06-29 18:08:56 +02:00
|
|
|
|
|
|
|
// table of contents data
|
2011-06-29 18:36:56 +02:00
|
|
|
tocMarker int
|
2011-06-29 18:08:56 +02:00
|
|
|
headerCount int
|
|
|
|
currentLevel int
|
|
|
|
toc *bytes.Buffer
|
|
|
|
|
Prevent generated header collisions, less naively.
> This is a rework of an earlier version of this code.
The automatic header ID generation code submitted in #125 has a subtle
bug where it will use the same ID for multiple headers with identical
text. In the case below, all the headers are rendered a `<h1
id="header">Header</h1>`.
```markdown
# Header
# Header
# Header
# Header
```
This change is a simple but robust approach that uses an incrementing
counter and pre-checking to prevent header collision. (The above would
be rendered as `header`, `header-1`, `header-2`, and `header-3`.) In
more complex cases, it will append a new counter suffix (`-1`), like so:
```markdown
# Header
# Header 1
# Header
# Header
```
This will generate `header`, `header-1`, `header-1-1`, and `header-1-2`.
This code has two additional changes over the prior version:
1. Rather than reimplementing @shurcooL’s anchor sanitization code, I
have imported it as from
`github.com/shurcooL/go/github_flavored_markdown/sanitized_anchor_name`.
2. The markdown block parser is now only interested in *generating* a
sanitized anchor name, not with ensuring its uniqueness. That code
has been moved to the HTML renderer. This means that if the HTML
renderer is modified to identify all unique headers prior to
rendering, the hackish nature of the collision detection can be
eliminated.
2014-11-01 23:35:35 +01:00
|
|
|
// Track header IDs to prevent ID collision in a single generation.
|
|
|
|
headerIDs map[string]int
|
|
|
|
|
2016-04-01 14:37:21 +02:00
|
|
|
w HTMLWriter
|
2016-03-30 14:37:03 +02:00
|
|
|
lastOutputLen int
|
|
|
|
disableTags int
|
2016-04-01 09:44:22 +02:00
|
|
|
|
|
|
|
extensions Extensions // This gives Smartypants renderer access to flags
|
2011-05-29 05:17:53 +02:00
|
|
|
}
|
|
|
|
|
2011-06-29 19:13:17 +02:00
|
|
|
const (
|
2015-05-25 21:41:06 +02:00
|
|
|
xhtmlClose = " />"
|
|
|
|
htmlClose = ">"
|
2011-06-29 19:13:17 +02:00
|
|
|
)
|
2011-05-29 05:17:53 +02:00
|
|
|
|
2016-04-01 14:37:21 +02:00
|
|
|
// HTMLRenderer creates and configures an HTML object, which
|
2011-07-07 19:56:45 +02:00
|
|
|
// satisfies the Renderer interface.
|
|
|
|
//
|
2016-04-01 09:49:23 +02:00
|
|
|
// flags is a set of HTMLFlags ORed together.
|
2011-07-07 19:56:45 +02:00
|
|
|
// title is the title of the document, and css is a URL for the document's
|
|
|
|
// stylesheet.
|
|
|
|
// title and css are only used when HTML_COMPLETE_PAGE is selected.
|
2016-04-01 14:37:21 +02:00
|
|
|
func HTMLRenderer(flags HTMLFlags, extensions Extensions, title string, css string) Renderer {
|
|
|
|
return HTMLRendererWithParameters(flags, extensions, title, css, HTMLRendererParameters{})
|
2014-05-24 09:55:13 +02:00
|
|
|
}
|
|
|
|
|
2016-04-01 14:37:21 +02:00
|
|
|
type HTMLWriter struct {
|
2016-03-30 14:56:53 +02:00
|
|
|
output bytes.Buffer
|
2015-11-03 20:43:00 +01:00
|
|
|
}
|
|
|
|
|
2016-04-01 14:37:21 +02:00
|
|
|
func (w *HTMLWriter) Write(p []byte) (n int, err error) {
|
2015-11-03 20:43:00 +01:00
|
|
|
return w.output.Write(p)
|
|
|
|
}
|
|
|
|
|
2016-04-01 14:37:21 +02:00
|
|
|
func (w *HTMLWriter) WriteString(s string) (n int, err error) {
|
2015-11-03 20:43:00 +01:00
|
|
|
return w.output.WriteString(s)
|
|
|
|
}
|
|
|
|
|
2016-04-01 14:37:21 +02:00
|
|
|
func (w *HTMLWriter) WriteByte(b byte) error {
|
2015-11-03 20:43:00 +01:00
|
|
|
return w.output.WriteByte(b)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Writes out a newline if the output is not pristine. Used at the beginning of
|
|
|
|
// every rendering func
|
2016-04-01 14:37:21 +02:00
|
|
|
func (w *HTMLWriter) Newline() {
|
2016-03-30 14:56:53 +02:00
|
|
|
w.WriteByte('\n')
|
2015-11-03 20:43:00 +01:00
|
|
|
}
|
|
|
|
|
2016-03-31 12:54:09 +02:00
|
|
|
func (r *HTML) Write(b []byte) (int, error) {
|
2015-11-09 20:14:32 +01:00
|
|
|
return r.w.Write(b)
|
|
|
|
}
|
|
|
|
|
2016-04-01 14:37:21 +02:00
|
|
|
func HTMLRendererWithParameters(flags HTMLFlags, extensions Extensions, title string,
|
|
|
|
css string, renderParameters HTMLRendererParameters) Renderer {
|
2011-05-29 05:17:53 +02:00
|
|
|
// configure the rendering engine
|
2011-05-31 05:44:52 +02:00
|
|
|
closeTag := htmlClose
|
2015-10-26 17:16:57 +01:00
|
|
|
if flags&UseXHTML != 0 {
|
2011-05-31 05:44:52 +02:00
|
|
|
closeTag = xhtmlClose
|
2011-05-29 05:17:53 +02:00
|
|
|
}
|
|
|
|
|
2014-05-24 20:29:39 +02:00
|
|
|
if renderParameters.FootnoteReturnLinkContents == "" {
|
|
|
|
renderParameters.FootnoteReturnLinkContents = `<sup>[return]</sup>`
|
|
|
|
}
|
|
|
|
|
2016-04-01 14:37:21 +02:00
|
|
|
var writer HTMLWriter
|
2016-03-31 12:54:09 +02:00
|
|
|
return &HTML{
|
2014-05-24 09:55:13 +02:00
|
|
|
flags: flags,
|
2016-04-01 09:44:22 +02:00
|
|
|
extensions: extensions,
|
2014-05-24 09:55:13 +02:00
|
|
|
closeTag: closeTag,
|
|
|
|
title: title,
|
|
|
|
css: css,
|
|
|
|
parameters: renderParameters,
|
2011-05-29 05:17:53 +02:00
|
|
|
|
2011-06-29 18:08:56 +02:00
|
|
|
headerCount: 0,
|
|
|
|
currentLevel: 0,
|
2011-06-29 18:36:56 +02:00
|
|
|
toc: new(bytes.Buffer),
|
2011-05-29 05:17:53 +02:00
|
|
|
|
Prevent generated header collisions, less naively.
> This is a rework of an earlier version of this code.
The automatic header ID generation code submitted in #125 has a subtle
bug where it will use the same ID for multiple headers with identical
text. In the case below, all the headers are rendered a `<h1
id="header">Header</h1>`.
```markdown
# Header
# Header
# Header
# Header
```
This change is a simple but robust approach that uses an incrementing
counter and pre-checking to prevent header collision. (The above would
be rendered as `header`, `header-1`, `header-2`, and `header-3`.) In
more complex cases, it will append a new counter suffix (`-1`), like so:
```markdown
# Header
# Header 1
# Header
# Header
```
This will generate `header`, `header-1`, `header-1-1`, and `header-1-2`.
This code has two additional changes over the prior version:
1. Rather than reimplementing @shurcooL’s anchor sanitization code, I
have imported it as from
`github.com/shurcooL/go/github_flavored_markdown/sanitized_anchor_name`.
2. The markdown block parser is now only interested in *generating* a
sanitized anchor name, not with ensuring its uniqueness. That code
has been moved to the HTML renderer. This means that if the HTML
renderer is modified to identify all unique headers prior to
rendering, the hackish nature of the collision detection can be
eliminated.
2014-11-01 23:35:35 +01:00
|
|
|
headerIDs: make(map[string]int),
|
|
|
|
|
2016-04-01 09:44:22 +02:00
|
|
|
w: writer,
|
2011-05-29 05:17:53 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2014-01-26 20:27:34 +01:00
|
|
|
// Using if statements is a bit faster than a switch statement. As the compiler
|
|
|
|
// improves, this should be unnecessary this is only worthwhile because
|
|
|
|
// attrEscape is the single largest CPU user in normal use.
|
|
|
|
// Also tried using map, but that gave a ~3x slowdown.
|
|
|
|
func escapeSingleChar(char byte) (string, bool) {
|
|
|
|
if char == '"' {
|
|
|
|
return """, true
|
|
|
|
}
|
|
|
|
if char == '&' {
|
|
|
|
return "&", true
|
|
|
|
}
|
|
|
|
if char == '<' {
|
|
|
|
return "<", true
|
|
|
|
}
|
|
|
|
if char == '>' {
|
|
|
|
return ">", true
|
|
|
|
}
|
|
|
|
return "", false
|
|
|
|
}
|
|
|
|
|
2016-03-31 12:54:09 +02:00
|
|
|
func (r *HTML) attrEscape(src []byte) {
|
2011-06-25 03:11:06 +02:00
|
|
|
org := 0
|
|
|
|
for i, ch := range src {
|
2014-01-26 20:27:34 +01:00
|
|
|
if entity, ok := escapeSingleChar(ch); ok {
|
2011-06-25 03:11:06 +02:00
|
|
|
if i > org {
|
|
|
|
// copy all the normal characters since the last escape
|
2015-11-04 21:49:18 +01:00
|
|
|
r.w.Write(src[org:i])
|
2011-06-25 03:11:06 +02:00
|
|
|
}
|
|
|
|
org = i + 1
|
2015-11-04 21:49:18 +01:00
|
|
|
r.w.WriteString(entity)
|
2011-05-29 05:17:53 +02:00
|
|
|
}
|
|
|
|
}
|
2011-06-25 03:11:06 +02:00
|
|
|
if org < len(src) {
|
2015-11-04 21:49:18 +01:00
|
|
|
r.w.Write(src[org:])
|
2011-06-25 03:11:06 +02:00
|
|
|
}
|
2011-05-29 05:17:53 +02:00
|
|
|
}
|
|
|
|
|
2016-03-30 10:57:02 +02:00
|
|
|
func attrEscape2(src []byte) []byte {
|
|
|
|
unesc := []byte(html.UnescapeString(string(src)))
|
|
|
|
esc1 := []byte(html.EscapeString(string(unesc)))
|
|
|
|
esc2 := bytes.Replace(esc1, []byte("""), []byte("""), -1)
|
|
|
|
return bytes.Replace(esc2, []byte("'"), []byte{'\''}, -1)
|
|
|
|
}
|
|
|
|
|
2016-03-31 12:54:09 +02:00
|
|
|
func (r *HTML) entityEscapeWithSkip(src []byte, skipRanges [][]int) {
|
2014-01-26 20:39:38 +01:00
|
|
|
end := 0
|
|
|
|
for _, rang := range skipRanges {
|
2015-11-04 21:49:18 +01:00
|
|
|
r.attrEscape(src[end:rang[0]])
|
|
|
|
r.w.Write(src[rang[0]:rang[1]])
|
2014-01-26 20:39:38 +01:00
|
|
|
end = rang[1]
|
|
|
|
}
|
2015-11-04 21:49:18 +01:00
|
|
|
r.attrEscape(src[end:])
|
2014-01-26 20:39:38 +01:00
|
|
|
}
|
|
|
|
|
2016-03-31 12:54:09 +02:00
|
|
|
func (r *HTML) TitleBlock(text []byte) {
|
2014-08-02 04:54:21 +02:00
|
|
|
text = bytes.TrimPrefix(text, []byte("% "))
|
|
|
|
text = bytes.Replace(text, []byte("\n% "), []byte("\n"), -1)
|
2015-11-04 21:49:18 +01:00
|
|
|
r.w.WriteString("<h1 class=\"title\">")
|
|
|
|
r.w.Write(text)
|
|
|
|
r.w.WriteString("\n</h1>")
|
2014-08-02 04:54:21 +02:00
|
|
|
}
|
|
|
|
|
2016-03-31 12:54:09 +02:00
|
|
|
func (r *HTML) BeginHeader(level int, id string) {
|
2015-11-04 21:49:18 +01:00
|
|
|
r.w.Newline()
|
2011-05-29 05:17:53 +02:00
|
|
|
|
2016-04-04 09:14:49 +02:00
|
|
|
if id == "" && r.extensions&TOC != 0 {
|
2015-10-26 19:42:41 +01:00
|
|
|
id = fmt.Sprintf("toc_%d", r.headerCount)
|
Prevent generated header collisions, less naively.
> This is a rework of an earlier version of this code.
The automatic header ID generation code submitted in #125 has a subtle
bug where it will use the same ID for multiple headers with identical
text. In the case below, all the headers are rendered a `<h1
id="header">Header</h1>`.
```markdown
# Header
# Header
# Header
# Header
```
This change is a simple but robust approach that uses an incrementing
counter and pre-checking to prevent header collision. (The above would
be rendered as `header`, `header-1`, `header-2`, and `header-3`.) In
more complex cases, it will append a new counter suffix (`-1`), like so:
```markdown
# Header
# Header 1
# Header
# Header
```
This will generate `header`, `header-1`, `header-1-1`, and `header-1-2`.
This code has two additional changes over the prior version:
1. Rather than reimplementing @shurcooL’s anchor sanitization code, I
have imported it as from
`github.com/shurcooL/go/github_flavored_markdown/sanitized_anchor_name`.
2. The markdown block parser is now only interested in *generating* a
sanitized anchor name, not with ensuring its uniqueness. That code
has been moved to the HTML renderer. This means that if the HTML
renderer is modified to identify all unique headers prior to
rendering, the hackish nature of the collision detection can be
eliminated.
2014-11-01 23:35:35 +01:00
|
|
|
}
|
|
|
|
|
2014-04-05 21:42:58 +02:00
|
|
|
if id != "" {
|
2015-10-26 19:42:41 +01:00
|
|
|
id = r.ensureUniqueHeaderID(id)
|
Allow configurable header ID prefix/suffixes.
This is specifically driven by the Hugo usecase where multiple documents
are often rendered into the same ultimate HTML page.
When a header ID is written to the output HTML format (either through
`HTML_TOC`, `EXTENSION_HEADER_IDS`, or `EXTENSION_AUTO_HEADER_IDS`), it
is possible that multiple documents will hvae identical header IDs. To
permit validation to pass, it is useful to have a per-document prefix or
suffix (in our case, an MD5 of the content filename, and we will be
using it as a suffix).
That is, two documents (`A` and `B`) that have the same header ID (`#
Reason {#reason}`), will end up having an actual header ID of the form
`#reason-DOCID` (e.g., `#reason-A`, `#reason-B`) with these HTML
parameters.
This is built on top of #126 (more intelligent collision detection for
`EXTENSION_AUTO_HEADER_IDS`).
2014-11-24 02:37:27 +01:00
|
|
|
|
2015-10-26 19:42:41 +01:00
|
|
|
if r.parameters.HeaderIDPrefix != "" {
|
|
|
|
id = r.parameters.HeaderIDPrefix + id
|
Allow configurable header ID prefix/suffixes.
This is specifically driven by the Hugo usecase where multiple documents
are often rendered into the same ultimate HTML page.
When a header ID is written to the output HTML format (either through
`HTML_TOC`, `EXTENSION_HEADER_IDS`, or `EXTENSION_AUTO_HEADER_IDS`), it
is possible that multiple documents will hvae identical header IDs. To
permit validation to pass, it is useful to have a per-document prefix or
suffix (in our case, an MD5 of the content filename, and we will be
using it as a suffix).
That is, two documents (`A` and `B`) that have the same header ID (`#
Reason {#reason}`), will end up having an actual header ID of the form
`#reason-DOCID` (e.g., `#reason-A`, `#reason-B`) with these HTML
parameters.
This is built on top of #126 (more intelligent collision detection for
`EXTENSION_AUTO_HEADER_IDS`).
2014-11-24 02:37:27 +01:00
|
|
|
}
|
|
|
|
|
2015-10-26 19:42:41 +01:00
|
|
|
if r.parameters.HeaderIDSuffix != "" {
|
|
|
|
id = id + r.parameters.HeaderIDSuffix
|
Allow configurable header ID prefix/suffixes.
This is specifically driven by the Hugo usecase where multiple documents
are often rendered into the same ultimate HTML page.
When a header ID is written to the output HTML format (either through
`HTML_TOC`, `EXTENSION_HEADER_IDS`, or `EXTENSION_AUTO_HEADER_IDS`), it
is possible that multiple documents will hvae identical header IDs. To
permit validation to pass, it is useful to have a per-document prefix or
suffix (in our case, an MD5 of the content filename, and we will be
using it as a suffix).
That is, two documents (`A` and `B`) that have the same header ID (`#
Reason {#reason}`), will end up having an actual header ID of the form
`#reason-DOCID` (e.g., `#reason-A`, `#reason-B`) with these HTML
parameters.
This is built on top of #126 (more intelligent collision detection for
`EXTENSION_AUTO_HEADER_IDS`).
2014-11-24 02:37:27 +01:00
|
|
|
}
|
|
|
|
|
2015-11-04 21:49:18 +01:00
|
|
|
r.w.WriteString(fmt.Sprintf("<h%d id=\"%s\">", level, id))
|
2011-05-29 05:17:53 +02:00
|
|
|
} else {
|
2015-11-04 21:49:18 +01:00
|
|
|
r.w.WriteString(fmt.Sprintf("<h%d>", level))
|
2011-05-29 05:17:53 +02:00
|
|
|
}
|
2015-10-26 19:25:23 +01:00
|
|
|
}
|
2011-06-29 18:08:56 +02:00
|
|
|
|
2016-03-31 12:54:09 +02:00
|
|
|
func (r *HTML) EndHeader(level int, id string, header []byte) {
|
2011-06-29 18:08:56 +02:00
|
|
|
// are we building a table of contents?
|
2016-04-04 09:14:49 +02:00
|
|
|
if r.extensions&TOC != 0 {
|
2015-11-04 21:14:02 +01:00
|
|
|
r.TocHeaderWithAnchor(header, level, id)
|
2011-06-29 18:08:56 +02:00
|
|
|
}
|
|
|
|
|
2015-11-04 21:49:18 +01:00
|
|
|
r.w.WriteString(fmt.Sprintf("</h%d>\n", level))
|
2011-05-29 05:17:53 +02:00
|
|
|
}
|
|
|
|
|
2016-03-31 12:54:09 +02:00
|
|
|
func (r *HTML) BlockHtml(text []byte) {
|
2015-10-26 19:42:41 +01:00
|
|
|
if r.flags&SkipHTML != 0 {
|
2011-06-29 03:46:35 +02:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2015-11-04 21:49:18 +01:00
|
|
|
r.w.Newline()
|
|
|
|
r.w.Write(text)
|
|
|
|
r.w.WriteByte('\n')
|
2011-05-29 05:17:53 +02:00
|
|
|
}
|
|
|
|
|
2016-03-31 12:54:09 +02:00
|
|
|
func (r *HTML) HRule() {
|
2015-11-04 21:49:18 +01:00
|
|
|
r.w.Newline()
|
|
|
|
r.w.WriteString("<hr")
|
|
|
|
r.w.WriteString(r.closeTag)
|
|
|
|
r.w.WriteByte('\n')
|
2011-05-29 05:17:53 +02:00
|
|
|
}
|
|
|
|
|
2016-03-31 12:54:09 +02:00
|
|
|
func (r *HTML) BlockCode(text []byte, lang string) {
|
2015-11-04 21:49:18 +01:00
|
|
|
r.w.Newline()
|
2011-05-29 05:17:53 +02:00
|
|
|
|
2011-06-29 23:38:35 +02:00
|
|
|
// parse out the language names/classes
|
|
|
|
count := 0
|
|
|
|
for _, elt := range strings.Fields(lang) {
|
|
|
|
if elt[0] == '.' {
|
|
|
|
elt = elt[1:]
|
2011-05-29 05:17:53 +02:00
|
|
|
}
|
2011-06-29 23:38:35 +02:00
|
|
|
if len(elt) == 0 {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
if count == 0 {
|
2015-11-04 21:49:18 +01:00
|
|
|
r.w.WriteString("<pre><code class=\"language-")
|
2011-06-29 23:38:35 +02:00
|
|
|
} else {
|
2015-11-04 21:49:18 +01:00
|
|
|
r.w.WriteByte(' ')
|
2011-06-29 23:38:35 +02:00
|
|
|
}
|
2015-11-04 21:49:18 +01:00
|
|
|
r.attrEscape([]byte(elt))
|
2011-06-29 23:38:35 +02:00
|
|
|
count++
|
2011-05-29 05:17:53 +02:00
|
|
|
}
|
|
|
|
|
2011-06-29 23:38:35 +02:00
|
|
|
if count == 0 {
|
2015-11-04 21:49:18 +01:00
|
|
|
r.w.WriteString("<pre><code>")
|
2011-06-29 23:38:35 +02:00
|
|
|
} else {
|
2015-11-04 21:49:18 +01:00
|
|
|
r.w.WriteString("\">")
|
2011-05-29 05:17:53 +02:00
|
|
|
}
|
|
|
|
|
2015-11-04 21:49:18 +01:00
|
|
|
r.attrEscape(text)
|
|
|
|
r.w.WriteString("</code></pre>\n")
|
2011-05-29 05:17:53 +02:00
|
|
|
}
|
|
|
|
|
2016-03-31 12:54:09 +02:00
|
|
|
func (r *HTML) BlockQuote(text []byte) {
|
2015-11-04 21:49:18 +01:00
|
|
|
r.w.Newline()
|
|
|
|
r.w.WriteString("<blockquote>\n")
|
|
|
|
r.w.Write(text)
|
|
|
|
r.w.WriteString("</blockquote>\n")
|
2011-05-29 05:17:53 +02:00
|
|
|
}
|
|
|
|
|
2016-04-01 10:44:59 +02:00
|
|
|
func (r *HTML) Table(header []byte, body []byte, columnData []CellAlignFlags) {
|
2015-11-04 21:49:18 +01:00
|
|
|
r.w.Newline()
|
|
|
|
r.w.WriteString("<table>\n<thead>\n")
|
|
|
|
r.w.Write(header)
|
|
|
|
r.w.WriteString("</thead>\n\n<tbody>\n")
|
|
|
|
r.w.Write(body)
|
|
|
|
r.w.WriteString("</tbody>\n</table>\n")
|
2011-05-29 05:17:53 +02:00
|
|
|
}
|
|
|
|
|
2016-03-31 12:54:09 +02:00
|
|
|
func (r *HTML) TableRow(text []byte) {
|
2015-11-04 21:49:18 +01:00
|
|
|
r.w.Newline()
|
|
|
|
r.w.WriteString("<tr>\n")
|
|
|
|
r.w.Write(text)
|
|
|
|
r.w.WriteString("\n</tr>\n")
|
|
|
|
}
|
|
|
|
|
|
|
|
func leadingNewline(out *bytes.Buffer) {
|
|
|
|
if out.Len() > 0 {
|
|
|
|
out.WriteByte('\n')
|
|
|
|
}
|
2011-05-29 05:17:53 +02:00
|
|
|
}
|
|
|
|
|
2016-04-01 10:44:59 +02:00
|
|
|
func (r *HTML) TableHeaderCell(out *bytes.Buffer, text []byte, align CellAlignFlags) {
|
2015-11-04 21:49:18 +01:00
|
|
|
leadingNewline(out)
|
2013-10-16 12:36:33 +02:00
|
|
|
switch align {
|
2015-10-26 17:16:57 +01:00
|
|
|
case TableAlignmentLeft:
|
2013-10-16 12:36:33 +02:00
|
|
|
out.WriteString("<th align=\"left\">")
|
2015-10-26 17:16:57 +01:00
|
|
|
case TableAlignmentRight:
|
2013-10-16 12:36:33 +02:00
|
|
|
out.WriteString("<th align=\"right\">")
|
2015-10-26 17:16:57 +01:00
|
|
|
case TableAlignmentCenter:
|
2013-10-16 12:36:33 +02:00
|
|
|
out.WriteString("<th align=\"center\">")
|
|
|
|
default:
|
|
|
|
out.WriteString("<th>")
|
|
|
|
}
|
|
|
|
|
|
|
|
out.Write(text)
|
|
|
|
out.WriteString("</th>")
|
|
|
|
}
|
|
|
|
|
2016-04-01 10:44:59 +02:00
|
|
|
func (r *HTML) TableCell(out *bytes.Buffer, text []byte, align CellAlignFlags) {
|
2015-11-04 21:49:18 +01:00
|
|
|
leadingNewline(out)
|
2011-05-29 05:17:53 +02:00
|
|
|
switch align {
|
2015-10-26 17:16:57 +01:00
|
|
|
case TableAlignmentLeft:
|
2011-05-30 19:06:20 +02:00
|
|
|
out.WriteString("<td align=\"left\">")
|
2015-10-26 17:16:57 +01:00
|
|
|
case TableAlignmentRight:
|
2011-05-30 19:06:20 +02:00
|
|
|
out.WriteString("<td align=\"right\">")
|
2015-10-26 17:16:57 +01:00
|
|
|
case TableAlignmentCenter:
|
2011-05-30 19:06:20 +02:00
|
|
|
out.WriteString("<td align=\"center\">")
|
2011-05-29 05:17:53 +02:00
|
|
|
default:
|
2011-05-30 19:06:20 +02:00
|
|
|
out.WriteString("<td>")
|
2011-05-29 05:17:53 +02:00
|
|
|
}
|
|
|
|
|
2011-05-30 19:06:20 +02:00
|
|
|
out.Write(text)
|
|
|
|
out.WriteString("</td>")
|
2011-05-29 05:17:53 +02:00
|
|
|
}
|
|
|
|
|
2016-03-31 12:54:09 +02:00
|
|
|
func (r *HTML) BeginFootnotes() {
|
2015-11-04 21:49:18 +01:00
|
|
|
r.w.WriteString("<div class=\"footnotes\">\n")
|
2015-11-01 20:57:30 +01:00
|
|
|
r.HRule()
|
|
|
|
r.BeginList(ListTypeOrdered)
|
2015-10-26 19:39:08 +01:00
|
|
|
}
|
|
|
|
|
2016-03-31 12:54:09 +02:00
|
|
|
func (r *HTML) EndFootnotes() {
|
2015-11-01 20:57:30 +01:00
|
|
|
r.EndList(ListTypeOrdered)
|
2015-11-04 21:49:18 +01:00
|
|
|
r.w.WriteString("</div>\n")
|
2013-06-25 03:18:47 +02:00
|
|
|
}
|
|
|
|
|
2016-03-31 12:54:09 +02:00
|
|
|
func (r *HTML) FootnoteItem(name, text []byte, flags ListType) {
|
2015-10-26 17:16:57 +01:00
|
|
|
if flags&ListItemContainsBlock != 0 || flags&ListItemBeginningOfList != 0 {
|
2015-11-04 21:49:18 +01:00
|
|
|
r.w.Newline()
|
2013-06-26 17:57:51 +02:00
|
|
|
}
|
2014-05-24 20:29:39 +02:00
|
|
|
slug := slugify(name)
|
2015-11-04 21:49:18 +01:00
|
|
|
r.w.WriteString(`<li id="`)
|
|
|
|
r.w.WriteString(`fn:`)
|
|
|
|
r.w.WriteString(r.parameters.FootnoteAnchorPrefix)
|
|
|
|
r.w.Write(slug)
|
|
|
|
r.w.WriteString(`">`)
|
|
|
|
r.w.Write(text)
|
2015-10-26 19:42:41 +01:00
|
|
|
if r.flags&FootnoteReturnLinks != 0 {
|
2015-11-04 21:49:18 +01:00
|
|
|
r.w.WriteString(` <a class="footnote-return" href="#`)
|
|
|
|
r.w.WriteString(`fnref:`)
|
|
|
|
r.w.WriteString(r.parameters.FootnoteAnchorPrefix)
|
|
|
|
r.w.Write(slug)
|
|
|
|
r.w.WriteString(`">`)
|
|
|
|
r.w.WriteString(r.parameters.FootnoteReturnLinkContents)
|
|
|
|
r.w.WriteString(`</a>`)
|
2014-05-24 20:29:39 +02:00
|
|
|
}
|
2015-11-04 21:49:18 +01:00
|
|
|
r.w.WriteString("</li>\n")
|
2013-06-26 17:57:51 +02:00
|
|
|
}
|
|
|
|
|
2016-03-31 12:54:09 +02:00
|
|
|
func (r *HTML) BeginList(flags ListType) {
|
2015-11-04 21:49:18 +01:00
|
|
|
r.w.Newline()
|
2011-06-25 23:02:46 +02:00
|
|
|
|
2015-10-26 17:16:57 +01:00
|
|
|
if flags&ListTypeDefinition != 0 {
|
2015-11-04 21:49:18 +01:00
|
|
|
r.w.WriteString("<dl>")
|
2015-10-26 17:16:57 +01:00
|
|
|
} else if flags&ListTypeOrdered != 0 {
|
2015-11-04 21:49:18 +01:00
|
|
|
r.w.WriteString("<ol>")
|
2011-05-29 05:17:53 +02:00
|
|
|
} else {
|
2015-11-04 21:49:18 +01:00
|
|
|
r.w.WriteString("<ul>")
|
2011-05-29 05:17:53 +02:00
|
|
|
}
|
2015-10-26 19:32:33 +01:00
|
|
|
}
|
|
|
|
|
2016-03-31 12:54:09 +02:00
|
|
|
func (r *HTML) EndList(flags ListType) {
|
2015-10-26 17:16:57 +01:00
|
|
|
if flags&ListTypeDefinition != 0 {
|
2015-11-04 21:49:18 +01:00
|
|
|
r.w.WriteString("</dl>\n")
|
2015-10-26 17:16:57 +01:00
|
|
|
} else if flags&ListTypeOrdered != 0 {
|
2015-11-04 21:49:18 +01:00
|
|
|
r.w.WriteString("</ol>\n")
|
2011-05-29 05:17:53 +02:00
|
|
|
} else {
|
2015-11-04 21:49:18 +01:00
|
|
|
r.w.WriteString("</ul>\n")
|
2011-05-29 05:17:53 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-03-31 12:54:09 +02:00
|
|
|
func (r *HTML) ListItem(text []byte, flags ListType) {
|
2015-10-26 17:16:57 +01:00
|
|
|
if (flags&ListItemContainsBlock != 0 && flags&ListTypeDefinition == 0) ||
|
|
|
|
flags&ListItemBeginningOfList != 0 {
|
2015-11-04 21:49:18 +01:00
|
|
|
r.w.Newline()
|
2011-07-01 19:19:42 +02:00
|
|
|
}
|
2015-10-26 17:16:57 +01:00
|
|
|
if flags&ListTypeTerm != 0 {
|
2015-11-04 21:49:18 +01:00
|
|
|
r.w.WriteString("<dt>")
|
2015-10-26 17:16:57 +01:00
|
|
|
} else if flags&ListTypeDefinition != 0 {
|
2015-11-04 21:49:18 +01:00
|
|
|
r.w.WriteString("<dd>")
|
2015-05-29 13:30:49 +02:00
|
|
|
} else {
|
2015-11-04 21:49:18 +01:00
|
|
|
r.w.WriteString("<li>")
|
2015-05-29 13:30:49 +02:00
|
|
|
}
|
2015-11-04 21:49:18 +01:00
|
|
|
r.w.Write(text)
|
2015-10-26 17:16:57 +01:00
|
|
|
if flags&ListTypeTerm != 0 {
|
2015-11-04 21:49:18 +01:00
|
|
|
r.w.WriteString("</dt>\n")
|
2015-10-26 17:16:57 +01:00
|
|
|
} else if flags&ListTypeDefinition != 0 {
|
2015-11-04 21:49:18 +01:00
|
|
|
r.w.WriteString("</dd>\n")
|
2015-05-29 13:30:49 +02:00
|
|
|
} else {
|
2015-11-04 21:49:18 +01:00
|
|
|
r.w.WriteString("</li>\n")
|
2015-05-29 13:30:49 +02:00
|
|
|
}
|
2011-05-29 05:17:53 +02:00
|
|
|
}
|
|
|
|
|
2016-03-31 12:54:09 +02:00
|
|
|
func (r *HTML) BeginParagraph() {
|
2015-11-04 21:49:18 +01:00
|
|
|
r.w.Newline()
|
|
|
|
r.w.WriteString("<p>")
|
2015-10-26 19:35:42 +01:00
|
|
|
}
|
|
|
|
|
2016-03-31 12:54:09 +02:00
|
|
|
func (r *HTML) EndParagraph() {
|
2015-11-04 21:49:18 +01:00
|
|
|
r.w.WriteString("</p>\n")
|
2011-05-29 05:17:53 +02:00
|
|
|
}
|
|
|
|
|
2016-03-31 12:54:09 +02:00
|
|
|
func (r *HTML) AutoLink(link []byte, kind LinkType) {
|
2014-01-26 20:39:38 +01:00
|
|
|
skipRanges := htmlEntity.FindAllIndex(link, -1)
|
2015-10-26 19:42:41 +01:00
|
|
|
if r.flags&Safelink != 0 && !isSafeLink(link) && kind != LinkTypeEmail {
|
2011-06-29 23:38:35 +02:00
|
|
|
// mark it but don't link it if it is not a safe link: no smartypants
|
2015-11-04 21:49:18 +01:00
|
|
|
r.w.WriteString("<tt>")
|
|
|
|
r.entityEscapeWithSkip(link, skipRanges)
|
|
|
|
r.w.WriteString("</tt>")
|
2011-06-29 21:00:54 +02:00
|
|
|
return
|
2011-05-29 05:17:53 +02:00
|
|
|
}
|
|
|
|
|
2015-11-04 21:49:18 +01:00
|
|
|
r.w.WriteString("<a href=\"")
|
2015-10-26 17:16:57 +01:00
|
|
|
if kind == LinkTypeEmail {
|
2015-11-04 21:49:18 +01:00
|
|
|
r.w.WriteString("mailto:")
|
2014-05-18 08:28:15 +02:00
|
|
|
} else {
|
2015-11-04 21:49:18 +01:00
|
|
|
r.maybeWriteAbsolutePrefix(link)
|
2011-05-29 05:17:53 +02:00
|
|
|
}
|
2014-05-18 08:28:15 +02:00
|
|
|
|
2015-11-04 21:49:18 +01:00
|
|
|
r.entityEscapeWithSkip(link, skipRanges)
|
2014-03-21 03:52:46 +01:00
|
|
|
|
2015-03-15 00:46:32 +01:00
|
|
|
var relAttrs []string
|
2015-10-26 19:42:41 +01:00
|
|
|
if r.flags&NofollowLinks != 0 && !isRelativeLink(link) {
|
2015-03-15 00:46:32 +01:00
|
|
|
relAttrs = append(relAttrs, "nofollow")
|
2014-03-21 03:52:46 +01:00
|
|
|
}
|
2015-10-26 19:42:41 +01:00
|
|
|
if r.flags&NoreferrerLinks != 0 && !isRelativeLink(link) {
|
2015-03-15 00:46:32 +01:00
|
|
|
relAttrs = append(relAttrs, "noreferrer")
|
|
|
|
}
|
|
|
|
if len(relAttrs) > 0 {
|
2015-11-04 21:49:18 +01:00
|
|
|
r.w.WriteString(fmt.Sprintf("\" rel=\"%s", strings.Join(relAttrs, " ")))
|
2015-03-15 00:46:32 +01:00
|
|
|
}
|
|
|
|
|
2014-03-21 03:52:46 +01:00
|
|
|
// blank target only add to external link
|
2015-10-26 19:42:41 +01:00
|
|
|
if r.flags&HrefTargetBlank != 0 && !isRelativeLink(link) {
|
2015-11-04 21:49:18 +01:00
|
|
|
r.w.WriteString("\" target=\"_blank")
|
2014-03-21 03:52:46 +01:00
|
|
|
}
|
|
|
|
|
2015-11-04 21:49:18 +01:00
|
|
|
r.w.WriteString("\">")
|
2011-05-29 05:17:53 +02:00
|
|
|
|
2011-06-29 23:38:35 +02:00
|
|
|
// Pretty print: if we get an email address as
|
|
|
|
// an actual URI, e.g. `mailto:foo@bar.com`, we don't
|
|
|
|
// want to print the `mailto:` prefix
|
2011-05-31 19:49:49 +02:00
|
|
|
switch {
|
2011-05-30 23:36:31 +02:00
|
|
|
case bytes.HasPrefix(link, []byte("mailto://")):
|
2015-11-04 21:49:18 +01:00
|
|
|
r.attrEscape(link[len("mailto://"):])
|
2011-05-31 19:49:49 +02:00
|
|
|
case bytes.HasPrefix(link, []byte("mailto:")):
|
2015-11-04 21:49:18 +01:00
|
|
|
r.attrEscape(link[len("mailto:"):])
|
2011-05-30 23:36:31 +02:00
|
|
|
default:
|
2015-11-04 21:49:18 +01:00
|
|
|
r.entityEscapeWithSkip(link, skipRanges)
|
2011-05-29 05:17:53 +02:00
|
|
|
}
|
|
|
|
|
2015-11-04 21:49:18 +01:00
|
|
|
r.w.WriteString("</a>")
|
2011-05-29 05:17:53 +02:00
|
|
|
}
|
|
|
|
|
2016-03-31 12:54:09 +02:00
|
|
|
func (r *HTML) CodeSpan(text []byte) {
|
2015-11-04 21:49:18 +01:00
|
|
|
r.w.WriteString("<code>")
|
|
|
|
r.attrEscape(text)
|
|
|
|
r.w.WriteString("</code>")
|
2011-05-29 05:17:53 +02:00
|
|
|
}
|
|
|
|
|
2016-03-31 12:54:09 +02:00
|
|
|
func (r *HTML) DoubleEmphasis(text []byte) {
|
2015-11-04 21:49:18 +01:00
|
|
|
r.w.WriteString("<strong>")
|
|
|
|
r.w.Write(text)
|
|
|
|
r.w.WriteString("</strong>")
|
2011-05-29 05:17:53 +02:00
|
|
|
}
|
|
|
|
|
2016-03-31 12:54:09 +02:00
|
|
|
func (r *HTML) Emphasis(text []byte) {
|
2011-05-29 05:17:53 +02:00
|
|
|
if len(text) == 0 {
|
2011-06-29 21:00:54 +02:00
|
|
|
return
|
2011-05-29 05:17:53 +02:00
|
|
|
}
|
2015-11-04 21:49:18 +01:00
|
|
|
r.w.WriteString("<em>")
|
|
|
|
r.w.Write(text)
|
|
|
|
r.w.WriteString("</em>")
|
2011-05-29 05:17:53 +02:00
|
|
|
}
|
|
|
|
|
2016-03-31 12:54:09 +02:00
|
|
|
func (r *HTML) maybeWriteAbsolutePrefix(link []byte) {
|
2015-10-26 19:42:41 +01:00
|
|
|
if r.parameters.AbsolutePrefix != "" && isRelativeLink(link) && link[0] != '.' {
|
2015-11-04 21:49:18 +01:00
|
|
|
r.w.WriteString(r.parameters.AbsolutePrefix)
|
2014-05-18 08:28:15 +02:00
|
|
|
if link[0] != '/' {
|
2015-11-04 21:49:18 +01:00
|
|
|
r.w.WriteByte('/')
|
2014-05-18 08:28:15 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-03-31 12:54:09 +02:00
|
|
|
func (r *HTML) Image(link []byte, title []byte, alt []byte) {
|
2015-10-26 19:42:41 +01:00
|
|
|
if r.flags&SkipImages != 0 {
|
2011-06-29 21:00:54 +02:00
|
|
|
return
|
2011-06-29 03:46:35 +02:00
|
|
|
}
|
|
|
|
|
2015-11-04 21:49:18 +01:00
|
|
|
r.w.WriteString("<img src=\"")
|
|
|
|
r.maybeWriteAbsolutePrefix(link)
|
|
|
|
r.attrEscape(link)
|
|
|
|
r.w.WriteString("\" alt=\"")
|
2011-05-29 05:17:53 +02:00
|
|
|
if len(alt) > 0 {
|
2015-11-04 21:49:18 +01:00
|
|
|
r.attrEscape(alt)
|
2011-05-29 05:17:53 +02:00
|
|
|
}
|
|
|
|
if len(title) > 0 {
|
2015-11-04 21:49:18 +01:00
|
|
|
r.w.WriteString("\" title=\"")
|
|
|
|
r.attrEscape(title)
|
2011-05-29 05:17:53 +02:00
|
|
|
}
|
|
|
|
|
2015-11-04 21:49:18 +01:00
|
|
|
r.w.WriteByte('"')
|
|
|
|
r.w.WriteString(r.closeTag)
|
2011-05-29 05:17:53 +02:00
|
|
|
}
|
|
|
|
|
2016-03-31 12:54:09 +02:00
|
|
|
func (r *HTML) LineBreak() {
|
2015-11-04 21:49:18 +01:00
|
|
|
r.w.WriteString("<br")
|
|
|
|
r.w.WriteString(r.closeTag)
|
|
|
|
r.w.WriteByte('\n')
|
2011-05-29 05:17:53 +02:00
|
|
|
}
|
|
|
|
|
2016-03-31 12:54:09 +02:00
|
|
|
func (r *HTML) Link(link []byte, title []byte, content []byte) {
|
2015-10-26 19:42:41 +01:00
|
|
|
if r.flags&SkipLinks != 0 {
|
2011-06-29 23:38:35 +02:00
|
|
|
// write the link text out but don't link it, just mark it with typewriter font
|
2015-11-04 21:49:18 +01:00
|
|
|
r.w.WriteString("<tt>")
|
|
|
|
r.attrEscape(content)
|
|
|
|
r.w.WriteString("</tt>")
|
2011-06-29 21:00:54 +02:00
|
|
|
return
|
2011-06-29 03:46:35 +02:00
|
|
|
}
|
2011-05-29 05:17:53 +02:00
|
|
|
|
2015-10-26 19:42:41 +01:00
|
|
|
if r.flags&Safelink != 0 && !isSafeLink(link) {
|
2011-06-29 23:38:35 +02:00
|
|
|
// write the link text out but don't link it, just mark it with typewriter font
|
2015-11-04 21:49:18 +01:00
|
|
|
r.w.WriteString("<tt>")
|
|
|
|
r.attrEscape(content)
|
|
|
|
r.w.WriteString("</tt>")
|
2011-06-29 21:00:54 +02:00
|
|
|
return
|
2011-05-29 05:17:53 +02:00
|
|
|
}
|
|
|
|
|
2015-11-04 21:49:18 +01:00
|
|
|
r.w.WriteString("<a href=\"")
|
|
|
|
r.maybeWriteAbsolutePrefix(link)
|
|
|
|
r.attrEscape(link)
|
2011-05-29 05:17:53 +02:00
|
|
|
if len(title) > 0 {
|
2015-11-04 21:49:18 +01:00
|
|
|
r.w.WriteString("\" title=\"")
|
|
|
|
r.attrEscape(title)
|
2011-05-29 05:17:53 +02:00
|
|
|
}
|
2015-03-15 00:46:32 +01:00
|
|
|
var relAttrs []string
|
2015-10-26 19:42:41 +01:00
|
|
|
if r.flags&NofollowLinks != 0 && !isRelativeLink(link) {
|
2015-03-15 00:46:32 +01:00
|
|
|
relAttrs = append(relAttrs, "nofollow")
|
2014-02-25 15:21:57 +01:00
|
|
|
}
|
2015-10-26 19:42:41 +01:00
|
|
|
if r.flags&NoreferrerLinks != 0 && !isRelativeLink(link) {
|
2015-03-15 00:46:32 +01:00
|
|
|
relAttrs = append(relAttrs, "noreferrer")
|
|
|
|
}
|
|
|
|
if len(relAttrs) > 0 {
|
2015-11-04 21:49:18 +01:00
|
|
|
r.w.WriteString(fmt.Sprintf("\" rel=\"%s", strings.Join(relAttrs, " ")))
|
2015-03-15 00:46:32 +01:00
|
|
|
}
|
|
|
|
|
2014-03-21 03:52:46 +01:00
|
|
|
// blank target only add to external link
|
2015-10-26 19:42:41 +01:00
|
|
|
if r.flags&HrefTargetBlank != 0 && !isRelativeLink(link) {
|
2015-11-04 21:49:18 +01:00
|
|
|
r.w.WriteString("\" target=\"_blank")
|
2014-03-21 03:52:46 +01:00
|
|
|
}
|
|
|
|
|
2015-11-04 21:49:18 +01:00
|
|
|
r.w.WriteString("\">")
|
|
|
|
r.w.Write(content)
|
|
|
|
r.w.WriteString("</a>")
|
2011-06-29 21:00:54 +02:00
|
|
|
return
|
2011-05-29 05:17:53 +02:00
|
|
|
}
|
|
|
|
|
2016-03-31 12:54:09 +02:00
|
|
|
func (r *HTML) RawHtmlTag(text []byte) {
|
2015-10-26 19:42:41 +01:00
|
|
|
if r.flags&SkipHTML != 0 {
|
2011-06-29 21:00:54 +02:00
|
|
|
return
|
2011-05-29 05:17:53 +02:00
|
|
|
}
|
2015-10-26 19:42:41 +01:00
|
|
|
if r.flags&SkipStyle != 0 && isHtmlTag(text, "style") {
|
2011-06-29 21:00:54 +02:00
|
|
|
return
|
2011-05-29 05:17:53 +02:00
|
|
|
}
|
2015-10-26 19:42:41 +01:00
|
|
|
if r.flags&SkipLinks != 0 && isHtmlTag(text, "a") {
|
2011-06-29 21:00:54 +02:00
|
|
|
return
|
2011-05-29 05:17:53 +02:00
|
|
|
}
|
2015-10-26 19:42:41 +01:00
|
|
|
if r.flags&SkipImages != 0 && isHtmlTag(text, "img") {
|
2011-06-29 21:00:54 +02:00
|
|
|
return
|
2011-05-29 05:17:53 +02:00
|
|
|
}
|
2015-11-04 21:49:18 +01:00
|
|
|
r.w.Write(text)
|
2011-05-29 05:17:53 +02:00
|
|
|
}
|
|
|
|
|
2016-03-31 12:54:09 +02:00
|
|
|
func (r *HTML) TripleEmphasis(text []byte) {
|
2015-11-04 21:49:18 +01:00
|
|
|
r.w.WriteString("<strong><em>")
|
|
|
|
r.w.Write(text)
|
|
|
|
r.w.WriteString("</em></strong>")
|
2011-05-29 05:17:53 +02:00
|
|
|
}
|
|
|
|
|
2016-03-31 12:54:09 +02:00
|
|
|
func (r *HTML) StrikeThrough(text []byte) {
|
2015-11-04 21:49:18 +01:00
|
|
|
r.w.WriteString("<del>")
|
|
|
|
r.w.Write(text)
|
|
|
|
r.w.WriteString("</del>")
|
2011-05-29 05:17:53 +02:00
|
|
|
}
|
|
|
|
|
2016-03-31 12:54:09 +02:00
|
|
|
func (r *HTML) FootnoteRef(ref []byte, id int) {
|
2013-06-25 03:18:47 +02:00
|
|
|
slug := slugify(ref)
|
2015-11-04 21:49:18 +01:00
|
|
|
r.w.WriteString(`<sup class="footnote-ref" id="`)
|
|
|
|
r.w.WriteString(`fnref:`)
|
|
|
|
r.w.WriteString(r.parameters.FootnoteAnchorPrefix)
|
|
|
|
r.w.Write(slug)
|
|
|
|
r.w.WriteString(`"><a rel="footnote" href="#`)
|
|
|
|
r.w.WriteString(`fn:`)
|
|
|
|
r.w.WriteString(r.parameters.FootnoteAnchorPrefix)
|
|
|
|
r.w.Write(slug)
|
|
|
|
r.w.WriteString(`">`)
|
|
|
|
r.w.WriteString(strconv.Itoa(id))
|
|
|
|
r.w.WriteString(`</a></sup>`)
|
2013-06-25 03:18:47 +02:00
|
|
|
}
|
|
|
|
|
2016-03-31 12:54:09 +02:00
|
|
|
func (r *HTML) Entity(entity []byte) {
|
2015-11-04 21:49:18 +01:00
|
|
|
r.w.Write(entity)
|
2011-06-29 18:08:56 +02:00
|
|
|
}
|
|
|
|
|
2016-03-31 12:54:09 +02:00
|
|
|
func (r *HTML) NormalText(text []byte) {
|
2016-04-01 09:44:22 +02:00
|
|
|
if r.extensions&Smartypants != 0 {
|
2015-11-04 21:49:18 +01:00
|
|
|
r.Smartypants(text)
|
2011-06-29 03:46:35 +02:00
|
|
|
} else {
|
2015-11-04 21:49:18 +01:00
|
|
|
r.attrEscape(text)
|
2011-06-29 03:46:35 +02:00
|
|
|
}
|
2011-05-29 05:17:53 +02:00
|
|
|
}
|
|
|
|
|
2016-03-31 12:54:09 +02:00
|
|
|
func (r *HTML) Smartypants(text []byte) {
|
2016-04-01 09:44:22 +02:00
|
|
|
r.w.Write(NewSmartypantsRenderer(r.extensions).Process(text))
|
2011-06-29 19:13:17 +02:00
|
|
|
}
|
|
|
|
|
2016-03-31 12:54:09 +02:00
|
|
|
func (r *HTML) DocumentHeader() {
|
2015-10-26 19:42:41 +01:00
|
|
|
if r.flags&CompletePage == 0 {
|
2011-06-29 18:08:56 +02:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
ending := ""
|
2015-10-26 19:42:41 +01:00
|
|
|
if r.flags&UseXHTML != 0 {
|
2015-11-04 21:49:18 +01:00
|
|
|
r.w.WriteString("<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" ")
|
|
|
|
r.w.WriteString("\"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">\n")
|
|
|
|
r.w.WriteString("<html xmlns=\"http://www.w3.org/1999/xhtml\">\n")
|
2011-06-29 18:08:56 +02:00
|
|
|
ending = " /"
|
|
|
|
} else {
|
2015-11-04 21:49:18 +01:00
|
|
|
r.w.WriteString("<!DOCTYPE html>\n")
|
|
|
|
r.w.WriteString("<html>\n")
|
2011-06-29 18:08:56 +02:00
|
|
|
}
|
2015-11-04 21:49:18 +01:00
|
|
|
r.w.WriteString("<head>\n")
|
|
|
|
r.w.WriteString(" <title>")
|
2015-11-01 20:57:30 +01:00
|
|
|
r.NormalText([]byte(r.title))
|
2015-11-04 21:49:18 +01:00
|
|
|
r.w.WriteString("</title>\n")
|
|
|
|
r.w.WriteString(" <meta name=\"GENERATOR\" content=\"Blackfriday Markdown Processor v")
|
|
|
|
r.w.WriteString(VERSION)
|
|
|
|
r.w.WriteString("\"")
|
|
|
|
r.w.WriteString(ending)
|
|
|
|
r.w.WriteString(">\n")
|
|
|
|
r.w.WriteString(" <meta charset=\"utf-8\"")
|
|
|
|
r.w.WriteString(ending)
|
|
|
|
r.w.WriteString(">\n")
|
2015-10-26 19:42:41 +01:00
|
|
|
if r.css != "" {
|
2015-11-04 21:49:18 +01:00
|
|
|
r.w.WriteString(" <link rel=\"stylesheet\" type=\"text/css\" href=\"")
|
|
|
|
r.attrEscape([]byte(r.css))
|
|
|
|
r.w.WriteString("\"")
|
|
|
|
r.w.WriteString(ending)
|
|
|
|
r.w.WriteString(">\n")
|
2011-06-29 18:08:56 +02:00
|
|
|
}
|
2015-11-04 21:49:18 +01:00
|
|
|
r.w.WriteString("</head>\n")
|
|
|
|
r.w.WriteString("<body>\n")
|
2011-06-29 18:36:56 +02:00
|
|
|
|
2015-11-09 20:28:23 +01:00
|
|
|
r.tocMarker = r.w.output.Len() // XXX
|
2011-06-29 18:08:56 +02:00
|
|
|
}
|
|
|
|
|
2016-03-31 12:54:09 +02:00
|
|
|
func (r *HTML) DocumentFooter() {
|
2011-06-29 18:36:56 +02:00
|
|
|
// finalize and insert the table of contents
|
2016-04-04 09:14:49 +02:00
|
|
|
if r.extensions&TOC != 0 {
|
2015-10-26 19:42:41 +01:00
|
|
|
r.TocFinalize()
|
2011-06-29 18:36:56 +02:00
|
|
|
|
|
|
|
// now we have to insert the table of contents into the document
|
|
|
|
var temp bytes.Buffer
|
|
|
|
|
|
|
|
// start by making a copy of everything after the document header
|
2015-11-09 20:28:23 +01:00
|
|
|
temp.Write(r.w.output.Bytes()[r.tocMarker:])
|
2011-06-29 18:36:56 +02:00
|
|
|
|
|
|
|
// now clear the copied material from the main output buffer
|
2015-11-09 20:28:23 +01:00
|
|
|
r.w.output.Truncate(r.tocMarker)
|
2011-06-29 18:36:56 +02:00
|
|
|
|
2011-06-29 21:24:15 +02:00
|
|
|
// corner case spacing issue
|
2015-10-26 19:42:41 +01:00
|
|
|
if r.flags&CompletePage != 0 {
|
2015-11-04 21:49:18 +01:00
|
|
|
r.w.WriteByte('\n')
|
2011-06-29 21:24:15 +02:00
|
|
|
}
|
|
|
|
|
2011-06-29 18:36:56 +02:00
|
|
|
// insert the table of contents
|
2015-11-04 21:49:18 +01:00
|
|
|
r.w.WriteString("<nav>\n")
|
|
|
|
r.w.Write(r.toc.Bytes())
|
|
|
|
r.w.WriteString("</nav>\n")
|
2011-06-29 18:36:56 +02:00
|
|
|
|
2011-06-29 21:24:15 +02:00
|
|
|
// corner case spacing issue
|
2016-04-04 09:14:49 +02:00
|
|
|
if r.flags&CompletePage == 0 && r.extensions&OmitContents == 0 {
|
2015-11-04 21:49:18 +01:00
|
|
|
r.w.WriteByte('\n')
|
2011-06-29 21:24:15 +02:00
|
|
|
}
|
|
|
|
|
2011-06-29 18:36:56 +02:00
|
|
|
// write out everything that came after it
|
2016-04-04 09:14:49 +02:00
|
|
|
if r.extensions&OmitContents == 0 {
|
2015-11-04 21:49:18 +01:00
|
|
|
r.w.Write(temp.Bytes())
|
2011-06-29 18:36:56 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-10-26 19:42:41 +01:00
|
|
|
if r.flags&CompletePage != 0 {
|
2015-11-04 21:49:18 +01:00
|
|
|
r.w.WriteString("\n</body>\n")
|
|
|
|
r.w.WriteString("</html>\n")
|
2011-05-29 05:17:53 +02:00
|
|
|
}
|
2011-06-29 18:08:56 +02:00
|
|
|
|
2011-05-29 05:17:53 +02:00
|
|
|
}
|
|
|
|
|
2016-03-31 12:54:09 +02:00
|
|
|
func (r *HTML) TocHeaderWithAnchor(text []byte, level int, anchor string) {
|
2015-10-26 19:42:41 +01:00
|
|
|
for level > r.currentLevel {
|
2011-06-29 18:36:56 +02:00
|
|
|
switch {
|
2015-10-26 19:42:41 +01:00
|
|
|
case bytes.HasSuffix(r.toc.Bytes(), []byte("</li>\n")):
|
2011-06-29 21:24:15 +02:00
|
|
|
// this sublist can nest underneath a header
|
2015-10-26 19:42:41 +01:00
|
|
|
size := r.toc.Len()
|
|
|
|
r.toc.Truncate(size - len("</li>\n"))
|
2011-06-29 18:36:56 +02:00
|
|
|
|
2015-10-26 19:42:41 +01:00
|
|
|
case r.currentLevel > 0:
|
|
|
|
r.toc.WriteString("<li>")
|
2011-06-29 18:36:56 +02:00
|
|
|
}
|
2015-10-26 19:42:41 +01:00
|
|
|
if r.toc.Len() > 0 {
|
|
|
|
r.toc.WriteByte('\n')
|
2011-06-29 21:24:15 +02:00
|
|
|
}
|
2015-10-26 19:42:41 +01:00
|
|
|
r.toc.WriteString("<ul>\n")
|
|
|
|
r.currentLevel++
|
2011-06-29 18:36:56 +02:00
|
|
|
}
|
|
|
|
|
2015-10-26 19:42:41 +01:00
|
|
|
for level < r.currentLevel {
|
|
|
|
r.toc.WriteString("</ul>")
|
|
|
|
if r.currentLevel > 1 {
|
|
|
|
r.toc.WriteString("</li>\n")
|
2011-06-29 18:36:56 +02:00
|
|
|
}
|
2015-10-26 19:42:41 +01:00
|
|
|
r.currentLevel--
|
2011-06-29 18:36:56 +02:00
|
|
|
}
|
|
|
|
|
2015-10-26 19:42:41 +01:00
|
|
|
r.toc.WriteString("<li><a href=\"#")
|
2014-10-27 21:49:28 +01:00
|
|
|
if anchor != "" {
|
2015-10-26 19:42:41 +01:00
|
|
|
r.toc.WriteString(anchor)
|
2014-10-27 21:49:28 +01:00
|
|
|
} else {
|
2015-10-26 19:42:41 +01:00
|
|
|
r.toc.WriteString("toc_")
|
|
|
|
r.toc.WriteString(strconv.Itoa(r.headerCount))
|
2014-10-27 21:49:28 +01:00
|
|
|
}
|
2015-10-26 19:42:41 +01:00
|
|
|
r.toc.WriteString("\">")
|
|
|
|
r.headerCount++
|
2011-06-29 18:36:56 +02:00
|
|
|
|
2015-10-26 19:42:41 +01:00
|
|
|
r.toc.Write(text)
|
2011-06-29 18:36:56 +02:00
|
|
|
|
2015-10-26 19:42:41 +01:00
|
|
|
r.toc.WriteString("</a></li>\n")
|
2011-06-29 18:36:56 +02:00
|
|
|
}
|
|
|
|
|
2016-03-31 12:54:09 +02:00
|
|
|
func (r *HTML) TocHeader(text []byte, level int) {
|
2015-10-26 19:42:41 +01:00
|
|
|
r.TocHeaderWithAnchor(text, level, "")
|
2014-10-27 21:49:28 +01:00
|
|
|
}
|
|
|
|
|
2016-03-31 12:54:09 +02:00
|
|
|
func (r *HTML) TocFinalize() {
|
2015-10-26 19:42:41 +01:00
|
|
|
for r.currentLevel > 1 {
|
|
|
|
r.toc.WriteString("</ul></li>\n")
|
|
|
|
r.currentLevel--
|
2011-05-29 05:17:53 +02:00
|
|
|
}
|
|
|
|
|
2015-10-26 19:42:41 +01:00
|
|
|
if r.currentLevel > 0 {
|
|
|
|
r.toc.WriteString("</ul>\n")
|
2011-05-29 05:17:53 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2011-05-30 01:00:31 +02:00
|
|
|
func isHtmlTag(tag []byte, tagname string) bool {
|
2013-04-18 02:15:47 +02:00
|
|
|
found, _ := findHtmlTagPos(tag, tagname)
|
|
|
|
return found
|
|
|
|
}
|
|
|
|
|
2014-01-21 23:45:43 +01:00
|
|
|
// Look for a character, but ignore it when it's in any kind of quotes, it
|
|
|
|
// might be JavaScript
|
|
|
|
func skipUntilCharIgnoreQuotes(html []byte, start int, char byte) int {
|
|
|
|
inSingleQuote := false
|
|
|
|
inDoubleQuote := false
|
|
|
|
inGraveQuote := false
|
|
|
|
i := start
|
|
|
|
for i < len(html) {
|
|
|
|
switch {
|
|
|
|
case html[i] == char && !inSingleQuote && !inDoubleQuote && !inGraveQuote:
|
|
|
|
return i
|
|
|
|
case html[i] == '\'':
|
|
|
|
inSingleQuote = !inSingleQuote
|
|
|
|
case html[i] == '"':
|
|
|
|
inDoubleQuote = !inDoubleQuote
|
|
|
|
case html[i] == '`':
|
|
|
|
inGraveQuote = !inGraveQuote
|
|
|
|
}
|
|
|
|
i++
|
|
|
|
}
|
|
|
|
return start
|
|
|
|
}
|
|
|
|
|
2013-04-18 02:15:47 +02:00
|
|
|
func findHtmlTagPos(tag []byte, tagname string) (bool, int) {
|
2011-05-29 05:17:53 +02:00
|
|
|
i := 0
|
|
|
|
if i < len(tag) && tag[0] != '<' {
|
2013-04-18 02:15:47 +02:00
|
|
|
return false, -1
|
2011-05-29 05:17:53 +02:00
|
|
|
}
|
|
|
|
i++
|
2013-04-13 21:26:29 +02:00
|
|
|
i = skipSpace(tag, i)
|
2011-05-29 05:17:53 +02:00
|
|
|
|
|
|
|
if i < len(tag) && tag[i] == '/' {
|
|
|
|
i++
|
|
|
|
}
|
|
|
|
|
2013-04-13 21:26:29 +02:00
|
|
|
i = skipSpace(tag, i)
|
2013-04-13 21:21:47 +02:00
|
|
|
j := 0
|
2011-06-29 00:02:12 +02:00
|
|
|
for ; i < len(tag); i, j = i+1, j+1 {
|
|
|
|
if j >= len(tagname) {
|
2011-05-29 05:17:53 +02:00
|
|
|
break
|
|
|
|
}
|
|
|
|
|
2013-04-13 21:34:37 +02:00
|
|
|
if strings.ToLower(string(tag[i]))[0] != tagname[j] {
|
2013-04-18 02:15:47 +02:00
|
|
|
return false, -1
|
2011-05-29 05:17:53 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if i == len(tag) {
|
2013-04-18 02:15:47 +02:00
|
|
|
return false, -1
|
|
|
|
}
|
|
|
|
|
2014-01-21 23:45:43 +01:00
|
|
|
rightAngle := skipUntilCharIgnoreQuotes(tag, i, '>')
|
|
|
|
if rightAngle > i {
|
|
|
|
return true, rightAngle
|
2011-05-29 05:17:53 +02:00
|
|
|
}
|
|
|
|
|
2013-04-18 02:15:47 +02:00
|
|
|
return false, -1
|
2011-05-29 05:17:53 +02:00
|
|
|
}
|
2011-06-29 23:38:35 +02:00
|
|
|
|
2014-01-22 00:14:35 +01:00
|
|
|
func skipUntilChar(text []byte, start int, char byte) int {
|
|
|
|
i := start
|
|
|
|
for i < len(text) && text[i] != char {
|
|
|
|
i++
|
|
|
|
}
|
|
|
|
return i
|
|
|
|
}
|
|
|
|
|
2013-04-13 21:26:29 +02:00
|
|
|
func skipSpace(tag []byte, i int) int {
|
|
|
|
for i < len(tag) && isspace(tag[i]) {
|
|
|
|
i++
|
|
|
|
}
|
|
|
|
return i
|
|
|
|
}
|
|
|
|
|
2015-04-07 20:59:42 +02:00
|
|
|
func skipChar(data []byte, start int, char byte) int {
|
|
|
|
i := start
|
|
|
|
for i < len(data) && data[i] == char {
|
|
|
|
i++
|
|
|
|
}
|
|
|
|
return i
|
|
|
|
}
|
|
|
|
|
2014-03-21 03:52:46 +01:00
|
|
|
func isRelativeLink(link []byte) (yes bool) {
|
|
|
|
// a tag begin with '#'
|
|
|
|
if link[0] == '#' {
|
2015-04-11 17:06:30 +02:00
|
|
|
return true
|
2014-03-21 03:52:46 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// link begin with '/' but not '//', the second maybe a protocol relative link
|
|
|
|
if len(link) >= 2 && link[0] == '/' && link[1] != '/' {
|
2015-04-11 17:06:30 +02:00
|
|
|
return true
|
2014-03-21 03:52:46 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// only the root '/'
|
|
|
|
if len(link) == 1 && link[0] == '/' {
|
2015-04-11 17:06:30 +02:00
|
|
|
return true
|
2014-03-21 03:52:46 +01:00
|
|
|
}
|
2015-02-20 10:06:55 +01:00
|
|
|
|
|
|
|
// current directory : begin with "./"
|
2015-04-11 17:06:30 +02:00
|
|
|
if bytes.HasPrefix(link, []byte("./")) {
|
|
|
|
return true
|
2015-02-20 10:06:55 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// parent directory : begin with "../"
|
2015-04-11 17:06:30 +02:00
|
|
|
if bytes.HasPrefix(link, []byte("../")) {
|
|
|
|
return true
|
2015-02-20 10:06:55 +01:00
|
|
|
}
|
|
|
|
|
2015-04-11 17:06:30 +02:00
|
|
|
return false
|
2014-03-21 03:52:46 +01:00
|
|
|
}
|
Prevent generated header collisions, less naively.
> This is a rework of an earlier version of this code.
The automatic header ID generation code submitted in #125 has a subtle
bug where it will use the same ID for multiple headers with identical
text. In the case below, all the headers are rendered a `<h1
id="header">Header</h1>`.
```markdown
# Header
# Header
# Header
# Header
```
This change is a simple but robust approach that uses an incrementing
counter and pre-checking to prevent header collision. (The above would
be rendered as `header`, `header-1`, `header-2`, and `header-3`.) In
more complex cases, it will append a new counter suffix (`-1`), like so:
```markdown
# Header
# Header 1
# Header
# Header
```
This will generate `header`, `header-1`, `header-1-1`, and `header-1-2`.
This code has two additional changes over the prior version:
1. Rather than reimplementing @shurcooL’s anchor sanitization code, I
have imported it as from
`github.com/shurcooL/go/github_flavored_markdown/sanitized_anchor_name`.
2. The markdown block parser is now only interested in *generating* a
sanitized anchor name, not with ensuring its uniqueness. That code
has been moved to the HTML renderer. This means that if the HTML
renderer is modified to identify all unique headers prior to
rendering, the hackish nature of the collision detection can be
eliminated.
2014-11-01 23:35:35 +01:00
|
|
|
|
2016-03-31 12:54:09 +02:00
|
|
|
func (r *HTML) ensureUniqueHeaderID(id string) string {
|
2015-10-26 19:42:41 +01:00
|
|
|
for count, found := r.headerIDs[id]; found; count, found = r.headerIDs[id] {
|
Prevent generated header collisions, less naively.
> This is a rework of an earlier version of this code.
The automatic header ID generation code submitted in #125 has a subtle
bug where it will use the same ID for multiple headers with identical
text. In the case below, all the headers are rendered a `<h1
id="header">Header</h1>`.
```markdown
# Header
# Header
# Header
# Header
```
This change is a simple but robust approach that uses an incrementing
counter and pre-checking to prevent header collision. (The above would
be rendered as `header`, `header-1`, `header-2`, and `header-3`.) In
more complex cases, it will append a new counter suffix (`-1`), like so:
```markdown
# Header
# Header 1
# Header
# Header
```
This will generate `header`, `header-1`, `header-1-1`, and `header-1-2`.
This code has two additional changes over the prior version:
1. Rather than reimplementing @shurcooL’s anchor sanitization code, I
have imported it as from
`github.com/shurcooL/go/github_flavored_markdown/sanitized_anchor_name`.
2. The markdown block parser is now only interested in *generating* a
sanitized anchor name, not with ensuring its uniqueness. That code
has been moved to the HTML renderer. This means that if the HTML
renderer is modified to identify all unique headers prior to
rendering, the hackish nature of the collision detection can be
eliminated.
2014-11-01 23:35:35 +01:00
|
|
|
tmp := fmt.Sprintf("%s-%d", id, count+1)
|
|
|
|
|
2015-10-26 19:42:41 +01:00
|
|
|
if _, tmpFound := r.headerIDs[tmp]; !tmpFound {
|
|
|
|
r.headerIDs[id] = count + 1
|
Prevent generated header collisions, less naively.
> This is a rework of an earlier version of this code.
The automatic header ID generation code submitted in #125 has a subtle
bug where it will use the same ID for multiple headers with identical
text. In the case below, all the headers are rendered a `<h1
id="header">Header</h1>`.
```markdown
# Header
# Header
# Header
# Header
```
This change is a simple but robust approach that uses an incrementing
counter and pre-checking to prevent header collision. (The above would
be rendered as `header`, `header-1`, `header-2`, and `header-3`.) In
more complex cases, it will append a new counter suffix (`-1`), like so:
```markdown
# Header
# Header 1
# Header
# Header
```
This will generate `header`, `header-1`, `header-1-1`, and `header-1-2`.
This code has two additional changes over the prior version:
1. Rather than reimplementing @shurcooL’s anchor sanitization code, I
have imported it as from
`github.com/shurcooL/go/github_flavored_markdown/sanitized_anchor_name`.
2. The markdown block parser is now only interested in *generating* a
sanitized anchor name, not with ensuring its uniqueness. That code
has been moved to the HTML renderer. This means that if the HTML
renderer is modified to identify all unique headers prior to
rendering, the hackish nature of the collision detection can be
eliminated.
2014-11-01 23:35:35 +01:00
|
|
|
id = tmp
|
|
|
|
} else {
|
|
|
|
id = id + "-1"
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-10-26 19:42:41 +01:00
|
|
|
if _, found := r.headerIDs[id]; !found {
|
|
|
|
r.headerIDs[id] = 0
|
Prevent generated header collisions, less naively.
> This is a rework of an earlier version of this code.
The automatic header ID generation code submitted in #125 has a subtle
bug where it will use the same ID for multiple headers with identical
text. In the case below, all the headers are rendered a `<h1
id="header">Header</h1>`.
```markdown
# Header
# Header
# Header
# Header
```
This change is a simple but robust approach that uses an incrementing
counter and pre-checking to prevent header collision. (The above would
be rendered as `header`, `header-1`, `header-2`, and `header-3`.) In
more complex cases, it will append a new counter suffix (`-1`), like so:
```markdown
# Header
# Header 1
# Header
# Header
```
This will generate `header`, `header-1`, `header-1-1`, and `header-1-2`.
This code has two additional changes over the prior version:
1. Rather than reimplementing @shurcooL’s anchor sanitization code, I
have imported it as from
`github.com/shurcooL/go/github_flavored_markdown/sanitized_anchor_name`.
2. The markdown block parser is now only interested in *generating* a
sanitized anchor name, not with ensuring its uniqueness. That code
has been moved to the HTML renderer. This means that if the HTML
renderer is modified to identify all unique headers prior to
rendering, the hackish nature of the collision detection can be
eliminated.
2014-11-01 23:35:35 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
return id
|
|
|
|
}
|
2016-03-30 10:57:02 +02:00
|
|
|
|
2016-03-31 12:54:09 +02:00
|
|
|
func (r *HTML) addAbsPrefix(link []byte) []byte {
|
2016-03-30 10:57:02 +02:00
|
|
|
if r.parameters.AbsolutePrefix != "" && isRelativeLink(link) && link[0] != '.' {
|
|
|
|
newDest := r.parameters.AbsolutePrefix
|
|
|
|
if link[0] != '/' {
|
|
|
|
newDest += "/"
|
|
|
|
}
|
|
|
|
newDest += string(link)
|
|
|
|
return []byte(newDest)
|
|
|
|
}
|
|
|
|
return link
|
|
|
|
}
|
|
|
|
|
2016-04-01 09:49:23 +02:00
|
|
|
func appendLinkAttrs(attrs []string, flags HTMLFlags, link []byte) []string {
|
2016-03-30 10:57:02 +02:00
|
|
|
if isRelativeLink(link) {
|
|
|
|
return attrs
|
|
|
|
}
|
|
|
|
val := []string{}
|
|
|
|
if flags&NofollowLinks != 0 {
|
|
|
|
val = append(val, "nofollow")
|
|
|
|
}
|
|
|
|
if flags&NoreferrerLinks != 0 {
|
|
|
|
val = append(val, "noreferrer")
|
|
|
|
}
|
|
|
|
if flags&HrefTargetBlank != 0 {
|
|
|
|
attrs = append(attrs, "target=\"_blank\"")
|
|
|
|
}
|
|
|
|
if len(val) == 0 {
|
|
|
|
return attrs
|
|
|
|
}
|
|
|
|
attr := fmt.Sprintf("rel=%q", strings.Join(val, " "))
|
|
|
|
return append(attrs, attr)
|
|
|
|
}
|
|
|
|
|
|
|
|
func isMailto(link []byte) bool {
|
|
|
|
return bytes.HasPrefix(link, []byte("mailto:"))
|
|
|
|
}
|
|
|
|
|
2016-04-04 13:08:35 +02:00
|
|
|
func needSkipLink(flags HTMLFlags, dest []byte) bool {
|
|
|
|
if flags&SkipLinks != 0 {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
return flags&Safelink != 0 && !isSafeLink(dest) && !isMailto(dest)
|
|
|
|
}
|
|
|
|
|
2016-03-30 10:57:02 +02:00
|
|
|
func isSmartypantable(node *Node) bool {
|
|
|
|
pt := node.Parent.Type
|
|
|
|
return pt != Link && pt != CodeBlock && pt != Code
|
|
|
|
}
|
|
|
|
|
|
|
|
func appendLanguageAttr(attrs []string, info []byte) []string {
|
|
|
|
infoWords := bytes.Split(info, []byte("\t "))
|
|
|
|
if len(infoWords) > 0 && len(infoWords[0]) > 0 {
|
|
|
|
attrs = append(attrs, fmt.Sprintf("class=\"language-%s\"", infoWords[0]))
|
|
|
|
}
|
|
|
|
return attrs
|
|
|
|
}
|
|
|
|
|
|
|
|
func tag(name string, attrs []string, selfClosing bool) []byte {
|
|
|
|
result := "<" + name
|
|
|
|
if attrs != nil && len(attrs) > 0 {
|
|
|
|
result += " " + strings.Join(attrs, " ")
|
|
|
|
}
|
|
|
|
if selfClosing {
|
|
|
|
result += " /"
|
|
|
|
}
|
|
|
|
return []byte(result + ">")
|
|
|
|
}
|
|
|
|
|
|
|
|
func footnoteRef(prefix string, node *Node) []byte {
|
|
|
|
urlFrag := prefix + string(slugify(node.Destination))
|
|
|
|
anchor := fmt.Sprintf(`<a rel="footnote" href="#fn:%s">%d</a>`, urlFrag, node.NoteID)
|
|
|
|
return []byte(fmt.Sprintf(`<sup class="footnote-ref" id="fnref:%s">%s</sup>`, urlFrag, anchor))
|
|
|
|
}
|
|
|
|
|
|
|
|
func footnoteItem(prefix string, slug []byte) []byte {
|
|
|
|
return []byte(fmt.Sprintf(`<li id="fn:%s%s">`, prefix, slug))
|
|
|
|
}
|
|
|
|
|
|
|
|
func footnoteReturnLink(prefix, returnLink string, slug []byte) []byte {
|
|
|
|
const format = ` <a class="footnote-return" href="#fnref:%s%s">%s</a>`
|
|
|
|
return []byte(fmt.Sprintf(format, prefix, slug, returnLink))
|
|
|
|
}
|
|
|
|
|
|
|
|
func itemOpenCR(node *Node) bool {
|
|
|
|
if node.Prev == nil {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
ld := node.Parent.ListData
|
2016-04-01 10:21:25 +02:00
|
|
|
return !ld.Tight && ld.ListFlags&ListTypeDefinition == 0
|
2016-03-30 10:57:02 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
func skipParagraphTags(node *Node) bool {
|
|
|
|
grandparent := node.Parent.Parent
|
2016-04-01 10:21:25 +02:00
|
|
|
if grandparent == nil || grandparent.Type != List {
|
2016-03-30 10:57:02 +02:00
|
|
|
return false
|
|
|
|
}
|
2016-04-01 10:21:25 +02:00
|
|
|
tightOrTerm := grandparent.Tight || node.Parent.ListFlags&ListTypeTerm != 0
|
2016-03-30 10:57:02 +02:00
|
|
|
return grandparent.Type == List && tightOrTerm
|
|
|
|
}
|
|
|
|
|
2016-04-01 10:44:59 +02:00
|
|
|
func cellAlignment(align CellAlignFlags) string {
|
2016-03-30 10:57:02 +02:00
|
|
|
switch align {
|
|
|
|
case TableAlignmentLeft:
|
|
|
|
return "left"
|
|
|
|
case TableAlignmentRight:
|
|
|
|
return "right"
|
|
|
|
case TableAlignmentCenter:
|
|
|
|
return "center"
|
|
|
|
default:
|
|
|
|
return ""
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-03-30 14:37:03 +02:00
|
|
|
func esc(text []byte, preserveEntities bool) []byte {
|
|
|
|
return attrEscape2(text)
|
|
|
|
}
|
|
|
|
|
|
|
|
func escCode(text []byte, preserveEntities bool) []byte {
|
|
|
|
e1 := []byte(html.EscapeString(string(text)))
|
|
|
|
e2 := bytes.Replace(e1, []byte("""), []byte("""), -1)
|
|
|
|
return bytes.Replace(e2, []byte("'"), []byte{'\''}, -1)
|
|
|
|
}
|
|
|
|
|
2016-03-31 12:54:09 +02:00
|
|
|
func (r *HTML) out(w io.Writer, text []byte) {
|
2016-03-30 14:37:03 +02:00
|
|
|
if r.disableTags > 0 {
|
2016-03-30 20:13:02 +02:00
|
|
|
w.Write(reHtmlTag.ReplaceAll(text, []byte{}))
|
2016-03-30 14:37:03 +02:00
|
|
|
} else {
|
2016-03-30 20:13:02 +02:00
|
|
|
w.Write(text)
|
2016-03-30 14:37:03 +02:00
|
|
|
}
|
|
|
|
r.lastOutputLen = len(text)
|
|
|
|
}
|
|
|
|
|
2016-03-31 12:54:09 +02:00
|
|
|
func (r *HTML) cr(w io.Writer) {
|
2016-03-30 14:37:03 +02:00
|
|
|
if r.lastOutputLen > 0 {
|
2016-03-30 20:13:02 +02:00
|
|
|
r.out(w, []byte{'\n'})
|
2016-03-30 14:37:03 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-03-31 12:54:09 +02:00
|
|
|
func (r *HTML) RenderNode(w io.Writer, node *Node, entering bool) {
|
2016-03-30 14:48:43 +02:00
|
|
|
attrs := []string{}
|
|
|
|
switch node.Type {
|
|
|
|
case Text:
|
2016-04-01 09:44:22 +02:00
|
|
|
r.out(w, node.Literal)
|
2016-03-30 14:48:43 +02:00
|
|
|
break
|
|
|
|
case Softbreak:
|
2016-03-30 20:13:02 +02:00
|
|
|
r.out(w, []byte("\n"))
|
2016-03-30 14:48:43 +02:00
|
|
|
// TODO: make it configurable via out(renderer.softbreak)
|
|
|
|
case Hardbreak:
|
2016-03-30 20:13:02 +02:00
|
|
|
r.out(w, tag("br", nil, true))
|
|
|
|
r.cr(w)
|
2016-03-30 14:48:43 +02:00
|
|
|
case Emph:
|
|
|
|
if entering {
|
2016-03-30 20:13:02 +02:00
|
|
|
r.out(w, tag("em", nil, false))
|
2016-03-30 14:48:43 +02:00
|
|
|
} else {
|
2016-03-30 20:13:02 +02:00
|
|
|
r.out(w, tag("/em", nil, false))
|
2016-03-30 14:48:43 +02:00
|
|
|
}
|
|
|
|
break
|
|
|
|
case Strong:
|
|
|
|
if entering {
|
2016-03-30 20:13:02 +02:00
|
|
|
r.out(w, tag("strong", nil, false))
|
2016-03-30 14:48:43 +02:00
|
|
|
} else {
|
2016-03-30 20:13:02 +02:00
|
|
|
r.out(w, tag("/strong", nil, false))
|
2016-03-30 14:48:43 +02:00
|
|
|
}
|
|
|
|
break
|
|
|
|
case Del:
|
|
|
|
if entering {
|
2016-03-30 20:13:02 +02:00
|
|
|
r.out(w, tag("del", nil, false))
|
2016-03-30 14:48:43 +02:00
|
|
|
} else {
|
2016-03-30 20:13:02 +02:00
|
|
|
r.out(w, tag("/del", nil, false))
|
2016-03-30 14:48:43 +02:00
|
|
|
}
|
2016-04-01 12:15:47 +02:00
|
|
|
case HTMLSpan:
|
2016-03-30 14:48:43 +02:00
|
|
|
//if options.safe {
|
2016-03-30 20:13:02 +02:00
|
|
|
// out(w, "<!-- raw HTML omitted -->")
|
2016-03-30 14:48:43 +02:00
|
|
|
//} else {
|
2016-03-30 20:13:02 +02:00
|
|
|
r.out(w, node.Literal)
|
2016-03-30 14:48:43 +02:00
|
|
|
//}
|
|
|
|
case Link:
|
|
|
|
// mark it but don't link it if it is not a safe link: no smartypants
|
|
|
|
dest := node.LinkData.Destination
|
2016-04-04 13:08:35 +02:00
|
|
|
if needSkipLink(r.flags, dest) {
|
2016-03-30 10:57:02 +02:00
|
|
|
if entering {
|
2016-03-30 20:13:02 +02:00
|
|
|
r.out(w, tag("tt", nil, false))
|
2016-03-30 10:57:02 +02:00
|
|
|
} else {
|
2016-03-30 20:13:02 +02:00
|
|
|
r.out(w, tag("/tt", nil, false))
|
2016-03-30 10:57:02 +02:00
|
|
|
}
|
2016-03-30 14:48:43 +02:00
|
|
|
} else {
|
2016-03-30 10:57:02 +02:00
|
|
|
if entering {
|
|
|
|
dest = r.addAbsPrefix(dest)
|
2016-03-30 14:48:43 +02:00
|
|
|
//if (!(options.safe && potentiallyUnsafe(node.destination))) {
|
|
|
|
attrs = append(attrs, fmt.Sprintf("href=%q", esc(dest, true)))
|
|
|
|
//}
|
|
|
|
if node.NoteID != 0 {
|
2016-03-30 20:13:02 +02:00
|
|
|
r.out(w, footnoteRef(r.parameters.FootnoteAnchorPrefix, node))
|
2016-03-30 14:48:43 +02:00
|
|
|
break
|
|
|
|
}
|
|
|
|
attrs = appendLinkAttrs(attrs, r.flags, dest)
|
|
|
|
if len(node.LinkData.Title) > 0 {
|
|
|
|
attrs = append(attrs, fmt.Sprintf("title=%q", esc(node.LinkData.Title, true)))
|
2016-03-30 10:57:02 +02:00
|
|
|
}
|
2016-03-30 20:13:02 +02:00
|
|
|
r.out(w, tag("a", attrs, false))
|
2016-03-30 10:57:02 +02:00
|
|
|
} else {
|
2016-03-30 14:48:43 +02:00
|
|
|
if node.NoteID != 0 {
|
|
|
|
break
|
2016-03-30 10:57:02 +02:00
|
|
|
}
|
2016-03-30 20:13:02 +02:00
|
|
|
r.out(w, tag("/a", nil, false))
|
2016-03-30 10:57:02 +02:00
|
|
|
}
|
2016-03-30 14:48:43 +02:00
|
|
|
}
|
|
|
|
case Image:
|
|
|
|
if entering {
|
|
|
|
dest := node.LinkData.Destination
|
|
|
|
dest = r.addAbsPrefix(dest)
|
|
|
|
if r.disableTags == 0 {
|
|
|
|
//if options.safe && potentiallyUnsafe(dest) {
|
2016-03-30 20:13:02 +02:00
|
|
|
//out(w, `<img src="" alt="`)
|
2016-03-30 14:48:43 +02:00
|
|
|
//} else {
|
2016-03-30 20:13:02 +02:00
|
|
|
r.out(w, []byte(fmt.Sprintf(`<img src="%s" alt="`, esc(dest, true))))
|
2016-03-30 14:48:43 +02:00
|
|
|
//}
|
2016-03-30 10:57:02 +02:00
|
|
|
}
|
2016-03-30 14:48:43 +02:00
|
|
|
r.disableTags++
|
|
|
|
} else {
|
|
|
|
r.disableTags--
|
|
|
|
if r.disableTags == 0 {
|
|
|
|
if node.LinkData.Title != nil {
|
2016-03-30 20:13:02 +02:00
|
|
|
r.out(w, []byte(`" title="`))
|
|
|
|
r.out(w, esc(node.LinkData.Title, true))
|
2016-03-30 10:57:02 +02:00
|
|
|
}
|
2016-03-30 20:13:02 +02:00
|
|
|
r.out(w, []byte(`" />`))
|
2016-03-30 14:48:43 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
case Code:
|
2016-03-30 20:13:02 +02:00
|
|
|
r.out(w, tag("code", nil, false))
|
|
|
|
r.out(w, escCode(node.Literal, false))
|
|
|
|
r.out(w, tag("/code", nil, false))
|
2016-03-30 14:48:43 +02:00
|
|
|
case Document:
|
|
|
|
break
|
|
|
|
case Paragraph:
|
|
|
|
if skipParagraphTags(node) {
|
|
|
|
break
|
|
|
|
}
|
|
|
|
if entering {
|
|
|
|
// TODO: untangle this clusterfuck about when the newlines need
|
|
|
|
// to be added and when not.
|
|
|
|
if node.Prev != nil {
|
|
|
|
t := node.Prev.Type
|
2016-04-01 12:15:47 +02:00
|
|
|
if t == HTMLBlock || t == List || t == Paragraph || t == Header || t == CodeBlock || t == BlockQuote || t == HorizontalRule {
|
2016-03-30 20:13:02 +02:00
|
|
|
r.cr(w)
|
2016-03-30 10:57:02 +02:00
|
|
|
}
|
|
|
|
}
|
2016-03-30 14:48:43 +02:00
|
|
|
if node.Parent.Type == BlockQuote && node.Prev == nil {
|
2016-03-30 20:13:02 +02:00
|
|
|
r.cr(w)
|
2016-03-30 10:57:02 +02:00
|
|
|
}
|
2016-03-30 20:13:02 +02:00
|
|
|
r.out(w, tag("p", attrs, false))
|
2016-03-30 14:48:43 +02:00
|
|
|
} else {
|
2016-03-30 20:13:02 +02:00
|
|
|
r.out(w, tag("/p", attrs, false))
|
2016-03-30 14:48:43 +02:00
|
|
|
if !(node.Parent.Type == Item && node.Next == nil) {
|
2016-03-30 20:13:02 +02:00
|
|
|
r.cr(w)
|
2016-03-30 10:57:02 +02:00
|
|
|
}
|
2016-03-30 14:48:43 +02:00
|
|
|
}
|
|
|
|
break
|
|
|
|
case BlockQuote:
|
|
|
|
if entering {
|
2016-03-30 20:13:02 +02:00
|
|
|
r.cr(w)
|
|
|
|
r.out(w, tag("blockquote", attrs, false))
|
2016-03-30 14:48:43 +02:00
|
|
|
} else {
|
2016-03-30 20:13:02 +02:00
|
|
|
r.out(w, tag("/blockquote", nil, false))
|
|
|
|
r.cr(w)
|
2016-03-30 14:48:43 +02:00
|
|
|
}
|
|
|
|
break
|
2016-04-01 12:15:47 +02:00
|
|
|
case HTMLBlock:
|
2016-03-30 20:13:02 +02:00
|
|
|
r.cr(w)
|
|
|
|
r.out(w, node.Literal)
|
|
|
|
r.cr(w)
|
2016-03-30 14:48:43 +02:00
|
|
|
case Header:
|
|
|
|
tagname := fmt.Sprintf("h%d", node.Level)
|
|
|
|
if entering {
|
|
|
|
if node.IsTitleblock {
|
|
|
|
attrs = append(attrs, `class="title"`)
|
2016-03-30 10:57:02 +02:00
|
|
|
}
|
2016-03-30 14:48:43 +02:00
|
|
|
if node.HeaderID != "" {
|
|
|
|
id := r.ensureUniqueHeaderID(node.HeaderID)
|
|
|
|
if r.parameters.HeaderIDPrefix != "" {
|
|
|
|
id = r.parameters.HeaderIDPrefix + id
|
2016-03-30 10:57:02 +02:00
|
|
|
}
|
2016-03-30 14:48:43 +02:00
|
|
|
if r.parameters.HeaderIDSuffix != "" {
|
|
|
|
id = id + r.parameters.HeaderIDSuffix
|
2016-03-30 10:57:02 +02:00
|
|
|
}
|
2016-03-30 14:48:43 +02:00
|
|
|
attrs = append(attrs, fmt.Sprintf(`id="%s"`, id))
|
2016-03-30 10:57:02 +02:00
|
|
|
}
|
2016-03-30 20:13:02 +02:00
|
|
|
r.cr(w)
|
|
|
|
r.out(w, tag(tagname, attrs, false))
|
2016-03-30 14:48:43 +02:00
|
|
|
} else {
|
2016-03-30 20:13:02 +02:00
|
|
|
r.out(w, tag("/"+tagname, nil, false))
|
2016-03-30 14:48:43 +02:00
|
|
|
if !(node.Parent.Type == Item && node.Next == nil) {
|
2016-03-30 20:13:02 +02:00
|
|
|
r.cr(w)
|
2016-03-30 10:57:02 +02:00
|
|
|
}
|
2016-03-30 14:48:43 +02:00
|
|
|
}
|
|
|
|
break
|
|
|
|
case HorizontalRule:
|
2016-03-30 20:13:02 +02:00
|
|
|
r.cr(w)
|
|
|
|
r.out(w, tag("hr", attrs, r.flags&UseXHTML != 0))
|
|
|
|
r.cr(w)
|
2016-03-30 14:48:43 +02:00
|
|
|
break
|
|
|
|
case List:
|
|
|
|
tagName := "ul"
|
2016-04-01 10:21:25 +02:00
|
|
|
if node.ListFlags&ListTypeOrdered != 0 {
|
2016-03-30 14:48:43 +02:00
|
|
|
tagName = "ol"
|
|
|
|
}
|
2016-04-01 10:21:25 +02:00
|
|
|
if node.ListFlags&ListTypeDefinition != 0 {
|
2016-03-30 14:48:43 +02:00
|
|
|
tagName = "dl"
|
|
|
|
}
|
|
|
|
if entering {
|
|
|
|
// var start = node.listStart;
|
|
|
|
// if (start !== null && start !== 1) {
|
|
|
|
// attrs.push(['start', start.toString()]);
|
|
|
|
// }
|
2016-03-30 20:13:02 +02:00
|
|
|
r.cr(w)
|
2016-04-01 10:21:25 +02:00
|
|
|
if node.Parent.Type == Item && node.Parent.Parent.Tight {
|
2016-03-30 20:13:02 +02:00
|
|
|
r.cr(w)
|
2016-03-30 10:57:02 +02:00
|
|
|
}
|
2016-03-30 20:13:02 +02:00
|
|
|
r.out(w, tag(tagName, attrs, false))
|
|
|
|
r.cr(w)
|
2016-03-30 14:48:43 +02:00
|
|
|
} else {
|
2016-03-30 20:13:02 +02:00
|
|
|
r.out(w, tag("/"+tagName, nil, false))
|
|
|
|
//cr(w)
|
2016-03-30 14:48:43 +02:00
|
|
|
//if node.parent.Type != Item {
|
2016-03-30 20:13:02 +02:00
|
|
|
// cr(w)
|
2016-03-30 14:48:43 +02:00
|
|
|
//}
|
|
|
|
if node.Parent.Type == Item && node.Next != nil {
|
2016-03-30 20:13:02 +02:00
|
|
|
r.cr(w)
|
2016-03-30 10:57:02 +02:00
|
|
|
}
|
2016-03-30 14:48:43 +02:00
|
|
|
if node.Parent.Type == Document || node.Parent.Type == BlockQuote {
|
2016-03-30 20:13:02 +02:00
|
|
|
r.cr(w)
|
2016-03-30 14:48:43 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
case Item:
|
|
|
|
tagName := "li"
|
2016-04-01 10:21:25 +02:00
|
|
|
if node.ListFlags&ListTypeDefinition != 0 {
|
2016-03-30 14:48:43 +02:00
|
|
|
tagName = "dd"
|
|
|
|
}
|
2016-04-01 10:21:25 +02:00
|
|
|
if node.ListFlags&ListTypeTerm != 0 {
|
2016-03-30 14:48:43 +02:00
|
|
|
tagName = "dt"
|
|
|
|
}
|
|
|
|
if entering {
|
|
|
|
if itemOpenCR(node) {
|
2016-03-30 20:13:02 +02:00
|
|
|
r.cr(w)
|
2016-03-30 10:57:02 +02:00
|
|
|
}
|
2016-03-30 14:48:43 +02:00
|
|
|
if node.ListData.RefLink != nil {
|
|
|
|
slug := slugify(node.ListData.RefLink)
|
2016-03-30 20:13:02 +02:00
|
|
|
r.out(w, footnoteItem(r.parameters.FootnoteAnchorPrefix, slug))
|
2016-03-30 14:48:43 +02:00
|
|
|
break
|
2016-03-30 10:57:02 +02:00
|
|
|
}
|
2016-03-30 20:13:02 +02:00
|
|
|
r.out(w, tag(tagName, nil, false))
|
2016-03-30 14:48:43 +02:00
|
|
|
} else {
|
|
|
|
if node.ListData.RefLink != nil {
|
|
|
|
slug := slugify(node.ListData.RefLink)
|
|
|
|
if r.flags&FootnoteReturnLinks != 0 {
|
2016-03-30 20:13:02 +02:00
|
|
|
r.out(w, footnoteReturnLink(r.parameters.FootnoteAnchorPrefix, r.parameters.FootnoteReturnLinkContents, slug))
|
2016-03-30 10:57:02 +02:00
|
|
|
}
|
|
|
|
}
|
2016-03-30 20:13:02 +02:00
|
|
|
r.out(w, tag("/"+tagName, nil, false))
|
|
|
|
r.cr(w)
|
2016-03-30 14:48:43 +02:00
|
|
|
}
|
|
|
|
case CodeBlock:
|
|
|
|
attrs = appendLanguageAttr(attrs, node.Info)
|
2016-03-30 20:13:02 +02:00
|
|
|
r.cr(w)
|
|
|
|
r.out(w, tag("pre", nil, false))
|
|
|
|
r.out(w, tag("code", attrs, false))
|
|
|
|
r.out(w, escCode(node.Literal, false))
|
|
|
|
r.out(w, tag("/code", nil, false))
|
|
|
|
r.out(w, tag("/pre", nil, false))
|
2016-03-30 14:48:43 +02:00
|
|
|
if node.Parent.Type != Item {
|
2016-03-30 20:13:02 +02:00
|
|
|
r.cr(w)
|
2016-03-30 14:48:43 +02:00
|
|
|
}
|
|
|
|
case Table:
|
|
|
|
if entering {
|
2016-03-30 20:13:02 +02:00
|
|
|
r.cr(w)
|
|
|
|
r.out(w, tag("table", nil, false))
|
2016-03-30 14:48:43 +02:00
|
|
|
} else {
|
2016-03-30 20:13:02 +02:00
|
|
|
r.out(w, tag("/table", nil, false))
|
|
|
|
r.cr(w)
|
2016-03-30 14:48:43 +02:00
|
|
|
}
|
|
|
|
case TableCell:
|
|
|
|
tagName := "td"
|
|
|
|
if node.IsHeader {
|
|
|
|
tagName = "th"
|
|
|
|
}
|
|
|
|
if entering {
|
|
|
|
align := cellAlignment(node.Align)
|
|
|
|
if align != "" {
|
|
|
|
attrs = append(attrs, fmt.Sprintf(`align="%s"`, align))
|
2016-03-30 10:57:02 +02:00
|
|
|
}
|
2016-03-30 14:48:43 +02:00
|
|
|
if node.Prev == nil {
|
2016-03-30 20:13:02 +02:00
|
|
|
r.cr(w)
|
2016-03-30 10:57:02 +02:00
|
|
|
}
|
2016-03-30 20:13:02 +02:00
|
|
|
r.out(w, tag(tagName, attrs, false))
|
2016-03-30 14:48:43 +02:00
|
|
|
} else {
|
2016-03-30 20:13:02 +02:00
|
|
|
r.out(w, tag("/"+tagName, nil, false))
|
|
|
|
r.cr(w)
|
2016-03-30 14:48:43 +02:00
|
|
|
}
|
|
|
|
case TableHead:
|
|
|
|
if entering {
|
2016-03-30 20:13:02 +02:00
|
|
|
r.cr(w)
|
|
|
|
r.out(w, tag("thead", nil, false))
|
2016-03-30 14:48:43 +02:00
|
|
|
} else {
|
2016-03-30 20:13:02 +02:00
|
|
|
r.out(w, tag("/thead", nil, false))
|
|
|
|
r.cr(w)
|
2016-03-30 14:48:43 +02:00
|
|
|
}
|
|
|
|
case TableBody:
|
|
|
|
if entering {
|
2016-03-30 20:13:02 +02:00
|
|
|
r.cr(w)
|
|
|
|
r.out(w, tag("tbody", nil, false))
|
2016-03-30 14:48:43 +02:00
|
|
|
// XXX: this is to adhere to a rather silly test. Should fix test.
|
|
|
|
if node.FirstChild == nil {
|
2016-03-30 20:13:02 +02:00
|
|
|
r.cr(w)
|
2016-03-30 10:57:02 +02:00
|
|
|
}
|
2016-03-30 14:48:43 +02:00
|
|
|
} else {
|
2016-03-30 20:13:02 +02:00
|
|
|
r.out(w, tag("/tbody", nil, false))
|
|
|
|
r.cr(w)
|
2016-03-30 10:57:02 +02:00
|
|
|
}
|
2016-03-30 14:48:43 +02:00
|
|
|
case TableRow:
|
|
|
|
if entering {
|
2016-03-30 20:13:02 +02:00
|
|
|
r.cr(w)
|
|
|
|
r.out(w, tag("tr", nil, false))
|
2016-03-30 14:48:43 +02:00
|
|
|
} else {
|
2016-03-30 20:13:02 +02:00
|
|
|
r.out(w, tag("/tr", nil, false))
|
|
|
|
r.cr(w)
|
2016-03-30 14:48:43 +02:00
|
|
|
}
|
|
|
|
default:
|
|
|
|
panic("Unknown node type " + node.Type.String())
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-04-04 09:14:49 +02:00
|
|
|
func (r *HTML) writeDocumentHeader(w *bytes.Buffer, sr *SPRenderer) {
|
|
|
|
if r.flags&CompletePage == 0 {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
ending := ""
|
|
|
|
if r.flags&UseXHTML != 0 {
|
|
|
|
w.WriteString("<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" ")
|
|
|
|
w.WriteString("\"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">\n")
|
|
|
|
w.WriteString("<html xmlns=\"http://www.w3.org/1999/xhtml\">\n")
|
|
|
|
ending = " /"
|
|
|
|
} else {
|
|
|
|
w.WriteString("<!DOCTYPE html>\n")
|
|
|
|
w.WriteString("<html>\n")
|
|
|
|
}
|
|
|
|
w.WriteString("<head>\n")
|
|
|
|
w.WriteString(" <title>")
|
|
|
|
if r.extensions&Smartypants != 0 {
|
|
|
|
w.Write(sr.Process([]byte(r.title)))
|
|
|
|
} else {
|
|
|
|
w.Write(esc([]byte(r.title), false))
|
|
|
|
}
|
|
|
|
w.WriteString("</title>\n")
|
|
|
|
w.WriteString(" <meta name=\"GENERATOR\" content=\"Blackfriday Markdown Processor v")
|
|
|
|
w.WriteString(VERSION)
|
|
|
|
w.WriteString("\"")
|
|
|
|
w.WriteString(ending)
|
|
|
|
w.WriteString(">\n")
|
|
|
|
w.WriteString(" <meta charset=\"utf-8\"")
|
|
|
|
w.WriteString(ending)
|
|
|
|
w.WriteString(">\n")
|
|
|
|
if r.css != "" {
|
|
|
|
w.WriteString(" <link rel=\"stylesheet\" type=\"text/css\" href=\"")
|
|
|
|
r.attrEscape([]byte(r.css))
|
|
|
|
w.WriteString("\"")
|
|
|
|
w.WriteString(ending)
|
|
|
|
w.WriteString(">\n")
|
|
|
|
}
|
|
|
|
w.WriteString("</head>\n")
|
|
|
|
w.WriteString("<body>\n\n")
|
|
|
|
}
|
|
|
|
|
|
|
|
func (r *HTML) writeDocumentFooter(w *bytes.Buffer) {
|
|
|
|
if r.flags&CompletePage == 0 {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
w.WriteString("\n</body>\n</html>\n")
|
|
|
|
}
|
|
|
|
|
2016-03-31 12:54:09 +02:00
|
|
|
func (r *HTML) Render(ast *Node) []byte {
|
2016-03-30 14:48:43 +02:00
|
|
|
//println("render_Blackfriday")
|
|
|
|
//dump(ast)
|
2016-04-01 09:44:22 +02:00
|
|
|
// Run Smartypants if it's enabled or simply escape text if not
|
|
|
|
sr := NewSmartypantsRenderer(r.extensions)
|
2016-04-01 11:36:56 +02:00
|
|
|
ast.Walk(func(node *Node, entering bool) {
|
2016-04-01 09:44:22 +02:00
|
|
|
if node.Type == Text {
|
|
|
|
if r.extensions&Smartypants != 0 {
|
|
|
|
node.Literal = sr.Process(node.Literal)
|
|
|
|
} else {
|
|
|
|
node.Literal = esc(node.Literal, false)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
})
|
2016-03-30 20:13:02 +02:00
|
|
|
var buff bytes.Buffer
|
2016-04-04 09:14:49 +02:00
|
|
|
r.writeDocumentHeader(&buff, sr)
|
2016-04-01 11:36:56 +02:00
|
|
|
ast.Walk(func(node *Node, entering bool) {
|
2016-03-30 20:13:02 +02:00
|
|
|
r.RenderNode(&buff, node, entering)
|
|
|
|
})
|
2016-04-04 09:14:49 +02:00
|
|
|
r.writeDocumentFooter(&buff)
|
2016-03-30 20:13:02 +02:00
|
|
|
return buff.Bytes()
|
2016-03-30 10:57:02 +02:00
|
|
|
}
|