mirror of
https://github.com/russross/blackfriday.git
synced 2024-03-22 13:40:34 +08:00
d7f18785f1
Move these two flags from HTML renderer's flags to extensions. Implement both since they were not yet implemented in the AST rewrite. Add tests. Note: the expected test strings differ very slightly from v1. The HTML produced by v2 has a few extra newlines compared to the old one, but it's now uniform with other sections of the generated document. If the newline placement gets cleaned up in the future, this will get fixed automatically, since the renderer is agnostic about the TOC list.
312 lines
6.4 KiB
Go
312 lines
6.4 KiB
Go
package blackfriday
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
)
|
|
|
|
type NodeType int
|
|
|
|
const (
|
|
Document NodeType = iota
|
|
BlockQuote
|
|
List
|
|
Item
|
|
Paragraph
|
|
Header
|
|
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",
|
|
Header: "Header",
|
|
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]
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
type LinkData struct {
|
|
Destination []byte
|
|
Title []byte
|
|
NoteID int
|
|
}
|
|
|
|
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 uint32
|
|
FenceOffset uint32
|
|
}
|
|
|
|
type TableCellData struct {
|
|
IsHeader bool // This tells if it's under the header row
|
|
Align CellAlignFlags // This holds the value for align attribute
|
|
}
|
|
|
|
type HeaderData struct {
|
|
Level uint32 // This holds the heading level number
|
|
HeaderID string // This might hold header 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
|
|
|
|
HeaderData // Populated if Type == Header
|
|
ListData // Populated if Type == List
|
|
CodeBlockData // Populated if Type == CodeBlock
|
|
LinkData // Populated if Type == Link
|
|
TableCellData // Populated if Type == TableCell
|
|
|
|
content []byte // Markdown content of the block nodes
|
|
open bool // Specifies an open block node that has not been finished to process yet
|
|
}
|
|
|
|
func NewNode(typ NodeType) *Node {
|
|
return &Node{
|
|
Type: typ,
|
|
open: true,
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
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
|
|
}
|
|
}
|
|
|
|
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 Header:
|
|
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
|
|
}
|
|
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
|
|
}
|
|
|
|
func (root *Node) Walk(visitor func(node *Node, entering bool)) {
|
|
walker := NewNodeWalker(root)
|
|
node, entering := walker.next()
|
|
for node != nil {
|
|
visitor(node, entering)
|
|
node, entering = walker.next()
|
|
}
|
|
}
|
|
|
|
type NodeWalker struct {
|
|
current *Node
|
|
root *Node
|
|
entering bool
|
|
}
|
|
|
|
func NewNodeWalker(root *Node) *NodeWalker {
|
|
return &NodeWalker{
|
|
current: root,
|
|
root: nil,
|
|
entering: true,
|
|
}
|
|
}
|
|
|
|
func (nw *NodeWalker) next() (*Node, bool) {
|
|
if nw.current == nil {
|
|
return nil, false
|
|
}
|
|
if nw.root == nil {
|
|
nw.root = nw.current
|
|
return nw.current, nw.entering
|
|
}
|
|
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
|
|
}
|
|
if nw.current == nw.root {
|
|
return nil, false
|
|
}
|
|
return nw.current, nw.entering
|
|
}
|
|
|
|
func (nw *NodeWalker) resumeAt(node *Node, entering bool) {
|
|
nw.current = node
|
|
nw.entering = entering
|
|
}
|
|
|
|
func dump(ast *Node) {
|
|
fmt.Println(dumpString(ast))
|
|
}
|
|
|
|
func dump_r(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 += dump_r(n, depth+1)
|
|
}
|
|
return result
|
|
}
|
|
|
|
func dumpString(ast *Node) string {
|
|
return dump_r(ast, 0)
|
|
}
|