package html
import (
"bytes"
"fmt"
"html"
"io"
"regexp"
"sort"
"strconv"
"strings"
"github.com/gomarkdown/markdown/ast"
"github.com/gomarkdown/markdown/parser"
)
// 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"
NoopenerLinks // Only link with rel="noopener"
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
LazyLoadImages // Include loading="lazy" with images
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 = ""
declaration = "]*>"
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 tag for a footnote return link, if the
// FootnoteReturnLinks flag is enabled. If blank, the string
// [return] is used.
FootnoteReturnLinkContents string
// CitationFormatString defines how a citation is rendered. If blank, the string
// [%s] 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
// can over-write for paragraph tag
ParagraphTag 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
"
}
r.Outs(w, ptag)
if !(IsListItem(para.Parent) && ast.GetNextNode(para) == nil) {
r.CR(w)
}
}
// Paragraph writes ast.Paragraph node
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)
}
}
// Code writes ast.Code node
func (r *Renderer) Code(w io.Writer, node *ast.Code) {
r.Outs(w, "")
EscapeHTML(w, node.Literal)
r.Outs(w, "
")
}
// HTMLBlock write ast.HTMLBlock node
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) 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) 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)
}
}
// Heading writes ast.Heading node
func (r *Renderer) Heading(w io.Writer, node *ast.Heading, entering bool) {
if entering {
r.headingEnter(w, node)
} else {
r.headingExit(w, node)
}
}
// HorizontalRule writes ast.HorizontalRule 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
")
code := TagWithAttributes("")
r.Outs(w, "
")
if !IsListItem(codeBlock.Parent) {
r.CR(w)
}
}
// Caption writes ast.Caption node
func (r *Renderer) Caption(w io.Writer, caption *ast.Caption, entering bool) {
if entering {
r.Outs(w, "") case *ast.Aside: tag := TagWithAttributes("