markdown/html_renderer.go

986 lines
25 KiB
Go
Raw Normal View History

2011-06-28 02:11:32 +00:00
// Copyright © 2011 Russ Ross <russ@russross.com>.
// Distributed under the Simplified BSD License.
2011-05-29 03:17:53 +00:00
// HTMLRenderer converts AST of parsed markdown document into HTML text
2011-05-29 03:17:53 +00:00
2018-01-25 21:01:19 +00:00
package markdown
2011-05-29 03:17:53 +00:00
import (
"bytes"
"fmt"
"io"
"regexp"
"strings"
2011-05-29 03:17:53 +00:00
)
// HTMLFlags control optional behavior of HTML renderer.
type HTMLFlags int
// HTML renderer configuration options.
2011-05-29 03:17:53 +00:00
const (
HTMLFlagsNone HTMLFlags = 0
SkipHTML HTMLFlags = 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
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
2018-01-26 08:11:58 +00:00
CommonHTMLFlags HTMLFlags = Smartypants | SmartypantsFractions | SmartypantsDashes | SmartypantsLatexDashes
2011-05-29 03:17:53 +00:00
)
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]+"
)
// HTMLRendererParameters is a collection of supplementary parameters tweaking
// the behavior of various parts of HTML renderer.
2016-04-01 12:37:21 +00:00
type HTMLRendererParameters struct {
// Prepend this text to each relative URL.
AbsolutePrefix string
2014-05-29 04:52:45 +00:00
// 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
// HTML_FOOTNOTE_RETURN_LINKS flag is enabled. If blank, the string
// <sup>[return]</sup> is used.
FootnoteReturnLinkContents 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)
Flags HTMLFlags // Flags allow customizing this renderer's behavior
}
// HTMLRenderer implements Renderer interface for HTML output.
2011-07-07 17:56:45 +00:00
//
// Do not create this directly, instead use the NewHTMLRenderer function.
2016-04-11 08:45:40 +00:00
type HTMLRenderer struct {
params HTMLRendererParameters
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
2011-05-29 03:17:53 +00:00
}
2011-06-29 17:13:17 +00:00
const (
xhtmlClose = " />"
htmlClose = ">"
2011-06-29 17:13:17 +00:00
)
2011-05-29 03:17:53 +00:00
2016-04-11 08:45:40 +00:00
// NewHTMLRenderer creates and configures an HTMLRenderer object, which
// satisfies the Renderer interface.
func NewHTMLRenderer(params HTMLRendererParameters) *HTMLRenderer {
2011-05-29 03:17:53 +00:00
// configure the rendering engine
closeTag := htmlClose
if params.Flags&UseXHTML != 0 {
closeTag = xhtmlClose
2011-05-29 03:17:53 +00:00
}
if params.FootnoteReturnLinkContents == "" {
params.FootnoteReturnLinkContents = `<sup>[return]</sup>`
}
2016-04-11 08:45:40 +00:00
return &HTMLRenderer{
params: params,
2011-05-29 03:17:53 +00:00
closeTag: closeTag,
headingIDs: make(map[string]int),
sr: NewSmartypantsRenderer(params.Flags),
2011-05-29 03:17:53 +00:00
}
}
func isHTMLTag(tag []byte, tagname string) bool {
found, _ := findHTMLTagPos(tag, tagname)
2013-04-18 00:15:47 +00:00
return found
}
2014-01-21 22:45:43 +00: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
}
func findHTMLTagPos(tag []byte, tagname string) (bool, int) {
2011-05-29 03:17:53 +00:00
i := 0
if i < len(tag) && tag[0] != '<' {
2013-04-18 00:15:47 +00:00
return false, -1
2011-05-29 03:17:53 +00:00
}
i++
2013-04-13 19:26:29 +00:00
i = skipSpace(tag, i)
2011-05-29 03:17:53 +00:00
if i < len(tag) && tag[i] == '/' {
i++
}
2013-04-13 19:26:29 +00:00
i = skipSpace(tag, i)
j := 0
2011-06-28 22:02:12 +00:00
for ; i < len(tag); i, j = i+1, j+1 {
if j >= len(tagname) {
2011-05-29 03:17:53 +00:00
break
}
2013-04-13 19:34:37 +00:00
if strings.ToLower(string(tag[i]))[0] != tagname[j] {
2013-04-18 00:15:47 +00:00
return false, -1
2011-05-29 03:17:53 +00:00
}
}
if i == len(tag) {
2013-04-18 00:15:47 +00:00
return false, -1
}
2014-01-21 22:45:43 +00:00
rightAngle := skipUntilCharIgnoreQuotes(tag, i, '>')
if rightAngle >= i {
2014-01-21 22:45:43 +00:00
return true, rightAngle
2011-05-29 03:17:53 +00:00
}
2013-04-18 00:15:47 +00:00
return false, -1
2011-05-29 03:17:53 +00:00
}
2014-03-21 02:52:46 +00:00
func isRelativeLink(link []byte) (yes bool) {
// a tag begin with '#'
if link[0] == '#' {
2015-04-11 15:06:30 +00:00
return true
2014-03-21 02:52:46 +00:00
}
// link begin with '/' but not '//', the second maybe a protocol relative link
if len(link) >= 2 && link[0] == '/' && link[1] != '/' {
2015-04-11 15:06:30 +00:00
return true
2014-03-21 02:52:46 +00:00
}
// only the root '/'
if len(link) == 1 && link[0] == '/' {
2015-04-11 15:06:30 +00:00
return true
2014-03-21 02:52:46 +00:00
}
// current directory : begin with "./"
2015-04-11 15:06:30 +00:00
if bytes.HasPrefix(link, []byte("./")) {
return true
}
// parent directory : begin with "../"
2015-04-11 15:06:30 +00:00
if bytes.HasPrefix(link, []byte("../")) {
return true
}
2015-04-11 15:06:30 +00:00
return false
2014-03-21 02:52:46 +00:00
}
func (r *HTMLRenderer) 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
}
2016-04-11 08:45:40 +00:00
func (r *HTMLRenderer) addAbsPrefix(link []byte) []byte {
if r.params.AbsolutePrefix != "" && isRelativeLink(link) && link[0] != '.' {
newDest := r.params.AbsolutePrefix
if link[0] != '/' {
newDest += "/"
}
newDest += string(link)
return []byte(newDest)
}
return link
}
func appendLinkAttrs(attrs []string, flags HTMLFlags, link []byte) []string {
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 11:08:35 +00:00
func needSkipLink(flags HTMLFlags, dest []byte) bool {
if flags&SkipLinks != 0 {
return true
}
return flags&Safelink != 0 && !isSafeLink(dest) && !isMailto(dest)
}
func isSmartypantable(node *Node) bool {
switch node.Parent.Data.(type) {
case *LinkData, *CodeBlockData, *CodeData:
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)
}
return append(attrs, fmt.Sprintf("class=\"language-%s\"", info[:endOfLang]))
}
func (r *HTMLRenderer) tag(w io.Writer, name string, attrs []string) {
io.WriteString(w, name)
if len(attrs) > 0 {
w.Write(spaceBytes)
io.WriteString(w, strings.Join(attrs, " "))
}
w.Write(gtBytes)
r.lastOutputLen = 1
}
func footnoteRef(prefix string, node *LinkData) string {
urlFrag := prefix + string(slugify(node.Destination))
anchor := fmt.Sprintf(`<a rel="footnote" href="#fn:%s">%d</a>`, urlFrag, node.NoteID)
return fmt.Sprintf(`<sup class="footnote-ref" id="fnref:%s">%s</sup>`, urlFrag, anchor)
}
func footnoteItem(prefix string, slug []byte) string {
return fmt.Sprintf(`<li id="fn:%s%s">`, prefix, slug)
}
func footnoteReturnLink(prefix, returnLink string, slug []byte) string {
const format = ` <a class="footnote-return" href="#fnref:%s%s">%s</a>`
return fmt.Sprintf(format, prefix, slug, returnLink)
}
func itemOpenCR(node *Node) bool {
if node.Prev == nil {
return false
}
ld := node.Parent.Data.(*ListData)
return !ld.Tight && ld.ListFlags&ListTypeDefinition == 0
}
func skipParagraphTags(node *Node) bool {
parent := node.Parent
grandparent := parent.Parent
if grandparent == nil || !isListData(grandparent.Data) {
return false
}
2018-01-27 06:23:27 +00:00
isParentTerm := isListItemTerm(parent)
grandparentListData := grandparent.Data.(*ListData)
tightOrTerm := grandparentListData.Tight || isParentTerm
return tightOrTerm
}
func cellAlignment(align CellAlignFlags) string {
switch align {
case TableAlignmentLeft:
return "left"
case TableAlignmentRight:
return "right"
case TableAlignmentCenter:
return "center"
default:
return ""
}
}
func (r *HTMLRenderer) out(w io.Writer, d []byte) {
r.lastOutputLen = len(d)
if r.disableTags > 0 {
d = htmlTagRe.ReplaceAll(d, []byte{})
}
w.Write(d)
}
func (r *HTMLRenderer) outs(w io.Writer, s string) {
r.lastOutputLen = len(s)
if r.disableTags > 0 {
s = htmlTagRe.ReplaceAllString(s, "")
}
io.WriteString(w, s)
}
2016-04-11 08:45:40 +00:00
func (r *HTMLRenderer) cr(w io.Writer) {
if r.lastOutputLen > 0 {
r.out(w, nlBytes)
}
}
var (
nlBytes = []byte{'\n'}
gtBytes = []byte{'>'}
spaceBytes = []byte{' '}
)
func headingOpenTagFromLevel(level int) string {
switch level {
case 1:
return "<h1"
case 2:
return "<h2"
case 3:
return "<h3"
case 4:
return "<h4"
case 5:
return "<h5"
default:
return "<h6"
}
}
func headingCloseTagFromLevel(level int) string {
switch level {
case 1:
return "</h1>"
case 2:
return "</h2>"
case 3:
return "</h3>"
case 4:
return "</h4>"
case 5:
return "</h5>"
default:
return "</h6>"
}
}
func (r *HTMLRenderer) outHRTag(w io.Writer) {
if r.params.Flags&UseXHTML == 0 {
r.out(w, []byte("<hr>"))
} else {
r.out(w, []byte("<hr />"))
}
}
func (r *HTMLRenderer) text(w io.Writer, node *Node, nodeData *TextData) {
if r.params.Flags&Smartypants != 0 {
var tmp bytes.Buffer
escapeHTML(&tmp, node.Literal)
r.sr.Process(w, tmp.Bytes())
} else {
if isLinkData(node.Parent.Data) {
escLink(w, node.Literal)
} else {
escapeHTML(w, node.Literal)
}
}
}
func (r *HTMLRenderer) hardBreak(w io.Writer, node *Node, nodeData *HardbreakData) {
s := "<br>"
if r.params.Flags&UseXHTML != 0 {
s = "<br />"
}
r.outs(w, s)
r.cr(w)
}
func (r *HTMLRenderer) openOrCloseTag(w io.Writer, isOpen bool, openTag string, closeTag string) {
if isOpen {
r.outs(w, openTag)
} else {
r.outs(w, closeTag)
}
}
func (r *HTMLRenderer) crOpenOrCloseTag(w io.Writer, isOpen bool, openTag string, closeTag string) {
if isOpen {
r.cr(w)
r.outs(w, openTag)
} else {
r.outs(w, closeTag)
r.cr(w)
}
}
func (r *HTMLRenderer) span(w io.Writer, node *Node, nodeData *HTMLSpanData) {
if r.params.Flags&SkipHTML != 0 {
return
}
r.out(w, node.Literal)
}
func (r *HTMLRenderer) link(w io.Writer, node *Node, nodeData *LinkData, entering bool) {
var attrs []string
// mark it but don't link it if it is not a safe link: no smartypants
dest := nodeData.Destination
if needSkipLink(r.params.Flags, dest) {
r.openOrCloseTag(w, entering, "<tt>", "</tt>")
return
}
if !entering {
if nodeData.NoteID == 0 {
r.out(w, []byte("</a>"))
}
return
}
// entering
dest = r.addAbsPrefix(dest)
var hrefBuf bytes.Buffer
hrefBuf.WriteString("href=\"")
escLink(&hrefBuf, dest)
hrefBuf.WriteByte('"')
attrs = append(attrs, hrefBuf.String())
if nodeData.NoteID != 0 {
r.outs(w, footnoteRef(r.params.FootnoteAnchorPrefix, nodeData))
return
}
attrs = appendLinkAttrs(attrs, r.params.Flags, dest)
if len(nodeData.Title) > 0 {
var titleBuff bytes.Buffer
titleBuff.WriteString("title=\"")
escapeHTML(&titleBuff, nodeData.Title)
titleBuff.WriteByte('"')
attrs = append(attrs, titleBuff.String())
}
r.tag(w, "<a", attrs)
}
func (r *HTMLRenderer) imageEnter(w io.Writer, node *Node, nodeData *ImageData) {
dest := nodeData.Destination
dest = r.addAbsPrefix(dest)
if r.disableTags == 0 {
//if options.safe && potentiallyUnsafe(dest) {
//out(w, `<img src="" alt="`)
//} else {
r.out(w, []byte(`<img src="`))
escLink(w, dest)
r.out(w, []byte(`" alt="`))
//}
}
r.disableTags++
}
func (r *HTMLRenderer) imageExit(w io.Writer, node *Node, nodeData *ImageData) {
r.disableTags--
if r.disableTags == 0 {
if nodeData.Title != nil {
r.out(w, []byte(`" title="`))
escapeHTML(w, nodeData.Title)
}
r.out(w, []byte(`" />`))
}
}
func (r *HTMLRenderer) paragraphEnter(w io.Writer, node *Node, nodeData *ParagraphData) {
// TODO: untangle this clusterfuck about when the newlines need
// to be added and when not.
if node.Prev != nil {
switch node.Prev.Data.(type) {
case *HTMLBlockData, *ListData, *ParagraphData, *HeadingData, *CodeBlockData, *BlockQuoteData, *HorizontalRuleData:
r.cr(w)
}
}
if isBlockQuoteData(node.Parent.Data) && node.Prev == nil {
r.cr(w)
}
r.out(w, []byte("<p>"))
}
func (r *HTMLRenderer) paragraphExit(w io.Writer, node *Node, nodeData *ParagraphData) {
2018-01-27 06:23:27 +00:00
r.outs(w, "</p>")
if !(isListItemData(node.Parent.Data) && node.Next == nil) {
r.cr(w)
}
}
func (r *HTMLRenderer) paragraph(w io.Writer, node *Node, nodeData *ParagraphData, entering bool) {
if skipParagraphTags(node) {
return
}
if entering {
r.paragraphEnter(w, node, nodeData)
} else {
r.paragraphExit(w, node, nodeData)
}
}
func (r *HTMLRenderer) image(w io.Writer, node *Node, nodeData *ImageData, entering bool) {
if entering {
r.imageEnter(w, node, nodeData)
} else {
r.imageExit(w, node, nodeData)
}
}
func (r *HTMLRenderer) code(w io.Writer, node *Node, nodeData *CodeData) {
r.outs(w, "<code>")
escapeHTML(w, node.Literal)
r.outs(w, "</code>")
}
func (r *HTMLRenderer) htmlBlock(w io.Writer, node *Node, nodeData *HTMLBlockData) {
if r.params.Flags&SkipHTML != 0 {
return
}
r.cr(w)
r.out(w, node.Literal)
r.cr(w)
}
func (r *HTMLRenderer) heading(w io.Writer, node *Node, nodeData *HeadingData, entering bool) {
if !entering {
closeTag := headingCloseTagFromLevel(nodeData.Level)
r.outs(w, closeTag)
2018-01-27 06:23:27 +00:00
if !(isListItemData(node.Parent.Data) && node.Next == nil) {
r.cr(w)
}
return
}
// entering
var attrs []string
if nodeData.IsTitleblock {
attrs = append(attrs, `class="title"`)
}
if nodeData.HeadingID != "" {
id := r.ensureUniqueHeadingID(nodeData.HeadingID)
if r.params.HeadingIDPrefix != "" {
id = r.params.HeadingIDPrefix + id
}
if r.params.HeadingIDSuffix != "" {
id = id + r.params.HeadingIDSuffix
}
attrID := `id="` + id + `"`
attrs = append(attrs, attrID)
}
r.cr(w)
openTag := headingOpenTagFromLevel(nodeData.Level)
r.tag(w, openTag, attrs)
}
func (r *HTMLRenderer) horizontalRule(w io.Writer) {
r.cr(w)
r.outHRTag(w)
r.cr(w)
}
func (r *HTMLRenderer) listEnter(w io.Writer, node *Node, nodeData *ListData) {
// TODO: attrs don't seem to be set
var attrs []string
openTag := "<ul"
if nodeData.ListFlags&ListTypeOrdered != 0 {
openTag = "<ol"
}
if nodeData.ListFlags&ListTypeDefinition != 0 {
openTag = "<dl"
}
if nodeData.IsFootnotesList {
r.outs(w, "\n<div class=\"footnotes\">\n\n")
r.outHRTag(w)
r.cr(w)
}
r.cr(w)
2018-01-27 06:23:27 +00:00
if isListItemData(node.Parent.Data) {
grand := node.Parent.Parent
if isListTight(grand.Data) {
r.cr(w)
}
}
r.tag(w, openTag, attrs)
r.cr(w)
}
func (r *HTMLRenderer) listExit(w io.Writer, node *Node, nodeData *ListData) {
closeTag := "</ul>"
if nodeData.ListFlags&ListTypeOrdered != 0 {
closeTag = "</ol>"
}
if nodeData.ListFlags&ListTypeDefinition != 0 {
closeTag = "</dl>"
}
r.outs(w, closeTag)
//cr(w)
//if node.parent.Type != Item {
// cr(w)
//}
2018-01-27 06:23:27 +00:00
if isListItemData(node.Parent.Data) && node.Next != nil {
r.cr(w)
}
if isDocumentData(node.Parent.Data) || isBlockQuoteData(node.Parent.Data) {
r.cr(w)
}
if nodeData.IsFootnotesList {
r.outs(w, "\n</div>\n")
}
}
func (r *HTMLRenderer) list(w io.Writer, node *Node, nodeData *ListData, entering bool) {
if entering {
r.listEnter(w, node, nodeData)
} else {
r.listExit(w, node, nodeData)
}
}
2018-01-27 06:23:27 +00:00
func (r *HTMLRenderer) listItem(w io.Writer, node *Node, nodeData *ListItemData, entering bool) {
if entering {
openTag := "<li>"
if nodeData.ListFlags&ListTypeDefinition != 0 {
openTag = "<dd>"
}
if nodeData.ListFlags&ListTypeTerm != 0 {
openTag = "<dt>"
}
if itemOpenCR(node) {
r.cr(w)
}
if nodeData.RefLink != nil {
slug := slugify(nodeData.RefLink)
r.outs(w, footnoteItem(r.params.FootnoteAnchorPrefix, slug))
return
}
r.outs(w, openTag)
} else {
closeTag := "</li>"
if nodeData.ListFlags&ListTypeDefinition != 0 {
closeTag = "</dd>"
}
if nodeData.ListFlags&ListTypeTerm != 0 {
closeTag = "</dt>"
}
if nodeData.RefLink != nil {
slug := slugify(nodeData.RefLink)
if r.params.Flags&FootnoteReturnLinks != 0 {
r.outs(w, footnoteReturnLink(r.params.FootnoteAnchorPrefix, r.params.FootnoteReturnLinkContents, slug))
}
}
r.outs(w, closeTag)
r.cr(w)
}
}
func (r *HTMLRenderer) codeBlock(w io.Writer, node *Node, nodeData *CodeBlockData) {
var attrs []string
attrs = appendLanguageAttr(attrs, nodeData.Info)
r.cr(w)
r.outs(w, "<pre>")
r.tag(w, "<code", attrs)
escapeHTML(w, node.Literal)
r.outs(w, "</code>")
r.outs(w, "</pre>")
2018-01-27 06:23:27 +00:00
if !isListItemData(node.Parent.Data) {
r.cr(w)
}
}
func (r *HTMLRenderer) tableCell(w io.Writer, node *Node, nodeData *TableCellData, entering bool) {
if !entering {
closeTag := "</td>"
if nodeData.IsHeader {
closeTag = "</th>"
}
r.outs(w, closeTag)
r.cr(w)
return
}
// entering
var attrs []string
openTag := "<td"
if nodeData.IsHeader {
openTag = "<th"
}
align := cellAlignment(nodeData.Align)
if align != "" {
attrs = append(attrs, fmt.Sprintf(`align="%s"`, align))
}
if node.Prev == nil {
r.cr(w)
}
r.tag(w, openTag, attrs)
}
func (r *HTMLRenderer) tableBody(w io.Writer, node *Node, nodeData *TableBodyData, 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 node.FirstChild == nil {
r.cr(w)
}
} else {
r.outs(w, "</tbody>")
r.cr(w)
}
}
// RenderNode is a default renderer of a single node of a syntax tree. For
// block nodes it will be called twice: first time with entering=true, second
// time with entering=false, so that it could know when it's working on an open
// tag and when on close. It writes the result to w.
//
// The return value is a way to tell the calling walker to adjust its walk
// pattern: e.g. it can terminate the traversal by returning Terminate. Or it
// can ask the walker to skip a subtree of this node by returning SkipChildren.
// The typical behavior is to return GoToNext, which asks for the usual
// traversal to the next node.
2016-04-11 08:45:40 +00:00
func (r *HTMLRenderer) RenderNode(w io.Writer, node *Node, entering bool) WalkStatus {
switch nodeData := node.Data.(type) {
case *TextData:
r.text(w, node, nodeData)
case *SoftbreakData:
r.cr(w)
// TODO: make it configurable via out(renderer.softbreak)
case *HardbreakData:
r.hardBreak(w, node, nodeData)
case *EmphData:
r.openOrCloseTag(w, entering, "<em>", "</em>")
case *StrongData:
r.openOrCloseTag(w, entering, "<strong>", "</strong>")
case *DelData:
r.openOrCloseTag(w, entering, "<del>", "</del>")
case *HTMLSpanData:
r.span(w, node, nodeData)
case *LinkData:
r.link(w, node, nodeData, entering)
case *ImageData:
if r.params.Flags&SkipImages != 0 {
2016-04-05 08:12:30 +00:00
return SkipChildren
}
r.image(w, node, nodeData, entering)
case *CodeData:
r.code(w, node, nodeData)
case *DocumentData:
// do nothing
case *ParagraphData:
r.paragraph(w, node, nodeData, entering)
case *BlockQuoteData:
r.crOpenOrCloseTag(w, entering, "<blockquote>", "</blockquote>")
case *HTMLBlockData:
r.htmlBlock(w, node, nodeData)
case *HeadingData:
r.heading(w, node, nodeData, entering)
case *HorizontalRuleData:
r.horizontalRule(w)
case *ListData:
r.list(w, node, nodeData, entering)
2018-01-27 06:23:27 +00:00
case *ListItemData:
r.listItem(w, node, nodeData, entering)
case *CodeBlockData:
r.codeBlock(w, node, nodeData)
case *TableData:
r.crOpenOrCloseTag(w, entering, "<table>", "</table>")
case *TableCellData:
r.tableCell(w, node, nodeData, entering)
case *TableHeadData:
r.crOpenOrCloseTag(w, entering, "<thead>", "</thead>")
case *TableBodyData:
r.tableBody(w, node, nodeData, entering)
case *TableRowData:
r.crOpenOrCloseTag(w, entering, "<tr>", "</tr>")
default:
//panic("Unknown node type " + node.Type.String())
panic(fmt.Sprintf("Unknown node type %T", node.Data))
}
return GoToNext
}
// RenderHeader writes HTML document preamble and TOC if requested.
func (r *HTMLRenderer) RenderHeader(w io.Writer, ast *Node) {
r.writeDocumentHeader(w)
if r.params.Flags&TOC != 0 {
r.writeTOC(w, ast)
}
}
// RenderFooter writes HTML document footer.
func (r *HTMLRenderer) RenderFooter(w io.Writer, ast *Node) {
if r.params.Flags&CompletePage == 0 {
return
}
io.WriteString(w, "\n</body>\n</html>\n")
}
func (r *HTMLRenderer) writeDocumentHeader(w io.Writer) {
if r.params.Flags&CompletePage == 0 {
return
}
ending := ""
if r.params.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.params.Flags&Smartypants != 0 {
r.sr.Process(w, []byte(r.params.Title))
} else {
escapeHTML(w, []byte(r.params.Title))
}
io.WriteString(w, "</title>\n")
2018-01-25 21:06:17 +00:00
io.WriteString(w, " <meta name=\"GENERATOR\" content=\"Markdown Processor for Go v")
io.WriteString(w, Version)
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.params.CSS != "" {
io.WriteString(w, " <link rel=\"stylesheet\" type=\"text/css\" href=\"")
escapeHTML(w, []byte(r.params.CSS))
io.WriteString(w, "\"")
io.WriteString(w, ending)
io.WriteString(w, ">\n")
}
if r.params.Icon != "" {
io.WriteString(w, " <link rel=\"icon\" type=\"image/x-icon\" href=\"")
escapeHTML(w, []byte(r.params.Icon))
io.WriteString(w, "\"")
io.WriteString(w, ending)
io.WriteString(w, ">\n")
}
io.WriteString(w, "</head>\n")
io.WriteString(w, "<body>\n\n")
}
func (r *HTMLRenderer) writeTOC(w io.Writer, ast *Node) {
buf := bytes.Buffer{}
inHeading := false
tocLevel := 0
headingCount := 0
ast.WalkFunc(func(node *Node, entering bool) WalkStatus {
if nodeData, ok := node.Data.(*HeadingData); ok && !nodeData.IsTitleblock {
inHeading = entering
if entering {
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++
} else {
buf.WriteString("</a>")
}
return GoToNext
}
if inHeading {
return r.RenderNode(&buf, node, entering)
}
return 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()
}