diff options
Diffstat (limited to 'vendor/github.com/gomarkdown/markdown/html/renderer.go')
-rw-r--r-- | vendor/github.com/gomarkdown/markdown/html/renderer.go | 1318 |
1 files changed, 1318 insertions, 0 deletions
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, " ") +} + +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 + ">" +} |