Платформа ЦРНП "Мирокод" для разработки проектов
https://git.mirocod.ru
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
354 lines
8.5 KiB
354 lines
8.5 KiB
package blackfriday |
|
|
|
import ( |
|
"bytes" |
|
"fmt" |
|
) |
|
|
|
// NodeType specifies a type of a single node of a syntax tree. Usually one |
|
// node (and its type) corresponds to a single markdown feature, e.g. emphasis |
|
// or code block. |
|
type NodeType int |
|
|
|
// Constants for identifying different types of nodes. See NodeType. |
|
const ( |
|
Document NodeType = iota |
|
BlockQuote |
|
List |
|
Item |
|
Paragraph |
|
Heading |
|
HorizontalRule |
|
Emph |
|
Strong |
|
Del |
|
Link |
|
Image |
|
Text |
|
HTMLBlock |
|
CodeBlock |
|
Softbreak |
|
Hardbreak |
|
Code |
|
HTMLSpan |
|
Table |
|
TableCell |
|
TableHead |
|
TableBody |
|
TableRow |
|
) |
|
|
|
var nodeTypeNames = []string{ |
|
Document: "Document", |
|
BlockQuote: "BlockQuote", |
|
List: "List", |
|
Item: "Item", |
|
Paragraph: "Paragraph", |
|
Heading: "Heading", |
|
HorizontalRule: "HorizontalRule", |
|
Emph: "Emph", |
|
Strong: "Strong", |
|
Del: "Del", |
|
Link: "Link", |
|
Image: "Image", |
|
Text: "Text", |
|
HTMLBlock: "HTMLBlock", |
|
CodeBlock: "CodeBlock", |
|
Softbreak: "Softbreak", |
|
Hardbreak: "Hardbreak", |
|
Code: "Code", |
|
HTMLSpan: "HTMLSpan", |
|
Table: "Table", |
|
TableCell: "TableCell", |
|
TableHead: "TableHead", |
|
TableBody: "TableBody", |
|
TableRow: "TableRow", |
|
} |
|
|
|
func (t NodeType) String() string { |
|
return nodeTypeNames[t] |
|
} |
|
|
|
// ListData contains fields relevant to a List and Item node type. |
|
type ListData struct { |
|
ListFlags ListType |
|
Tight bool // Skip <p>s around list item data if true |
|
BulletChar byte // '*', '+' or '-' in bullet lists |
|
Delimiter byte // '.' or ')' after the number in ordered lists |
|
RefLink []byte // If not nil, turns this list item into a footnote item and triggers different rendering |
|
IsFootnotesList bool // This is a list of footnotes |
|
} |
|
|
|
// LinkData contains fields relevant to a Link node type. |
|
type LinkData struct { |
|
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. |
|
} |
|
|
|
// CodeBlockData contains fields relevant to a CodeBlock node type. |
|
type CodeBlockData struct { |
|
IsFenced bool // Specifies whether it's a fenced code block or an indented one |
|
Info []byte // This holds the info string |
|
FenceChar byte |
|
FenceLength int |
|
FenceOffset int |
|
} |
|
|
|
// TableCellData contains fields relevant to a TableCell node type. |
|
type TableCellData struct { |
|
IsHeader bool // This tells if it's under the header row |
|
Align CellAlignFlags // This holds the value for align attribute |
|
} |
|
|
|
// HeadingData contains fields relevant to a Heading node type. |
|
type HeadingData struct { |
|
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 |
|
} |
|
|
|
// 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 { |
|
Type NodeType // Determines the type of the node |
|
Parent *Node // Points to the parent |
|
FirstChild *Node // Points to the first child, if any |
|
LastChild *Node // Points to the last child, if any |
|
Prev *Node // Previous sibling; nil if it's the first child |
|
Next *Node // Next sibling; nil if it's the last child |
|
|
|
Literal []byte // Text contents of the leaf nodes |
|
|
|
HeadingData // Populated if Type is Heading |
|
ListData // Populated if Type is List |
|
CodeBlockData // Populated if Type is CodeBlock |
|
LinkData // Populated if Type is Link |
|
TableCellData // Populated if Type is TableCell |
|
|
|
content []byte // Markdown content of the block nodes |
|
open bool // Specifies an open block node that has not been finished to process yet |
|
} |
|
|
|
// NewNode allocates a node of a specified type. |
|
func NewNode(typ NodeType) *Node { |
|
return &Node{ |
|
Type: typ, |
|
open: true, |
|
} |
|
} |
|
|
|
func (n *Node) String() string { |
|
ellipsis := "" |
|
snippet := n.Literal |
|
if len(snippet) > 16 { |
|
snippet = snippet[:16] |
|
ellipsis = "..." |
|
} |
|
return fmt.Sprintf("%s: '%s%s'", n.Type, snippet, ellipsis) |
|
} |
|
|
|
// Unlink removes node 'n' from the tree. |
|
// It panics if the node is nil. |
|
func (n *Node) Unlink() { |
|
if n.Prev != nil { |
|
n.Prev.Next = n.Next |
|
} else if n.Parent != nil { |
|
n.Parent.FirstChild = n.Next |
|
} |
|
if n.Next != nil { |
|
n.Next.Prev = n.Prev |
|
} else if n.Parent != nil { |
|
n.Parent.LastChild = n.Prev |
|
} |
|
n.Parent = nil |
|
n.Next = nil |
|
n.Prev = 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.Unlink() |
|
child.Parent = n |
|
if n.LastChild != nil { |
|
n.LastChild.Next = child |
|
child.Prev = n.LastChild |
|
n.LastChild = child |
|
} else { |
|
n.FirstChild = child |
|
n.LastChild = child |
|
} |
|
} |
|
|
|
// InsertBefore inserts 'sibling' immediately before 'n'. |
|
// It panics if either node is nil. |
|
func (n *Node) InsertBefore(sibling *Node) { |
|
sibling.Unlink() |
|
sibling.Prev = n.Prev |
|
if sibling.Prev != nil { |
|
sibling.Prev.Next = sibling |
|
} |
|
sibling.Next = n |
|
n.Prev = sibling |
|
sibling.Parent = n.Parent |
|
if sibling.Prev == nil { |
|
sibling.Parent.FirstChild = sibling |
|
} |
|
} |
|
|
|
func (n *Node) isContainer() bool { |
|
switch n.Type { |
|
case Document: |
|
fallthrough |
|
case BlockQuote: |
|
fallthrough |
|
case List: |
|
fallthrough |
|
case Item: |
|
fallthrough |
|
case Paragraph: |
|
fallthrough |
|
case Heading: |
|
fallthrough |
|
case Emph: |
|
fallthrough |
|
case Strong: |
|
fallthrough |
|
case Del: |
|
fallthrough |
|
case Link: |
|
fallthrough |
|
case Image: |
|
fallthrough |
|
case Table: |
|
fallthrough |
|
case TableHead: |
|
fallthrough |
|
case TableBody: |
|
fallthrough |
|
case TableRow: |
|
fallthrough |
|
case TableCell: |
|
return true |
|
default: |
|
return false |
|
} |
|
} |
|
|
|
func (n *Node) canContain(t NodeType) bool { |
|
if n.Type == List { |
|
return t == Item |
|
} |
|
if n.Type == Document || n.Type == BlockQuote || n.Type == Item { |
|
return t != Item |
|
} |
|
if n.Type == Table { |
|
return t == TableHead || t == TableBody |
|
} |
|
if n.Type == TableHead || n.Type == TableBody { |
|
return t == TableRow |
|
} |
|
if n.Type == TableRow { |
|
return t == TableCell |
|
} |
|
return false |
|
} |
|
|
|
// WalkStatus allows NodeVisitor to have some control over the tree traversal. |
|
// It is returned from NodeVisitor and different values allow Node.Walk to |
|
// decide which node to go to next. |
|
type WalkStatus int |
|
|
|
const ( |
|
// GoToNext is the default traversal of every node. |
|
GoToNext WalkStatus = iota |
|
// SkipChildren tells walker to skip all children of current node. |
|
SkipChildren |
|
// Terminate tells walker to terminate the traversal. |
|
Terminate |
|
) |
|
|
|
// NodeVisitor is a callback to be called when traversing the syntax tree. |
|
// 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 func(node *Node, entering bool) WalkStatus |
|
|
|
// Walk is a convenience method that instantiates a walker and starts a |
|
// traversal of subtree rooted at n. |
|
func (n *Node) Walk(visitor NodeVisitor) { |
|
w := newNodeWalker(n) |
|
for w.current != nil { |
|
status := visitor(w.current, w.entering) |
|
switch status { |
|
case GoToNext: |
|
w.next() |
|
case SkipChildren: |
|
w.entering = false |
|
w.next() |
|
case Terminate: |
|
return |
|
} |
|
} |
|
} |
|
|
|
type nodeWalker struct { |
|
current *Node |
|
root *Node |
|
entering bool |
|
} |
|
|
|
func newNodeWalker(root *Node) *nodeWalker { |
|
return &nodeWalker{ |
|
current: root, |
|
root: root, |
|
entering: true, |
|
} |
|
} |
|
|
|
func (nw *nodeWalker) next() { |
|
if (!nw.current.isContainer() || !nw.entering) && nw.current == nw.root { |
|
nw.current = nil |
|
return |
|
} |
|
if nw.entering && nw.current.isContainer() { |
|
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 |
|
nw.entering = false |
|
} else { |
|
nw.current = nw.current.Next |
|
nw.entering = true |
|
} |
|
} |
|
|
|
func dump(ast *Node) { |
|
fmt.Println(dumpString(ast)) |
|
} |
|
|
|
func dumpR(ast *Node, depth int) string { |
|
if ast == nil { |
|
return "" |
|
} |
|
indent := bytes.Repeat([]byte("\t"), depth) |
|
content := ast.Literal |
|
if content == nil { |
|
content = ast.content |
|
} |
|
result := fmt.Sprintf("%s%s(%q)\n", indent, ast.Type, content) |
|
for n := ast.FirstChild; n != nil; n = n.Next { |
|
result += dumpR(n, depth+1) |
|
} |
|
return result |
|
} |
|
|
|
func dumpString(ast *Node) string { |
|
return dumpR(ast, 0) |
|
}
|
|
|