summaryrefslogtreecommitdiffstats
path: root/vendor/github.com/gomarkdown/markdown/html
diff options
context:
space:
mode:
Diffstat (limited to 'vendor/github.com/gomarkdown/markdown/html')
-rw-r--r--vendor/github.com/gomarkdown/markdown/html/callouts.go42
-rw-r--r--vendor/github.com/gomarkdown/markdown/html/doc.go43
-rw-r--r--vendor/github.com/gomarkdown/markdown/html/esc.go50
-rw-r--r--vendor/github.com/gomarkdown/markdown/html/renderer.go1318
-rw-r--r--vendor/github.com/gomarkdown/markdown/html/smartypants.go444
5 files changed, 1897 insertions, 0 deletions
diff --git a/vendor/github.com/gomarkdown/markdown/html/callouts.go b/vendor/github.com/gomarkdown/markdown/html/callouts.go
new file mode 100644
index 00000000..e377af22
--- /dev/null
+++ b/vendor/github.com/gomarkdown/markdown/html/callouts.go
@@ -0,0 +1,42 @@
+package html
+
+import (
+ "bytes"
+ "io"
+
+ "github.com/gomarkdown/markdown/ast"
+ "github.com/gomarkdown/markdown/parser"
+)
+
+// EscapeHTMLCallouts writes html-escaped d to w. It escapes &, <, > and " characters, *but*
+// expands callouts <<N>> with the callout HTML, i.e. by calling r.callout() with a newly created
+// ast.Callout node.
+func (r *Renderer) EscapeHTMLCallouts(w io.Writer, d []byte) {
+ ld := len(d)
+Parse:
+ for i := 0; i < ld; i++ {
+ for _, comment := range r.opts.Comments {
+ if !bytes.HasPrefix(d[i:], comment) {
+ break
+ }
+
+ lc := len(comment)
+ if i+lc < ld {
+ if id, consumed := parser.IsCallout(d[i+lc:]); consumed > 0 {
+ // We have seen a callout
+ callout := &ast.Callout{ID: id}
+ r.callout(w, callout)
+ i += consumed + lc - 1
+ continue Parse
+ }
+ }
+ }
+
+ escSeq := Escaper[d[i]]
+ if escSeq != nil {
+ w.Write(escSeq)
+ } else {
+ w.Write([]byte{d[i]})
+ }
+ }
+}
diff --git a/vendor/github.com/gomarkdown/markdown/html/doc.go b/vendor/github.com/gomarkdown/markdown/html/doc.go
new file mode 100644
index 00000000..f837c63d
--- /dev/null
+++ b/vendor/github.com/gomarkdown/markdown/html/doc.go
@@ -0,0 +1,43 @@
+/*
+Package html implements HTML renderer of parsed markdown document.
+
+Configuring and customizing a renderer
+
+A renderer can be configured with multiple options:
+
+ import "github.com/gomarkdown/markdown/html"
+
+ flags := html.CommonFlags | html.CompletePage | html.HrefTargetBlank
+ opts := html.RenderOptions{
+ TItle: "A custom title",
+ Flags: flags,
+ }
+ renderer := html.NewRenderer(opts)
+
+You can also re-use most of the logic and customize rendering of selected nodes
+by providing node render hook.
+This is most useful for rendering nodes that allow for design choices, like
+links or code blocks.
+
+ import (
+ "github.com/gomarkdown/markdown/html"
+ "github.com/gomarkdown/markdown/ast"
+ )
+
+ // a very dummy render hook that will output "code_replacements" instead of
+ // <code>${content}</code> emitted by html.Renderer
+ func renderHookCodeBlock(w io.Writer, node *ast.Node, entering bool) (ast.WalkStatus, bool) {
+ _, ok := node.Data.(*ast.CodeBlockData)
+ if !ok {
+ return ast.GoToNext, false
+ }
+ io.WriteString(w, "code_replacement")
+ return ast.GoToNext, true
+ }
+
+ opts := html.RendererOptions{
+ RenderNodeHook: renderHookCodeBlock,
+ }
+ renderer := html.NewRenderer(opts)
+*/
+package html
diff --git a/vendor/github.com/gomarkdown/markdown/html/esc.go b/vendor/github.com/gomarkdown/markdown/html/esc.go
new file mode 100644
index 00000000..89ec9a27
--- /dev/null
+++ b/vendor/github.com/gomarkdown/markdown/html/esc.go
@@ -0,0 +1,50 @@
+package html
+
+import (
+ "html"
+ "io"
+)
+
+var Escaper = [256][]byte{
+ '&': []byte("&amp;"),
+ '<': []byte("&lt;"),
+ '>': []byte("&gt;"),
+ '"': []byte("&quot;"),
+}
+
+// EscapeHTML writes html-escaped d to w. It escapes &, <, > and " characters.
+func EscapeHTML(w io.Writer, d []byte) {
+ var start, end int
+ n := len(d)
+ for end < n {
+ escSeq := Escaper[d[end]]
+ if escSeq != nil {
+ w.Write(d[start:end])
+ w.Write(escSeq)
+ start = end + 1
+ }
+ end++
+ }
+ if start < n && end <= n {
+ w.Write(d[start:end])
+ }
+}
+
+func escLink(w io.Writer, text []byte) {
+ unesc := html.UnescapeString(string(text))
+ EscapeHTML(w, []byte(unesc))
+}
+
+// Escape writes the text to w, but skips the escape character.
+func Escape(w io.Writer, text []byte) {
+ esc := false
+ for i := 0; i < len(text); i++ {
+ if text[i] == '\\' {
+ esc = !esc
+ }
+ if esc && text[i] == '\\' {
+ continue
+ }
+ w.Write([]byte{text[i]})
+ }
+}
diff --git a/vendor/github.com/gomarkdown/markdown/html/renderer.go b/vendor/github.com/gomarkdown/markdown/html/renderer.go
new file mode 100644
index 00000000..367f7dfa
--- /dev/null
+++ b/vendor/github.com/gomarkdown/markdown/html/renderer.go
@@ -0,0 +1,1318 @@
+package html
+
+import (
+ "bytes"
+ "fmt"
+ "io"
+ "regexp"
+ "sort"
+ "strconv"
+ "strings"
+
+ "github.com/gomarkdown/markdown/ast"
+)
+
+// Flags control optional behavior of HTML renderer.
+type Flags int
+
+// IDTag is the tag used for tag identification, it defaults to "id", some renderers
+// may wish to override this and use e.g. "anchor".
+var IDTag = "id"
+
+// HTML renderer configuration options.
+const (
+ FlagsNone Flags = 0
+ SkipHTML Flags = 1 << iota // Skip preformatted HTML blocks
+ 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
+ FootnoteNoHRTag // Do not output an HR after starting a footnote list.
+ Smartypants // Enable smart punctuation substitutions
+ SmartypantsFractions // Enable smart fractions (with Smartypants)
+ SmartypantsDashes // Enable smart dashes (with Smartypants)
+ SmartypantsLatexDashes // Enable LaTeX-style dashes (with Smartypants)
+ SmartypantsAngledQuotes // Enable angled double quotes (with Smartypants) for double quotes rendering
+ SmartypantsQuotesNBSP // Enable « French guillemets » (with Smartypants)
+ TOC // Generate a table of contents
+
+ CommonFlags Flags = Smartypants | SmartypantsFractions | SmartypantsDashes | SmartypantsLatexDashes
+)
+
+var (
+ htmlTagRe = regexp.MustCompile("(?i)^" + htmlTag)
+)
+
+const (
+ htmlTag = "(?:" + openTag + "|" + closeTag + "|" + htmlComment + "|" +
+ processingInstruction + "|" + declaration + "|" + cdata + ")"
+ closeTag = "</" + tagName + "\\s*[>]"
+ openTag = "<" + tagName + attribute + "*" + "\\s*/?>"
+ attribute = "(?:" + "\\s+" + attributeName + attributeValueSpec + "?)"
+ attributeValue = "(?:" + unquotedValue + "|" + singleQuotedValue + "|" + doubleQuotedValue + ")"
+ attributeValueSpec = "(?:" + "\\s*=" + "\\s*" + attributeValue + ")"
+ attributeName = "[a-zA-Z_:][a-zA-Z0-9:._-]*"
+ cdata = "<!\\[CDATA\\[[\\s\\S]*?\\]\\]>"
+ declaration = "<![A-Z]+" + "\\s+[^>]*>"
+ doubleQuotedValue = "\"[^\"]*\""
+ htmlComment = "<!---->|<!--(?:-?[^>-])(?:-?[^-])*-->"
+ processingInstruction = "[<][?].*?[?][>]"
+ singleQuotedValue = "'[^']*'"
+ tagName = "[A-Za-z][A-Za-z0-9-]*"
+ unquotedValue = "[^\"'=<>`\\x00-\\x20]+"
+)
+
+// RenderNodeFunc allows reusing most of Renderer logic and replacing
+// rendering of some nodes. If it returns false, Renderer.RenderNode
+// will execute its logic. If it returns true, Renderer.RenderNode will
+// skip rendering this node and will return WalkStatus
+type RenderNodeFunc func(w io.Writer, node ast.Node, entering bool) (ast.WalkStatus, bool)
+
+// RendererOptions is a collection of supplementary parameters tweaking
+// the behavior of various parts of HTML renderer.
+type RendererOptions struct {
+ // Prepend this text to each relative URL.
+ AbsolutePrefix string
+ // Add this text to each footnote anchor, to ensure uniqueness.
+ FootnoteAnchorPrefix string
+ // Show this text inside the <a> tag for a footnote return link, if the
+ // FootnoteReturnLinks flag is enabled. If blank, the string
+ // <sup>[return]</sup> is used.
+ FootnoteReturnLinkContents string
+ // CitationFormatString defines how a citation is rendered. If blnck, the string
+ // <sup>[%s]</sup> is used. Where %s will be substituted with the citation target.
+ CitationFormatString string
+ // If set, add this text to the front of each Heading ID, to ensure uniqueness.
+ HeadingIDPrefix string
+ // If set, add this text to the back of each Heading ID, to ensure uniqueness.
+ HeadingIDSuffix string
+
+ Title string // Document title (used if CompletePage is set)
+ CSS string // Optional CSS file URL (used if CompletePage is set)
+ Icon string // Optional icon file URL (used if CompletePage is set)
+ Head []byte // Optional head data injected in the <head> section (used if CompletePage is set)
+
+ Flags Flags // Flags allow customizing this renderer's behavior
+
+ // if set, called at the start of RenderNode(). Allows replacing
+ // rendering of some nodes
+ RenderNodeHook RenderNodeFunc
+
+ // Comments is a list of comments the renderer should detect when
+ // parsing code blocks and detecting callouts.
+ Comments [][]byte
+
+ // Generator is a meta tag that is inserted in the generated HTML so show what rendered it. It should not include the closing tag.
+ // Defaults (note content quote is not closed) to ` <meta name="GENERATOR" content="github.com/gomarkdown/markdown markdown processor for Go`
+ Generator string
+}
+
+// Renderer implements Renderer interface for HTML output.
+//
+// Do not create this directly, instead use the NewRenderer function.
+type Renderer struct {
+ opts RendererOptions
+
+ closeTag string // how to end singleton tags: either " />" or ">"
+
+ // Track heading IDs to prevent ID collision in a single generation.
+ headingIDs map[string]int
+
+ lastOutputLen int
+ disableTags int
+
+ sr *SPRenderer
+
+ documentMatter ast.DocumentMatters // keep track of front/main/back matter.
+}
+
+// NewRenderer creates and configures an Renderer object, which
+// satisfies the Renderer interface.
+func NewRenderer(opts RendererOptions) *Renderer {
+ // configure the rendering engine
+ closeTag := ">"
+ if opts.Flags&UseXHTML != 0 {
+ closeTag = " />"
+ }
+
+ if opts.FootnoteReturnLinkContents == "" {
+ opts.FootnoteReturnLinkContents = `<sup>[return]</sup>`
+ }
+ if opts.CitationFormatString == "" {
+ opts.CitationFormatString = `<sup>[%s]</sup>`
+ }
+ if opts.Generator == "" {
+ opts.Generator = ` <meta name="GENERATOR" content="github.com/gomarkdown/markdown markdown processor for Go`
+ }
+
+ return &Renderer{
+ opts: opts,
+
+ closeTag: closeTag,
+ headingIDs: make(map[string]int),
+
+ sr: NewSmartypantsRenderer(opts.Flags),
+ }
+}
+
+func isHTMLTag(tag []byte, tagname string) bool {
+ found, _ := findHTMLTagPos(tag, tagname)
+ return found
+}
+
+// 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
+}
+
+func findHTMLTagPos(tag []byte, tagname string) (bool, int) {
+ i := 0
+ if i < len(tag) && tag[0] != '<' {
+ return false, -1
+ }
+ i++
+ i = skipSpace(tag, i)
+
+ if i < len(tag) && tag[i] == '/' {
+ i++
+ }
+
+ i = skipSpace(tag, i)
+ j := 0
+ for ; i < len(tag); i, j = i+1, j+1 {
+ if j >= len(tagname) {
+ break
+ }
+
+ if strings.ToLower(string(tag[i]))[0] != tagname[j] {
+ return false, -1
+ }
+ }
+
+ if i == len(tag) {
+ return false, -1
+ }
+
+ rightAngle := skipUntilCharIgnoreQuotes(tag, i, '>')
+ if rightAngle >= i {
+ return true, rightAngle
+ }
+
+ return false, -1
+}
+
+func isRelativeLink(link []byte) (yes bool) {
+ // a tag begin with '#'
+ if link[0] == '#' {
+ return true
+ }
+
+ // link begin with '/' but not '//', the second maybe a protocol relative link
+ if len(link) >= 2 && link[0] == '/' && link[1] != '/' {
+ return true
+ }
+
+ // only the root '/'
+ if len(link) == 1 && link[0] == '/' {
+ return true
+ }
+
+ // current directory : begin with "./"
+ if bytes.HasPrefix(link, []byte("./")) {
+ return true
+ }
+
+ // parent directory : begin with "../"
+ if bytes.HasPrefix(link, []byte("../")) {
+ return true
+ }
+
+ return false
+}
+
+func (r *Renderer) ensureUniqueHeadingID(id string) string {
+ for count, found := r.headingIDs[id]; found; count, found = r.headingIDs[id] {
+ tmp := fmt.Sprintf("%s-%d", id, count+1)
+
+ if _, tmpFound := r.headingIDs[tmp]; !tmpFound {
+ r.headingIDs[id] = count + 1
+ id = tmp
+ } else {
+ id = id + "-1"
+ }
+ }
+
+ if _, found := r.headingIDs[id]; !found {
+ r.headingIDs[id] = 0
+ }
+
+ return id
+}
+
+func (r *Renderer) addAbsPrefix(link []byte) []byte {
+ if r.opts.AbsolutePrefix != "" && isRelativeLink(link) && link[0] != '.' {
+ newDest := r.opts.AbsolutePrefix
+ if link[0] != '/' {
+ newDest += "/"
+ }
+ newDest += string(link)
+ return []byte(newDest)
+ }
+ return link
+}
+
+func appendLinkAttrs(attrs []string, flags Flags, link []byte) []string {
+ if isRelativeLink(link) {
+ return attrs
+ }
+ var 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:"))
+}
+
+func needSkipLink(flags Flags, dest []byte) bool {
+ if flags&SkipLinks != 0 {
+ return true
+ }
+ return flags&Safelink != 0 && !isSafeLink(dest) && !isMailto(dest)
+}
+
+func isSmartypantable(node ast.Node) bool {
+ switch node.GetParent().(type) {
+ case *ast.Link, *ast.CodeBlock, *ast.Code:
+ return false
+ }
+ return true
+}
+
+func appendLanguageAttr(attrs []string, info []byte) []string {
+ if len(info) == 0 {
+ return attrs
+ }
+ endOfLang := bytes.IndexAny(info, "\t ")
+ if endOfLang < 0 {
+ endOfLang = len(info)
+ }
+ s := `class="language-` + string(info[:endOfLang]) + `"`
+ return append(attrs, s)
+}
+
+func (r *Renderer) outTag(w io.Writer, name string, attrs []string) {
+ s := name
+ if len(attrs) > 0 {
+ s += " " + strings.Join(attrs, " ")
+ }
+ io.WriteString(w, s+">")
+ r.lastOutputLen = 1
+}
+
+func footnoteRef(prefix string, node *ast.Link) string {
+ urlFrag := prefix + string(slugify(node.Destination))
+ nStr := strconv.Itoa(node.NoteID)
+ anchor := `<a href="#fn:` + urlFrag + `">` + nStr + `</a>`
+ return `<sup class="footnote-ref" id="fnref:` + urlFrag + `">` + anchor + `</sup>`
+}
+
+func footnoteItem(prefix string, slug []byte) string {
+ return `<li id="fn:` + prefix + string(slug) + `">`
+}
+
+func footnoteReturnLink(prefix, returnLink string, slug []byte) string {
+ return ` <a class="footnote-return" href="#fnref:` + prefix + string(slug) + `">` + returnLink + `</a>`
+}
+
+func listItemOpenCR(listItem *ast.ListItem) bool {
+ if ast.GetPrevNode(listItem) == nil {
+ return false
+ }
+ ld := listItem.Parent.(*ast.List)
+ return !ld.Tight && ld.ListFlags&ast.ListTypeDefinition == 0
+}
+
+func skipParagraphTags(para *ast.Paragraph) bool {
+ parent := para.Parent
+ grandparent := parent.GetParent()
+ if grandparent == nil || !isList(grandparent) {
+ return false
+ }
+ isParentTerm := isListItemTerm(parent)
+ grandparentListData := grandparent.(*ast.List)
+ tightOrTerm := grandparentListData.Tight || isParentTerm
+ return tightOrTerm
+}
+
+func (r *Renderer) out(w io.Writer, d []byte) {
+ r.lastOutputLen = len(d)
+ if r.disableTags > 0 {
+ d = htmlTagRe.ReplaceAll(d, []byte{})
+ }
+ w.Write(d)
+}
+
+func (r *Renderer) outs(w io.Writer, s string) {
+ r.lastOutputLen = len(s)
+ if r.disableTags > 0 {
+ s = htmlTagRe.ReplaceAllString(s, "")
+ }
+ io.WriteString(w, s)
+}
+
+func (r *Renderer) cr(w io.Writer) {
+ if r.lastOutputLen > 0 {
+ r.outs(w, "\n")
+ }
+}
+
+var (
+ openHTags = []string{"<h1", "<h2", "<h3", "<h4", "<h5"}
+ closeHTags = []string{"</h1>", "</h2>", "</h3>", "</h4>", "</h5>"}
+)
+
+func headingOpenTagFromLevel(level int) string {
+ if level < 1 || level > 5 {
+ return "<h6"
+ }
+ return openHTags[level-1]
+}
+
+func headingCloseTagFromLevel(level int) string {
+ if level < 1 || level > 5 {
+ return "</h6>"
+ }
+ return closeHTags[level-1]
+}
+
+func (r *Renderer) outHRTag(w io.Writer, attrs []string) {
+ hr := tagWithAttributes("<hr", attrs)
+ r.outOneOf(w, r.opts.Flags&UseXHTML == 0, hr, "<hr />")
+}
+
+func (r *Renderer) text(w io.Writer, text *ast.Text) {
+ if r.opts.Flags&Smartypants != 0 {
+ var tmp bytes.Buffer
+ EscapeHTML(&tmp, text.Literal)
+ r.sr.Process(w, tmp.Bytes())
+ } else {
+ _, parentIsLink := text.Parent.(*ast.Link)
+ if parentIsLink {
+ escLink(w, text.Literal)
+ } else {
+ EscapeHTML(w, text.Literal)
+ }
+ }
+}
+
+func (r *Renderer) hardBreak(w io.Writer, node *ast.Hardbreak) {
+ r.outOneOf(w, r.opts.Flags&UseXHTML == 0, "<br>", "<br />")
+ r.cr(w)
+}
+
+func (r *Renderer) nonBlockingSpace(w io.Writer, node *ast.NonBlockingSpace) {
+ r.outs(w, "&nbsp;")
+}
+
+func (r *Renderer) outOneOf(w io.Writer, outFirst bool, first string, second string) {
+ if outFirst {
+ r.outs(w, first)
+ } else {
+ r.outs(w, second)
+ }
+}
+
+func (r *Renderer) outOneOfCr(w io.Writer, outFirst bool, first string, second string) {
+ if outFirst {
+ r.cr(w)
+ r.outs(w, first)
+ } else {
+ r.outs(w, second)
+ r.cr(w)
+ }
+}
+
+func (r *Renderer) htmlSpan(w io.Writer, span *ast.HTMLSpan) {
+ if r.opts.Flags&SkipHTML == 0 {
+ r.out(w, span.Literal)
+ }
+}
+
+func (r *Renderer) linkEnter(w io.Writer, link *ast.Link) {
+ var attrs []string
+ dest := link.Destination
+ dest = r.addAbsPrefix(dest)
+ var hrefBuf bytes.Buffer
+ hrefBuf.WriteString("href=\"")
+ escLink(&hrefBuf, dest)
+ hrefBuf.WriteByte('"')
+ attrs = append(attrs, hrefBuf.String())
+ if link.NoteID != 0 {
+ r.outs(w, footnoteRef(r.opts.FootnoteAnchorPrefix, link))
+ return
+ }
+
+ attrs = appendLinkAttrs(attrs, r.opts.Flags, dest)
+ if len(link.Title) > 0 {
+ var titleBuff bytes.Buffer
+ titleBuff.WriteString("title=\"")
+ EscapeHTML(&titleBuff, link.Title)
+ titleBuff.WriteByte('"')
+ attrs = append(attrs, titleBuff.String())
+ }
+ r.outTag(w, "<a", attrs)
+}
+
+func (r *Renderer) linkExit(w io.Writer, link *ast.Link) {
+ if link.NoteID == 0 {
+ r.outs(w, "</a>")
+ }
+}
+
+func (r *Renderer) link(w io.Writer, link *ast.Link, entering bool) {
+ // mark it but don't link it if it is not a safe link: no smartypants
+ if needSkipLink(r.opts.Flags, link.Destination) {
+ r.outOneOf(w, entering, "<tt>", "</tt>")
+ return
+ }
+
+ if entering {
+ r.linkEnter(w, link)
+ } else {
+ r.linkExit(w, link)
+ }
+}
+
+func (r *Renderer) imageEnter(w io.Writer, image *ast.Image) {
+ dest := image.Destination
+ dest = r.addAbsPrefix(dest)
+ if r.disableTags == 0 {
+ //if options.safe && potentiallyUnsafe(dest) {
+ //out(w, `<img src="" alt="`)
+ //} else {
+ r.outs(w, `<img src="`)
+ escLink(w, dest)
+ r.outs(w, `" alt="`)
+ //}
+ }
+ r.disableTags++
+}
+
+func (r *Renderer) imageExit(w io.Writer, image *ast.Image) {
+ r.disableTags--
+ if r.disableTags == 0 {
+ if image.Title != nil {
+ r.outs(w, `" title="`)
+ EscapeHTML(w, image.Title)
+ }
+ r.outs(w, `" />`)
+ }
+}
+
+func (r *Renderer) paragraphEnter(w io.Writer, para *ast.Paragraph) {
+ // TODO: untangle this clusterfuck about when the newlines need
+ // to be added and when not.
+ prev := ast.GetPrevNode(para)
+ if prev != nil {
+ switch prev.(type) {
+ case *ast.HTMLBlock, *ast.List, *ast.Paragraph, *ast.Heading, *ast.CaptionFigure, *ast.CodeBlock, *ast.BlockQuote, *ast.Aside, *ast.HorizontalRule:
+ r.cr(w)
+ }
+ }
+
+ if prev == nil {
+ _, isParentBlockQuote := para.Parent.(*ast.BlockQuote)
+ if isParentBlockQuote {
+ r.cr(w)
+ }
+ _, isParentAside := para.Parent.(*ast.Aside)
+ if isParentAside {
+ r.cr(w)
+ }
+ }
+
+ tag := tagWithAttributes("<p", BlockAttrs(para))
+ r.outs(w, tag)
+}
+
+func (r *Renderer) paragraphExit(w io.Writer, para *ast.Paragraph) {
+ r.outs(w, "</p>")
+ if !(isListItem(para.Parent) && ast.GetNextNode(para) == nil) {
+ r.cr(w)
+ }
+}
+
+func (r *Renderer) paragraph(w io.Writer, para *ast.Paragraph, entering bool) {
+ if skipParagraphTags(para) {
+ return
+ }
+ if entering {
+ r.paragraphEnter(w, para)
+ } else {
+ r.paragraphExit(w, para)
+ }
+}
+func (r *Renderer) image(w io.Writer, node *ast.Image, entering bool) {
+ if entering {
+ r.imageEnter(w, node)
+ } else {
+ r.imageExit(w, node)
+ }
+}
+
+func (r *Renderer) code(w io.Writer, node *ast.Code) {
+ r.outs(w, "<code>")
+ EscapeHTML(w, node.Literal)
+ r.outs(w, "</code>")
+}
+
+func (r *Renderer) htmlBlock(w io.Writer, node *ast.HTMLBlock) {
+ if r.opts.Flags&SkipHTML != 0 {
+ return
+ }
+ r.cr(w)
+ r.out(w, node.Literal)
+ r.cr(w)
+}
+
+func (r *Renderer) headingEnter(w io.Writer, nodeData *ast.Heading) {
+ var attrs []string
+ var class string
+ // TODO(miek): add helper functions for coalescing these classes.
+ if nodeData.IsTitleblock {
+ class = "title"
+ }
+ if nodeData.IsSpecial {
+ if class != "" {
+ class += " special"
+ } else {
+ class = "special"
+ }
+ }
+ if class != "" {
+ attrs = []string{`class="` + class + `"`}
+ }
+ if nodeData.HeadingID != "" {
+ id := r.ensureUniqueHeadingID(nodeData.HeadingID)
+ if r.opts.HeadingIDPrefix != "" {
+ id = r.opts.HeadingIDPrefix + id
+ }
+ if r.opts.HeadingIDSuffix != "" {
+ id = id + r.opts.HeadingIDSuffix
+ }
+ attrID := `id="` + id + `"`
+ attrs = append(attrs, attrID)
+ }
+ attrs = append(attrs, BlockAttrs(nodeData)...)
+ r.cr(w)
+ r.outTag(w, headingOpenTagFromLevel(nodeData.Level), attrs)
+}
+
+func (r *Renderer) headingExit(w io.Writer, heading *ast.Heading) {
+ r.outs(w, headingCloseTagFromLevel(heading.Level))
+ if !(isListItem(heading.Parent) && ast.GetNextNode(heading) == nil) {
+ r.cr(w)
+ }
+}
+
+func (r *Renderer) heading(w io.Writer, node *ast.Heading, entering bool) {
+ if entering {
+ r.headingEnter(w, node)
+ } else {
+ r.headingExit(w, node)
+ }
+}
+
+func (r *Renderer) horizontalRule(w io.Writer, node *ast.HorizontalRule) {
+ r.cr(w)
+ r.outHRTag(w, BlockAttrs(node))
+ r.cr(w)
+}
+
+func (r *Renderer) listEnter(w io.Writer, nodeData *ast.List) {
+ // TODO: attrs don't seem to be set
+ var attrs []string
+
+ if nodeData.IsFootnotesList {
+ r.outs(w, "\n<div class=\"footnotes\">\n\n")
+ if r.opts.Flags&FootnoteNoHRTag == 0 {
+ r.outHRTag(w, nil)
+ r.cr(w)
+ }
+ }
+ r.cr(w)
+ if isListItem(nodeData.Parent) {
+ grand := nodeData.Parent.GetParent()
+ if isListTight(grand) {
+ r.cr(w)
+ }
+ }
+
+ openTag := "<ul"
+ if nodeData.ListFlags&ast.ListTypeOrdered != 0 {
+ if nodeData.Start > 0 {
+ attrs = append(attrs, fmt.Sprintf(`start="%d"`, nodeData.Start))
+ }
+ openTag = "<ol"
+ }
+ if nodeData.ListFlags&ast.ListTypeDefinition != 0 {
+ openTag = "<dl"
+ }
+ attrs = append(attrs, BlockAttrs(nodeData)...)
+ r.outTag(w, openTag, attrs)
+ r.cr(w)
+}
+
+func (r *Renderer) listExit(w io.Writer, list *ast.List) {
+ closeTag := "</ul>"
+ if list.ListFlags&ast.ListTypeOrdered != 0 {
+ closeTag = "</ol>"
+ }
+ if list.ListFlags&ast.ListTypeDefinition != 0 {
+ closeTag = "</dl>"
+ }
+ r.outs(w, closeTag)
+
+ //cr(w)
+ //if node.parent.Type != Item {
+ // cr(w)
+ //}
+ parent := list.Parent
+ switch parent.(type) {
+ case *ast.ListItem:
+ if ast.GetNextNode(list) != nil {
+ r.cr(w)
+ }
+ case *ast.Document, *ast.BlockQuote, *ast.Aside:
+ r.cr(w)
+ }
+
+ if list.IsFootnotesList {
+ r.outs(w, "\n</div>\n")
+ }
+}
+
+func (r *Renderer) list(w io.Writer, list *ast.List, entering bool) {
+ if entering {
+ r.listEnter(w, list)
+ } else {
+ r.listExit(w, list)
+ }
+}
+
+func (r *Renderer) listItemEnter(w io.Writer, listItem *ast.ListItem) {
+ if listItemOpenCR(listItem) {
+ r.cr(w)
+ }
+ if listItem.RefLink != nil {
+ slug := slugify(listItem.RefLink)
+ r.outs(w, footnoteItem(r.opts.FootnoteAnchorPrefix, slug))
+ return
+ }
+
+ openTag := "<li>"
+ if listItem.ListFlags&ast.ListTypeDefinition != 0 {
+ openTag = "<dd>"
+ }
+ if listItem.ListFlags&ast.ListTypeTerm != 0 {
+ openTag = "<dt>"
+ }
+ r.outs(w, openTag)
+}
+
+func (r *Renderer) listItemExit(w io.Writer, listItem *ast.ListItem) {
+ if listItem.RefLink != nil && r.opts.Flags&FootnoteReturnLinks != 0 {
+ slug := slugify(listItem.RefLink)
+ prefix := r.opts.FootnoteAnchorPrefix
+ link := r.opts.FootnoteReturnLinkContents
+ s := footnoteReturnLink(prefix, link, slug)
+ r.outs(w, s)
+ }
+
+ closeTag := "</li>"
+ if listItem.ListFlags&ast.ListTypeDefinition != 0 {
+ closeTag = "</dd>"
+ }
+ if listItem.ListFlags&ast.ListTypeTerm != 0 {
+ closeTag = "</dt>"
+ }
+ r.outs(w, closeTag)
+ r.cr(w)
+}
+
+func (r *Renderer) listItem(w io.Writer, listItem *ast.ListItem, entering bool) {
+ if entering {
+ r.listItemEnter(w, listItem)
+ } else {
+ r.listItemExit(w, listItem)
+ }
+}
+
+func (r *Renderer) codeBlock(w io.Writer, codeBlock *ast.CodeBlock) {
+ var attrs []string
+ // TODO(miek): this can add multiple class= attribute, they should be coalesced into one.
+ // This is probably true for some other elements as well
+ attrs = appendLanguageAttr(attrs, codeBlock.Info)
+ attrs = append(attrs, BlockAttrs(codeBlock)...)
+ r.cr(w)
+
+ r.outs(w, "<pre>")
+ code := tagWithAttributes("<code", attrs)
+ r.outs(w, code)
+ if r.opts.Comments != nil {
+ r.EscapeHTMLCallouts(w, codeBlock.Literal)
+ } else {
+ EscapeHTML(w, codeBlock.Literal)
+ }
+ r.outs(w, "</code>")
+ r.outs(w, "</pre>")
+ if !isListItem(codeBlock.Parent) {
+ r.cr(w)
+ }
+}
+
+func (r *Renderer) caption(w io.Writer, caption *ast.Caption, entering bool) {
+ if entering {
+ r.outs(w, "<figcaption>")
+ return
+ }
+ r.outs(w, "</figcaption>")
+}
+
+func (r *Renderer) captionFigure(w io.Writer, figure *ast.CaptionFigure, entering bool) {
+ // TODO(miek): copy more generic ways of mmark over to here.
+ fig := "<figure"
+ if figure.HeadingID != "" {
+ fig += ` id="` + figure.HeadingID + `">`
+ } else {
+ fig += ">"
+ }
+ r.outOneOf(w, entering, fig, "\n</figure>\n")
+}
+
+func (r *Renderer) tableCell(w io.Writer, tableCell *ast.TableCell, entering bool) {
+ if !entering {
+ r.outOneOf(w, tableCell.IsHeader, "</th>", "</td>")
+ r.cr(w)
+ return
+ }
+
+ // entering
+ var attrs []string
+ openTag := "<td"
+ if tableCell.IsHeader {
+ openTag = "<th"
+ }
+ align := tableCell.Align.String()
+ if align != "" {
+ attrs = append(attrs, fmt.Sprintf(`align="%s"`, align))
+ }
+ if ast.GetPrevNode(tableCell) == nil {
+ r.cr(w)
+ }
+ r.outTag(w, openTag, attrs)
+}
+
+func (r *Renderer) tableBody(w io.Writer, node *ast.TableBody, entering bool) {
+ if entering {
+ r.cr(w)
+ r.outs(w, "<tbody>")
+ // XXX: this is to adhere to a rather silly test. Should fix test.
+ if ast.GetFirstChild(node) == nil {
+ r.cr(w)
+ }
+ } else {
+ r.outs(w, "</tbody>")
+ r.cr(w)
+ }
+}
+
+func (r *Renderer) matter(w io.Writer, node *ast.DocumentMatter, entering bool) {
+ if !entering {
+ return
+ }
+ if r.documentMatter != ast.DocumentMatterNone {
+ r.outs(w, "</section>\n")
+ }
+ switch node.Matter {
+ case ast.DocumentMatterFront:
+ r.outs(w, `<section data-matter="front">`)
+ case ast.DocumentMatterMain:
+ r.outs(w, `<section data-matter="main">`)
+ case ast.DocumentMatterBack:
+ r.outs(w, `<section data-matter="back">`)
+ }
+ r.documentMatter = node.Matter
+}
+
+func (r *Renderer) citation(w io.Writer, node *ast.Citation) {
+ for i, c := range node.Destination {
+ attr := []string{`class="none"`}
+ switch node.Type[i] {
+ case ast.CitationTypeNormative:
+ attr[0] = `class="normative"`
+ case ast.CitationTypeInformative:
+ attr[0] = `class="informative"`
+ case ast.CitationTypeSuppressed:
+ attr[0] = `class="suppressed"`
+ }
+ r.outTag(w, "<cite", attr)
+ r.outs(w, fmt.Sprintf(`<a href="#%s">`+r.opts.CitationFormatString+`</a>`, c, c))
+ r.outs(w, "</cite>")
+ }
+}
+
+func (r *Renderer) callout(w io.Writer, node *ast.Callout) {
+ attr := []string{`class="callout"`}
+ r.outTag(w, "<span", attr)
+ r.out(w, node.ID)
+ r.outs(w, "</span>")
+}
+
+func (r *Renderer) index(w io.Writer, node *ast.Index) {
+ // there is no in-text representation.
+ attr := []string{`class="index"`, fmt.Sprintf(`id="%s"`, node.ID)}
+ r.outTag(w, "<span", attr)
+ r.outs(w, "</span>")
+}
+
+// RenderNode renders a markdown node to HTML
+func (r *Renderer) RenderNode(w io.Writer, node ast.Node, entering bool) ast.WalkStatus {
+ if r.opts.RenderNodeHook != nil {
+ status, didHandle := r.opts.RenderNodeHook(w, node, entering)
+ if didHandle {
+ return status
+ }
+ }
+ switch node := node.(type) {
+ case *ast.Text:
+ r.text(w, node)
+ case *ast.Softbreak:
+ r.cr(w)
+ // TODO: make it configurable via out(renderer.softbreak)
+ case *ast.Hardbreak:
+ r.hardBreak(w, node)
+ case *ast.NonBlockingSpace:
+ r.nonBlockingSpace(w, node)
+ case *ast.Emph:
+ r.outOneOf(w, entering, "<em>", "</em>")
+ case *ast.Strong:
+ r.outOneOf(w, entering, "<strong>", "</strong>")
+ case *ast.Del:
+ r.outOneOf(w, entering, "<del>", "</del>")
+ case *ast.BlockQuote:
+ tag := tagWithAttributes("<blockquote", BlockAttrs(node))
+ r.outOneOfCr(w, entering, tag, "</blockquote>")
+ case *ast.Aside:
+ tag := tagWithAttributes("<aside", BlockAttrs(node))
+ r.outOneOfCr(w, entering, tag, "</aside>")
+ case *ast.Link:
+ r.link(w, node, entering)
+ case *ast.CrossReference:
+ link := &ast.Link{Destination: append([]byte("#"), node.Destination...)}
+ r.link(w, link, entering)
+ case *ast.Citation:
+ r.citation(w, node)
+ case *ast.Image:
+ if r.opts.Flags&SkipImages != 0 {
+ return ast.SkipChildren
+ }
+ r.image(w, node, entering)
+ case *ast.Code:
+ r.code(w, node)
+ case *ast.CodeBlock:
+ r.codeBlock(w, node)
+ case *ast.Caption:
+ r.caption(w, node, entering)
+ case *ast.CaptionFigure:
+ r.captionFigure(w, node, entering)
+ case *ast.Document:
+ // do nothing
+ case *ast.Paragraph:
+ r.paragraph(w, node, entering)
+ case *ast.HTMLSpan:
+ r.htmlSpan(w, node)
+ case *ast.HTMLBlock:
+ r.htmlBlock(w, node)
+ case *ast.Heading:
+ r.heading(w, node, entering)
+ case *ast.HorizontalRule:
+ r.horizontalRule(w, node)
+ case *ast.List:
+ r.list(w, node, entering)
+ case *ast.ListItem:
+ r.listItem(w, node, entering)
+ case *ast.Table:
+ tag := tagWithAttributes("<table", BlockAttrs(node))
+ r.outOneOfCr(w, entering, tag, "</table>")
+ case *ast.TableCell:
+ r.tableCell(w, node, entering)
+ case *ast.TableHeader:
+ r.outOneOfCr(w, entering, "<thead>", "</thead>")
+ case *ast.TableBody:
+ r.tableBody(w, node, entering)
+ case *ast.TableRow:
+ r.outOneOfCr(w, entering, "<tr>", "</tr>")
+ case *ast.TableFooter:
+ r.outOneOfCr(w, entering, "<tfoot>", "</tfoot>")
+ case *ast.Math:
+ r.outOneOf(w, true, `<span class="math inline">\(`, `\)</span>`)
+ EscapeHTML(w, node.Literal)
+ r.outOneOf(w, false, `<span class="math inline">\(`, `\)</span>`)
+ case *ast.MathBlock:
+ r.outOneOf(w, entering, `<p><span class="math display">\[`, `\]</span></p>`)
+ if entering {
+ EscapeHTML(w, node.Literal)
+ }
+ case *ast.DocumentMatter:
+ r.matter(w, node, entering)
+ case *ast.Callout:
+ r.callout(w, node)
+ case *ast.Index:
+ r.index(w, node)
+ case *ast.Subscript:
+ r.outOneOf(w, true, "<sub>", "</sub>")
+ if entering {
+ Escape(w, node.Literal)
+ }
+ r.outOneOf(w, false, "<sub>", "</sub>")
+ case *ast.Superscript:
+ r.outOneOf(w, true, "<sup>", "</sup>")
+ if entering {
+ Escape(w, node.Literal)
+ }
+ r.outOneOf(w, false, "<sup>", "</sup>")
+ case *ast.Footnotes:
+ // nothing by default; just output the list.
+ default:
+ panic(fmt.Sprintf("Unknown node %T", node))
+ }
+ return ast.GoToNext
+}
+
+// RenderHeader writes HTML document preamble and TOC if requested.
+func (r *Renderer) RenderHeader(w io.Writer, ast ast.Node) {
+ r.writeDocumentHeader(w)
+ if r.opts.Flags&TOC != 0 {
+ r.writeTOC(w, ast)
+ }
+}
+
+// RenderFooter writes HTML document footer.
+func (r *Renderer) RenderFooter(w io.Writer, _ ast.Node) {
+ if r.documentMatter != ast.DocumentMatterNone {
+ r.outs(w, "</section>\n")
+ }
+
+ if r.opts.Flags&CompletePage == 0 {
+ return
+ }
+ io.WriteString(w, "\n</body>\n</html>\n")
+}
+
+func (r *Renderer) writeDocumentHeader(w io.Writer) {
+ if r.opts.Flags&CompletePage == 0 {
+ return
+ }
+ ending := ""
+ if r.opts.Flags&UseXHTML != 0 {
+ io.WriteString(w, "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" ")
+ io.WriteString(w, "\"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">\n")
+ io.WriteString(w, "<html xmlns=\"http://www.w3.org/1999/xhtml\">\n")
+ ending = " /"
+ } else {
+ io.WriteString(w, "<!DOCTYPE html>\n")
+ io.WriteString(w, "<html>\n")
+ }
+ io.WriteString(w, "<head>\n")
+ io.WriteString(w, " <title>")
+ if r.opts.Flags&Smartypants != 0 {
+ r.sr.Process(w, []byte(r.opts.Title))
+ } else {
+ EscapeHTML(w, []byte(r.opts.Title))
+ }
+ io.WriteString(w, "</title>\n")
+ io.WriteString(w, r.opts.Generator)
+ io.WriteString(w, "\"")
+ io.WriteString(w, ending)
+ io.WriteString(w, ">\n")
+ io.WriteString(w, " <meta charset=\"utf-8\"")
+ io.WriteString(w, ending)
+ io.WriteString(w, ">\n")
+ if r.opts.CSS != "" {
+ io.WriteString(w, " <link rel=\"stylesheet\" type=\"text/css\" href=\"")
+ EscapeHTML(w, []byte(r.opts.CSS))
+ io.WriteString(w, "\"")
+ io.WriteString(w, ending)
+ io.WriteString(w, ">\n")
+ }
+ if r.opts.Icon != "" {
+ io.WriteString(w, " <link rel=\"icon\" type=\"image/x-icon\" href=\"")
+ EscapeHTML(w, []byte(r.opts.Icon))
+ io.WriteString(w, "\"")
+ io.WriteString(w, ending)
+ io.WriteString(w, ">\n")
+ }
+ if r.opts.Head != nil {
+ w.Write(r.opts.Head)
+ }
+ io.WriteString(w, "</head>\n")
+ io.WriteString(w, "<body>\n\n")
+}
+
+func (r *Renderer) writeTOC(w io.Writer, doc ast.Node) {
+ buf := bytes.Buffer{}
+
+ inHeading := false
+ tocLevel := 0
+ headingCount := 0
+
+ ast.WalkFunc(doc, func(node ast.Node, entering bool) ast.WalkStatus {
+ if nodeData, ok := node.(*ast.Heading); ok && !nodeData.IsTitleblock {
+ inHeading = entering
+ if !entering {
+ buf.WriteString("</a>")
+ return ast.GoToNext
+ }
+ nodeData.HeadingID = fmt.Sprintf("toc_%d", headingCount)
+ if nodeData.Level == tocLevel {
+ buf.WriteString("</li>\n\n<li>")
+ } else if nodeData.Level < tocLevel {
+ for nodeData.Level < tocLevel {
+ tocLevel--
+ buf.WriteString("</li>\n</ul>")
+ }
+ buf.WriteString("</li>\n\n<li>")
+ } else {
+ for nodeData.Level > tocLevel {
+ tocLevel++
+ buf.WriteString("\n<ul>\n<li>")
+ }
+ }
+
+ fmt.Fprintf(&buf, `<a href="#toc_%d">`, headingCount)
+ headingCount++
+ return ast.GoToNext
+ }
+
+ if inHeading {
+ return r.RenderNode(&buf, node, entering)
+ }
+
+ return ast.GoToNext
+ })
+
+ for ; tocLevel > 0; tocLevel-- {
+ buf.WriteString("</li>\n</ul>")
+ }
+
+ if buf.Len() > 0 {
+ io.WriteString(w, "<nav>\n")
+ w.Write(buf.Bytes())
+ io.WriteString(w, "\n\n</nav>\n")
+ }
+ r.lastOutputLen = buf.Len()
+}
+
+func isList(node ast.Node) bool {
+ _, ok := node.(*ast.List)
+ return ok
+}
+
+func isListTight(node ast.Node) bool {
+ if list, ok := node.(*ast.List); ok {
+ return list.Tight
+ }
+ return false
+}
+
+func isListItem(node ast.Node) bool {
+ _, ok := node.(*ast.ListItem)
+ return ok
+}
+
+func isListItemTerm(node ast.Node) bool {
+ data, ok := node.(*ast.ListItem)
+ return ok && data.ListFlags&ast.ListTypeTerm != 0
+}
+
+// TODO: move to internal package
+func skipSpace(data []byte, i int) int {
+ n := len(data)
+ for i < n && isSpace(data[i]) {
+ i++
+ }
+ return i
+}
+
+// TODO: move to internal package
+var validUris = [][]byte{[]byte("http://"), []byte("https://"), []byte("ftp://"), []byte("mailto://")}
+var validPaths = [][]byte{[]byte("/"), []byte("./"), []byte("../")}
+
+func isSafeLink(link []byte) bool {
+ for _, path := range validPaths {
+ if len(link) >= len(path) && bytes.Equal(link[:len(path)], path) {
+ if len(link) == len(path) {
+ return true
+ } else if isAlnum(link[len(path)]) {
+ return true
+ }
+ }
+ }
+
+ for _, prefix := range validUris {
+ // TODO: handle unicode here
+ // case-insensitive prefix test
+ if len(link) > len(prefix) && bytes.Equal(bytes.ToLower(link[:len(prefix)]), prefix) && isAlnum(link[len(prefix)]) {
+ return true
+ }
+ }
+
+ return false
+}
+
+// TODO: move to internal package
+// Create a url-safe slug for fragments
+func slugify(in []byte) []byte {
+ if len(in) == 0 {
+ return in
+ }
+ out := make([]byte, 0, len(in))
+ sym := false
+
+ for _, ch := range in {
+ if isAlnum(ch) {
+ sym = false
+ out = append(out, ch)
+ } else if sym {
+ continue
+ } else {
+ out = append(out, '-')
+ sym = true
+ }
+ }
+ var a, b int
+ var ch byte
+ for a, ch = range out {
+ if ch != '-' {
+ break
+ }
+ }
+ for b = len(out) - 1; b > 0; b-- {
+ if out[b] != '-' {
+ break
+ }
+ }
+ return out[a : b+1]
+}
+
+// TODO: move to internal package
+// isAlnum returns true if c is a digit or letter
+// TODO: check when this is looking for ASCII alnum and when it should use unicode
+func isAlnum(c byte) bool {
+ return (c >= '0' && c <= '9') || isLetter(c)
+}
+
+// isSpace returns true if c is a white-space charactr
+func isSpace(c byte) bool {
+ return c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == '\f' || c == '\v'
+}
+
+// isLetter returns true if c is ascii letter
+func isLetter(c byte) bool {
+ return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')
+}
+
+// isPunctuation returns true if c is a punctuation symbol.
+func isPunctuation(c byte) bool {
+ for _, r := range []byte("!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~") {
+ if c == r {
+ return true
+ }
+ }
+ return false
+}
+
+// BlockAttrs takes a node and checks if it has block level attributes set. If so it
+// will return a slice each containing a "key=value(s)" string.
+func BlockAttrs(node ast.Node) []string {
+ var attr *ast.Attribute
+ if c := node.AsContainer(); c != nil && c.Attribute != nil {
+ attr = c.Attribute
+ }
+ if l := node.AsLeaf(); l != nil && l.Attribute != nil {
+ attr = l.Attribute
+ }
+ if attr == nil {
+ return nil
+ }
+
+ var s []string
+ if attr.ID != nil {
+ s = append(s, fmt.Sprintf(`%s="%s"`, IDTag, attr.ID))
+ }
+
+ classes := ""
+ for _, c := range attr.Classes {
+ classes += " " + string(c)
+ }
+ if classes != "" {
+ s = append(s, fmt.Sprintf(`class="%s"`, classes[1:])) // skip space we added.
+ }
+
+ // sort the attributes so it remain stable between runs
+ var keys = []string{}
+ for k, _ := range attr.Attrs {
+ keys = append(keys, k)
+ }
+ sort.Strings(keys)
+ for _, k := range keys {
+ s = append(s, fmt.Sprintf(`%s="%s"`, k, attr.Attrs[k]))
+ }
+
+ return s
+}
+
+func tagWithAttributes(name string, attrs []string) string {
+ s := name
+ if len(attrs) > 0 {
+ s += " " + strings.Join(attrs, " ")
+ }
+ return s + ">"
+}
diff --git a/vendor/github.com/gomarkdown/markdown/html/smartypants.go b/vendor/github.com/gomarkdown/markdown/html/smartypants.go
new file mode 100644
index 00000000..a09866b0
--- /dev/null
+++ b/vendor/github.com/gomarkdown/markdown/html/smartypants.go
@@ -0,0 +1,444 @@
+package html
+
+import (
+ "bytes"
+ "io"
+)
+
+// SmartyPants rendering
+
+// SPRenderer is a struct containing state of a Smartypants renderer.
+type SPRenderer struct {
+ inSingleQuote bool
+ inDoubleQuote bool
+ callbacks [256]smartCallback
+}
+
+func wordBoundary(c byte) bool {
+ return c == 0 || isSpace(c) || isPunctuation(c)
+}
+
+func tolower(c byte) byte {
+ if c >= 'A' && c <= 'Z' {
+ return c - 'A' + 'a'
+ }
+ return c
+}
+
+func isdigit(c byte) bool {
+ return c >= '0' && c <= '9'
+}
+
+func smartQuoteHelper(out *bytes.Buffer, previousChar byte, nextChar byte, quote byte, isOpen *bool, addNBSP bool) bool {
+ // edge of the buffer is likely to be a tag that we don't get to see,
+ // so we treat it like text sometimes
+
+ // enumerate all sixteen possibilities for (previousChar, nextChar)
+ // each can be one of {0, space, punct, other}
+ switch {
+ case previousChar == 0 && nextChar == 0:
+ // context is not any help here, so toggle
+ *isOpen = !*isOpen
+ case isSpace(previousChar) && nextChar == 0:
+ // [ "] might be [ "<code>foo...]
+ *isOpen = true
+ case isPunctuation(previousChar) && nextChar == 0:
+ // [!"] hmm... could be [Run!"] or [("<code>...]
+ *isOpen = false
+ case /* isnormal(previousChar) && */ nextChar == 0:
+ // [a"] is probably a close
+ *isOpen = false
+ case previousChar == 0 && isSpace(nextChar):
+ // [" ] might be [...foo</code>" ]
+ *isOpen = false
+ case isSpace(previousChar) && isSpace(nextChar):
+ // [ " ] context is not any help here, so toggle
+ *isOpen = !*isOpen
+ case isPunctuation(previousChar) && isSpace(nextChar):
+ // [!" ] is probably a close
+ *isOpen = false
+ case /* isnormal(previousChar) && */ isSpace(nextChar):
+ // [a" ] this is one of the easy cases
+ *isOpen = false
+ case previousChar == 0 && isPunctuation(nextChar):
+ // ["!] hmm... could be ["$1.95] or [</code>"!...]
+ *isOpen = false
+ case isSpace(previousChar) && isPunctuation(nextChar):
+ // [ "!] looks more like [ "$1.95]
+ *isOpen = true
+ case isPunctuation(previousChar) && isPunctuation(nextChar):
+ // [!"!] context is not any help here, so toggle
+ *isOpen = !*isOpen
+ case /* isnormal(previousChar) && */ isPunctuation(nextChar):
+ // [a"!] is probably a close
+ *isOpen = false
+ case previousChar == 0 /* && isnormal(nextChar) */ :
+ // ["a] is probably an open
+ *isOpen = true
+ case isSpace(previousChar) /* && isnormal(nextChar) */ :
+ // [ "a] this is one of the easy cases
+ *isOpen = true
+ case isPunctuation(previousChar) /* && isnormal(nextChar) */ :
+ // [!"a] is probably an open
+ *isOpen = true
+ default:
+ // [a'b] maybe a contraction?
+ *isOpen = false
+ }
+
+ // Note that with the limited lookahead, this non-breaking
+ // space will also be appended to single double quotes.
+ if addNBSP && !*isOpen {
+ out.WriteString("&nbsp;")
+ }
+
+ out.WriteByte('&')
+ if *isOpen {
+ out.WriteByte('l')
+ } else {
+ out.WriteByte('r')
+ }
+ out.WriteByte(quote)
+ out.WriteString("quo;")
+
+ if addNBSP && *isOpen {
+ out.WriteString("&nbsp;")
+ }
+
+ return true
+}
+
+func (r *SPRenderer) smartSingleQuote(out *bytes.Buffer, previousChar byte, text []byte) int {
+ if len(text) >= 2 {
+ t1 := tolower(text[1])
+
+ if t1 == '\'' {
+ nextChar := byte(0)
+ if len(text) >= 3 {
+ nextChar = text[2]
+ }
+ if smartQuoteHelper(out, previousChar, nextChar, 'd', &r.inDoubleQuote, false) {
+ return 1
+ }
+ }
+
+ if (t1 == 's' || t1 == 't' || t1 == 'm' || t1 == 'd') && (len(text) < 3 || wordBoundary(text[2])) {
+ out.WriteString("&rsquo;")
+ return 0
+ }
+
+ if len(text) >= 3 {
+ t2 := tolower(text[2])
+
+ if ((t1 == 'r' && t2 == 'e') || (t1 == 'l' && t2 == 'l') || (t1 == 'v' && t2 == 'e')) &&
+ (len(text) < 4 || wordBoundary(text[3])) {
+ out.WriteString("&rsquo;")
+ return 0
+ }
+ }
+ }
+
+ nextChar := byte(0)
+ if len(text) > 1 {
+ nextChar = text[1]
+ }
+ if smartQuoteHelper(out, previousChar, nextChar, 's', &r.inSingleQuote, false) {
+ return 0
+ }
+
+ out.WriteByte(text[0])
+ return 0
+}
+
+func (r *SPRenderer) smartParens(out *bytes.Buffer, previousChar byte, text []byte) int {
+ if len(text) >= 3 {
+ t1 := tolower(text[1])
+ t2 := tolower(text[2])
+
+ if t1 == 'c' && t2 == ')' {
+ out.WriteString("&copy;")
+ return 2
+ }
+
+ if t1 == 'r' && t2 == ')' {
+ out.WriteString("&reg;")
+ return 2
+ }
+
+ if len(text) >= 4 && t1 == 't' && t2 == 'm' && text[3] == ')' {
+ out.WriteString("&trade;")
+ return 3
+ }
+ }
+
+ out.WriteByte(text[0])
+ return 0
+}
+
+func (r *SPRenderer) smartDash(out *bytes.Buffer, previousChar byte, text []byte) int {
+ if len(text) >= 2 {
+ if text[1] == '-' {
+ out.WriteString("&mdash;")
+ return 1
+ }
+
+ if wordBoundary(previousChar) && wordBoundary(text[1]) {
+ out.WriteString("&ndash;")
+ return 0
+ }
+ }
+
+ out.WriteByte(text[0])
+ return 0
+}
+
+func (r *SPRenderer) smartDashLatex(out *bytes.Buffer, previousChar byte, text []byte) int {
+ if len(text) >= 3 && text[1] == '-' && text[2] == '-' {
+ out.WriteString("&mdash;")
+ return 2
+ }
+ if len(text) >= 2 && text[1] == '-' {
+ out.WriteString("&ndash;")
+ return 1
+ }
+
+ out.WriteByte(text[0])
+ return 0
+}
+
+func (r *SPRenderer) smartAmpVariant(out *bytes.Buffer, previousChar byte, text []byte, quote byte, addNBSP bool) int {
+ if bytes.HasPrefix(text, []byte("&quot;")) {
+ nextChar := byte(0)
+ if len(text) >= 7 {
+ nextChar = text[6]
+ }
+ if smartQuoteHelper(out, previousChar, nextChar, quote, &r.inDoubleQuote, addNBSP) {
+ return 5
+ }
+ }
+
+ if bytes.HasPrefix(text, []byte("&#0;")) {
+ return 3
+ }
+
+ out.WriteByte('&')
+ return 0
+}
+
+func (r *SPRenderer) smartAmp(angledQuotes, addNBSP bool) func(*bytes.Buffer, byte, []byte) int {
+ var quote byte = 'd'
+ if angledQuotes {
+ quote = 'a'
+ }
+
+ return func(out *bytes.Buffer, previousChar byte, text []byte) int {
+ return r.smartAmpVariant(out, previousChar, text, quote, addNBSP)
+ }
+}
+
+func (r *SPRenderer) smartPeriod(out *bytes.Buffer, previousChar byte, text []byte) int {
+ if len(text) >= 3 && text[1] == '.' && text[2] == '.' {
+ out.WriteString("&hellip;")
+ return 2
+ }
+
+ if len(text) >= 5 && text[1] == ' ' && text[2] == '.' && text[3] == ' ' && text[4] == '.' {
+ out.WriteString("&hellip;")
+ return 4
+ }
+
+ out.WriteByte(text[0])
+ return 0
+}
+
+func (r *SPRenderer) smartBacktick(out *bytes.Buffer, previousChar byte, text []byte) int {
+ if len(text) >= 2 && text[1] == '`' {
+ nextChar := byte(0)
+ if len(text) >= 3 {
+ nextChar = text[2]
+ }
+ if smartQuoteHelper(out, previousChar, nextChar, 'd', &r.inDoubleQuote, false) {
+ return 1
+ }
+ }
+
+ out.WriteByte(text[0])
+ return 0
+}
+
+func (r *SPRenderer) smartNumberGeneric(out *bytes.Buffer, previousChar byte, text []byte) int {
+ if wordBoundary(previousChar) && previousChar != '/' && len(text) >= 3 {
+ // is it of the form digits/digits(word boundary)?, i.e., \d+/\d+\b
+ // note: check for regular slash (/) or fraction slash (⁄, 0x2044, or 0xe2 81 84 in utf-8)
+ // and avoid changing dates like 1/23/2005 into fractions.
+ numEnd := 0
+ for len(text) > numEnd && isdigit(text[numEnd]) {
+ numEnd++
+ }
+ if numEnd == 0 {
+ out.WriteByte(text[0])
+ return 0
+ }
+ denStart := numEnd + 1
+ if len(text) > numEnd+3 && text[numEnd] == 0xe2 && text[numEnd+1] == 0x81 && text[numEnd+2] == 0x84 {
+ denStart = numEnd + 3
+ } else if len(text) < numEnd+2 || text[numEnd] != '/' {
+ out.WriteByte(text[0])
+ return 0
+ }
+ denEnd := denStart
+ for len(text) > denEnd && isdigit(text[denEnd]) {
+ denEnd++
+ }
+ if denEnd == denStart {
+ out.WriteByte(text[0])
+ return 0
+ }
+ if len(text) == denEnd || wordBoundary(text[denEnd]) && text[denEnd] != '/' {
+ out.WriteString("<sup>")
+ out.Write(text[:numEnd])
+ out.WriteString("</sup>&frasl;<sub>")
+ out.Write(text[denStart:denEnd])
+ out.WriteString("</sub>")
+ return denEnd - 1
+ }
+ }
+
+ out.WriteByte(text[0])
+ return 0
+}
+
+func (r *SPRenderer) smartNumber(out *bytes.Buffer, previousChar byte, text []byte) int {
+ if wordBoundary(previousChar) && previousChar != '/' && len(text) >= 3 {
+ if text[0] == '1' && text[1] == '/' && text[2] == '2' {
+ if len(text) < 4 || wordBoundary(text[3]) && text[3] != '/' {
+ out.WriteString("&frac12;")
+ return 2
+ }
+ }
+
+ if text[0] == '1' && text[1] == '/' && text[2] == '4' {
+ if len(text) < 4 || wordBoundary(text[3]) && text[3] != '/' || (len(text) >= 5 && tolower(text[3]) == 't' && tolower(text[4]) == 'h') {
+ out.WriteString("&frac14;")
+ return 2
+ }
+ }
+
+ if text[0] == '3' && text[1] == '/' && text[2] == '4' {
+ if len(text) < 4 || wordBoundary(text[3]) && text[3] != '/' || (len(text) >= 6 && tolower(text[3]) == 't' && tolower(text[4]) == 'h' && tolower(text[5]) == 's') {
+ out.WriteString("&frac34;")
+ return 2
+ }
+ }
+ }
+
+ out.WriteByte(text[0])
+ return 0
+}
+
+func (r *SPRenderer) smartDoubleQuoteVariant(out *bytes.Buffer, previousChar byte, text []byte, quote byte) int {
+ nextChar := byte(0)
+ if len(text) > 1 {
+ nextChar = text[1]
+ }
+ if !smartQuoteHelper(out, previousChar, nextChar, quote, &r.inDoubleQuote, false) {
+ out.WriteString("&quot;")
+ }
+
+ return 0
+}
+
+func (r *SPRenderer) smartDoubleQuote(out *bytes.Buffer, previousChar byte, text []byte) int {
+ return r.smartDoubleQuoteVariant(out, previousChar, text, 'd')
+}
+
+func (r *SPRenderer) smartAngledDoubleQuote(out *bytes.Buffer, previousChar byte, text []byte) int {
+ return r.smartDoubleQuoteVariant(out, previousChar, text, 'a')
+}
+
+func (r *SPRenderer) smartLeftAngle(out *bytes.Buffer, previousChar byte, text []byte) int {
+ i := 0
+
+ for i < len(text) && text[i] != '>' {
+ i++
+ }
+
+ out.Write(text[:i+1])
+ return i
+}
+
+type smartCallback func(out *bytes.Buffer, previousChar byte, text []byte) int
+
+// NewSmartypantsRenderer constructs a Smartypants renderer object.
+func NewSmartypantsRenderer(flags Flags) *SPRenderer {
+ var (
+ r SPRenderer
+
+ smartAmpAngled = r.smartAmp(true, false)
+ smartAmpAngledNBSP = r.smartAmp(true, true)
+ smartAmpRegular = r.smartAmp(false, false)
+ smartAmpRegularNBSP = r.smartAmp(false, true)
+
+ addNBSP = flags&SmartypantsQuotesNBSP != 0
+ )
+
+ if flags&SmartypantsAngledQuotes == 0 {
+ r.callbacks['"'] = r.smartDoubleQuote
+ if !addNBSP {
+ r.callbacks['&'] = smartAmpRegular
+ } else {
+ r.callbacks['&'] = smartAmpRegularNBSP
+ }
+ } else {
+ r.callbacks['"'] = r.smartAngledDoubleQuote
+ if !addNBSP {
+ r.callbacks['&'] = smartAmpAngled
+ } else {
+ r.callbacks['&'] = smartAmpAngledNBSP
+ }
+ }
+ r.callbacks['\''] = r.smartSingleQuote
+ r.callbacks['('] = r.smartParens
+ if flags&SmartypantsDashes != 0 {
+ if flags&SmartypantsLatexDashes == 0 {
+ r.callbacks['-'] = r.smartDash
+ } else {
+ r.callbacks['-'] = r.smartDashLatex
+ }
+ }
+ r.callbacks['.'] = r.smartPeriod
+ if flags&SmartypantsFractions == 0 {
+ r.callbacks['1'] = r.smartNumber
+ r.callbacks['3'] = r.smartNumber
+ } else {
+ for ch := '1'; ch <= '9'; ch++ {
+ r.callbacks[ch] = r.smartNumberGeneric
+ }
+ }
+ r.callbacks['<'] = r.smartLeftAngle
+ r.callbacks['`'] = r.smartBacktick
+ return &r
+}
+
+// Process is the entry point of the Smartypants renderer.
+func (r *SPRenderer) Process(w io.Writer, text []byte) {
+ mark := 0
+ for i := 0; i < len(text); i++ {
+ if action := r.callbacks[text[i]]; action != nil {
+ if i > mark {
+ w.Write(text[mark:i])
+ }
+ previousChar := byte(0)
+ if i > 0 {
+ previousChar = text[i-1]
+ }
+ var tmp bytes.Buffer
+ i += action(&tmp, previousChar, text[i:])
+ w.Write(tmp.Bytes())
+ mark = i + 1
+ }
+ }
+ if mark < len(text) {
+ w.Write(text[mark:])
+ }
+}