mirror of
https://github.com/status-im/markdown.git
synced 2025-02-20 15:28:05 +00:00
complete refactor
This commit is contained in:
parent
7e13677aaf
commit
242d66580e
307
ast/node.go
307
ast/node.go
@ -3,6 +3,7 @@ package ast
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"reflect"
|
||||
)
|
||||
|
||||
// ListType contains bitwise or'ed flags for list and list item objects.
|
||||
@ -33,19 +34,79 @@ const (
|
||||
TableAlignmentCenter = (TableAlignmentLeft | TableAlignmentRight)
|
||||
)
|
||||
|
||||
// NodeData represents data field of Node
|
||||
type NodeData interface{}
|
||||
|
||||
// DocumentData represents top-level document node
|
||||
type DocumentData struct {
|
||||
// Node defines an ast node
|
||||
type Node interface {
|
||||
AsTreeNode() *TreeNode
|
||||
GetParent() Node
|
||||
GetChildren() []Node
|
||||
FirstChild() Node
|
||||
LastChild() Node
|
||||
}
|
||||
|
||||
// BlockQuoteData represents data for block quote node
|
||||
type BlockQuoteData struct {
|
||||
// TreeNode is a common part of all nodes, used to represent tree and contain
|
||||
// data that all nodes have in common
|
||||
type TreeNode struct {
|
||||
Parent Node
|
||||
Children []Node
|
||||
|
||||
Literal []byte // Text contents of the leaf nodes
|
||||
Content []byte // Markdown content of the block nodes
|
||||
}
|
||||
|
||||
// ListData represents data list node
|
||||
type ListData struct {
|
||||
// AsTreeNode returns itself as *TreeNode
|
||||
func (n *TreeNode) AsTreeNode() *TreeNode {
|
||||
res := n
|
||||
//fmt.Printf("TreeNode.AsTreeNode() called. n: %p, res: %p %v\n", n, res, res)
|
||||
return res
|
||||
}
|
||||
|
||||
// GetParent returns parent
|
||||
func (n *TreeNode) GetParent() Node {
|
||||
return n.Parent
|
||||
}
|
||||
|
||||
// GetChildren returns children
|
||||
func (n *TreeNode) GetChildren() []Node {
|
||||
return n.Children
|
||||
}
|
||||
|
||||
// PanicIfTreeNode will panic if node is *TreeNode
|
||||
func PanicIfTreeNode(node Node) {
|
||||
if _, ok := node.(*TreeNode); ok {
|
||||
panic(fmt.Sprintf("%v is TreeNode", node))
|
||||
}
|
||||
}
|
||||
|
||||
// AddChild adds child node to parent node
|
||||
func AddChild(parent Node, child Node) {
|
||||
PanicIfTreeNode(parent)
|
||||
PanicIfTreeNode(child)
|
||||
pn := parent.AsTreeNode()
|
||||
pn.Parent = parent
|
||||
pn.Children = append(pn.Children, child)
|
||||
}
|
||||
|
||||
func isNilNode(i Node) bool {
|
||||
if i == nil {
|
||||
return true
|
||||
}
|
||||
return reflect.ValueOf(i).IsNil()
|
||||
}
|
||||
|
||||
// Document represents document
|
||||
type Document struct {
|
||||
TreeNode
|
||||
}
|
||||
|
||||
// BlockQuote represents data for block quote node
|
||||
type BlockQuote struct {
|
||||
TreeNode
|
||||
}
|
||||
|
||||
// List represents data list node
|
||||
type List struct {
|
||||
TreeNode
|
||||
|
||||
ListFlags ListType
|
||||
Tight bool // Skip <p>s around list item data if true
|
||||
BulletChar byte // '*', '+' or '-' in bullet lists
|
||||
@ -54,8 +115,10 @@ type ListData struct {
|
||||
IsFootnotesList bool // This is a list of footnotes
|
||||
}
|
||||
|
||||
// ListItemData represents data for list item node
|
||||
type ListItemData struct {
|
||||
// ListItem represents data for list item node
|
||||
type ListItem struct {
|
||||
TreeNode
|
||||
|
||||
ListFlags ListType
|
||||
Tight bool // Skip <p>s around list item data if true
|
||||
BulletChar byte // '*', '+' or '-' in bullet lists
|
||||
@ -64,57 +127,72 @@ type ListItemData struct {
|
||||
IsFootnotesList bool // This is a list of footnotes
|
||||
}
|
||||
|
||||
// ParagraphData represents data for paragraph node
|
||||
type ParagraphData struct {
|
||||
// Paragraph represents data for paragraph node
|
||||
type Paragraph struct {
|
||||
TreeNode
|
||||
}
|
||||
|
||||
// HeadingData contains fields relevant to a Heading node type.
|
||||
type HeadingData struct {
|
||||
// Heading contains fields relevant to a Heading node type.
|
||||
type Heading struct {
|
||||
TreeNode
|
||||
|
||||
Level int // This holds the heading level number
|
||||
HeadingID string // This might hold heading ID, if present
|
||||
IsTitleblock bool // Specifies whether it's a title block
|
||||
}
|
||||
|
||||
// HorizontalRuleData represents data for horizontal rule node
|
||||
type HorizontalRuleData struct {
|
||||
// HorizontalRule represents data for horizontal rule node
|
||||
type HorizontalRule struct {
|
||||
TreeNode
|
||||
}
|
||||
|
||||
// EmphData represents data for emp node
|
||||
type EmphData struct {
|
||||
// Emph represents data for emp node
|
||||
type Emph struct {
|
||||
TreeNode
|
||||
}
|
||||
|
||||
// StrongData represents data for strong node
|
||||
type StrongData struct {
|
||||
// Strong represents data for strong node
|
||||
type Strong struct {
|
||||
TreeNode
|
||||
}
|
||||
|
||||
// DelData represents data for del node
|
||||
type DelData struct {
|
||||
// Del represents data for del node
|
||||
type Del struct {
|
||||
TreeNode
|
||||
}
|
||||
|
||||
// LinkData represents data for link node
|
||||
type LinkData struct {
|
||||
// Link represents data for link node
|
||||
type Link struct {
|
||||
TreeNode
|
||||
|
||||
Destination []byte // Destination is what goes into a href
|
||||
Title []byte // Title is the tooltip thing that goes in a title attribute
|
||||
NoteID int // NoteID contains a serial number of a footnote, zero if it's not a footnote
|
||||
Footnote *Node // If it's a footnote, this is a direct link to the footnote Node. Otherwise nil.
|
||||
Footnote Node // If it's a footnote, this is a direct link to the footnote Node. Otherwise nil.
|
||||
}
|
||||
|
||||
// ImageData represents data for image node
|
||||
type ImageData struct {
|
||||
// Image represents data for image node
|
||||
type Image struct {
|
||||
TreeNode
|
||||
|
||||
Destination []byte // Destination is what goes into a href
|
||||
Title []byte // Title is the tooltip thing that goes in a title attribute
|
||||
}
|
||||
|
||||
// TextData represents data for text node
|
||||
type TextData struct {
|
||||
// Text represents data for text node
|
||||
type Text struct {
|
||||
TreeNode
|
||||
}
|
||||
|
||||
// HTMLBlockData represents data for html node
|
||||
type HTMLBlockData struct {
|
||||
// HTMLBlock represents data for html node
|
||||
type HTMLBlock struct {
|
||||
TreeNode
|
||||
}
|
||||
|
||||
// CodeBlockData contains fields relevant to a CodeBlock node type.
|
||||
type CodeBlockData struct {
|
||||
// CodeBlock contains fields relevant to a CodeBlock node type.
|
||||
type CodeBlock struct {
|
||||
TreeNode
|
||||
|
||||
IsFenced bool // Specifies whether it's a fenced code block or an indented one
|
||||
Info []byte // This holds the info string
|
||||
FenceChar byte
|
||||
@ -122,66 +200,56 @@ type CodeBlockData struct {
|
||||
FenceOffset int
|
||||
}
|
||||
|
||||
// SoftbreakData represents data for softbreak node
|
||||
// Softbreak represents data for softbreak node
|
||||
// Note: not used currently
|
||||
type SoftbreakData struct {
|
||||
type Softbreak struct {
|
||||
TreeNode
|
||||
}
|
||||
|
||||
// HardbreakData represents data for hard break node
|
||||
type HardbreakData struct {
|
||||
// Hardbreak represents data for hard break node
|
||||
type Hardbreak struct {
|
||||
TreeNode
|
||||
}
|
||||
|
||||
// CodeData represents data for code node
|
||||
type CodeData struct {
|
||||
// Code represents data for code node
|
||||
type Code struct {
|
||||
TreeNode
|
||||
}
|
||||
|
||||
// HTMLSpanData represents data for html span node
|
||||
type HTMLSpanData struct {
|
||||
// HTMLSpan represents data for html span node
|
||||
type HTMLSpan struct {
|
||||
TreeNode
|
||||
}
|
||||
|
||||
// TableData represents data for table node
|
||||
type TableData struct {
|
||||
// Table represents data for table node
|
||||
type Table struct {
|
||||
TreeNode
|
||||
}
|
||||
|
||||
// TableCellData contains fields relevant to a table cell node type.
|
||||
type TableCellData struct {
|
||||
// TableCell contains fields relevant to a table cell node type.
|
||||
type TableCell struct {
|
||||
TreeNode
|
||||
|
||||
IsHeader bool // This tells if it's under the header row
|
||||
Align CellAlignFlags // This holds the value for align attribute
|
||||
}
|
||||
|
||||
// TableHeadData represents data for a table head node
|
||||
type TableHeadData struct {
|
||||
// TableHead represents data for a table head node
|
||||
type TableHead struct {
|
||||
TreeNode
|
||||
}
|
||||
|
||||
// TableBodyData represents data for a tablef body node
|
||||
type TableBodyData struct {
|
||||
// TableBody represents data for a tablef body node
|
||||
type TableBody struct {
|
||||
TreeNode
|
||||
}
|
||||
|
||||
// TableRowData represents data for a table row node
|
||||
type TableRowData struct {
|
||||
}
|
||||
|
||||
// Node is a single element in the abstract syntax tree of the parsed document.
|
||||
// It holds connections to the structurally neighboring nodes and, for certain
|
||||
// types of nodes, additional information that might be needed when rendering.
|
||||
type Node struct {
|
||||
Parent *Node // Points to the parent
|
||||
Children []*Node
|
||||
|
||||
Literal []byte // Text contents of the leaf nodes
|
||||
|
||||
Data NodeData
|
||||
|
||||
Content []byte // Markdown content of the block nodes
|
||||
}
|
||||
|
||||
// NewNode allocates a node of a specified type.
|
||||
func NewNode(d NodeData) *Node {
|
||||
return &Node{
|
||||
Data: d,
|
||||
}
|
||||
// TableRow represents data for a table row node
|
||||
type TableRow struct {
|
||||
TreeNode
|
||||
}
|
||||
|
||||
/*
|
||||
func (n *Node) String() string {
|
||||
ellipsis := ""
|
||||
snippet := n.Literal
|
||||
@ -191,8 +259,9 @@ func (n *Node) String() string {
|
||||
}
|
||||
return fmt.Sprintf("%T: '%s%s'", n.Data, snippet, ellipsis)
|
||||
}
|
||||
*/
|
||||
|
||||
func removeNodeFromArray(a []*Node, node *Node) []*Node {
|
||||
func removeNodeFromArray(a []Node, node Node) []Node {
|
||||
n := len(a)
|
||||
for i := 0; i < n; i++ {
|
||||
if a[i] == node {
|
||||
@ -203,28 +272,32 @@ func removeNodeFromArray(a []*Node, node *Node) []*Node {
|
||||
}
|
||||
|
||||
// RemoveFromTree removes this node from tree
|
||||
func (n *Node) RemoveFromTree() {
|
||||
if n.Parent == nil {
|
||||
func RemoveFromTree(n Node) {
|
||||
nt := n.AsTreeNode()
|
||||
if nt.Parent == nil {
|
||||
return
|
||||
}
|
||||
// important: don't clear n.Children if n has no parent
|
||||
// we're called from AppendChild and that might happen on a node
|
||||
// that accumulated Children but hasn't been inserted into the tree
|
||||
n.Parent.Children = removeNodeFromArray(n.Parent.Children, n)
|
||||
n.Parent = nil
|
||||
n.Children = nil
|
||||
p := nt.Parent.AsTreeNode()
|
||||
p.Children = removeNodeFromArray(p.Children, n)
|
||||
nt.Parent = nil
|
||||
nt.Children = nil
|
||||
}
|
||||
|
||||
// AppendChild adds a node 'child' as a child of 'n'.
|
||||
// It panics if either node is nil.
|
||||
func (n *Node) AppendChild(child *Node) {
|
||||
child.RemoveFromTree()
|
||||
child.Parent = n
|
||||
n.Children = append(n.Children, child)
|
||||
func AppendChild(n Node, child Node) {
|
||||
childTN := child.AsTreeNode()
|
||||
RemoveFromTree(child)
|
||||
childTN.Parent = n
|
||||
nTN := n.AsTreeNode()
|
||||
nTN.Children = append(nTN.Children, child)
|
||||
}
|
||||
|
||||
// LastChild returns last child of this node
|
||||
func (n *Node) LastChild() *Node {
|
||||
func (n *TreeNode) LastChild() Node {
|
||||
a := n.Children
|
||||
if len(a) > 0 {
|
||||
return a[len(a)-1]
|
||||
@ -233,7 +306,7 @@ func (n *Node) LastChild() *Node {
|
||||
}
|
||||
|
||||
// FirstChild returns first child of this node
|
||||
func (n *Node) FirstChild() *Node {
|
||||
func (n *TreeNode) FirstChild() Node {
|
||||
a := n.Children
|
||||
if len(a) > 0 {
|
||||
return a[0]
|
||||
@ -241,12 +314,12 @@ func (n *Node) FirstChild() *Node {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Next returns next sibling of this node
|
||||
func (n *Node) Next() *Node {
|
||||
if n.Parent == nil {
|
||||
// NextNode returns next sibling of this node
|
||||
func NextNode(n Node) Node {
|
||||
if isNilNode(n.GetParent()) {
|
||||
return nil
|
||||
}
|
||||
a := n.Parent.Children
|
||||
a := n.GetParent().AsTreeNode().Children
|
||||
len := len(a) - 1
|
||||
for i := 0; i < len; i++ {
|
||||
if a[i] == n {
|
||||
@ -256,12 +329,12 @@ func (n *Node) Next() *Node {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Prev returns previous sibling of this node
|
||||
func (n *Node) Prev() *Node {
|
||||
if n.Parent == nil {
|
||||
// PrevNode returns sibling node before n
|
||||
func PrevNode(n Node) Node {
|
||||
if isNilNode(n.GetParent()) {
|
||||
return nil
|
||||
}
|
||||
a := n.Parent.Children
|
||||
a := n.GetParent().AsTreeNode().Children
|
||||
len := len(a)
|
||||
for i := 1; i < len; i++ {
|
||||
if a[i] == n {
|
||||
@ -271,10 +344,10 @@ func (n *Node) Prev() *Node {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *Node) isContainer() bool {
|
||||
func isContainer(n Node) bool {
|
||||
// list of non-containers is smaller so we check against that for speed
|
||||
switch n.Data.(type) {
|
||||
case *HorizontalRuleData, *TextData, *HTMLBlockData, *CodeBlockData, *SoftbreakData, *HardbreakData, *CodeData, *HTMLSpanData:
|
||||
switch n.(type) {
|
||||
case *HorizontalRule, *Text, *HTMLBlock, *CodeBlock, *Softbreak, *Hardbreak, *Code, *HTMLSpan:
|
||||
return false
|
||||
default:
|
||||
return true
|
||||
@ -299,20 +372,20 @@ const (
|
||||
// Called twice for every node: once with entering=true when the branch is
|
||||
// first visited, then with entering=false after all the children are done.
|
||||
type NodeVisitor interface {
|
||||
Visit(node *Node, entering bool) WalkStatus
|
||||
Visit(node Node, entering bool) WalkStatus
|
||||
}
|
||||
|
||||
// NodeVisitorFunc casts a function to match NodeVisitor interface
|
||||
type NodeVisitorFunc func(node *Node, entering bool) WalkStatus
|
||||
type NodeVisitorFunc func(node Node, entering bool) WalkStatus
|
||||
|
||||
// Visit calls visitor function
|
||||
func (f NodeVisitorFunc) Visit(node *Node, entering bool) WalkStatus {
|
||||
func (f NodeVisitorFunc) Visit(node Node, entering bool) WalkStatus {
|
||||
return f(node, entering)
|
||||
}
|
||||
|
||||
// Walk is a convenience method that instantiates a walker and starts a
|
||||
// traversal of subtree rooted at n.
|
||||
func (n *Node) Walk(visitor NodeVisitor) {
|
||||
func Walk(n Node, visitor NodeVisitor) {
|
||||
w := newNodeWalker(n)
|
||||
for w.current != nil {
|
||||
status := visitor.Visit(w.current, w.entering)
|
||||
@ -329,18 +402,18 @@ func (n *Node) Walk(visitor NodeVisitor) {
|
||||
}
|
||||
|
||||
// WalkFunc is like Walk but accepts just a callback function
|
||||
func (n *Node) WalkFunc(f NodeVisitorFunc) {
|
||||
func WalkFunc(n Node, f NodeVisitorFunc) {
|
||||
visitor := NodeVisitorFunc(f)
|
||||
n.Walk(visitor)
|
||||
Walk(n, visitor)
|
||||
}
|
||||
|
||||
type nodeWalker struct {
|
||||
current *Node
|
||||
root *Node
|
||||
current Node
|
||||
root Node
|
||||
entering bool
|
||||
}
|
||||
|
||||
func newNodeWalker(root *Node) *nodeWalker {
|
||||
func newNodeWalker(root Node) *nodeWalker {
|
||||
return &nodeWalker{
|
||||
current: root,
|
||||
root: root,
|
||||
@ -349,46 +422,46 @@ func newNodeWalker(root *Node) *nodeWalker {
|
||||
}
|
||||
|
||||
func (nw *nodeWalker) next() {
|
||||
if (!nw.current.isContainer() || !nw.entering) && nw.current == nw.root {
|
||||
if (!isContainer(nw.current) || !nw.entering) && nw.current == nw.root {
|
||||
nw.current = nil
|
||||
return
|
||||
}
|
||||
if nw.entering && nw.current.isContainer() {
|
||||
if nw.entering && isContainer(nw.current) {
|
||||
if nw.current.FirstChild() != nil {
|
||||
nw.current = nw.current.FirstChild()
|
||||
nw.entering = true
|
||||
} else {
|
||||
nw.entering = false
|
||||
}
|
||||
} else if nw.current.Next() == nil {
|
||||
nw.current = nw.current.Parent
|
||||
} else if NextNode(nw.current) == nil {
|
||||
nw.current = nw.current.GetParent()
|
||||
nw.entering = false
|
||||
} else {
|
||||
nw.current = nw.current.Next()
|
||||
nw.current = NextNode(nw.current)
|
||||
nw.entering = true
|
||||
}
|
||||
}
|
||||
|
||||
func dump(ast *Node) {
|
||||
func dump(ast Node) {
|
||||
fmt.Println(dumpString(ast))
|
||||
}
|
||||
|
||||
func dumpR(ast *Node, depth int) string {
|
||||
func dumpR(ast Node, depth int) string {
|
||||
if ast == nil {
|
||||
return ""
|
||||
}
|
||||
indent := bytes.Repeat([]byte("\t"), depth)
|
||||
content := ast.Literal
|
||||
content := ast.AsTreeNode().Literal
|
||||
if content == nil {
|
||||
content = ast.Content
|
||||
content = ast.AsTreeNode().Content
|
||||
}
|
||||
result := fmt.Sprintf("%s%T(%q)\n", indent, ast.Data, content)
|
||||
for _, n := range ast.Children {
|
||||
result := fmt.Sprintf("%s%T(%q)\n", indent, ast, content)
|
||||
for _, n := range ast.GetChildren() {
|
||||
result += dumpR(n, depth+1)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func dumpString(ast *Node) string {
|
||||
func dumpString(ast Node) string {
|
||||
return dumpR(ast, 0)
|
||||
}
|
||||
|
@ -22,7 +22,7 @@ func execRecoverableTestSuite(t *testing.T, tests []string, params TestParams, s
|
||||
// the integration server. When developing, though, crash dump is often
|
||||
// preferable, so recovery can be easily turned off with doRecover = false.
|
||||
var candidate string
|
||||
const doRecover = true
|
||||
const doRecover = false
|
||||
if doRecover {
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
|
273
html/renderer.go
273
html/renderer.go
@ -65,7 +65,7 @@ const (
|
||||
// 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)
|
||||
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.
|
||||
@ -291,9 +291,9 @@ func needSkipLink(flags Flags, dest []byte) bool {
|
||||
return flags&Safelink != 0 && !isSafeLink(dest) && !isMailto(dest)
|
||||
}
|
||||
|
||||
func isSmartypantable(node *ast.Node) bool {
|
||||
switch node.Parent.Data.(type) {
|
||||
case *ast.LinkData, *ast.CodeBlockData, *ast.CodeData:
|
||||
func isSmartypantable(node ast.Node) bool {
|
||||
switch node.GetParent().(type) {
|
||||
case *ast.Link, *ast.CodeBlock, *ast.Code:
|
||||
return false
|
||||
}
|
||||
return true
|
||||
@ -320,7 +320,7 @@ func (r *Renderer) outTag(w io.Writer, name string, attrs []string) {
|
||||
r.lastOutputLen = 1
|
||||
}
|
||||
|
||||
func footnoteRef(prefix string, node *ast.LinkData) string {
|
||||
func footnoteRef(prefix string, node *ast.Link) string {
|
||||
urlFrag := prefix + string(slugify(node.Destination))
|
||||
nStr := strconv.Itoa(node.NoteID)
|
||||
anchor := `<a rel="footnote" href="#fn:` + urlFrag + `">` + nStr + `</a>`
|
||||
@ -335,22 +335,22 @@ func footnoteReturnLink(prefix, returnLink string, slug []byte) string {
|
||||
return ` <a class="footnote-return" href="#fnref:` + prefix + string(slug) + `">` + returnLink + `</a>`
|
||||
}
|
||||
|
||||
func itemOpenCR(node *ast.Node) bool {
|
||||
if node.Prev() == nil {
|
||||
func itemOpenCR(node ast.Node) bool {
|
||||
if ast.PrevNode(node) == nil {
|
||||
return false
|
||||
}
|
||||
ld := node.Parent.Data.(*ast.ListData)
|
||||
ld := node.GetParent().(*ast.List)
|
||||
return !ld.Tight && ld.ListFlags&ast.ListTypeDefinition == 0
|
||||
}
|
||||
|
||||
func skipParagraphTags(node *ast.Node) bool {
|
||||
parent := node.Parent
|
||||
grandparent := parent.Parent
|
||||
if grandparent == nil || !isListData(grandparent.Data) {
|
||||
func skipParagraphTags(node ast.Node) bool {
|
||||
parent := node.GetParent()
|
||||
grandparent := parent.GetParent()
|
||||
if grandparent == nil || !isListData(grandparent) {
|
||||
return false
|
||||
}
|
||||
isParentTerm := isListItemTerm(parent)
|
||||
grandparentListData := grandparent.Data.(*ast.ListData)
|
||||
grandparentListData := grandparent.(*ast.List)
|
||||
tightOrTerm := grandparentListData.Tight || isParentTerm
|
||||
return tightOrTerm
|
||||
}
|
||||
@ -414,13 +414,13 @@ func (r *Renderer) outHRTag(w io.Writer) {
|
||||
r.outOneOf(w, r.opts.Flags&UseXHTML == 0, "<hr>", "<hr />")
|
||||
}
|
||||
|
||||
func (r *Renderer) text(w io.Writer, node *ast.Node, nodeData *ast.TextData) {
|
||||
func (r *Renderer) text(w io.Writer, node *ast.Text) {
|
||||
if r.opts.Flags&Smartypants != 0 {
|
||||
var tmp bytes.Buffer
|
||||
EscapeHTML(&tmp, node.Literal)
|
||||
r.sr.Process(w, tmp.Bytes())
|
||||
} else {
|
||||
if isLinkData(node.Parent.Data) {
|
||||
if isLinkData(node.GetParent()) {
|
||||
escLink(w, node.Literal)
|
||||
} else {
|
||||
EscapeHTML(w, node.Literal)
|
||||
@ -428,7 +428,7 @@ func (r *Renderer) text(w io.Writer, node *ast.Node, nodeData *ast.TextData) {
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Renderer) hardBreak(w io.Writer, node *ast.Node, nodeData *ast.HardbreakData) {
|
||||
func (r *Renderer) hardBreak(w io.Writer, node *ast.Hardbreak) {
|
||||
r.outOneOf(w, r.opts.Flags&UseXHTML == 0, "<br>", "<br />")
|
||||
r.cr(w)
|
||||
}
|
||||
@ -451,13 +451,13 @@ func (r *Renderer) outOneOfCr(w io.Writer, outFirst bool, first string, second s
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Renderer) span(w io.Writer, node *ast.Node, nodeData *ast.HTMLSpanData) {
|
||||
func (r *Renderer) span(w io.Writer, node *ast.HTMLSpan) {
|
||||
if r.opts.Flags&SkipHTML == 0 {
|
||||
r.out(w, node.Literal)
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Renderer) linkEnter(w io.Writer, node *ast.Node, nodeData *ast.LinkData) {
|
||||
func (r *Renderer) linkEnter(w io.Writer, nodeData *ast.Link) {
|
||||
var attrs []string
|
||||
dest := nodeData.Destination
|
||||
dest = r.addAbsPrefix(dest)
|
||||
@ -482,13 +482,13 @@ func (r *Renderer) linkEnter(w io.Writer, node *ast.Node, nodeData *ast.LinkData
|
||||
r.outTag(w, "<a", attrs)
|
||||
}
|
||||
|
||||
func (r *Renderer) linkExit(w io.Writer, node *ast.Node, nodeData *ast.LinkData) {
|
||||
func (r *Renderer) linkExit(w io.Writer, nodeData *ast.Link) {
|
||||
if nodeData.NoteID == 0 {
|
||||
r.outs(w, "</a>")
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Renderer) link(w io.Writer, node *ast.Node, nodeData *ast.LinkData, entering bool) {
|
||||
func (r *Renderer) link(w io.Writer, nodeData *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, nodeData.Destination) {
|
||||
r.outOneOf(w, entering, "<tt>", "</tt>")
|
||||
@ -496,13 +496,13 @@ func (r *Renderer) link(w io.Writer, node *ast.Node, nodeData *ast.LinkData, ent
|
||||
}
|
||||
|
||||
if entering {
|
||||
r.linkEnter(w, node, nodeData)
|
||||
r.linkEnter(w, nodeData)
|
||||
} else {
|
||||
r.linkExit(w, node, nodeData)
|
||||
r.linkExit(w, nodeData)
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Renderer) imageEnter(w io.Writer, node *ast.Node, nodeData *ast.ImageData) {
|
||||
func (r *Renderer) imageEnter(w io.Writer, nodeData *ast.Image) {
|
||||
dest := nodeData.Destination
|
||||
dest = r.addAbsPrefix(dest)
|
||||
if r.disableTags == 0 {
|
||||
@ -517,7 +517,7 @@ func (r *Renderer) imageEnter(w io.Writer, node *ast.Node, nodeData *ast.ImageDa
|
||||
r.disableTags++
|
||||
}
|
||||
|
||||
func (r *Renderer) imageExit(w io.Writer, node *ast.Node, nodeData *ast.ImageData) {
|
||||
func (r *Renderer) imageExit(w io.Writer, nodeData *ast.Image) {
|
||||
r.disableTags--
|
||||
if r.disableTags == 0 {
|
||||
if nodeData.Title != nil {
|
||||
@ -528,54 +528,54 @@ func (r *Renderer) imageExit(w io.Writer, node *ast.Node, nodeData *ast.ImageDat
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Renderer) paragraphEnter(w io.Writer, node *ast.Node, nodeData *ast.ParagraphData) {
|
||||
func (r *Renderer) paragraphEnter(w io.Writer, nodeData *ast.Paragraph) {
|
||||
// TODO: untangle this clusterfuck about when the newlines need
|
||||
// to be added and when not.
|
||||
prev := node.Prev()
|
||||
prev := ast.PrevNode(nodeData)
|
||||
if prev != nil {
|
||||
switch prev.Data.(type) {
|
||||
case *ast.HTMLBlockData, *ast.ListData, *ast.ParagraphData, *ast.HeadingData, *ast.CodeBlockData, *ast.BlockQuoteData, *ast.HorizontalRuleData:
|
||||
switch prev.(type) {
|
||||
case *ast.HTMLBlock, *ast.List, *ast.Paragraph, *ast.Heading, *ast.CodeBlock, *ast.BlockQuote, *ast.HorizontalRule:
|
||||
r.cr(w)
|
||||
}
|
||||
}
|
||||
if isBlockQuoteData(node.Parent.Data) && prev == nil {
|
||||
if isBlockQuoteData(nodeData.Parent) && prev == nil {
|
||||
r.cr(w)
|
||||
}
|
||||
r.outs(w, "<p>")
|
||||
}
|
||||
|
||||
func (r *Renderer) paragraphExit(w io.Writer, node *ast.Node, nodeData *ast.ParagraphData) {
|
||||
func (r *Renderer) paragraphExit(w io.Writer, node *ast.Paragraph) {
|
||||
r.outs(w, "</p>")
|
||||
if !(isListItemData(node.Parent.Data) && node.Next() == nil) {
|
||||
if !(isListItemData(node.Parent) && ast.NextNode(node) == nil) {
|
||||
r.cr(w)
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Renderer) paragraph(w io.Writer, node *ast.Node, nodeData *ast.ParagraphData, entering bool) {
|
||||
func (r *Renderer) paragraph(w io.Writer, node *ast.Paragraph, entering bool) {
|
||||
if skipParagraphTags(node) {
|
||||
return
|
||||
}
|
||||
if entering {
|
||||
r.paragraphEnter(w, node, nodeData)
|
||||
r.paragraphEnter(w, node)
|
||||
} else {
|
||||
r.paragraphExit(w, node, nodeData)
|
||||
r.paragraphExit(w, node)
|
||||
}
|
||||
}
|
||||
func (r *Renderer) image(w io.Writer, node *ast.Node, nodeData *ast.ImageData, entering bool) {
|
||||
func (r *Renderer) image(w io.Writer, node *ast.Image, entering bool) {
|
||||
if entering {
|
||||
r.imageEnter(w, node, nodeData)
|
||||
r.imageEnter(w, node)
|
||||
} else {
|
||||
r.imageExit(w, node, nodeData)
|
||||
r.imageExit(w, node)
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Renderer) code(w io.Writer, node *ast.Node, nodeData *ast.CodeData) {
|
||||
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.Node, nodeData *ast.HTMLBlockData) {
|
||||
func (r *Renderer) htmlBlock(w io.Writer, node *ast.HTMLBlock) {
|
||||
if r.opts.Flags&SkipHTML != 0 {
|
||||
return
|
||||
}
|
||||
@ -584,7 +584,7 @@ func (r *Renderer) htmlBlock(w io.Writer, node *ast.Node, nodeData *ast.HTMLBloc
|
||||
r.cr(w)
|
||||
}
|
||||
|
||||
func (r *Renderer) headingEnter(w io.Writer, node *ast.Node, nodeData *ast.HeadingData) {
|
||||
func (r *Renderer) headingEnter(w io.Writer, nodeData *ast.Heading) {
|
||||
var attrs []string
|
||||
if nodeData.IsTitleblock {
|
||||
attrs = append(attrs, `class="title"`)
|
||||
@ -604,18 +604,18 @@ func (r *Renderer) headingEnter(w io.Writer, node *ast.Node, nodeData *ast.Headi
|
||||
r.outTag(w, headingOpenTagFromLevel(nodeData.Level), attrs)
|
||||
}
|
||||
|
||||
func (r *Renderer) headingExit(w io.Writer, node *ast.Node, nodeData *ast.HeadingData) {
|
||||
func (r *Renderer) headingExit(w io.Writer, nodeData *ast.Heading) {
|
||||
r.outs(w, headingCloseTagFromLevel(nodeData.Level))
|
||||
if !(isListItemData(node.Parent.Data) && node.Next() == nil) {
|
||||
if !(isListItemData(nodeData.Parent) && ast.NextNode(nodeData) == nil) {
|
||||
r.cr(w)
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Renderer) heading(w io.Writer, node *ast.Node, nodeData *ast.HeadingData, entering bool) {
|
||||
func (r *Renderer) heading(w io.Writer, node *ast.Heading, entering bool) {
|
||||
if entering {
|
||||
r.headingEnter(w, node, nodeData)
|
||||
r.headingEnter(w, node)
|
||||
} else {
|
||||
r.headingExit(w, node, nodeData)
|
||||
r.headingExit(w, node)
|
||||
}
|
||||
}
|
||||
|
||||
@ -625,7 +625,7 @@ func (r *Renderer) horizontalRule(w io.Writer) {
|
||||
r.cr(w)
|
||||
}
|
||||
|
||||
func (r *Renderer) listEnter(w io.Writer, node *ast.Node, nodeData *ast.ListData) {
|
||||
func (r *Renderer) listEnter(w io.Writer, nodeData *ast.List) {
|
||||
// TODO: attrs don't seem to be set
|
||||
var attrs []string
|
||||
|
||||
@ -635,9 +635,9 @@ func (r *Renderer) listEnter(w io.Writer, node *ast.Node, nodeData *ast.ListData
|
||||
r.cr(w)
|
||||
}
|
||||
r.cr(w)
|
||||
if isListItemData(node.Parent.Data) {
|
||||
grand := node.Parent.Parent
|
||||
if isListTight(grand.Data) {
|
||||
if isListItemData(nodeData.Parent) {
|
||||
grand := nodeData.Parent.GetParent()
|
||||
if isListTight(grand) {
|
||||
r.cr(w)
|
||||
}
|
||||
}
|
||||
@ -653,12 +653,12 @@ func (r *Renderer) listEnter(w io.Writer, node *ast.Node, nodeData *ast.ListData
|
||||
r.cr(w)
|
||||
}
|
||||
|
||||
func (r *Renderer) listExit(w io.Writer, node *ast.Node, nodeData *ast.ListData) {
|
||||
func (r *Renderer) listExit(w io.Writer, node *ast.List) {
|
||||
closeTag := "</ul>"
|
||||
if nodeData.ListFlags&ast.ListTypeOrdered != 0 {
|
||||
if node.ListFlags&ast.ListTypeOrdered != 0 {
|
||||
closeTag = "</ol>"
|
||||
}
|
||||
if nodeData.ListFlags&ast.ListTypeDefinition != 0 {
|
||||
if node.ListFlags&ast.ListTypeDefinition != 0 {
|
||||
closeTag = "</dl>"
|
||||
}
|
||||
r.outs(w, closeTag)
|
||||
@ -667,46 +667,46 @@ func (r *Renderer) listExit(w io.Writer, node *ast.Node, nodeData *ast.ListData)
|
||||
//if node.parent.Type != Item {
|
||||
// cr(w)
|
||||
//}
|
||||
if isListItemData(node.Parent.Data) && node.Next() != nil {
|
||||
if isListItemData(node.Parent) && ast.NextNode(node) != nil {
|
||||
r.cr(w)
|
||||
}
|
||||
if isDocumentData(node.Parent.Data) || isBlockQuoteData(node.Parent.Data) {
|
||||
if isDocumentData(node.Parent) || isBlockQuoteData(node.Parent) {
|
||||
r.cr(w)
|
||||
}
|
||||
if nodeData.IsFootnotesList {
|
||||
if node.IsFootnotesList {
|
||||
r.outs(w, "\n</div>\n")
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Renderer) list(w io.Writer, node *ast.Node, nodeData *ast.ListData, entering bool) {
|
||||
func (r *Renderer) list(w io.Writer, node *ast.List, entering bool) {
|
||||
if entering {
|
||||
r.listEnter(w, node, nodeData)
|
||||
r.listEnter(w, node)
|
||||
} else {
|
||||
r.listExit(w, node, nodeData)
|
||||
r.listExit(w, node)
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Renderer) listItemEnter(w io.Writer, node *ast.Node, nodeData *ast.ListItemData) {
|
||||
func (r *Renderer) listItemEnter(w io.Writer, node *ast.ListItem) {
|
||||
if itemOpenCR(node) {
|
||||
r.cr(w)
|
||||
}
|
||||
if nodeData.RefLink != nil {
|
||||
slug := slugify(nodeData.RefLink)
|
||||
if node.RefLink != nil {
|
||||
slug := slugify(node.RefLink)
|
||||
r.outs(w, footnoteItem(r.opts.FootnoteAnchorPrefix, slug))
|
||||
return
|
||||
}
|
||||
|
||||
openTag := "<li>"
|
||||
if nodeData.ListFlags&ast.ListTypeDefinition != 0 {
|
||||
if node.ListFlags&ast.ListTypeDefinition != 0 {
|
||||
openTag = "<dd>"
|
||||
}
|
||||
if nodeData.ListFlags&ast.ListTypeTerm != 0 {
|
||||
if node.ListFlags&ast.ListTypeTerm != 0 {
|
||||
openTag = "<dt>"
|
||||
}
|
||||
r.outs(w, openTag)
|
||||
}
|
||||
|
||||
func (r *Renderer) listItemExit(w io.Writer, node *ast.Node, nodeData *ast.ListItemData) {
|
||||
func (r *Renderer) listItemExit(w io.Writer, nodeData *ast.ListItem) {
|
||||
if nodeData.RefLink != nil && r.opts.Flags&FootnoteReturnLinks != 0 {
|
||||
slug := slugify(nodeData.RefLink)
|
||||
prefix := r.opts.FootnoteAnchorPrefix
|
||||
@ -726,31 +726,31 @@ func (r *Renderer) listItemExit(w io.Writer, node *ast.Node, nodeData *ast.ListI
|
||||
r.cr(w)
|
||||
}
|
||||
|
||||
func (r *Renderer) listItem(w io.Writer, node *ast.Node, nodeData *ast.ListItemData, entering bool) {
|
||||
func (r *Renderer) listItem(w io.Writer, node *ast.ListItem, entering bool) {
|
||||
if entering {
|
||||
r.listItemEnter(w, node, nodeData)
|
||||
r.listItemEnter(w, node)
|
||||
} else {
|
||||
r.listItemExit(w, node, nodeData)
|
||||
r.listItemExit(w, node)
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Renderer) codeBlock(w io.Writer, node *ast.Node, nodeData *ast.CodeBlockData) {
|
||||
func (r *Renderer) codeBlock(w io.Writer, node *ast.CodeBlock) {
|
||||
var attrs []string
|
||||
attrs = appendLanguageAttr(attrs, nodeData.Info)
|
||||
attrs = appendLanguageAttr(attrs, node.Info)
|
||||
r.cr(w)
|
||||
r.outs(w, "<pre>")
|
||||
r.outTag(w, "<code", attrs)
|
||||
EscapeHTML(w, node.Literal)
|
||||
r.outs(w, "</code>")
|
||||
r.outs(w, "</pre>")
|
||||
if !isListItemData(node.Parent.Data) {
|
||||
if !isListItemData(node.Parent) {
|
||||
r.cr(w)
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Renderer) tableCell(w io.Writer, node *ast.Node, nodeData *ast.TableCellData, entering bool) {
|
||||
func (r *Renderer) tableCell(w io.Writer, node *ast.TableCell, entering bool) {
|
||||
if !entering {
|
||||
r.outOneOf(w, nodeData.IsHeader, "</th>", "</td>")
|
||||
r.outOneOf(w, node.IsHeader, "</th>", "</td>")
|
||||
r.cr(w)
|
||||
return
|
||||
}
|
||||
@ -758,20 +758,20 @@ func (r *Renderer) tableCell(w io.Writer, node *ast.Node, nodeData *ast.TableCel
|
||||
// entering
|
||||
var attrs []string
|
||||
openTag := "<td"
|
||||
if nodeData.IsHeader {
|
||||
if node.IsHeader {
|
||||
openTag = "<th"
|
||||
}
|
||||
align := cellAlignment(nodeData.Align)
|
||||
align := cellAlignment(node.Align)
|
||||
if align != "" {
|
||||
attrs = append(attrs, fmt.Sprintf(`align="%s"`, align))
|
||||
}
|
||||
if node.Prev() == nil {
|
||||
if ast.PrevNode(node) == nil {
|
||||
r.cr(w)
|
||||
}
|
||||
r.outTag(w, openTag, attrs)
|
||||
}
|
||||
|
||||
func (r *Renderer) tableBody(w io.Writer, node *ast.Node, nodeData *ast.TableBodyData, entering bool) {
|
||||
func (r *Renderer) tableBody(w io.Writer, node *ast.TableBody, entering bool) {
|
||||
if entering {
|
||||
r.cr(w)
|
||||
r.outs(w, "<tbody>")
|
||||
@ -795,75 +795,76 @@ func (r *Renderer) tableBody(w io.Writer, node *ast.Node, nodeData *ast.TableBod
|
||||
// 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.
|
||||
func (r *Renderer) RenderNode(w io.Writer, node *ast.Node, entering bool) ast.WalkStatus {
|
||||
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 nodeData := node.Data.(type) {
|
||||
case *ast.TextData:
|
||||
r.text(w, node, nodeData)
|
||||
case *ast.SoftbreakData:
|
||||
ast.PanicIfTreeNode(node)
|
||||
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.HardbreakData:
|
||||
r.hardBreak(w, node, nodeData)
|
||||
case *ast.EmphData:
|
||||
case *ast.Hardbreak:
|
||||
r.hardBreak(w, node)
|
||||
case *ast.Emph:
|
||||
r.outOneOf(w, entering, "<em>", "</em>")
|
||||
case *ast.StrongData:
|
||||
case *ast.Strong:
|
||||
r.outOneOf(w, entering, "<strong>", "</strong>")
|
||||
case *ast.DelData:
|
||||
case *ast.Del:
|
||||
r.outOneOf(w, entering, "<del>", "</del>")
|
||||
case *ast.BlockQuoteData:
|
||||
case *ast.BlockQuote:
|
||||
r.outOneOfCr(w, entering, "<blockquote>", "</blockquote>")
|
||||
case *ast.LinkData:
|
||||
r.link(w, node, nodeData, entering)
|
||||
case *ast.ImageData:
|
||||
case *ast.Link:
|
||||
r.link(w, node, entering)
|
||||
case *ast.Image:
|
||||
if r.opts.Flags&SkipImages != 0 {
|
||||
return ast.SkipChildren
|
||||
}
|
||||
r.image(w, node, nodeData, entering)
|
||||
case *ast.CodeData:
|
||||
r.code(w, node, nodeData)
|
||||
case *ast.CodeBlockData:
|
||||
r.codeBlock(w, node, nodeData)
|
||||
case *ast.DocumentData:
|
||||
r.image(w, node, entering)
|
||||
case *ast.Code:
|
||||
r.code(w, node)
|
||||
case *ast.CodeBlock:
|
||||
r.codeBlock(w, node)
|
||||
case *ast.Document:
|
||||
// do nothing
|
||||
case *ast.ParagraphData:
|
||||
r.paragraph(w, node, nodeData, entering)
|
||||
case *ast.HTMLSpanData:
|
||||
r.span(w, node, nodeData)
|
||||
case *ast.HTMLBlockData:
|
||||
r.htmlBlock(w, node, nodeData)
|
||||
case *ast.HeadingData:
|
||||
r.heading(w, node, nodeData, entering)
|
||||
case *ast.HorizontalRuleData:
|
||||
case *ast.Paragraph:
|
||||
r.paragraph(w, node, entering)
|
||||
case *ast.HTMLSpan:
|
||||
r.span(w, node)
|
||||
case *ast.HTMLBlock:
|
||||
r.htmlBlock(w, node)
|
||||
case *ast.Heading:
|
||||
r.heading(w, node, entering)
|
||||
case *ast.HorizontalRule:
|
||||
r.horizontalRule(w)
|
||||
case *ast.ListData:
|
||||
r.list(w, node, nodeData, entering)
|
||||
case *ast.ListItemData:
|
||||
r.listItem(w, node, nodeData, entering)
|
||||
case *ast.TableData:
|
||||
case *ast.List:
|
||||
r.list(w, node, entering)
|
||||
case *ast.ListItem:
|
||||
r.listItem(w, node, entering)
|
||||
case *ast.Table:
|
||||
r.outOneOfCr(w, entering, "<table>", "</table>")
|
||||
case *ast.TableCellData:
|
||||
r.tableCell(w, node, nodeData, entering)
|
||||
case *ast.TableHeadData:
|
||||
case *ast.TableCell:
|
||||
r.tableCell(w, node, entering)
|
||||
case *ast.TableHead:
|
||||
r.outOneOfCr(w, entering, "<thead>", "</thead>")
|
||||
case *ast.TableBodyData:
|
||||
r.tableBody(w, node, nodeData, entering)
|
||||
case *ast.TableRowData:
|
||||
case *ast.TableBody:
|
||||
r.tableBody(w, node, entering)
|
||||
case *ast.TableRow:
|
||||
r.outOneOfCr(w, entering, "<tr>", "</tr>")
|
||||
default:
|
||||
//panic("Unknown node type " + node.Type.String())
|
||||
panic(fmt.Sprintf("Unknown node type %T", node.Data))
|
||||
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) {
|
||||
func (r *Renderer) RenderHeader(w io.Writer, ast ast.Node) {
|
||||
r.writeDocumentHeader(w)
|
||||
if r.opts.Flags&TOC != 0 {
|
||||
r.writeTOC(w, ast)
|
||||
@ -871,7 +872,7 @@ func (r *Renderer) RenderHeader(w io.Writer, ast *ast.Node) {
|
||||
}
|
||||
|
||||
// RenderFooter writes HTML document footer.
|
||||
func (r *Renderer) RenderFooter(w io.Writer, ast *ast.Node) {
|
||||
func (r *Renderer) RenderFooter(w io.Writer, ast ast.Node) {
|
||||
if r.opts.Flags&CompletePage == 0 {
|
||||
return
|
||||
}
|
||||
@ -925,15 +926,15 @@ func (r *Renderer) writeDocumentHeader(w io.Writer) {
|
||||
io.WriteString(w, "<body>\n\n")
|
||||
}
|
||||
|
||||
func (r *Renderer) writeTOC(w io.Writer, doc *ast.Node) {
|
||||
func (r *Renderer) writeTOC(w io.Writer, doc ast.Node) {
|
||||
buf := bytes.Buffer{}
|
||||
|
||||
inHeading := false
|
||||
tocLevel := 0
|
||||
headingCount := 0
|
||||
|
||||
doc.WalkFunc(func(node *ast.Node, entering bool) ast.WalkStatus {
|
||||
if nodeData, ok := node.Data.(*ast.HeadingData); ok && !nodeData.IsTitleblock {
|
||||
ast.WalkFunc(doc, func(node ast.Node, entering bool) ast.WalkStatus {
|
||||
if nodeData, ok := node.(*ast.Heading); ok && !nodeData.IsTitleblock {
|
||||
inHeading = entering
|
||||
if entering {
|
||||
nodeData.HeadingID = fmt.Sprintf("toc_%d", headingCount)
|
||||
@ -979,40 +980,40 @@ func (r *Renderer) writeTOC(w io.Writer, doc *ast.Node) {
|
||||
r.lastOutputLen = buf.Len()
|
||||
}
|
||||
|
||||
func isListData(d ast.NodeData) bool {
|
||||
_, ok := d.(*ast.ListData)
|
||||
func isListData(n ast.Node) bool {
|
||||
_, ok := n.(*ast.List)
|
||||
return ok
|
||||
}
|
||||
|
||||
func isListTight(d ast.NodeData) bool {
|
||||
if listData, ok := d.(*ast.ListData); ok {
|
||||
func isListTight(d ast.Node) bool {
|
||||
if listData, ok := d.(*ast.List); ok {
|
||||
return listData.Tight
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func isListItemData(d ast.NodeData) bool {
|
||||
_, ok := d.(*ast.ListItemData)
|
||||
func isListItemData(d ast.Node) bool {
|
||||
_, ok := d.(*ast.ListItem)
|
||||
return ok
|
||||
}
|
||||
|
||||
func isListItemTerm(node *ast.Node) bool {
|
||||
data, ok := node.Data.(*ast.ListItemData)
|
||||
func isListItemTerm(node ast.Node) bool {
|
||||
data, ok := node.(*ast.ListItem)
|
||||
return ok && data.ListFlags&ast.ListTypeTerm != 0
|
||||
}
|
||||
|
||||
func isLinkData(d ast.NodeData) bool {
|
||||
_, ok := d.(*ast.LinkData)
|
||||
func isLinkData(d ast.Node) bool {
|
||||
_, ok := d.(*ast.Link)
|
||||
return ok
|
||||
}
|
||||
|
||||
func isBlockQuoteData(d ast.NodeData) bool {
|
||||
_, ok := d.(*ast.BlockQuoteData)
|
||||
func isBlockQuoteData(d ast.Node) bool {
|
||||
_, ok := d.(*ast.BlockQuote)
|
||||
return ok
|
||||
}
|
||||
|
||||
func isDocumentData(d ast.NodeData) bool {
|
||||
_, ok := d.(*ast.DocumentData)
|
||||
func isDocumentData(d ast.Node) bool {
|
||||
_, ok := d.(*ast.Document)
|
||||
return ok
|
||||
}
|
||||
|
||||
|
@ -9,7 +9,7 @@ import (
|
||||
"github.com/gomarkdown/markdown/parser"
|
||||
)
|
||||
|
||||
func renderHookEmpty(w io.Writer, node *ast.Node, entering bool) (ast.WalkStatus, bool) {
|
||||
func renderHookEmpty(w io.Writer, node ast.Node, entering bool) (ast.WalkStatus, bool) {
|
||||
return ast.GoToNext, true
|
||||
}
|
||||
|
||||
@ -32,8 +32,8 @@ func TestRenderNodeHookEmpty(t *testing.T) {
|
||||
doTestsParam(t, tests, params)
|
||||
}
|
||||
|
||||
func renderHookCodeBlock(w io.Writer, node *ast.Node, entering bool) (ast.WalkStatus, bool) {
|
||||
_, ok := node.Data.(*ast.CodeBlockData)
|
||||
func renderHookCodeBlock(w io.Writer, node ast.Node, entering bool) (ast.WalkStatus, bool) {
|
||||
_, ok := node.(*ast.CodeBlock)
|
||||
if !ok {
|
||||
return ast.GoToNext, false
|
||||
}
|
||||
|
10
markdown.go
10
markdown.go
@ -15,7 +15,7 @@ type Renderer interface {
|
||||
// every leaf node and twice for every non-leaf node (first with
|
||||
// entering=true, then with entering=false). The method should write its
|
||||
// rendition of the node to writer w.
|
||||
RenderNode(w io.Writer, node *ast.Node, entering bool) ast.WalkStatus
|
||||
RenderNode(w io.Writer, node ast.Node, entering bool) ast.WalkStatus
|
||||
|
||||
// RenderHeader is a method that allows the renderer to produce some
|
||||
// content preceding the main body of the output document. The header is
|
||||
@ -28,18 +28,18 @@ type Renderer interface {
|
||||
//
|
||||
// The output should be written to the supplied writer w. If your
|
||||
// implementation has no header to write, supply an empty implementation.
|
||||
RenderHeader(w io.Writer, ast *ast.Node)
|
||||
RenderHeader(w io.Writer, ast ast.Node)
|
||||
|
||||
// RenderFooter is a symmetric counterpart of RenderHeader.
|
||||
RenderFooter(w io.Writer, ast *ast.Node)
|
||||
RenderFooter(w io.Writer, ast ast.Node)
|
||||
}
|
||||
|
||||
// Render uses renderer to convert parsed markdown document into a different format.
|
||||
// For example to convert into HTML, use html.Renderer
|
||||
func Render(doc *ast.Node, renderer Renderer) []byte {
|
||||
func Render(doc ast.Node, renderer Renderer) []byte {
|
||||
var buf bytes.Buffer
|
||||
renderer.RenderHeader(&buf, doc)
|
||||
doc.WalkFunc(func(node *ast.Node, entering bool) ast.WalkStatus {
|
||||
ast.WalkFunc(doc, func(node ast.Node, entering bool) ast.WalkStatus {
|
||||
return renderer.RenderNode(&buf, node, entering)
|
||||
})
|
||||
renderer.RenderFooter(&buf, doc)
|
||||
|
@ -179,7 +179,7 @@ func (p *Parser) block(data []byte) {
|
||||
// or
|
||||
// ______
|
||||
if p.isHRule(data) {
|
||||
p.addBlock(&ast.HorizontalRuleData{}, nil)
|
||||
p.addBlock(&ast.HorizontalRule{}, nil)
|
||||
i := skipUntilChar(data, 0, '\n')
|
||||
data = data[i:]
|
||||
continue
|
||||
@ -250,10 +250,10 @@ func (p *Parser) block(data []byte) {
|
||||
p.nesting--
|
||||
}
|
||||
|
||||
func (p *Parser) addBlock(d ast.NodeData, content []byte) *ast.Node {
|
||||
func (p *Parser) addBlock(n ast.Node, content []byte) ast.Node {
|
||||
p.closeUnmatchedBlocks()
|
||||
container := p.addChild(d, 0)
|
||||
container.Content = content
|
||||
container := p.addChild(n, 0)
|
||||
container.AsTreeNode().Content = content
|
||||
return container
|
||||
}
|
||||
|
||||
@ -307,7 +307,7 @@ func (p *Parser) prefixHeading(data []byte) int {
|
||||
if id == "" && p.extensions&AutoHeadingIDs != 0 {
|
||||
id = SanitizeAnchorName(string(data[i:end]))
|
||||
}
|
||||
d := &ast.HeadingData{
|
||||
d := &ast.Heading{
|
||||
HeadingID: id,
|
||||
Level: level,
|
||||
}
|
||||
@ -357,7 +357,7 @@ func (p *Parser) titleBlock(data []byte, doRender bool) int {
|
||||
consumed := len(data)
|
||||
data = bytes.TrimPrefix(data, []byte("% "))
|
||||
data = bytes.Replace(data, []byte("\n% "), []byte("\n"), -1)
|
||||
d := &ast.HeadingData{
|
||||
d := &ast.Heading{
|
||||
Level: 1,
|
||||
IsTitleblock: true,
|
||||
}
|
||||
@ -455,13 +455,14 @@ func (p *Parser) html(data []byte, doRender bool) int {
|
||||
if doRender {
|
||||
// trim newlines
|
||||
end := backChar(data, i, '\n')
|
||||
finalizeHTMLBlock(p.addBlock(&ast.HTMLBlockData{}, data[:end]))
|
||||
finalizeHTMLBlock(p.addBlock(&ast.HTMLBlock{}, data[:end]))
|
||||
}
|
||||
|
||||
return i
|
||||
}
|
||||
|
||||
func finalizeHTMLBlock(block *ast.Node) {
|
||||
func finalizeHTMLBlock(blockNode ast.Node) {
|
||||
block := blockNode.AsTreeNode()
|
||||
block.Literal = block.Content
|
||||
block.Content = nil
|
||||
}
|
||||
@ -475,7 +476,7 @@ func (p *Parser) htmlComment(data []byte, doRender bool) int {
|
||||
if doRender {
|
||||
// trim trailing newlines
|
||||
end := backChar(data, size, '\n')
|
||||
block := p.addBlock(&ast.HTMLBlockData{}, data[:end])
|
||||
block := p.addBlock(&ast.HTMLBlock{}, data[:end])
|
||||
finalizeHTMLBlock(block)
|
||||
}
|
||||
return size
|
||||
@ -506,7 +507,7 @@ func (p *Parser) htmlHr(data []byte, doRender bool) int {
|
||||
if doRender {
|
||||
// trim newlines
|
||||
end := backChar(data, size, '\n')
|
||||
finalizeHTMLBlock(p.addBlock(&ast.HTMLBlockData{}, data[:end]))
|
||||
finalizeHTMLBlock(p.addBlock(&ast.HTMLBlock{}, data[:end]))
|
||||
}
|
||||
return size
|
||||
}
|
||||
@ -741,7 +742,7 @@ func (p *Parser) fencedCodeBlock(data []byte, doRender bool) int {
|
||||
}
|
||||
|
||||
if doRender {
|
||||
d := &ast.CodeBlockData{
|
||||
d := &ast.CodeBlock{
|
||||
IsFenced: true,
|
||||
}
|
||||
block := p.addBlock(d, work.Bytes()) // TODO: get rid of temp buffer
|
||||
@ -765,7 +766,8 @@ func unescapeString(str []byte) []byte {
|
||||
return str
|
||||
}
|
||||
|
||||
func finalizeCodeBlock(block *ast.Node, code *ast.CodeBlockData) {
|
||||
func finalizeCodeBlock(blockNode ast.Node, code *ast.CodeBlock) {
|
||||
block := blockNode.AsTreeNode()
|
||||
if code.IsFenced {
|
||||
newlinePos := bytes.IndexByte(block.Content, '\n')
|
||||
firstLine := block.Content[:newlinePos]
|
||||
@ -779,15 +781,15 @@ func finalizeCodeBlock(block *ast.Node, code *ast.CodeBlockData) {
|
||||
}
|
||||
|
||||
func (p *Parser) table(data []byte) int {
|
||||
table := p.addBlock(&ast.TableData{}, nil)
|
||||
table := p.addBlock(&ast.Table{}, nil)
|
||||
i, columns := p.tableHeader(data)
|
||||
if i == 0 {
|
||||
p.tip = table.Parent
|
||||
table.RemoveFromTree()
|
||||
p.tip = table.GetParent()
|
||||
ast.RemoveFromTree(table)
|
||||
return 0
|
||||
}
|
||||
|
||||
p.addBlock(&ast.TableBodyData{}, nil)
|
||||
p.addBlock(&ast.TableBody{}, nil)
|
||||
|
||||
for i < len(data) {
|
||||
pipes, rowStart := 0, i
|
||||
@ -920,14 +922,14 @@ func (p *Parser) tableHeader(data []byte) (size int, columns []ast.CellAlignFlag
|
||||
return
|
||||
}
|
||||
|
||||
p.addBlock(&ast.TableHeadData{}, nil)
|
||||
p.addBlock(&ast.TableHead{}, nil)
|
||||
p.tableRow(header, columns, true)
|
||||
size = skipCharN(data, i, '\n', 1)
|
||||
return
|
||||
}
|
||||
|
||||
func (p *Parser) tableRow(data []byte, columns []ast.CellAlignFlags, header bool) {
|
||||
p.addBlock(&ast.TableRowData{}, nil)
|
||||
p.addBlock(&ast.TableRow{}, nil)
|
||||
i, col := 0, 0
|
||||
|
||||
if data[i] == '|' && !isBackslashEscaped(data, i) {
|
||||
@ -954,7 +956,7 @@ func (p *Parser) tableRow(data []byte, columns []ast.CellAlignFlags, header bool
|
||||
cellEnd--
|
||||
}
|
||||
|
||||
d := &ast.TableCellData{
|
||||
d := &ast.TableCell{
|
||||
IsHeader: header,
|
||||
Align: columns[col],
|
||||
}
|
||||
@ -963,7 +965,7 @@ func (p *Parser) tableRow(data []byte, columns []ast.CellAlignFlags, header bool
|
||||
|
||||
// pad it out with empty columns to get the right number
|
||||
for ; col < len(columns); col++ {
|
||||
d := &ast.TableCellData{
|
||||
d := &ast.TableCell{
|
||||
IsHeader: header,
|
||||
Align: columns[col],
|
||||
}
|
||||
@ -1002,7 +1004,7 @@ func (p *Parser) terminateBlockquote(data []byte, beg, end int) bool {
|
||||
|
||||
// parse a blockquote fragment
|
||||
func (p *Parser) quote(data []byte) int {
|
||||
block := p.addBlock(&ast.BlockQuoteData{}, nil)
|
||||
block := p.addBlock(&ast.BlockQuote{}, nil)
|
||||
var raw bytes.Buffer
|
||||
beg, end := 0, 0
|
||||
for beg < len(data) {
|
||||
@ -1085,7 +1087,7 @@ func (p *Parser) code(data []byte) int {
|
||||
|
||||
work.WriteByte('\n')
|
||||
|
||||
d := &ast.CodeBlockData{
|
||||
d := &ast.CodeBlock{
|
||||
IsFenced: false,
|
||||
}
|
||||
block := p.addBlock(d, work.Bytes()) // TODO: get rid of temp buffer
|
||||
@ -1148,7 +1150,7 @@ func (p *Parser) dliPrefix(data []byte) int {
|
||||
func (p *Parser) list(data []byte, flags ast.ListType) int {
|
||||
i := 0
|
||||
flags |= ast.ListItemBeginningOfList
|
||||
d := &ast.ListData{
|
||||
d := &ast.List{
|
||||
ListFlags: flags,
|
||||
Tight: true,
|
||||
}
|
||||
@ -1166,7 +1168,7 @@ func (p *Parser) list(data []byte, flags ast.ListType) int {
|
||||
flags &= ^ast.ListItemBeginningOfList
|
||||
}
|
||||
|
||||
above := block.Parent
|
||||
above := block.GetParent()
|
||||
finalizeList(block, d)
|
||||
p.tip = above
|
||||
return i
|
||||
@ -1174,14 +1176,14 @@ func (p *Parser) list(data []byte, flags ast.ListType) int {
|
||||
|
||||
// Returns true if block ends with a blank line, descending if needed
|
||||
// into lists and sublists.
|
||||
func endsWithBlankLine(block *ast.Node) bool {
|
||||
func endsWithBlankLine(block ast.Node) bool {
|
||||
// TODO: figure this out. Always false now.
|
||||
for block != nil {
|
||||
//if block.lastLineBlank {
|
||||
//return true
|
||||
//}
|
||||
switch block.Data.(type) {
|
||||
case *ast.ListData, *ast.ListItemData:
|
||||
switch block.(type) {
|
||||
case *ast.List, *ast.ListItem:
|
||||
block = block.LastChild()
|
||||
default:
|
||||
return false
|
||||
@ -1190,8 +1192,8 @@ func endsWithBlankLine(block *ast.Node) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func finalizeList(block *ast.Node, listData *ast.ListData) {
|
||||
items := block.Parent.Children
|
||||
func finalizeList(block ast.Node, listData *ast.List) {
|
||||
items := block.GetParent().GetChildren()
|
||||
lastItemIdx := len(items) - 1
|
||||
for i, item := range items {
|
||||
isLastItem := i == lastItemIdx
|
||||
@ -1202,7 +1204,7 @@ func finalizeList(block *ast.Node, listData *ast.ListData) {
|
||||
}
|
||||
// recurse into children of list item, to see if there are spaces
|
||||
// between any of them:
|
||||
subItems := item.Parent.Children
|
||||
subItems := item.GetParent().GetChildren()
|
||||
lastSubItemIdx := len(subItems) - 1
|
||||
for i, subItem := range subItems {
|
||||
isLastSubItem := i == lastSubItemIdx
|
||||
@ -1376,7 +1378,7 @@ gatherlines:
|
||||
|
||||
rawBytes := raw.Bytes()
|
||||
|
||||
d := &ast.ListItemData{
|
||||
d := &ast.ListItem{
|
||||
ListFlags: *flags,
|
||||
Tight: false,
|
||||
BulletChar: bulletChar,
|
||||
@ -1395,13 +1397,12 @@ gatherlines:
|
||||
}
|
||||
} else {
|
||||
// intermediate render of inline item
|
||||
child := p.addChild(&ast.Paragraph{}, 0)
|
||||
if sublist > 0 {
|
||||
child := p.addChild(&ast.ParagraphData{}, 0)
|
||||
child.Content = rawBytes[:sublist]
|
||||
child.AsTreeNode().Content = rawBytes[:sublist]
|
||||
p.block(rawBytes[sublist:])
|
||||
} else {
|
||||
child := p.addChild(&ast.ParagraphData{}, 0)
|
||||
child.Content = rawBytes
|
||||
child.AsTreeNode().Content = rawBytes
|
||||
}
|
||||
}
|
||||
return line
|
||||
@ -1427,7 +1428,7 @@ func (p *Parser) renderParagraph(data []byte) {
|
||||
end--
|
||||
}
|
||||
|
||||
p.addBlock(&ast.ParagraphData{}, data[beg:end])
|
||||
p.addBlock(&ast.Paragraph{}, data[beg:end])
|
||||
}
|
||||
|
||||
func (p *Parser) paragraph(data []byte) int {
|
||||
@ -1487,7 +1488,7 @@ func (p *Parser) paragraph(data []byte) int {
|
||||
id = SanitizeAnchorName(string(data[prev:eol]))
|
||||
}
|
||||
|
||||
d := &ast.HeadingData{
|
||||
d := &ast.Heading{
|
||||
Level: level,
|
||||
HeadingID: id,
|
||||
}
|
||||
|
100
parser/inline.go
100
parser/inline.go
@ -23,7 +23,7 @@ var (
|
||||
// data is the complete block being rendered
|
||||
// offset is the number of valid chars before the current cursor
|
||||
|
||||
func (p *Parser) inline(currBlock *ast.Node, data []byte) {
|
||||
func (p *Parser) inline(currBlock ast.Node, data []byte) {
|
||||
// handlers might call us recursively: enforce a maximum depth
|
||||
if p.nesting >= p.maxNesting || len(data) == 0 {
|
||||
return
|
||||
@ -38,9 +38,9 @@ func (p *Parser) inline(currBlock *ast.Node, data []byte) {
|
||||
end++
|
||||
} else {
|
||||
// Copy inactive chars into the output.
|
||||
currBlock.AppendChild(newTextNode(data[beg:end]))
|
||||
ast.AppendChild(currBlock, newTextNode(data[beg:end]))
|
||||
if node != nil {
|
||||
currBlock.AppendChild(node)
|
||||
ast.AppendChild(currBlock, node)
|
||||
}
|
||||
// Skip past whatever the callback used.
|
||||
beg = end + consumed
|
||||
@ -54,13 +54,13 @@ func (p *Parser) inline(currBlock *ast.Node, data []byte) {
|
||||
if data[end-1] == '\n' {
|
||||
end--
|
||||
}
|
||||
currBlock.AppendChild(newTextNode(data[beg:end]))
|
||||
ast.AppendChild(currBlock, newTextNode(data[beg:end]))
|
||||
}
|
||||
p.nesting--
|
||||
}
|
||||
|
||||
// single and double emphasis parsing
|
||||
func emphasis(p *Parser, data []byte, offset int) (int, *ast.Node) {
|
||||
func emphasis(p *Parser, data []byte, offset int) (int, ast.Node) {
|
||||
data = data[offset:]
|
||||
c := data[0]
|
||||
|
||||
@ -105,7 +105,7 @@ func emphasis(p *Parser, data []byte, offset int) (int, *ast.Node) {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
func codeSpan(p *Parser, data []byte, offset int) (int, *ast.Node) {
|
||||
func codeSpan(p *Parser, data []byte, offset int) (int, ast.Node) {
|
||||
data = data[offset:]
|
||||
|
||||
// count the number of backticks in the delimiter
|
||||
@ -139,7 +139,7 @@ func codeSpan(p *Parser, data []byte, offset int) (int, *ast.Node) {
|
||||
|
||||
// render the code span
|
||||
if fBegin != fEnd {
|
||||
code := ast.NewNode(&ast.CodeData{})
|
||||
code := &ast.Code{}
|
||||
code.Literal = data[fBegin:fEnd]
|
||||
return end, code
|
||||
}
|
||||
@ -148,13 +148,13 @@ func codeSpan(p *Parser, data []byte, offset int) (int, *ast.Node) {
|
||||
}
|
||||
|
||||
// newline preceded by two spaces becomes <br>
|
||||
func maybeLineBreak(p *Parser, data []byte, offset int) (int, *ast.Node) {
|
||||
func maybeLineBreak(p *Parser, data []byte, offset int) (int, ast.Node) {
|
||||
origOffset := offset
|
||||
offset = skipChar(data, offset, ' ')
|
||||
|
||||
if offset < len(data) && data[offset] == '\n' {
|
||||
if offset-origOffset >= 2 {
|
||||
return offset - origOffset + 1, ast.NewNode(&ast.HardbreakData{})
|
||||
return offset - origOffset + 1, &ast.Hardbreak{}
|
||||
}
|
||||
return offset - origOffset, nil
|
||||
}
|
||||
@ -162,9 +162,9 @@ func maybeLineBreak(p *Parser, data []byte, offset int) (int, *ast.Node) {
|
||||
}
|
||||
|
||||
// newline without two spaces works when HardLineBreak is enabled
|
||||
func lineBreak(p *Parser, data []byte, offset int) (int, *ast.Node) {
|
||||
func lineBreak(p *Parser, data []byte, offset int) (int, ast.Node) {
|
||||
if p.extensions&HardLineBreak != 0 {
|
||||
return 1, ast.NewNode(&ast.HardbreakData{})
|
||||
return 1, &ast.Hardbreak{}
|
||||
}
|
||||
return 0, nil
|
||||
}
|
||||
@ -185,14 +185,14 @@ func isReferenceStyleLink(data []byte, pos int, t linkType) bool {
|
||||
return pos < len(data)-1 && data[pos] == '[' && data[pos+1] != '^'
|
||||
}
|
||||
|
||||
func maybeImage(p *Parser, data []byte, offset int) (int, *ast.Node) {
|
||||
func maybeImage(p *Parser, data []byte, offset int) (int, ast.Node) {
|
||||
if offset < len(data)-1 && data[offset+1] == '[' {
|
||||
return link(p, data, offset)
|
||||
}
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
func maybeInlineFootnote(p *Parser, data []byte, offset int) (int, *ast.Node) {
|
||||
func maybeInlineFootnote(p *Parser, data []byte, offset int) (int, ast.Node) {
|
||||
if offset < len(data)-1 && data[offset+1] == '[' {
|
||||
return link(p, data, offset)
|
||||
}
|
||||
@ -200,7 +200,7 @@ func maybeInlineFootnote(p *Parser, data []byte, offset int) (int, *ast.Node) {
|
||||
}
|
||||
|
||||
// '[': parse a link or an image or a footnote
|
||||
func link(p *Parser, data []byte, offset int) (int, *ast.Node) {
|
||||
func link(p *Parser, data []byte, offset int) (int, ast.Node) {
|
||||
// no links allowed inside regular links, footnote, and deferred footnotes
|
||||
if p.insideLink && (offset > 0 && data[offset-1] == '[' || len(data)-1 > offset && data[offset+1] == '^') {
|
||||
return 0, nil
|
||||
@ -269,7 +269,7 @@ func link(p *Parser, data []byte, offset int) (int, *ast.Node) {
|
||||
|
||||
txtE := i
|
||||
i++
|
||||
var footnoteNode *ast.Node
|
||||
var footnoteNode ast.Node
|
||||
|
||||
// skip any amount of whitespace or newline
|
||||
// (this is much more lax than original markdown syntax)
|
||||
@ -444,7 +444,7 @@ func link(p *Parser, data []byte, offset int) (int, *ast.Node) {
|
||||
}
|
||||
}
|
||||
|
||||
footnoteNode = ast.NewNode(&ast.ListItemData{})
|
||||
footnoteNode = &ast.ListItem{}
|
||||
if t == linkInlineFootnote {
|
||||
// create a new reference
|
||||
noteID = len(p.notes) + 1
|
||||
@ -512,16 +512,15 @@ func link(p *Parser, data []byte, offset int) (int, *ast.Node) {
|
||||
}
|
||||
|
||||
// call the relevant rendering function
|
||||
var linkNode *ast.Node
|
||||
var linkNode ast.Node
|
||||
switch t {
|
||||
case linkNormal:
|
||||
d := &ast.LinkData{
|
||||
linkNode = &ast.Link{
|
||||
Destination: normalizeURI(uLink),
|
||||
Title: title,
|
||||
}
|
||||
linkNode = ast.NewNode(d)
|
||||
if len(altContent) > 0 {
|
||||
linkNode.AppendChild(newTextNode(altContent))
|
||||
ast.AppendChild(linkNode, newTextNode(altContent))
|
||||
} else {
|
||||
// links cannot contain other links, so turn off link parsing
|
||||
// temporarily and recurse
|
||||
@ -532,22 +531,20 @@ func link(p *Parser, data []byte, offset int) (int, *ast.Node) {
|
||||
}
|
||||
|
||||
case linkImg:
|
||||
d := &ast.ImageData{
|
||||
linkNode = &ast.Image{
|
||||
Destination: uLink,
|
||||
Title: title,
|
||||
}
|
||||
linkNode = ast.NewNode(d)
|
||||
linkNode.AppendChild(newTextNode(data[1:txtE]))
|
||||
ast.AppendChild(linkNode, newTextNode(data[1:txtE]))
|
||||
i++
|
||||
|
||||
case linkInlineFootnote, linkDeferredFootnote:
|
||||
d := &ast.LinkData{
|
||||
linkNode = &ast.Link{
|
||||
Destination: link,
|
||||
Title: title,
|
||||
NoteID: noteID,
|
||||
Footnote: footnoteNode,
|
||||
}
|
||||
linkNode = ast.NewNode(d)
|
||||
if t == linkInlineFootnote {
|
||||
i++
|
||||
}
|
||||
@ -599,7 +596,7 @@ const (
|
||||
)
|
||||
|
||||
// '<' when tags or autolinks are allowed
|
||||
func leftAngle(p *Parser, data []byte, offset int) (int, *ast.Node) {
|
||||
func leftAngle(p *Parser, data []byte, offset int) (int, ast.Node) {
|
||||
data = data[offset:]
|
||||
altype, end := tagLength(data)
|
||||
if size := p.inlineHTMLComment(data); size > 0 {
|
||||
@ -611,18 +608,17 @@ func leftAngle(p *Parser, data []byte, offset int) (int, *ast.Node) {
|
||||
unescapeText(&uLink, data[1:end+1-2])
|
||||
if uLink.Len() > 0 {
|
||||
link := uLink.Bytes()
|
||||
d := &ast.LinkData{
|
||||
node := &ast.Link{
|
||||
Destination: link,
|
||||
}
|
||||
node := ast.NewNode(d)
|
||||
if altype == emailAutolink {
|
||||
d.Destination = append([]byte("mailto:"), link...)
|
||||
node.Destination = append([]byte("mailto:"), link...)
|
||||
}
|
||||
node.AppendChild(newTextNode(stripMailto(link)))
|
||||
ast.AppendChild(node, newTextNode(stripMailto(link)))
|
||||
return end, node
|
||||
}
|
||||
} else {
|
||||
htmlTag := ast.NewNode(&ast.HTMLSpanData{})
|
||||
htmlTag := &ast.HTMLSpan{}
|
||||
htmlTag.Literal = data[:end]
|
||||
return end, htmlTag
|
||||
}
|
||||
@ -634,12 +630,12 @@ func leftAngle(p *Parser, data []byte, offset int) (int, *ast.Node) {
|
||||
// '\\' backslash escape
|
||||
var escapeChars = []byte("\\`*_{}[]()#+-.!:|&<>~")
|
||||
|
||||
func escape(p *Parser, data []byte, offset int) (int, *ast.Node) {
|
||||
func escape(p *Parser, data []byte, offset int) (int, ast.Node) {
|
||||
data = data[offset:]
|
||||
|
||||
if len(data) > 1 {
|
||||
if p.extensions&BackslashLineBreak != 0 && data[1] == '\n' {
|
||||
return 2, ast.NewNode(&ast.HardbreakData{})
|
||||
return 2, &ast.Hardbreak{}
|
||||
}
|
||||
if bytes.IndexByte(escapeChars, data[1]) < 0 {
|
||||
return 0, nil
|
||||
@ -674,7 +670,7 @@ func unescapeText(ob *bytes.Buffer, src []byte) {
|
||||
|
||||
// '&' escaped when it doesn't belong to an entity
|
||||
// valid entities are assumed to be anything matching &#?[A-Za-z0-9]+;
|
||||
func entity(p *Parser, data []byte, offset int) (int, *ast.Node) {
|
||||
func entity(p *Parser, data []byte, offset int) (int, ast.Node) {
|
||||
data = data[offset:]
|
||||
|
||||
end := skipCharN(data, 1, '#', 1)
|
||||
@ -729,7 +725,7 @@ var protocolPrefixes = [][]byte{
|
||||
|
||||
const shortestPrefix = 6 // len("ftp://"), the shortest of the above
|
||||
|
||||
func maybeAutoLink(p *Parser, data []byte, offset int) (int, *ast.Node) {
|
||||
func maybeAutoLink(p *Parser, data []byte, offset int) (int, ast.Node) {
|
||||
// quick check to rule out most false hits
|
||||
if p.insideLink || len(data) < offset+shortestPrefix {
|
||||
return 0, nil
|
||||
@ -746,7 +742,7 @@ func maybeAutoLink(p *Parser, data []byte, offset int) (int, *ast.Node) {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
func autoLink(p *Parser, data []byte, offset int) (int, *ast.Node) {
|
||||
func autoLink(p *Parser, data []byte, offset int) (int, ast.Node) {
|
||||
// Now a more expensive check to see if we're not inside an anchor element
|
||||
anchorStart := offset
|
||||
offsetFromAnchor := 0
|
||||
@ -757,7 +753,7 @@ func autoLink(p *Parser, data []byte, offset int) (int, *ast.Node) {
|
||||
|
||||
anchorStr := anchorRe.Find(data[anchorStart:])
|
||||
if anchorStr != nil {
|
||||
anchorClose := ast.NewNode(&ast.HTMLSpanData{})
|
||||
anchorClose := &ast.HTMLSpan{}
|
||||
anchorClose.Literal = anchorStr[offsetFromAnchor:]
|
||||
return len(anchorStr) - offsetFromAnchor, anchorClose
|
||||
}
|
||||
@ -856,11 +852,10 @@ func autoLink(p *Parser, data []byte, offset int) (int, *ast.Node) {
|
||||
unescapeText(&uLink, data[:linkEnd])
|
||||
|
||||
if uLink.Len() > 0 {
|
||||
d := &ast.LinkData{
|
||||
node := &ast.Link{
|
||||
Destination: uLink.Bytes(),
|
||||
}
|
||||
node := ast.NewNode(d)
|
||||
node.AppendChild(newTextNode(uLink.Bytes()))
|
||||
ast.AppendChild(node, newTextNode(uLink.Bytes()))
|
||||
return linkEnd, node
|
||||
}
|
||||
|
||||
@ -1078,7 +1073,7 @@ func helperFindEmphChar(data []byte, c byte) int {
|
||||
return 0
|
||||
}
|
||||
|
||||
func helperEmphasis(p *Parser, data []byte, c byte) (int, *ast.Node) {
|
||||
func helperEmphasis(p *Parser, data []byte, c byte) (int, ast.Node) {
|
||||
i := 0
|
||||
|
||||
// skip one symbol if coming from emph3
|
||||
@ -1109,7 +1104,7 @@ func helperEmphasis(p *Parser, data []byte, c byte) (int, *ast.Node) {
|
||||
}
|
||||
}
|
||||
|
||||
emph := ast.NewNode(&ast.EmphData{})
|
||||
emph := &ast.Emph{}
|
||||
p.inline(emph, data[:i])
|
||||
return i + 1, emph
|
||||
}
|
||||
@ -1118,7 +1113,7 @@ func helperEmphasis(p *Parser, data []byte, c byte) (int, *ast.Node) {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
func helperDoubleEmphasis(p *Parser, data []byte, c byte) (int, *ast.Node) {
|
||||
func helperDoubleEmphasis(p *Parser, data []byte, c byte) (int, ast.Node) {
|
||||
i := 0
|
||||
|
||||
for i < len(data) {
|
||||
@ -1129,11 +1124,10 @@ func helperDoubleEmphasis(p *Parser, data []byte, c byte) (int, *ast.Node) {
|
||||
i += length
|
||||
|
||||
if i+1 < len(data) && data[i] == c && data[i+1] == c && i > 0 && !isSpace(data[i-1]) {
|
||||
var nodeData ast.NodeData = &ast.StrongData{}
|
||||
var node ast.Node = &ast.Strong{}
|
||||
if c == '~' {
|
||||
nodeData = &ast.DelData{}
|
||||
node = &ast.Del{}
|
||||
}
|
||||
node := ast.NewNode(nodeData)
|
||||
p.inline(node, data[:i])
|
||||
return i + 2, node
|
||||
}
|
||||
@ -1142,7 +1136,7 @@ func helperDoubleEmphasis(p *Parser, data []byte, c byte) (int, *ast.Node) {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
func helperTripleEmphasis(p *Parser, data []byte, offset int, c byte) (int, *ast.Node) {
|
||||
func helperTripleEmphasis(p *Parser, data []byte, offset int, c byte) (int, ast.Node) {
|
||||
i := 0
|
||||
origData := data
|
||||
data = data[offset:]
|
||||
@ -1162,9 +1156,9 @@ func helperTripleEmphasis(p *Parser, data []byte, offset int, c byte) (int, *ast
|
||||
switch {
|
||||
case i+2 < len(data) && data[i+1] == c && data[i+2] == c:
|
||||
// triple symbol found
|
||||
strong := ast.NewNode(&ast.StrongData{})
|
||||
em := ast.NewNode(&ast.EmphData{})
|
||||
strong.AppendChild(em)
|
||||
strong := &ast.Strong{}
|
||||
em := &ast.Emph{}
|
||||
ast.AppendChild(strong, em)
|
||||
p.inline(em, data[:i])
|
||||
return i + 3, strong
|
||||
case i+1 < len(data) && data[i+1] == c:
|
||||
@ -1186,10 +1180,8 @@ func helperTripleEmphasis(p *Parser, data []byte, offset int, c byte) (int, *ast
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
func newTextNode(s []byte) *ast.Node {
|
||||
node := ast.NewNode(&ast.TextData{})
|
||||
node.Literal = s
|
||||
return node
|
||||
func newTextNode(d []byte) *ast.Text {
|
||||
return &ast.Text{ast.TreeNode{Literal: d}}
|
||||
}
|
||||
|
||||
func normalizeURI(s []byte) []byte {
|
||||
|
@ -46,7 +46,7 @@ const (
|
||||
)
|
||||
|
||||
// for each character that triggers a response when parsing inline data.
|
||||
type inlineParser func(p *Parser, data []byte, offset int) (int, *ast.Node)
|
||||
type inlineParser func(p *Parser, data []byte, offset int) (int, ast.Node)
|
||||
|
||||
// ReferenceOverrideFunc is expected to be called with a reference string and
|
||||
// return either a valid Reference type that the reference string maps to or
|
||||
@ -75,7 +75,7 @@ type Parser struct {
|
||||
ReferenceOverride ReferenceOverrideFunc
|
||||
|
||||
// after parsing, this is AST root of parsed markdown text
|
||||
Doc *ast.Node
|
||||
Doc ast.Node
|
||||
|
||||
extensions Extensions
|
||||
|
||||
@ -90,9 +90,9 @@ type Parser struct {
|
||||
// in notes. Slice is nil if footnotes not enabled.
|
||||
notes []*reference
|
||||
|
||||
tip *ast.Node // = doc
|
||||
oldTip *ast.Node
|
||||
lastMatchedContainer *ast.Node // = doc
|
||||
tip ast.Node // = doc
|
||||
oldTip ast.Node
|
||||
lastMatchedContainer ast.Node // = doc
|
||||
allClosed bool
|
||||
}
|
||||
|
||||
@ -109,7 +109,7 @@ func NewWithExtensions(extension Extensions) *Parser {
|
||||
refs: make(map[string]*reference),
|
||||
maxNesting: 16,
|
||||
insideLink: false,
|
||||
Doc: ast.NewNode(&ast.DocumentData{}),
|
||||
Doc: &ast.Document{},
|
||||
extensions: extension,
|
||||
allClosed: true,
|
||||
}
|
||||
@ -165,41 +165,43 @@ func (p *Parser) getRef(refid string) (ref *reference, found bool) {
|
||||
return ref, found
|
||||
}
|
||||
|
||||
func (p *Parser) finalize(block *ast.Node) {
|
||||
above := block.Parent
|
||||
p.tip = above
|
||||
func (p *Parser) finalize(block ast.Node) {
|
||||
p.tip = block.GetParent()
|
||||
}
|
||||
|
||||
func (p *Parser) addChild(d ast.NodeData, offset uint32) *ast.Node {
|
||||
return p.addExistingChild(ast.NewNode(d), offset)
|
||||
func (p *Parser) addChild(n ast.Node, offset uint32) ast.Node {
|
||||
return p.addExistingChild(n, offset)
|
||||
}
|
||||
|
||||
func canNodeContain(n *ast.Node, v ast.NodeData) bool {
|
||||
switch n.Data.(type) {
|
||||
case *ast.ListData:
|
||||
func canNodeContain(n ast.Node, v ast.Node) bool {
|
||||
switch n.(type) {
|
||||
case *ast.List:
|
||||
return isListItemData(v)
|
||||
case *ast.DocumentData, *ast.BlockQuoteData, *ast.ListItemData:
|
||||
case *ast.Document, *ast.BlockQuote, *ast.ListItem:
|
||||
return !isListItemData(v)
|
||||
case *ast.TableData:
|
||||
case *ast.Table:
|
||||
switch v.(type) {
|
||||
case *ast.TableHeadData, *ast.TableBodyData:
|
||||
case *ast.TableHead, *ast.TableBody:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
case *ast.TableHeadData, *ast.TableBodyData:
|
||||
case *ast.TableHead, *ast.TableBody:
|
||||
return isTableRowData(v)
|
||||
case *ast.TableRowData:
|
||||
case *ast.TableRow:
|
||||
return isTableCellData(v)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (p *Parser) addExistingChild(node *ast.Node, offset uint32) *ast.Node {
|
||||
for !canNodeContain(p.tip, node.Data) {
|
||||
func (p *Parser) addExistingChild(node ast.Node, offset uint32) ast.Node {
|
||||
if _, ok := node.(*ast.TreeNode); ok {
|
||||
panic(fmt.Sprintf("adding %v", node))
|
||||
}
|
||||
for !canNodeContain(p.tip, node) {
|
||||
p.finalize(p.tip)
|
||||
}
|
||||
p.tip.AppendChild(node)
|
||||
ast.AppendChild(p.tip, node)
|
||||
p.tip = node
|
||||
return node
|
||||
}
|
||||
@ -207,7 +209,7 @@ func (p *Parser) addExistingChild(node *ast.Node, offset uint32) *ast.Node {
|
||||
func (p *Parser) closeUnmatchedBlocks() {
|
||||
if !p.allClosed {
|
||||
for p.oldTip != p.lastMatchedContainer {
|
||||
parent := p.oldTip.Parent
|
||||
parent := p.oldTip.GetParent()
|
||||
p.finalize(p.oldTip)
|
||||
p.oldTip = parent
|
||||
}
|
||||
@ -232,18 +234,18 @@ type Reference struct {
|
||||
// tree can then be rendered with a default or custom renderer, or
|
||||
// analyzed/transformed by the caller to whatever non-standard needs they have.
|
||||
// The return value is the root node of the syntax tree.
|
||||
func (p *Parser) Parse(input []byte) *ast.Node {
|
||||
func (p *Parser) Parse(input []byte) ast.Node {
|
||||
p.block(input)
|
||||
// Walk the tree and finish up some of unfinished blocks
|
||||
for p.tip != nil {
|
||||
p.finalize(p.tip)
|
||||
}
|
||||
// Walk the tree again and process inline markdown in each block
|
||||
p.Doc.WalkFunc(func(node *ast.Node, entering bool) ast.WalkStatus {
|
||||
switch node.Data.(type) {
|
||||
case *ast.ParagraphData, *ast.HeadingData, *ast.TableCellData:
|
||||
p.inline(node, node.Content)
|
||||
node.Content = nil
|
||||
ast.WalkFunc(p.Doc, func(node ast.Node, entering bool) ast.WalkStatus {
|
||||
switch node.(type) {
|
||||
case *ast.Paragraph, *ast.Heading, *ast.TableCell:
|
||||
p.inline(node, node.AsTreeNode().Content)
|
||||
node.AsTreeNode().Content = nil
|
||||
}
|
||||
return ast.GoToNext
|
||||
})
|
||||
@ -256,7 +258,7 @@ func (p *Parser) parseRefsToAST() {
|
||||
return
|
||||
}
|
||||
p.tip = p.Doc
|
||||
d := &ast.ListData{
|
||||
d := &ast.List{
|
||||
IsFootnotesList: true,
|
||||
ListFlags: ast.ListTypeOrdered,
|
||||
}
|
||||
@ -270,7 +272,7 @@ func (p *Parser) parseRefsToAST() {
|
||||
ref := p.notes[i]
|
||||
p.addExistingChild(ref.footnote, 0)
|
||||
block := ref.footnote
|
||||
blockData := block.Data.(*ast.ListItemData)
|
||||
blockData := block.(*ast.ListItem)
|
||||
blockData.ListFlags = flags | ast.ListTypeOrdered
|
||||
blockData.RefLink = ref.link
|
||||
if ref.hasBlock {
|
||||
@ -281,14 +283,14 @@ func (p *Parser) parseRefsToAST() {
|
||||
}
|
||||
flags &^= ast.ListItemBeginningOfList | ast.ListItemContainsBlock
|
||||
}
|
||||
above := block.Parent
|
||||
above := block.GetParent()
|
||||
finalizeList(block, d)
|
||||
p.tip = above
|
||||
block.WalkFunc(func(node *ast.Node, entering bool) ast.WalkStatus {
|
||||
switch node.Data.(type) {
|
||||
case *ast.ParagraphData, *ast.HeadingData:
|
||||
p.inline(node, node.Content)
|
||||
node.Content = nil
|
||||
ast.WalkFunc(block, func(node ast.Node, entering bool) ast.WalkStatus {
|
||||
switch node.(type) {
|
||||
case *ast.Paragraph, *ast.Heading:
|
||||
p.inline(node, node.AsTreeNode().Content)
|
||||
node.AsTreeNode().Content = nil
|
||||
}
|
||||
return ast.GoToNext
|
||||
})
|
||||
@ -365,7 +367,7 @@ type reference struct {
|
||||
title []byte
|
||||
noteID int // 0 if not a footnote ref
|
||||
hasBlock bool
|
||||
footnote *ast.Node // a link to the Item node within a list of footnotes
|
||||
footnote ast.Node // a link to the Item node within a list of footnotes
|
||||
|
||||
text []byte // only gets populated by refOverride feature with Reference.Text
|
||||
}
|
||||
@ -770,17 +772,17 @@ func slugify(in []byte) []byte {
|
||||
return out[a : b+1]
|
||||
}
|
||||
|
||||
func isTableRowData(d ast.NodeData) bool {
|
||||
_, ok := d.(*ast.TableRowData)
|
||||
func isTableRowData(d ast.Node) bool {
|
||||
_, ok := d.(*ast.TableRow)
|
||||
return ok
|
||||
}
|
||||
|
||||
func isTableCellData(d ast.NodeData) bool {
|
||||
_, ok := d.(*ast.TableCellData)
|
||||
func isTableCellData(d ast.Node) bool {
|
||||
_, ok := d.(*ast.TableCell)
|
||||
return ok
|
||||
}
|
||||
|
||||
func isListItemData(d ast.NodeData) bool {
|
||||
_, ok := d.(*ast.ListItemData)
|
||||
func isListItemData(d ast.Node) bool {
|
||||
_, ok := d.(*ast.ListItem)
|
||||
return ok
|
||||
}
|
||||
|
@ -157,3 +157,33 @@ PASS
|
||||
ok github.com/gomarkdown/markdown 43.477s
|
||||
```
|
||||
It's slower (but opens up possibilities for further improvements).
|
||||
|
||||
After refactoring to make ast.Node a top-level thing.
|
||||
```
|
||||
BenchmarkEscapeHTML-8 2000000 829 ns/op 0 B/op 0 allocs/op
|
||||
BenchmarkSmartDoubleQuotes-8 300000 3998 ns/op 6192 B/op 31 allocs/op
|
||||
BenchmarkReferenceAmps-8 50000 27389 ns/op 15480 B/op 153 allocs/op
|
||||
BenchmarkReferenceAutoLinks-8 50000 23106 ns/op 14656 B/op 137 allocs/op
|
||||
BenchmarkReferenceBackslashEscapes-8 10000 112435 ns/op 36696 B/op 315 allocs/op
|
||||
BenchmarkReferenceBlockquotesWithCodeBlocks-8 200000 9227 ns/op 7856 B/op 46 allocs/op
|
||||
BenchmarkReferenceCodeBlocks-8 200000 10469 ns/op 9248 B/op 54 allocs/op
|
||||
BenchmarkReferenceCodeSpans-8 200000 10522 ns/op 8368 B/op 54 allocs/op
|
||||
BenchmarkReferenceHardWrappedPara-8 200000 6354 ns/op 6784 B/op 34 allocs/op
|
||||
BenchmarkReferenceHorizontalRules-8 50000 32393 ns/op 13952 B/op 87 allocs/op
|
||||
BenchmarkReferenceInlineHTMLAdvances-8 200000 6894 ns/op 7238 B/op 40 allocs/op
|
||||
BenchmarkReferenceInlineHTMLSimple-8 50000 32942 ns/op 15864 B/op 110 allocs/op
|
||||
BenchmarkReferenceInlineHTMLComments-8 200000 8181 ns/op 7776 B/op 44 allocs/op
|
||||
BenchmarkReferenceLinksInline-8 100000 21679 ns/op 14400 B/op 148 allocs/op
|
||||
BenchmarkReferenceLinksReference-8 20000 83928 ns/op 36688 B/op 473 allocs/op
|
||||
BenchmarkReferenceLinksShortcut-8 100000 22053 ns/op 13872 B/op 153 allocs/op
|
||||
BenchmarkReferenceLiterQuotesInTitles-8 100000 10784 ns/op 9296 B/op 81 allocs/op
|
||||
BenchmarkReferenceMarkdownBasics-8 5000 237097 ns/op 87760 B/op 480 allocs/op
|
||||
BenchmarkReferenceMarkdownSyntax-8 1000 1465402 ns/op 300769 B/op 1896 allocs/op
|
||||
BenchmarkReferenceNestedBlockquotes-8 200000 7461 ns/op 7152 B/op 45 allocs/op
|
||||
BenchmarkReferenceOrderedAndUnorderedLists-8 5000 212256 ns/op 53724 B/op 553 allocs/op
|
||||
BenchmarkReferenceStrongAndEm-8 100000 13018 ns/op 9264 B/op 72 allocs/op
|
||||
BenchmarkReferenceTabs-8 100000 15005 ns/op 10752 B/op 71 allocs/op
|
||||
BenchmarkReferenceTidyness-8 200000 10308 ns/op 8292 B/op 58 allocs/op
|
||||
PASS
|
||||
ok github.com/gomarkdown/markdown 42.176s
|
||||
```
|
||||
|
Loading…
x
Reference in New Issue
Block a user