mirror of
https://github.com/yuin/goldmark
synced 2025-03-04 23:04:52 +00:00
first commit
This commit is contained in:
commit
dd89404e04
46 changed files with 24605 additions and 0 deletions
13
.gitignore
vendored
Normal file
13
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
# Binaries for programs and plugins
|
||||||
|
*.exe
|
||||||
|
*.exe~
|
||||||
|
*.dll
|
||||||
|
*.so
|
||||||
|
*.dylib
|
||||||
|
|
||||||
|
# Test binary, build with `go test -c`
|
||||||
|
*.test
|
||||||
|
*.pprof
|
||||||
|
|
||||||
|
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||||
|
*.out
|
||||||
7
Makefile
Normal file
7
Makefile
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
.PHONY: test
|
||||||
|
|
||||||
|
test:
|
||||||
|
go test -coverprofile=profile.out -coverpkg=github.com/yuin/goldmark,github.com/yuin/goldmark/ast,github.com/yuin/goldmark/extension,github.com/yuin/goldmark/extension/ast,github.com/yuin/goldmark/parser,github.com/yuin/goldmark/renderer,github.com/yuin/goldmark/renderer/html,github.com/yuin/goldmark/text,github.com/yuin/goldmark/util ./...
|
||||||
|
|
||||||
|
cov: test
|
||||||
|
go tool cover -html=profile.out
|
||||||
129
README.md
Normal file
129
README.md
Normal file
|
|
@ -0,0 +1,129 @@
|
||||||
|
goldmark
|
||||||
|
==========================================
|
||||||
|
|
||||||
|
> A markdown parser written in Go. Easy to extend, standard compliant, well structured.
|
||||||
|
|
||||||
|
goldmark is compliant to CommonMark 0.29.
|
||||||
|
|
||||||
|
Motivation
|
||||||
|
----------------------
|
||||||
|
I need a markdown parser for Go that meets following conditions:
|
||||||
|
|
||||||
|
- Easy to extend.
|
||||||
|
- Markdown is poor in document expressions compared with other light markup languages like restructuredText.
|
||||||
|
- We have extended a markdown syntax. i.e. : PHPMarkdownExtra, Github Flavored Markdown.
|
||||||
|
- Standard compliant.
|
||||||
|
- Markdown has many dialects.
|
||||||
|
- Github Flavored Markdown is widely used and it is based on CommonMark aside from whether CommonMark is good specification or not.
|
||||||
|
- CommonMark is too complicated and hard to implement.
|
||||||
|
- Well structured.
|
||||||
|
- AST based, and preserves source potision of nodes.
|
||||||
|
- Written in pure Go.
|
||||||
|
|
||||||
|
[golang-commonmark](https://gitlab.com/golang-commonmark/markdown) may be a good choice, but it seems copy of the [markdown-it](https://github.com/markdown-it) .
|
||||||
|
|
||||||
|
[blackfriday.v2](https://github.com/russross/blackfriday/tree/v2) is a fast and widely used implementation, but it is not CommonMark compliant and can not be extended from outside of the package since it's AST is not interfaces but structs.
|
||||||
|
|
||||||
|
As mentioned above, CommonMark is too complicated and hard to implement, So Markdown parsers base on CommonMark barely exist.
|
||||||
|
|
||||||
|
Usage
|
||||||
|
----------------------
|
||||||
|
|
||||||
|
Convert Markdown documents with the CommonMark compliant mode:
|
||||||
|
|
||||||
|
```go
|
||||||
|
var buf bytes.Buffer
|
||||||
|
if err := goldmark.Convert(source, &buf); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Customize a parser and a renderer:
|
||||||
|
|
||||||
|
```go
|
||||||
|
md := goldmark.NewMarkdown(
|
||||||
|
goldmark.WithExtensions(extension.GFM),
|
||||||
|
goldmark.WithParserOptions(
|
||||||
|
parser.WithHeadingID(),
|
||||||
|
),
|
||||||
|
goldmark.WithRendererOptions(
|
||||||
|
html.WithSoftLineBreak(true),
|
||||||
|
html.WithXHTML(true),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
var buf bytes.Buffer
|
||||||
|
if err := md.Convert(source, &buf); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Parser and Renderer options
|
||||||
|
------------------------------
|
||||||
|
|
||||||
|
### Parser options
|
||||||
|
|
||||||
|
| Functional option | Type | Description |
|
||||||
|
| ----------------- | ---- | ----------- |
|
||||||
|
| `parser.WithBlockParsers` | List of `util.PrioritizedSlice` whose elements are `parser.BlockParser` | Parsers for parsing block level elements. |
|
||||||
|
| `parser.WithInlineParsers` | List of `util.PrioritizedSlice` whose elements are `parser.InlineParser` | Parsers for parsing inline level elements. |
|
||||||
|
| `parser.WithParagraphTransformers` | List of `util.PrioritizedSlice` whose elements are `parser.ParagraphTransformer` | Transformers for transforming paragraph nodes. |
|
||||||
|
| `parser.WithHeadingID` | `-` | Enables custom heading ids( `{#custom-id}` ) and auto heading ids. |
|
||||||
|
| `parser.WithFilterTags` | `...string` | HTML tag names forbidden in HTML blocks and Raw HTMLs. |
|
||||||
|
|
||||||
|
### HTML Renderer options
|
||||||
|
|
||||||
|
| Functional option | Type | Description |
|
||||||
|
| ----------------- | ---- | ----------- |
|
||||||
|
| `html.WithWriter` | `html.Writer` | `html.Writer` for writing contents to an `io.Writer`. |
|
||||||
|
| `html.WithSoftLineBreak` | `-` | Render new lines as `<br>` if true, otherwise `\n` .|
|
||||||
|
| `html.WithXHTML` | `-` | Render as XHTML. |
|
||||||
|
|
||||||
|
### Built-in extensions
|
||||||
|
|
||||||
|
- `extension.Table`
|
||||||
|
- `extension.Strikethrough`
|
||||||
|
- `extension.Linkify`
|
||||||
|
- `extension.TaskList`
|
||||||
|
- `extension.GFM`
|
||||||
|
- This extension enables Table, Strikethrough, Linkify and TaskList.
|
||||||
|
In addition, this extension sets some tags to `parser.FilterTags` .
|
||||||
|
|
||||||
|
Create extensions
|
||||||
|
--------------------
|
||||||
|
**TODO**
|
||||||
|
|
||||||
|
See `extension` directory for examples of extensions.
|
||||||
|
|
||||||
|
Summary:
|
||||||
|
|
||||||
|
1. Define AST Node as a struct in which `ast.BaseBlock` or `ast.BaseInline` is embedded.
|
||||||
|
2. Write a parser that implements `parser.BlockParser` or `parser.InlineParser`.
|
||||||
|
3. Write a renderer that implements `renderer.NodeRenderer`.
|
||||||
|
4. Define your goldmark extension that implements `goldmark.Extender`.
|
||||||
|
|
||||||
|
Benchmark
|
||||||
|
--------------------
|
||||||
|
You can run this benchmark in the `_benchmark` directory.
|
||||||
|
|
||||||
|
blackfriday v2 is fastest, but it is not CommonMark compiliant and not an extensible.
|
||||||
|
|
||||||
|
Though goldmark builds clean extensible AST structure and get full compliance with
|
||||||
|
Commonmark, it is resonably fast and less memory consumption.
|
||||||
|
|
||||||
|
```
|
||||||
|
BenchmarkGoldMark-4 200 7291402 ns/op 2259603 B/op 16867 allocs/op
|
||||||
|
BenchmarkGolangCommonMark-4 200 7709939 ns/op 3053760 B/op 18682 allocs/op
|
||||||
|
BenchmarkBlackFriday-4 300 5776369 ns/op 3356386 B/op 17480 allocs/op
|
||||||
|
```
|
||||||
|
|
||||||
|
Donation
|
||||||
|
--------------------
|
||||||
|
BTC: 1NEDSyUmo4SMTDP83JJQSWi1MvQUGGNMZB
|
||||||
|
|
||||||
|
License
|
||||||
|
--------------------
|
||||||
|
MIT
|
||||||
|
|
||||||
|
Author
|
||||||
|
--------------------
|
||||||
|
Yusuke Inuzuka
|
||||||
9702
_benchmark/_data.md
Normal file
9702
_benchmark/_data.md
Normal file
File diff suppressed because it is too large
Load diff
55
_benchmark/benchmark_test.go
Normal file
55
_benchmark/benchmark_test.go
Normal file
|
|
@ -0,0 +1,55 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"io/ioutil"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/yuin/goldmark"
|
||||||
|
"github.com/yuin/goldmark/renderer/html"
|
||||||
|
|
||||||
|
"gitlab.com/golang-commonmark/markdown"
|
||||||
|
|
||||||
|
"gopkg.in/russross/blackfriday.v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
func BenchmarkGoldMark(b *testing.B) {
|
||||||
|
b.ResetTimer()
|
||||||
|
source, err := ioutil.ReadFile("_data.md")
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
markdown := goldmark.New(goldmark.WithRendererOptions(html.WithXHTML()))
|
||||||
|
var out bytes.Buffer
|
||||||
|
markdown.Convert([]byte(""), &out)
|
||||||
|
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
out.Reset()
|
||||||
|
if err := markdown.Convert(source, &out); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkGolangCommonMark(b *testing.B) {
|
||||||
|
b.ResetTimer()
|
||||||
|
source, err := ioutil.ReadFile("_data.md")
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
md := markdown.New(markdown.XHTMLOutput(true))
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
md.RenderToString(source)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkBlackFriday(b *testing.B) {
|
||||||
|
b.ResetTimer()
|
||||||
|
source, err := ioutil.ReadFile("_data.md")
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
blackfriday.Run(source)
|
||||||
|
}
|
||||||
|
}
|
||||||
5194
_test/spec.json
Normal file
5194
_test/spec.json
Normal file
File diff suppressed because it is too large
Load diff
342
ast/ast.go
Normal file
342
ast/ast.go
Normal file
|
|
@ -0,0 +1,342 @@
|
||||||
|
// Package ast defines AST nodes that represent markdown elements.
|
||||||
|
package ast
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
textm "github.com/yuin/goldmark/text"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// A NodeType indicates what type a node belongs to.
|
||||||
|
type NodeType int
|
||||||
|
|
||||||
|
const (
|
||||||
|
// BlockNode indicates that a node is kind of block nodes.
|
||||||
|
BlockNode NodeType = iota + 1
|
||||||
|
// InlineNode indicates that a node is kind of inline nodes.
|
||||||
|
InlineNode
|
||||||
|
)
|
||||||
|
|
||||||
|
// A Node interface defines basic AST node functionalities.
|
||||||
|
type Node interface {
|
||||||
|
// Type returns a type of this node.
|
||||||
|
Type() NodeType
|
||||||
|
|
||||||
|
// NextSibling returns a next sibling node of this node.
|
||||||
|
NextSibling() Node
|
||||||
|
|
||||||
|
// PreviousSibling returns a previous sibling node of this node.
|
||||||
|
PreviousSibling() Node
|
||||||
|
|
||||||
|
// Parent returns a parent node of this node.
|
||||||
|
Parent() Node
|
||||||
|
|
||||||
|
// SetParent sets a parent node to this node.
|
||||||
|
SetParent(Node)
|
||||||
|
|
||||||
|
// SetPreviousSibling sets a previous sibling node to this node.
|
||||||
|
SetPreviousSibling(Node)
|
||||||
|
|
||||||
|
// SetNextSibling sets a next sibling node to this node.
|
||||||
|
SetNextSibling(Node)
|
||||||
|
|
||||||
|
// HasChildren returns true if this node has any children, otherwise false.
|
||||||
|
HasChildren() bool
|
||||||
|
|
||||||
|
// ChildCount returns a total number of children.
|
||||||
|
ChildCount() int
|
||||||
|
|
||||||
|
// FirstChild returns a first child of this node.
|
||||||
|
FirstChild() Node
|
||||||
|
|
||||||
|
// LastChild returns a last child of this node.
|
||||||
|
LastChild() Node
|
||||||
|
|
||||||
|
// AppendChild append a node child to the tail of the children.
|
||||||
|
AppendChild(self, child Node)
|
||||||
|
|
||||||
|
// RemoveChild removes a node child from this node.
|
||||||
|
// If a node child is not children of this node, RemoveChild nothing to do.
|
||||||
|
RemoveChild(self, child Node)
|
||||||
|
|
||||||
|
// RemoveChildren removes all children from this node.
|
||||||
|
RemoveChildren(self Node)
|
||||||
|
|
||||||
|
// ReplaceChild replace a node v1 with a node insertee.
|
||||||
|
// If v1 is not children of this node, ReplaceChild append a insetee to the
|
||||||
|
// tail of the children.
|
||||||
|
ReplaceChild(self, v1, insertee Node)
|
||||||
|
|
||||||
|
// InsertBefore inserts a node insertee before a node v1.
|
||||||
|
// If v1 is not children of this node, InsertBefore append a insetee to the
|
||||||
|
// tail of the children.
|
||||||
|
InsertBefore(self, v1, insertee Node)
|
||||||
|
|
||||||
|
// InsertAfterinserts a node insertee after a node v1.
|
||||||
|
// If v1 is not children of this node, InsertBefore append a insetee to the
|
||||||
|
// tail of the children.
|
||||||
|
InsertAfter(self, v1, insertee Node)
|
||||||
|
|
||||||
|
// Dump dumps an AST tree structure to stdout.
|
||||||
|
// This function completely aimed for debugging.
|
||||||
|
// level is a indent level. Implementer should indent informations with
|
||||||
|
// 2 * level spaces.
|
||||||
|
Dump(source []byte, level int)
|
||||||
|
|
||||||
|
// Text returns text values of this node.
|
||||||
|
Text(source []byte) []byte
|
||||||
|
|
||||||
|
// HasBlankPreviousLines returns true if the row before this node is blank,
|
||||||
|
// otherwise false.
|
||||||
|
// This method is valid only for block nodes.
|
||||||
|
HasBlankPreviousLines() bool
|
||||||
|
|
||||||
|
// SetBlankPreviousLines sets whether the row before this node is blank.
|
||||||
|
// This method is valid only for block nodes.
|
||||||
|
SetBlankPreviousLines(v bool)
|
||||||
|
|
||||||
|
// Lines returns text segments that hold positions in a source.
|
||||||
|
// This method is valid only for block nodes.
|
||||||
|
Lines() *textm.Segments
|
||||||
|
|
||||||
|
// SetLines sets text segments that hold positions in a source.
|
||||||
|
// This method is valid only for block nodes.
|
||||||
|
SetLines(*textm.Segments)
|
||||||
|
|
||||||
|
// IsRaw returns true if contents should be rendered as 'raw' contents.
|
||||||
|
IsRaw() bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// A BaseNode struct implements the Node interface.
|
||||||
|
type BaseNode struct {
|
||||||
|
firstChild Node
|
||||||
|
lastChild Node
|
||||||
|
parent Node
|
||||||
|
next Node
|
||||||
|
prev Node
|
||||||
|
}
|
||||||
|
|
||||||
|
func ensureIsolated(v Node) {
|
||||||
|
if p := v.Parent(); p != nil {
|
||||||
|
p.RemoveChild(p, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// HasChildren implements Node.HasChildren .
|
||||||
|
func (n *BaseNode) HasChildren() bool {
|
||||||
|
return n.firstChild != nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetPreviousSibling implements Node.SetPreviousSibling .
|
||||||
|
func (n *BaseNode) SetPreviousSibling(v Node) {
|
||||||
|
n.prev = v
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetNextSibling implements Node.SetNextSibling .
|
||||||
|
func (n *BaseNode) SetNextSibling(v Node) {
|
||||||
|
n.next = v
|
||||||
|
}
|
||||||
|
|
||||||
|
// PreviousSibling implements Node.PreviousSibling .
|
||||||
|
func (n *BaseNode) PreviousSibling() Node {
|
||||||
|
return n.prev
|
||||||
|
}
|
||||||
|
|
||||||
|
// NextSibling implements Node.NextSibling .
|
||||||
|
func (n *BaseNode) NextSibling() Node {
|
||||||
|
return n.next
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoveChild implements Node.RemoveChild .
|
||||||
|
func (n *BaseNode) RemoveChild(self, v Node) {
|
||||||
|
if v.Parent() != self {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
prev := v.PreviousSibling()
|
||||||
|
next := v.NextSibling()
|
||||||
|
if prev != nil {
|
||||||
|
prev.SetNextSibling(next)
|
||||||
|
} else {
|
||||||
|
n.firstChild = next
|
||||||
|
}
|
||||||
|
if next != nil {
|
||||||
|
next.SetPreviousSibling(prev)
|
||||||
|
} else {
|
||||||
|
n.lastChild = prev
|
||||||
|
}
|
||||||
|
v.SetParent(nil)
|
||||||
|
v.SetPreviousSibling(nil)
|
||||||
|
v.SetNextSibling(nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoveChildren implements Node.RemoveChildren .
|
||||||
|
func (n *BaseNode) RemoveChildren(self Node) {
|
||||||
|
for c := n.firstChild; c != nil; c = c.NextSibling() {
|
||||||
|
c.SetParent(nil)
|
||||||
|
c.SetPreviousSibling(nil)
|
||||||
|
c.SetNextSibling(nil)
|
||||||
|
}
|
||||||
|
n.firstChild = nil
|
||||||
|
n.lastChild = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// FirstChild implements Node.FirstChild .
|
||||||
|
func (n *BaseNode) FirstChild() Node {
|
||||||
|
return n.firstChild
|
||||||
|
}
|
||||||
|
|
||||||
|
// LastChild implements Node.LastChild .
|
||||||
|
func (n *BaseNode) LastChild() Node {
|
||||||
|
return n.lastChild
|
||||||
|
}
|
||||||
|
|
||||||
|
// ChildCount implements Node.ChildCount .
|
||||||
|
func (n *BaseNode) ChildCount() int {
|
||||||
|
count := 0
|
||||||
|
for c := n.firstChild; c != nil; c = c.NextSibling() {
|
||||||
|
count++
|
||||||
|
}
|
||||||
|
return count
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parent implements Node.Parent .
|
||||||
|
func (n *BaseNode) Parent() Node {
|
||||||
|
return n.parent
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetParent implements Node.SetParent .
|
||||||
|
func (n *BaseNode) SetParent(v Node) {
|
||||||
|
n.parent = v
|
||||||
|
}
|
||||||
|
|
||||||
|
// AppendChild implements Node.AppendChild .
|
||||||
|
func (n *BaseNode) AppendChild(self, v Node) {
|
||||||
|
ensureIsolated(v)
|
||||||
|
if n.firstChild == nil {
|
||||||
|
n.firstChild = v
|
||||||
|
v.SetNextSibling(nil)
|
||||||
|
v.SetPreviousSibling(nil)
|
||||||
|
} else {
|
||||||
|
last := n.lastChild
|
||||||
|
last.SetNextSibling(v)
|
||||||
|
v.SetPreviousSibling(last)
|
||||||
|
}
|
||||||
|
v.SetParent(self)
|
||||||
|
n.lastChild = v
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReplaceChild implements Node.ReplaceChild .
|
||||||
|
func (n *BaseNode) ReplaceChild(self, v1, insertee Node) {
|
||||||
|
n.InsertBefore(self, v1, insertee)
|
||||||
|
n.RemoveChild(self, v1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// InsertAfter implements Node.InsertAfter .
|
||||||
|
func (n *BaseNode) InsertAfter(self, v1, insertee Node) {
|
||||||
|
n.InsertBefore(self, v1.NextSibling(), insertee)
|
||||||
|
}
|
||||||
|
|
||||||
|
// InsertBefore implements Node.InsertBefore .
|
||||||
|
func (n *BaseNode) InsertBefore(self, v1, insertee Node) {
|
||||||
|
if v1 == nil {
|
||||||
|
n.AppendChild(self, insertee)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ensureIsolated(insertee)
|
||||||
|
if v1.Parent() == self {
|
||||||
|
c := v1
|
||||||
|
prev := c.PreviousSibling()
|
||||||
|
if prev != nil {
|
||||||
|
prev.SetNextSibling(insertee)
|
||||||
|
insertee.SetPreviousSibling(prev)
|
||||||
|
} else {
|
||||||
|
n.firstChild = insertee
|
||||||
|
insertee.SetPreviousSibling(nil)
|
||||||
|
}
|
||||||
|
insertee.SetNextSibling(c)
|
||||||
|
c.SetPreviousSibling(insertee)
|
||||||
|
insertee.SetParent(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Text implements Node.Text .
|
||||||
|
func (n *BaseNode) Text(source []byte) []byte {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
for c := n.firstChild; c != nil; c = c.NextSibling() {
|
||||||
|
buf.Write(c.Text(source))
|
||||||
|
}
|
||||||
|
return buf.Bytes()
|
||||||
|
}
|
||||||
|
|
||||||
|
// DumpHelper is a helper function to implement Node.Dump.
|
||||||
|
// name is a name of the node.
|
||||||
|
// kv is pairs of an attribute name and an attribute value.
|
||||||
|
// cb is a function called after wrote a name and attributes.
|
||||||
|
func DumpHelper(v Node, source []byte, level int, name string, kv map[string]string, cb func(int)) {
|
||||||
|
indent := strings.Repeat(" ", level)
|
||||||
|
fmt.Printf("%s%s {\n", indent, name)
|
||||||
|
indent2 := strings.Repeat(" ", level+1)
|
||||||
|
if v.Type() == BlockNode {
|
||||||
|
fmt.Printf("%sRawText: \"", indent2)
|
||||||
|
for i := 0; i < v.Lines().Len(); i++ {
|
||||||
|
line := v.Lines().At(i)
|
||||||
|
fmt.Printf("%s", line.Value(source))
|
||||||
|
}
|
||||||
|
fmt.Printf("\"\n")
|
||||||
|
fmt.Printf("%sHasBlankPreviousLines: %v\n", indent2, v.HasBlankPreviousLines())
|
||||||
|
}
|
||||||
|
if kv != nil {
|
||||||
|
for name, value := range kv {
|
||||||
|
fmt.Printf("%s%s: %s\n", indent2, name, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if cb != nil {
|
||||||
|
cb(level + 1)
|
||||||
|
}
|
||||||
|
for c := v.FirstChild(); c != nil; c = c.NextSibling() {
|
||||||
|
c.Dump(source, level+1)
|
||||||
|
}
|
||||||
|
fmt.Printf("%s}\n", indent)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WalkStatus represents a current status of the Walk function.
|
||||||
|
type WalkStatus int
|
||||||
|
|
||||||
|
const (
|
||||||
|
// WalkStop indicates no more walking needed.
|
||||||
|
WalkStop = iota + 1
|
||||||
|
|
||||||
|
// WalkSkipChildren indicates that Walk wont walk on children of current
|
||||||
|
// node.
|
||||||
|
WalkSkipChildren
|
||||||
|
|
||||||
|
// WalkContinue indicates that Walk can continue to walk.
|
||||||
|
WalkContinue
|
||||||
|
)
|
||||||
|
|
||||||
|
// Walker is a function that will be called when Walk find a
|
||||||
|
// new node.
|
||||||
|
// entering is set true before walks children, false after walked children.
|
||||||
|
// If Walker returns error, Walk function immediately stop walking.
|
||||||
|
type Walker func(n Node, entering bool) (WalkStatus, error)
|
||||||
|
|
||||||
|
// Walk walks a AST tree by the depth first search algorighm.
|
||||||
|
func Walk(n Node, walker Walker) error {
|
||||||
|
status, err := walker(n, true)
|
||||||
|
if err != nil || status == WalkStop {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if status != WalkSkipChildren {
|
||||||
|
for c := n.FirstChild(); c != nil; c = c.NextSibling() {
|
||||||
|
if err := Walk(c, walker); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
status, err = walker(n, false)
|
||||||
|
if err != nil || status == WalkStop {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
364
ast/block.go
Normal file
364
ast/block.go
Normal file
|
|
@ -0,0 +1,364 @@
|
||||||
|
package ast
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
textm "github.com/yuin/goldmark/text"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// A BaseBlock struct implements the Node interface.
|
||||||
|
type BaseBlock struct {
|
||||||
|
BaseNode
|
||||||
|
blankPreviousLines bool
|
||||||
|
lines *textm.Segments
|
||||||
|
}
|
||||||
|
|
||||||
|
// Type implements Node.Type
|
||||||
|
func (b *BaseBlock) Type() NodeType {
|
||||||
|
return BlockNode
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsRaw implements Node.IsRaw
|
||||||
|
func (b *BaseBlock) IsRaw() bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// HasBlankPreviousLines implements Node.HasBlankPreviousLines.
|
||||||
|
func (b *BaseBlock) HasBlankPreviousLines() bool {
|
||||||
|
return b.blankPreviousLines
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetBlankPreviousLines implements Node.SetBlankPreviousLines.
|
||||||
|
func (b *BaseBlock) SetBlankPreviousLines(v bool) {
|
||||||
|
b.blankPreviousLines = v
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lines implements Node.Lines
|
||||||
|
func (b *BaseBlock) Lines() *textm.Segments {
|
||||||
|
if b.lines == nil {
|
||||||
|
b.lines = textm.NewSegments()
|
||||||
|
}
|
||||||
|
return b.lines
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetLines implements Node.SetLines
|
||||||
|
func (b *BaseBlock) SetLines(v *textm.Segments) {
|
||||||
|
b.lines = v
|
||||||
|
}
|
||||||
|
|
||||||
|
// A Document struct is a root node of Markdown text.
|
||||||
|
type Document struct {
|
||||||
|
BaseBlock
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dump impelements Node.Dump .
|
||||||
|
func (n *Document) Dump(source []byte, level int) {
|
||||||
|
DumpHelper(n, source, level, "Document", nil, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewDocument returns a new Document node.
|
||||||
|
func NewDocument() *Document {
|
||||||
|
return &Document{
|
||||||
|
BaseBlock: BaseBlock{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A TextBlock struct is a node whose lines
|
||||||
|
// should be rendered without any containers.
|
||||||
|
type TextBlock struct {
|
||||||
|
BaseBlock
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dump impelements Node.Dump .
|
||||||
|
func (n *TextBlock) Dump(source []byte, level int) {
|
||||||
|
DumpHelper(n, source, level, "TextBlock", nil, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewTextBlock returns a new TextBlock node.
|
||||||
|
func NewTextBlock() *TextBlock {
|
||||||
|
return &TextBlock{
|
||||||
|
BaseBlock: BaseBlock{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A Paragraph struct represents a paragraph of Markdown text.
|
||||||
|
type Paragraph struct {
|
||||||
|
BaseBlock
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dump impelements Node.Dump .
|
||||||
|
func (n *Paragraph) Dump(source []byte, level int) {
|
||||||
|
DumpHelper(n, source, level, "Paragraph", nil, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewParagraph returns a new Paragraph node.
|
||||||
|
func NewParagraph() *Paragraph {
|
||||||
|
return &Paragraph{
|
||||||
|
BaseBlock: BaseBlock{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsParagraph returns true if given node implements the Paragraph interface,
|
||||||
|
// otherwise false.
|
||||||
|
func IsParagraph(node Node) bool {
|
||||||
|
_, ok := node.(*Paragraph)
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
|
// A Heading struct represents headings like SetextHeading and ATXHeading.
|
||||||
|
type Heading struct {
|
||||||
|
BaseBlock
|
||||||
|
// Level returns a level of this heading.
|
||||||
|
// This value is between 1 and 6.
|
||||||
|
Level int
|
||||||
|
|
||||||
|
// ID returns an ID of this heading.
|
||||||
|
ID []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dump impelements Node.Dump .
|
||||||
|
func (n *Heading) Dump(source []byte, level int) {
|
||||||
|
m := map[string]string{
|
||||||
|
"Level": fmt.Sprintf("%d", n.Level),
|
||||||
|
}
|
||||||
|
DumpHelper(n, source, level, "Heading", m, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewHeading returns a new Heading node.
|
||||||
|
func NewHeading(level int) *Heading {
|
||||||
|
return &Heading{
|
||||||
|
BaseBlock: BaseBlock{},
|
||||||
|
Level: level,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A ThemanticBreak struct represents a themantic break of Markdown text.
|
||||||
|
type ThemanticBreak struct {
|
||||||
|
BaseBlock
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dump impelements Node.Dump .
|
||||||
|
func (n *ThemanticBreak) Dump(source []byte, level int) {
|
||||||
|
DumpHelper(n, source, level, "ThemanticBreak", nil, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewThemanticBreak returns a new ThemanticBreak node.
|
||||||
|
func NewThemanticBreak() *ThemanticBreak {
|
||||||
|
return &ThemanticBreak{
|
||||||
|
BaseBlock: BaseBlock{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A CodeBlock interface represents an indented code block of Markdown text.
|
||||||
|
type CodeBlock struct {
|
||||||
|
BaseBlock
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsRaw implements Node.IsRaw.
|
||||||
|
func (n *CodeBlock) IsRaw() bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dump impelements Node.Dump .
|
||||||
|
func (n *CodeBlock) Dump(source []byte, level int) {
|
||||||
|
DumpHelper(n, source, level, "CodeBlock", nil, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewCodeBlock returns a new CodeBlock node.
|
||||||
|
func NewCodeBlock() *CodeBlock {
|
||||||
|
return &CodeBlock{
|
||||||
|
BaseBlock: BaseBlock{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A FencedCodeBlock struct represents a fenced code block of Markdown text.
|
||||||
|
type FencedCodeBlock struct {
|
||||||
|
BaseBlock
|
||||||
|
// Info returns a info text of this fenced code block.
|
||||||
|
Info *Text
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsRaw implements Node.IsRaw.
|
||||||
|
func (n *FencedCodeBlock) IsRaw() bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dump impelements Node.Dump .
|
||||||
|
func (n *FencedCodeBlock) Dump(source []byte, level int) {
|
||||||
|
m := map[string]string{}
|
||||||
|
if n.Info != nil {
|
||||||
|
m["Info"] = fmt.Sprintf("\"%s\"", n.Info.Text(source))
|
||||||
|
}
|
||||||
|
DumpHelper(n, source, level, "FencedCodeBlock", m, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewFencedCodeBlock return a new FencedCodeBlock node.
|
||||||
|
func NewFencedCodeBlock(info *Text) *FencedCodeBlock {
|
||||||
|
return &FencedCodeBlock{
|
||||||
|
BaseBlock: BaseBlock{},
|
||||||
|
Info: info,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A Blockquote struct represents an blockquote block of Markdown text.
|
||||||
|
type Blockquote struct {
|
||||||
|
BaseBlock
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dump impelements Node.Dump .
|
||||||
|
func (n *Blockquote) Dump(source []byte, level int) {
|
||||||
|
DumpHelper(n, source, level, "Blockquote", nil, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewBlockquote returns a new Blockquote node.
|
||||||
|
func NewBlockquote() *Blockquote {
|
||||||
|
return &Blockquote{
|
||||||
|
BaseBlock: BaseBlock{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A List structr represents a list of Markdown text.
|
||||||
|
type List struct {
|
||||||
|
BaseBlock
|
||||||
|
|
||||||
|
// Marker is a markar character like '-', '+', ')' and '.'.
|
||||||
|
Marker byte
|
||||||
|
|
||||||
|
// IsTight is a true if this list is a 'tight' list.
|
||||||
|
// See https://spec.commonmark.org/0.29/#loose for details.
|
||||||
|
IsTight bool
|
||||||
|
|
||||||
|
// Start is an initial number of this ordered list.
|
||||||
|
// If this list is not an ordered list, Start is 0.
|
||||||
|
Start int
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsOrdered returns true if this list is an ordered list, otherwise false.
|
||||||
|
func (l *List) IsOrdered() bool {
|
||||||
|
return l.Marker == '.' || l.Marker == ')'
|
||||||
|
}
|
||||||
|
|
||||||
|
// CanContinue returns true if this list can continue with
|
||||||
|
// given mark and a list type, otherwise false.
|
||||||
|
func (l *List) CanContinue(marker byte, isOrdered bool) bool {
|
||||||
|
return marker == l.Marker && isOrdered == l.IsOrdered()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dump implements Node.Dump.
|
||||||
|
func (l *List) Dump(source []byte, level int) {
|
||||||
|
name := "List"
|
||||||
|
if l.IsOrdered() {
|
||||||
|
name = "OrderedList"
|
||||||
|
}
|
||||||
|
m := map[string]string{
|
||||||
|
"Marker": fmt.Sprintf("%c", l.Marker),
|
||||||
|
"Tight": fmt.Sprintf("%v", l.IsTight),
|
||||||
|
}
|
||||||
|
if l.IsOrdered() {
|
||||||
|
m["Start"] = fmt.Sprintf("%d", l.Start)
|
||||||
|
}
|
||||||
|
DumpHelper(l, source, level, name, m, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewList returns a new List node.
|
||||||
|
func NewList(marker byte) *List {
|
||||||
|
return &List{
|
||||||
|
BaseBlock: BaseBlock{},
|
||||||
|
Marker: marker,
|
||||||
|
IsTight: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A ListItem struct represents a list item of Markdown text.
|
||||||
|
type ListItem struct {
|
||||||
|
BaseBlock
|
||||||
|
|
||||||
|
// Offset is an offset potision of this item.
|
||||||
|
Offset int
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dump implements Node.Dump.
|
||||||
|
func (n *ListItem) Dump(source []byte, level int) {
|
||||||
|
DumpHelper(n, source, level, "ListItem", nil, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewListItem returns a new ListItem node.
|
||||||
|
func NewListItem(offset int) *ListItem {
|
||||||
|
return &ListItem{
|
||||||
|
BaseBlock: BaseBlock{},
|
||||||
|
Offset: offset,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// HTMLBlockType represents kinds of an html blocks.
|
||||||
|
// See https://spec.commonmark.org/0.29/#html-blocks
|
||||||
|
type HTMLBlockType int
|
||||||
|
|
||||||
|
const (
|
||||||
|
// HTMLBlockType1 represents type 1 html blocks
|
||||||
|
HTMLBlockType1 = iota + 1
|
||||||
|
// HTMLBlockType2 represents type 2 html blocks
|
||||||
|
HTMLBlockType2
|
||||||
|
// HTMLBlockType3 represents type 3 html blocks
|
||||||
|
HTMLBlockType3
|
||||||
|
// HTMLBlockType4 represents type 4 html blocks
|
||||||
|
HTMLBlockType4
|
||||||
|
// HTMLBlockType5 represents type 5 html blocks
|
||||||
|
HTMLBlockType5
|
||||||
|
// HTMLBlockType6 represents type 6 html blocks
|
||||||
|
HTMLBlockType6
|
||||||
|
// HTMLBlockType7 represents type 7 html blocks
|
||||||
|
HTMLBlockType7
|
||||||
|
)
|
||||||
|
|
||||||
|
// An HTMLBlock struct represents an html block of Markdown text.
|
||||||
|
type HTMLBlock struct {
|
||||||
|
BaseBlock
|
||||||
|
|
||||||
|
// Type is a type of this html block.
|
||||||
|
HTMLBlockType HTMLBlockType
|
||||||
|
|
||||||
|
// ClosureLine is a line that closes this html block.
|
||||||
|
ClosureLine textm.Segment
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsRaw implements Node.IsRaw.
|
||||||
|
func (n *HTMLBlock) IsRaw() bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// HasClosure returns true if this html block has a closure line,
|
||||||
|
// otherwise false.
|
||||||
|
func (n *HTMLBlock) HasClosure() bool {
|
||||||
|
return n.ClosureLine.Start >= 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dump implements Node.Dump.
|
||||||
|
func (n *HTMLBlock) Dump(source []byte, level int) {
|
||||||
|
indent := strings.Repeat(" ", level)
|
||||||
|
fmt.Printf("%s%s {\n", indent, "HTMLBlock")
|
||||||
|
indent2 := strings.Repeat(" ", level+1)
|
||||||
|
fmt.Printf("%sRawText: \"", indent2)
|
||||||
|
for i := 0; i < n.Lines().Len(); i++ {
|
||||||
|
s := n.Lines().At(i)
|
||||||
|
fmt.Print(string(source[s.Start:s.Stop]))
|
||||||
|
}
|
||||||
|
fmt.Printf("\"\n")
|
||||||
|
for c := n.FirstChild(); c != nil; c = c.NextSibling() {
|
||||||
|
c.Dump(source, level+1)
|
||||||
|
}
|
||||||
|
if n.HasClosure() {
|
||||||
|
cl := n.ClosureLine
|
||||||
|
fmt.Printf("%sClosure: \"%s\"\n", indent2, string(cl.Value(source)))
|
||||||
|
}
|
||||||
|
fmt.Printf("%s}\n", indent)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewHTMLBlock returns a new HTMLBlock node.
|
||||||
|
func NewHTMLBlock(typ HTMLBlockType) *HTMLBlock {
|
||||||
|
return &HTMLBlock{
|
||||||
|
BaseBlock: BaseBlock{},
|
||||||
|
HTMLBlockType: typ,
|
||||||
|
ClosureLine: textm.NewSegment(-1, -1),
|
||||||
|
}
|
||||||
|
}
|
||||||
371
ast/inline.go
Normal file
371
ast/inline.go
Normal file
|
|
@ -0,0 +1,371 @@
|
||||||
|
package ast
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
textm "github.com/yuin/goldmark/text"
|
||||||
|
"github.com/yuin/goldmark/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
// A BaseInline struct implements the Node interface.
|
||||||
|
type BaseInline struct {
|
||||||
|
BaseNode
|
||||||
|
}
|
||||||
|
|
||||||
|
// Type implements Node.Type
|
||||||
|
func (b *BaseInline) Type() NodeType {
|
||||||
|
return InlineNode
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsRaw implements Node.IsRaw
|
||||||
|
func (b *BaseInline) IsRaw() bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// HasBlankPreviousLines implements Node.HasBlankPreviousLines.
|
||||||
|
func (b *BaseInline) HasBlankPreviousLines() bool {
|
||||||
|
panic("can not call with inline nodes.")
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetBlankPreviousLines implements Node.SetBlankPreviousLines.
|
||||||
|
func (b *BaseInline) SetBlankPreviousLines(v bool) {
|
||||||
|
panic("can not call with inline nodes.")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lines implements Node.Lines
|
||||||
|
func (b *BaseInline) Lines() *textm.Segments {
|
||||||
|
panic("can not call with inline nodes.")
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetLines implements Node.SetLines
|
||||||
|
func (b *BaseInline) SetLines(v *textm.Segments) {
|
||||||
|
panic("can not call with inline nodes.")
|
||||||
|
}
|
||||||
|
|
||||||
|
// A Text struct represents a textual content of the Markdown text.
|
||||||
|
type Text struct {
|
||||||
|
BaseInline
|
||||||
|
// Segment is a position in a source text.
|
||||||
|
Segment textm.Segment
|
||||||
|
|
||||||
|
flags uint8
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
textSoftLineBreak = 1 << iota
|
||||||
|
textHardLineBreak
|
||||||
|
textRaw
|
||||||
|
)
|
||||||
|
|
||||||
|
// Inline implements Inline.Inline.
|
||||||
|
func (n *Text) Inline() {
|
||||||
|
}
|
||||||
|
|
||||||
|
// SoftLineBreak returns true if this node ends with a new line,
|
||||||
|
// otherwise false.
|
||||||
|
func (n *Text) SoftLineBreak() bool {
|
||||||
|
return n.flags&textSoftLineBreak != 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetSoftLineBreak sets whether this node ends with a new line.
|
||||||
|
func (n *Text) SetSoftLineBreak(v bool) {
|
||||||
|
if v {
|
||||||
|
n.flags |= textSoftLineBreak
|
||||||
|
} else {
|
||||||
|
n.flags = n.flags &^ textHardLineBreak
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsRaw returns true if this text should be rendered without unescaping
|
||||||
|
// back slash escapes and resolving references.
|
||||||
|
func (n *Text) IsRaw() bool {
|
||||||
|
return n.flags&textRaw != 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetRaw sets whether this text should be rendered as raw contents.
|
||||||
|
func (n *Text) SetRaw(v bool) {
|
||||||
|
if v {
|
||||||
|
n.flags |= textRaw
|
||||||
|
} else {
|
||||||
|
n.flags = n.flags &^ textRaw
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// HardLineBreak returns true if this node ends with a hard line break.
|
||||||
|
// See https://spec.commonmark.org/0.29/#hard-line-breaks for details.
|
||||||
|
func (n *Text) HardLineBreak() bool {
|
||||||
|
return n.flags&textHardLineBreak != 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetHardLineBreak sets whether this node ends with a hard line break.
|
||||||
|
func (n *Text) SetHardLineBreak(v bool) {
|
||||||
|
if v {
|
||||||
|
n.flags |= textHardLineBreak
|
||||||
|
} else {
|
||||||
|
n.flags = n.flags &^ textHardLineBreak
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Merge merges a Node n into this node.
|
||||||
|
// Merge returns true if given node has been merged, otherwise false.
|
||||||
|
func (n *Text) Merge(node Node, source []byte) bool {
|
||||||
|
t, ok := node.(*Text)
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if n.Segment.Stop != t.Segment.Start || t.Segment.Padding != 0 || source[n.Segment.Stop-1] == '\n' || t.IsRaw() != n.IsRaw() {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
n.Segment.Stop = t.Segment.Stop
|
||||||
|
n.SetSoftLineBreak(t.SoftLineBreak())
|
||||||
|
n.SetHardLineBreak(t.HardLineBreak())
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Text implements Node.Text.
|
||||||
|
func (n *Text) Text(source []byte) []byte {
|
||||||
|
return n.Segment.Value(source)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dump implements Node.Dump.
|
||||||
|
func (n *Text) Dump(source []byte, level int) {
|
||||||
|
fmt.Printf("%sText: \"%s\"\n", strings.Repeat(" ", level), strings.TrimRight(string(n.Text(source)), "\n"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewText returns a new Text node.
|
||||||
|
func NewText() *Text {
|
||||||
|
return &Text{
|
||||||
|
BaseInline: BaseInline{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewTextSegment returns a new Text node with given source potision.
|
||||||
|
func NewTextSegment(v textm.Segment) *Text {
|
||||||
|
return &Text{
|
||||||
|
BaseInline: BaseInline{},
|
||||||
|
Segment: v,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewRawTextSegment returns a new Text node with given source position.
|
||||||
|
// The new node should be rendered as raw contents.
|
||||||
|
func NewRawTextSegment(v textm.Segment) *Text {
|
||||||
|
t := &Text{
|
||||||
|
BaseInline: BaseInline{},
|
||||||
|
Segment: v,
|
||||||
|
}
|
||||||
|
t.SetRaw(true)
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
|
||||||
|
// MergeOrAppendTextSegment merges a given s into the last child of the parent if
|
||||||
|
// it can be merged, otherwise creates a new Text node and appends it to after current
|
||||||
|
// last child.
|
||||||
|
func MergeOrAppendTextSegment(parent Node, s textm.Segment) {
|
||||||
|
last := parent.LastChild()
|
||||||
|
t, ok := last.(*Text)
|
||||||
|
if ok && t.Segment.Stop == s.Start && !t.SoftLineBreak() {
|
||||||
|
ts := t.Segment
|
||||||
|
t.Segment = ts.WithStop(s.Stop)
|
||||||
|
} else {
|
||||||
|
parent.AppendChild(parent, NewTextSegment(s))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MergeOrReplaceTextSegment merges a given s into a previous sibling of the node n
|
||||||
|
// if a previous sibling of the node n is *Text, otherwise replaces Node n with s.
|
||||||
|
func MergeOrReplaceTextSegment(parent Node, n Node, s textm.Segment) {
|
||||||
|
prev := n.PreviousSibling()
|
||||||
|
if t, ok := prev.(*Text); ok && t.Segment.Stop == s.Start && !t.SoftLineBreak() {
|
||||||
|
t.Segment = t.Segment.WithStop(s.Stop)
|
||||||
|
parent.RemoveChild(parent, n)
|
||||||
|
} else {
|
||||||
|
parent.ReplaceChild(parent, n, NewTextSegment(s))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A CodeSpan struct represents a code span of Markdown text.
|
||||||
|
type CodeSpan struct {
|
||||||
|
BaseInline
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inline implements Inline.Inline .
|
||||||
|
func (n *CodeSpan) Inline() {
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsBlank returns true if this node consists of spaces, otherwise false.
|
||||||
|
func (n *CodeSpan) IsBlank(source []byte) bool {
|
||||||
|
for c := n.FirstChild(); c != nil; c = c.NextSibling() {
|
||||||
|
text := c.(*Text).Segment
|
||||||
|
if !util.IsBlank(text.Value(source)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dump implements Node.Dump
|
||||||
|
func (n *CodeSpan) Dump(source []byte, level int) {
|
||||||
|
DumpHelper(n, source, level, "CodeSpan", nil, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewCodeSpan returns a new CodeSpan node.
|
||||||
|
func NewCodeSpan() *CodeSpan {
|
||||||
|
return &CodeSpan{
|
||||||
|
BaseInline: BaseInline{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// An Emphasis struct represents an emphasis of Markdown text.
|
||||||
|
type Emphasis struct {
|
||||||
|
BaseInline
|
||||||
|
|
||||||
|
// Level is a level of the emphasis.
|
||||||
|
Level int
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inline implements Inline.Inline.
|
||||||
|
func (n *Emphasis) Inline() {
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dump implements Node.Dump.
|
||||||
|
func (n *Emphasis) Dump(source []byte, level int) {
|
||||||
|
DumpHelper(n, source, level, fmt.Sprintf("Emphasis(%d)", n.Level), nil, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewEmphasis returns a new Emphasis node with given level.
|
||||||
|
func NewEmphasis(level int) *Emphasis {
|
||||||
|
return &Emphasis{
|
||||||
|
BaseInline: BaseInline{},
|
||||||
|
Level: level,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type baseLink struct {
|
||||||
|
BaseInline
|
||||||
|
|
||||||
|
// Destination is a destination(URL) of this link.
|
||||||
|
Destination []byte
|
||||||
|
|
||||||
|
// Title is a title of this link.
|
||||||
|
Title []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inline implements Inline.Inline.
|
||||||
|
func (n *baseLink) Inline() {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *baseLink) Dump(source []byte, level int) {
|
||||||
|
m := map[string]string{}
|
||||||
|
m["Destination"] = string(n.Destination)
|
||||||
|
m["Title"] = string(n.Title)
|
||||||
|
DumpHelper(n, source, level, "Link", m, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// A Link struct represents a link of the Markdown text.
|
||||||
|
type Link struct {
|
||||||
|
baseLink
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewLink returns a new Link node.
|
||||||
|
func NewLink() *Link {
|
||||||
|
c := &Link{
|
||||||
|
baseLink: baseLink{
|
||||||
|
BaseInline: BaseInline{},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// An Image struct represents an image of the Markdown text.
|
||||||
|
type Image struct {
|
||||||
|
baseLink
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dump implements Node.Dump.
|
||||||
|
func (n *Image) Dump(source []byte, level int) {
|
||||||
|
m := map[string]string{}
|
||||||
|
m["Destination"] = string(n.Destination)
|
||||||
|
m["Title"] = string(n.Title)
|
||||||
|
DumpHelper(n, source, level, "Image", m, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewImage returns a new Image node.
|
||||||
|
func NewImage(link *Link) *Image {
|
||||||
|
c := &Image{
|
||||||
|
baseLink: baseLink{
|
||||||
|
BaseInline: BaseInline{},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
c.Destination = link.Destination
|
||||||
|
c.Title = link.Title
|
||||||
|
for n := link.FirstChild(); n != nil; {
|
||||||
|
next := n.NextSibling()
|
||||||
|
link.RemoveChild(link, n)
|
||||||
|
c.AppendChild(c, n)
|
||||||
|
n = next
|
||||||
|
}
|
||||||
|
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// AutoLinkType defines kind of auto links.
|
||||||
|
type AutoLinkType int
|
||||||
|
|
||||||
|
const (
|
||||||
|
// AutoLinkEmail indicates that an autolink is an email address.
|
||||||
|
AutoLinkEmail = iota + 1
|
||||||
|
// AutoLinkURL indicates that an autolink is a generic URL.
|
||||||
|
AutoLinkURL
|
||||||
|
)
|
||||||
|
|
||||||
|
// An AutoLink struct represents an autolink of the Markdown text.
|
||||||
|
type AutoLink struct {
|
||||||
|
BaseInline
|
||||||
|
// Value is a link text of this node.
|
||||||
|
Value *Text
|
||||||
|
// Type is a type of this autolink.
|
||||||
|
AutoLinkType AutoLinkType
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inline implements Inline.Inline.
|
||||||
|
func (n *AutoLink) Inline() {}
|
||||||
|
|
||||||
|
// Dump implenets Node.Dump
|
||||||
|
func (n *AutoLink) Dump(source []byte, level int) {
|
||||||
|
segment := n.Value.Segment
|
||||||
|
m := map[string]string{
|
||||||
|
"Value": string(segment.Value(source)),
|
||||||
|
}
|
||||||
|
DumpHelper(n, source, level, "AutoLink", m, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewAutoLink returns a new AutoLink node.
|
||||||
|
func NewAutoLink(typ AutoLinkType, value *Text) *AutoLink {
|
||||||
|
return &AutoLink{
|
||||||
|
BaseInline: BaseInline{},
|
||||||
|
Value: value,
|
||||||
|
AutoLinkType: typ,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A RawHTML struct represents an inline raw HTML of the Markdown text.
|
||||||
|
type RawHTML struct {
|
||||||
|
BaseInline
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inline implements Inline.Inline.
|
||||||
|
func (n *RawHTML) Inline() {}
|
||||||
|
|
||||||
|
// Dump implements Node.Dump.
|
||||||
|
func (n *RawHTML) Dump(source []byte, level int) {
|
||||||
|
DumpHelper(n, source, level, "RawHTML", nil, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewRawHTML returns a new RawHTML node.
|
||||||
|
func NewRawHTML() *RawHTML {
|
||||||
|
return &RawHTML{
|
||||||
|
BaseInline: BaseInline{},
|
||||||
|
}
|
||||||
|
}
|
||||||
53
commonmark_test.go
Normal file
53
commonmark_test.go
Normal file
|
|
@ -0,0 +1,53 @@
|
||||||
|
package goldmark
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"io/ioutil"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/yuin/goldmark/renderer/html"
|
||||||
|
)
|
||||||
|
|
||||||
|
type commonmarkSpecTestCase struct {
|
||||||
|
Markdown string `json:"markdown"`
|
||||||
|
HTML string `json:"html"`
|
||||||
|
Example int `json:"example"`
|
||||||
|
StartLine int `json:"start_line"`
|
||||||
|
EndLine int `json:"end_line"`
|
||||||
|
Section string `json:"section"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSpec(t *testing.T) {
|
||||||
|
bs, err := ioutil.ReadFile("_test/spec.json")
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
var testCases []commonmarkSpecTestCase
|
||||||
|
if err := json.Unmarshal(bs, &testCases); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
markdown := New(WithRendererOptions(html.WithXHTML()))
|
||||||
|
for _, testCase := range testCases {
|
||||||
|
var out bytes.Buffer
|
||||||
|
if err := markdown.Convert([]byte(testCase.Markdown), &out); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
if !bytes.Equal(bytes.TrimSpace(out.Bytes()), bytes.TrimSpace([]byte(testCase.HTML))) {
|
||||||
|
format := `============= case %d ================
|
||||||
|
Markdown:
|
||||||
|
-----------
|
||||||
|
%s
|
||||||
|
|
||||||
|
Expected:
|
||||||
|
----------
|
||||||
|
%s
|
||||||
|
|
||||||
|
Actual
|
||||||
|
---------
|
||||||
|
%s
|
||||||
|
`
|
||||||
|
t.Errorf(format, testCase.Example, testCase.Markdown, testCase.HTML, out.Bytes())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
23
extension/ast/strikethrough.go
Normal file
23
extension/ast/strikethrough.go
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
// Package ast defines AST nodes that represents extension's elements
|
||||||
|
package ast
|
||||||
|
|
||||||
|
import (
|
||||||
|
gast "github.com/yuin/goldmark/ast"
|
||||||
|
)
|
||||||
|
|
||||||
|
// A Strikethrough struct represents a strikethrough of GFM text.
|
||||||
|
type Strikethrough struct {
|
||||||
|
gast.BaseInline
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *Strikethrough) Inline() {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *Strikethrough) Dump(source []byte, level int) {
|
||||||
|
gast.DumpHelper(n, source, level, "Strikethrough", nil, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewStrikethrough returns a new Strikethrough node.
|
||||||
|
func NewStrikethrough() *Strikethrough {
|
||||||
|
return &Strikethrough{}
|
||||||
|
}
|
||||||
113
extension/ast/table.go
Normal file
113
extension/ast/table.go
Normal file
|
|
@ -0,0 +1,113 @@
|
||||||
|
package ast
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
gast "github.com/yuin/goldmark/ast"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Alignment is a text alignment of table cells.
|
||||||
|
type Alignment int
|
||||||
|
|
||||||
|
const (
|
||||||
|
// AlignLeft indicates text should be left justified.
|
||||||
|
AlignLeft Alignment = iota + 1
|
||||||
|
|
||||||
|
// AlignRight indicates text should be right justified.
|
||||||
|
AlignRight
|
||||||
|
|
||||||
|
// AlignCenter indicates text should be centered.
|
||||||
|
AlignCenter
|
||||||
|
|
||||||
|
// AlignNone indicates text should be aligned by default manner.
|
||||||
|
AlignNone
|
||||||
|
)
|
||||||
|
|
||||||
|
func (a Alignment) String() string {
|
||||||
|
switch a {
|
||||||
|
case AlignLeft:
|
||||||
|
return "left"
|
||||||
|
case AlignRight:
|
||||||
|
return "right"
|
||||||
|
case AlignCenter:
|
||||||
|
return "center"
|
||||||
|
case AlignNone:
|
||||||
|
return "none"
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// A Table struct represents a table of Markdown(GFM) text.
|
||||||
|
type Table struct {
|
||||||
|
gast.BaseBlock
|
||||||
|
|
||||||
|
// Alignments returns alignments of the columns.
|
||||||
|
Alignments []Alignment
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dump implements Node.Dump
|
||||||
|
func (n *Table) Dump(source []byte, level int) {
|
||||||
|
gast.DumpHelper(n, source, level, "Table", nil, func(level int) {
|
||||||
|
indent := strings.Repeat(" ", level)
|
||||||
|
fmt.Printf("%sAlignments {\n", indent)
|
||||||
|
for i, alignment := range n.Alignments {
|
||||||
|
indent2 := strings.Repeat(" ", level+1)
|
||||||
|
fmt.Printf("%s%s", indent2, alignment.String())
|
||||||
|
if i != len(n.Alignments)-1 {
|
||||||
|
fmt.Println("")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fmt.Printf("\n%s}\n", indent)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewTable returns a new Table node.
|
||||||
|
func NewTable() *Table {
|
||||||
|
return &Table{
|
||||||
|
Alignments: []Alignment{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A TableRow struct represents a table row of Markdown(GFM) text.
|
||||||
|
type TableRow struct {
|
||||||
|
gast.BaseBlock
|
||||||
|
Alignments []Alignment
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dump implements Node.Dump.
|
||||||
|
func (n *TableRow) Dump(source []byte, level int) {
|
||||||
|
gast.DumpHelper(n, source, level, "TableRow", nil, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewTableRow returns a new TableRow node.
|
||||||
|
func NewTableRow(alignments []Alignment) *TableRow {
|
||||||
|
return &TableRow{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A TableHeader struct represents a table header of Markdown(GFM) text.
|
||||||
|
type TableHeader struct {
|
||||||
|
*TableRow
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewTableHeader returns a new TableHeader node.
|
||||||
|
func NewTableHeader(row *TableRow) *TableHeader {
|
||||||
|
return &TableHeader{row}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A TableCell struct represents a table cell of a Markdown(GFM) text.
|
||||||
|
type TableCell struct {
|
||||||
|
gast.BaseBlock
|
||||||
|
Alignment Alignment
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dump implements Node.Dump.
|
||||||
|
func (n *TableCell) Dump(source []byte, level int) {
|
||||||
|
gast.DumpHelper(n, source, level, "TableCell", nil, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewTableCell returns a new TableCell node.
|
||||||
|
func NewTableCell() *TableCell {
|
||||||
|
return &TableCell{
|
||||||
|
Alignment: AlignNone,
|
||||||
|
}
|
||||||
|
}
|
||||||
27
extension/ast/tasklist.go
Normal file
27
extension/ast/tasklist.go
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
package ast
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
gast "github.com/yuin/goldmark/ast"
|
||||||
|
)
|
||||||
|
|
||||||
|
// A TaskCheckBox struct represents a checkbox of a task list.
|
||||||
|
type TaskCheckBox struct {
|
||||||
|
gast.BaseInline
|
||||||
|
IsChecked bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dump impelemtns Node.Dump.
|
||||||
|
func (n *TaskCheckBox) Dump(source []byte, level int) {
|
||||||
|
m := map[string]string{
|
||||||
|
"Checked": fmt.Sprintf("%v", n.IsChecked),
|
||||||
|
}
|
||||||
|
gast.DumpHelper(n, source, level, "TaskCheckBox", m, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewTaskCheckBox returns a new TaskCheckBox node.
|
||||||
|
func NewTaskCheckBox(checked bool) *TaskCheckBox {
|
||||||
|
return &TaskCheckBox{
|
||||||
|
IsChecked: checked,
|
||||||
|
}
|
||||||
|
}
|
||||||
32
extension/gfm.go
Normal file
32
extension/gfm.go
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
package extension
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/yuin/goldmark"
|
||||||
|
"github.com/yuin/goldmark/parser"
|
||||||
|
)
|
||||||
|
|
||||||
|
type gfm struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
// GFM is an extension that provides Github Flavored markdown functionalities.
|
||||||
|
var GFM = &gfm{}
|
||||||
|
|
||||||
|
var filterTags = []string{
|
||||||
|
"title",
|
||||||
|
"textarea",
|
||||||
|
"style",
|
||||||
|
"xmp",
|
||||||
|
"iframe",
|
||||||
|
"noembed",
|
||||||
|
"noframes",
|
||||||
|
"script",
|
||||||
|
"plaintext",
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *gfm) Extend(m goldmark.Markdown) {
|
||||||
|
m.Parser().AddOption(parser.WithFilterTags(filterTags...))
|
||||||
|
Linkify.Extend(m)
|
||||||
|
Table.Extend(m)
|
||||||
|
Strikethrough.Extend(m)
|
||||||
|
TaskList.Extend(m)
|
||||||
|
}
|
||||||
125
extension/linkify.go
Normal file
125
extension/linkify.go
Normal file
|
|
@ -0,0 +1,125 @@
|
||||||
|
package extension
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"github.com/yuin/goldmark"
|
||||||
|
"github.com/yuin/goldmark/ast"
|
||||||
|
"github.com/yuin/goldmark/parser"
|
||||||
|
"github.com/yuin/goldmark/text"
|
||||||
|
"github.com/yuin/goldmark/util"
|
||||||
|
"regexp"
|
||||||
|
)
|
||||||
|
|
||||||
|
var wwwURLRegxp = regexp.MustCompile(`^www\.[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,6}\b(?:[-a-zA-Z0-9@:%_\+.~#?&//=\(\);]*)`)
|
||||||
|
|
||||||
|
var urlRegexp = regexp.MustCompile(`^(?:http|https|ftp):\/\/(?:www\.)?[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_\+.~#?&//=\(\);]*)`)
|
||||||
|
|
||||||
|
var emailRegexp = regexp.MustCompile(`^[a-zA-Z0-9\.\-_\+]+@([a-zA-Z0-9\.\-_]+)`)
|
||||||
|
|
||||||
|
type linkifyParser struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
var defaultLinkifyParser = &linkifyParser{}
|
||||||
|
|
||||||
|
// NewLinkifyParser return a new InlineParser can parse
|
||||||
|
// text that seems like a URL.
|
||||||
|
func NewLinkifyParser() parser.InlineParser {
|
||||||
|
return defaultLinkifyParser
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *linkifyParser) Trigger() []byte {
|
||||||
|
// ' ' indicates any white spaces and a line head
|
||||||
|
return []byte{' ', '*', '_', '~', '('}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *linkifyParser) Parse(parent ast.Node, block text.Reader, pc parser.Context) ast.Node {
|
||||||
|
line, segment := block.PeekLine()
|
||||||
|
consumes := 0
|
||||||
|
start := segment.Start
|
||||||
|
c := line[0]
|
||||||
|
// advance if current position is not a line head.
|
||||||
|
if c == ' ' || c == '*' || c == '_' || c == '~' || c == '(' {
|
||||||
|
consumes++
|
||||||
|
start++
|
||||||
|
line = line[1:]
|
||||||
|
}
|
||||||
|
|
||||||
|
var m []int
|
||||||
|
typ := ast.AutoLinkType(ast.AutoLinkEmail)
|
||||||
|
typ = ast.AutoLinkURL
|
||||||
|
m = urlRegexp.FindSubmatchIndex(line)
|
||||||
|
if m == nil {
|
||||||
|
m = wwwURLRegxp.FindSubmatchIndex(line)
|
||||||
|
}
|
||||||
|
if m != nil {
|
||||||
|
lastChar := line[m[1]-1]
|
||||||
|
if lastChar == '.' {
|
||||||
|
m[1]--
|
||||||
|
} else if lastChar == ')' {
|
||||||
|
closing := 0
|
||||||
|
for i := m[1] - 1; i >= m[0]; i-- {
|
||||||
|
if line[i] == ')' {
|
||||||
|
closing++
|
||||||
|
} else if line[i] == '(' {
|
||||||
|
closing--
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if closing > 0 {
|
||||||
|
m[1]--
|
||||||
|
}
|
||||||
|
} else if lastChar == ';' {
|
||||||
|
i := m[1] - 2
|
||||||
|
for ; i >= m[0]; i-- {
|
||||||
|
if util.IsAlphaNumeric(line[i]) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if i != m[1]-2 {
|
||||||
|
if line[i] == '&' {
|
||||||
|
m[1] -= m[1] - i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if m == nil {
|
||||||
|
typ = ast.AutoLinkEmail
|
||||||
|
m = emailRegexp.FindSubmatchIndex(line)
|
||||||
|
if m == nil || bytes.IndexByte(line[m[2]:m[3]], '.') < 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
lastChar := line[m[1]-1]
|
||||||
|
if lastChar == '.' {
|
||||||
|
m[1]--
|
||||||
|
} else if lastChar == '-' || lastChar == '_' {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if m == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if consumes != 0 {
|
||||||
|
s := segment.WithStop(segment.Start + 1)
|
||||||
|
ast.MergeOrAppendTextSegment(parent, s)
|
||||||
|
}
|
||||||
|
consumes += m[1]
|
||||||
|
block.Advance(consumes)
|
||||||
|
n := ast.NewTextSegment(text.NewSegment(start, start+m[1]))
|
||||||
|
return ast.NewAutoLink(typ, n)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *linkifyParser) CloseBlock(parent ast.Node, pc parser.Context) {
|
||||||
|
// nothing to do
|
||||||
|
}
|
||||||
|
|
||||||
|
type linkify struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
// Linkify is an extension that allow you to parse text that seems like a URL.
|
||||||
|
var Linkify = &linkify{}
|
||||||
|
|
||||||
|
func (e *linkify) Extend(m goldmark.Markdown) {
|
||||||
|
m.Parser().AddOption(parser.WithInlineParsers(
|
||||||
|
util.Prioritized(NewLinkifyParser(), 999),
|
||||||
|
))
|
||||||
|
}
|
||||||
111
extension/strikethrough.go
Normal file
111
extension/strikethrough.go
Normal file
|
|
@ -0,0 +1,111 @@
|
||||||
|
package extension
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/yuin/goldmark"
|
||||||
|
gast "github.com/yuin/goldmark/ast"
|
||||||
|
"github.com/yuin/goldmark/extension/ast"
|
||||||
|
"github.com/yuin/goldmark/parser"
|
||||||
|
"github.com/yuin/goldmark/renderer"
|
||||||
|
"github.com/yuin/goldmark/renderer/html"
|
||||||
|
"github.com/yuin/goldmark/text"
|
||||||
|
"github.com/yuin/goldmark/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
type strikethroughDelimiterProcessor struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *strikethroughDelimiterProcessor) IsDelimiter(b byte) bool {
|
||||||
|
return b == '~'
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *strikethroughDelimiterProcessor) CanOpenCloser(opener, closer *parser.Delimiter) bool {
|
||||||
|
return opener.Char == closer.Char
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *strikethroughDelimiterProcessor) OnMatch(consumes int) gast.Node {
|
||||||
|
return ast.NewStrikethrough()
|
||||||
|
}
|
||||||
|
|
||||||
|
var defaultStrikethroughDelimiterProcessor = &strikethroughDelimiterProcessor{}
|
||||||
|
|
||||||
|
type strikethroughParser struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
var defaultStrikethroughParser = &strikethroughParser{}
|
||||||
|
|
||||||
|
// NewStrikethroughParser return a new InlineParser that parses
|
||||||
|
// strikethrough expressions.
|
||||||
|
func NewStrikethroughParser() parser.InlineParser {
|
||||||
|
return defaultStrikethroughParser
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *strikethroughParser) Trigger() []byte {
|
||||||
|
return []byte{'~'}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *strikethroughParser) Parse(parent gast.Node, block text.Reader, pc parser.Context) gast.Node {
|
||||||
|
before := block.PrecendingCharacter()
|
||||||
|
line, segment := block.PeekLine()
|
||||||
|
node := parser.ScanDelimiter(line, before, 2, defaultStrikethroughDelimiterProcessor)
|
||||||
|
if node == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
node.Segment = segment.WithStop(segment.Start + node.OriginalLength)
|
||||||
|
block.Advance(node.OriginalLength)
|
||||||
|
pc.PushDelimiter(node)
|
||||||
|
return node
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *strikethroughParser) CloseBlock(parent gast.Node, pc parser.Context) {
|
||||||
|
// nothing to do
|
||||||
|
}
|
||||||
|
|
||||||
|
// StrikethroughHTMLRenderer is a renderer.NodeRenderer implementation that
|
||||||
|
// renders Strikethrough nodes.
|
||||||
|
type StrikethroughHTMLRenderer struct {
|
||||||
|
html.Config
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewStrikethroughHTMLRenderer returns a new StrikethroughHTMLRenderer.
|
||||||
|
func NewStrikethroughHTMLRenderer(opts ...html.Option) renderer.NodeRenderer {
|
||||||
|
r := &StrikethroughHTMLRenderer{
|
||||||
|
Config: html.NewConfig(),
|
||||||
|
}
|
||||||
|
for _, opt := range opts {
|
||||||
|
opt.SetHTMLOption(&r.Config)
|
||||||
|
}
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render implements renderer.NodeRenderer.Render.
|
||||||
|
func (r *StrikethroughHTMLRenderer) Render(writer util.BufWriter, source []byte, n gast.Node, entering bool) (gast.WalkStatus, error) {
|
||||||
|
switch node := n.(type) {
|
||||||
|
case *ast.Strikethrough:
|
||||||
|
return r.renderStrikethrough(writer, source, node, entering), nil
|
||||||
|
}
|
||||||
|
return gast.WalkContinue, renderer.NotSupported
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *StrikethroughHTMLRenderer) renderStrikethrough(w util.BufWriter, source []byte, n *ast.Strikethrough, entering bool) gast.WalkStatus {
|
||||||
|
if entering {
|
||||||
|
w.WriteString("<del>")
|
||||||
|
} else {
|
||||||
|
w.WriteString("</del>")
|
||||||
|
}
|
||||||
|
return gast.WalkContinue
|
||||||
|
}
|
||||||
|
|
||||||
|
type strikethrough struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
// Strikethrough is an extension that allow you to use strikethrough expression like '~~text~~' .
|
||||||
|
var Strikethrough = &strikethrough{}
|
||||||
|
|
||||||
|
func (e *strikethrough) Extend(m goldmark.Markdown) {
|
||||||
|
m.Parser().AddOption(parser.WithInlineParsers(
|
||||||
|
util.Prioritized(NewStrikethroughParser(), 500),
|
||||||
|
))
|
||||||
|
m.Renderer().AddOption(renderer.WithNodeRenderers(
|
||||||
|
util.Prioritized(NewStrikethroughHTMLRenderer(), 500),
|
||||||
|
))
|
||||||
|
}
|
||||||
233
extension/table.go
Normal file
233
extension/table.go
Normal file
|
|
@ -0,0 +1,233 @@
|
||||||
|
package extension
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"regexp"
|
||||||
|
|
||||||
|
"github.com/yuin/goldmark"
|
||||||
|
gast "github.com/yuin/goldmark/ast"
|
||||||
|
"github.com/yuin/goldmark/extension/ast"
|
||||||
|
"github.com/yuin/goldmark/parser"
|
||||||
|
"github.com/yuin/goldmark/renderer"
|
||||||
|
"github.com/yuin/goldmark/renderer/html"
|
||||||
|
"github.com/yuin/goldmark/text"
|
||||||
|
"github.com/yuin/goldmark/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
var tableDelimRegexp = regexp.MustCompile(`^[\s\-\|\:]+$`)
|
||||||
|
var tableDelimLeft = regexp.MustCompile(`^\s*\:\-+\s*$`)
|
||||||
|
var tableDelimRight = regexp.MustCompile(`^\s*\-+\:\s*$`)
|
||||||
|
var tableDelimCenter = regexp.MustCompile(`^\s*\:\-+\:\s*$`)
|
||||||
|
var tableDelimNone = regexp.MustCompile(`^\s*\-+\s*$`)
|
||||||
|
|
||||||
|
type tableParagraphTransformer struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
var defaultTableParagraphTransformer = &tableParagraphTransformer{}
|
||||||
|
|
||||||
|
// NewTableParagraphTransformer returns a new ParagraphTransformer
|
||||||
|
// that can transform pargraphs into tables.
|
||||||
|
func NewTableParagraphTransformer() parser.ParagraphTransformer {
|
||||||
|
return defaultTableParagraphTransformer
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *tableParagraphTransformer) Transform(node *gast.Paragraph, pc parser.Context) {
|
||||||
|
lines := node.Lines()
|
||||||
|
if lines.Len() < 2 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
alignments := b.parseDelimiter(lines.At(1), pc)
|
||||||
|
if alignments == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
header := b.parseRow(lines.At(0), alignments, pc)
|
||||||
|
if header == nil || len(alignments) != header.ChildCount() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
table := ast.NewTable()
|
||||||
|
table.Alignments = alignments
|
||||||
|
table.AppendChild(table, ast.NewTableHeader(header))
|
||||||
|
if lines.Len() > 2 {
|
||||||
|
for i := 2; i < lines.Len(); i++ {
|
||||||
|
table.AppendChild(table, b.parseRow(lines.At(i), alignments, pc))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
node.Parent().InsertBefore(node.Parent(), node, table)
|
||||||
|
node.Parent().RemoveChild(node.Parent(), node)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *tableParagraphTransformer) parseRow(segment text.Segment, alignments []ast.Alignment, pc parser.Context) *ast.TableRow {
|
||||||
|
line := segment.Value(pc.Source())
|
||||||
|
pos := 0
|
||||||
|
pos += util.TrimLeftSpaceLength(line)
|
||||||
|
limit := len(line)
|
||||||
|
limit -= util.TrimRightSpaceLength(line)
|
||||||
|
row := ast.NewTableRow(alignments)
|
||||||
|
if len(line) > 0 && line[pos] == '|' {
|
||||||
|
pos++
|
||||||
|
}
|
||||||
|
if len(line) > 0 && line[limit-1] == '|' {
|
||||||
|
limit--
|
||||||
|
}
|
||||||
|
for i := 0; pos < limit; i++ {
|
||||||
|
closure := util.FindClosure(line[pos:], byte(0), '|', true, false)
|
||||||
|
if closure < 0 {
|
||||||
|
closure = len(line[pos:])
|
||||||
|
}
|
||||||
|
node := ast.NewTableCell()
|
||||||
|
segment := text.NewSegment(segment.Start+pos, segment.Start+pos+closure)
|
||||||
|
segment = segment.TrimLeftSpace(pc.Source())
|
||||||
|
segment = segment.TrimRightSpace(pc.Source())
|
||||||
|
node.Lines().Append(segment)
|
||||||
|
node.Alignment = alignments[i]
|
||||||
|
row.AppendChild(row, node)
|
||||||
|
pos += closure + 1
|
||||||
|
}
|
||||||
|
return row
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *tableParagraphTransformer) parseDelimiter(segment text.Segment, pc parser.Context) []ast.Alignment {
|
||||||
|
line := segment.Value(pc.Source())
|
||||||
|
if !tableDelimRegexp.Match(line) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
cols := bytes.Split(line, []byte{'|'})
|
||||||
|
if util.IsBlank(cols[0]) {
|
||||||
|
cols = cols[1:]
|
||||||
|
}
|
||||||
|
if len(cols) > 0 && util.IsBlank(cols[len(cols)-1]) {
|
||||||
|
cols = cols[:len(cols)-1]
|
||||||
|
}
|
||||||
|
|
||||||
|
var alignments []ast.Alignment
|
||||||
|
for _, col := range cols {
|
||||||
|
if tableDelimLeft.Match(col) {
|
||||||
|
if alignments == nil {
|
||||||
|
alignments = []ast.Alignment{}
|
||||||
|
}
|
||||||
|
alignments = append(alignments, ast.AlignLeft)
|
||||||
|
} else if tableDelimRight.Match(col) {
|
||||||
|
if alignments == nil {
|
||||||
|
alignments = []ast.Alignment{}
|
||||||
|
}
|
||||||
|
alignments = append(alignments, ast.AlignRight)
|
||||||
|
} else if tableDelimCenter.Match(col) {
|
||||||
|
if alignments == nil {
|
||||||
|
alignments = []ast.Alignment{}
|
||||||
|
}
|
||||||
|
alignments = append(alignments, ast.AlignCenter)
|
||||||
|
} else if tableDelimNone.Match(col) {
|
||||||
|
if alignments == nil {
|
||||||
|
alignments = []ast.Alignment{}
|
||||||
|
}
|
||||||
|
alignments = append(alignments, ast.AlignNone)
|
||||||
|
} else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return alignments
|
||||||
|
}
|
||||||
|
|
||||||
|
// TableHTMLRenderer is a renderer.NodeRenderer implementation that
|
||||||
|
// renders Table nodes.
|
||||||
|
type TableHTMLRenderer struct {
|
||||||
|
html.Config
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewTableHTMLRenderer returns a new TableHTMLRenderer.
|
||||||
|
func NewTableHTMLRenderer(opts ...html.Option) renderer.NodeRenderer {
|
||||||
|
r := &TableHTMLRenderer{
|
||||||
|
Config: html.NewConfig(),
|
||||||
|
}
|
||||||
|
for _, opt := range opts {
|
||||||
|
opt.SetHTMLOption(&r.Config)
|
||||||
|
}
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render implements renderer.Renderer.Render.
|
||||||
|
func (r *TableHTMLRenderer) Render(writer util.BufWriter, source []byte, n gast.Node, entering bool) (gast.WalkStatus, error) {
|
||||||
|
switch node := n.(type) {
|
||||||
|
case *ast.Table:
|
||||||
|
return r.renderTable(writer, source, node, entering), nil
|
||||||
|
case *ast.TableHeader:
|
||||||
|
return r.renderTableHeader(writer, source, node, entering), nil
|
||||||
|
case *ast.TableRow:
|
||||||
|
return r.renderTableRow(writer, source, node, entering), nil
|
||||||
|
case *ast.TableCell:
|
||||||
|
return r.renderTableCell(writer, source, node, entering), nil
|
||||||
|
}
|
||||||
|
return gast.WalkContinue, renderer.NotSupported
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *TableHTMLRenderer) renderTable(w util.BufWriter, source []byte, n *ast.Table, entering bool) gast.WalkStatus {
|
||||||
|
if entering {
|
||||||
|
w.WriteString("<table>\n")
|
||||||
|
} else {
|
||||||
|
w.WriteString("</table>\n")
|
||||||
|
}
|
||||||
|
return gast.WalkContinue
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *TableHTMLRenderer) renderTableHeader(w util.BufWriter, source []byte, n *ast.TableHeader, entering bool) gast.WalkStatus {
|
||||||
|
if entering {
|
||||||
|
w.WriteString("<thead>\n")
|
||||||
|
w.WriteString("<tr>\n")
|
||||||
|
} else {
|
||||||
|
w.WriteString("</tr>\n")
|
||||||
|
w.WriteString("</thead>\n")
|
||||||
|
if n.NextSibling() != nil {
|
||||||
|
w.WriteString("<tbody>\n")
|
||||||
|
}
|
||||||
|
if n.Parent().LastChild() == n {
|
||||||
|
w.WriteString("</tbody>\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return gast.WalkContinue
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *TableHTMLRenderer) renderTableRow(w util.BufWriter, source []byte, n *ast.TableRow, entering bool) gast.WalkStatus {
|
||||||
|
if entering {
|
||||||
|
w.WriteString("<tr>\n")
|
||||||
|
} else {
|
||||||
|
w.WriteString("</tr>\n")
|
||||||
|
if n.Parent().LastChild() == n {
|
||||||
|
w.WriteString("</tbody>\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return gast.WalkContinue
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *TableHTMLRenderer) renderTableCell(w util.BufWriter, source []byte, n *ast.TableCell, entering bool) gast.WalkStatus {
|
||||||
|
tag := "td"
|
||||||
|
if n.Parent().Parent().FirstChild() == n.Parent() {
|
||||||
|
tag = "th"
|
||||||
|
}
|
||||||
|
if entering {
|
||||||
|
align := ""
|
||||||
|
if n.Alignment != ast.AlignNone {
|
||||||
|
align = fmt.Sprintf(` align="%s"`, n.Alignment.String())
|
||||||
|
}
|
||||||
|
fmt.Fprintf(w, "<%s%s>", tag, align)
|
||||||
|
} else {
|
||||||
|
fmt.Fprintf(w, "</%s>\n", tag)
|
||||||
|
}
|
||||||
|
return gast.WalkContinue
|
||||||
|
}
|
||||||
|
|
||||||
|
type table struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
// Table is an extension that allow you to use GFM tables .
|
||||||
|
var Table = &table{}
|
||||||
|
|
||||||
|
func (e *table) Extend(m goldmark.Markdown) {
|
||||||
|
m.Parser().AddOption(parser.WithParagraphTransformers(
|
||||||
|
util.Prioritized(NewTableParagraphTransformer(), 200),
|
||||||
|
))
|
||||||
|
m.Renderer().AddOption(renderer.WithNodeRenderers(
|
||||||
|
util.Prioritized(NewTableHTMLRenderer(), 500),
|
||||||
|
))
|
||||||
|
}
|
||||||
118
extension/tasklist.go
Normal file
118
extension/tasklist.go
Normal file
|
|
@ -0,0 +1,118 @@
|
||||||
|
package extension
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/yuin/goldmark"
|
||||||
|
gast "github.com/yuin/goldmark/ast"
|
||||||
|
"github.com/yuin/goldmark/extension/ast"
|
||||||
|
"github.com/yuin/goldmark/parser"
|
||||||
|
"github.com/yuin/goldmark/renderer"
|
||||||
|
"github.com/yuin/goldmark/renderer/html"
|
||||||
|
"github.com/yuin/goldmark/text"
|
||||||
|
"github.com/yuin/goldmark/util"
|
||||||
|
"regexp"
|
||||||
|
)
|
||||||
|
|
||||||
|
var taskListRegexp = regexp.MustCompile(`^\[([\sxX])\]\s*`)
|
||||||
|
|
||||||
|
type taskCheckBoxParser struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
var defaultTaskCheckBoxParser = &taskCheckBoxParser{}
|
||||||
|
|
||||||
|
// NewTaskCheckBoxParser returns a new InlineParser that can parse
|
||||||
|
// checkboxes in list items.
|
||||||
|
// This parser must take precedence over the parser.LinkParser.
|
||||||
|
func NewTaskCheckBoxParser() parser.InlineParser {
|
||||||
|
return defaultTaskCheckBoxParser
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *taskCheckBoxParser) Trigger() []byte {
|
||||||
|
return []byte{'['}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *taskCheckBoxParser) Parse(parent gast.Node, block text.Reader, pc parser.Context) gast.Node {
|
||||||
|
// Given AST structure must be like
|
||||||
|
// - List
|
||||||
|
// - ListItem : parent.Parent
|
||||||
|
// - TextBlock : parent
|
||||||
|
// (current line)
|
||||||
|
if parent.Parent() == nil || parent.Parent().FirstChild() != parent {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := parent.Parent().(*gast.ListItem); !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
line, _ := block.PeekLine()
|
||||||
|
m := taskListRegexp.FindSubmatchIndex(line)
|
||||||
|
if m == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
value := line[m[2]:m[3]][0]
|
||||||
|
block.Advance(m[1])
|
||||||
|
checked := value == 'x' || value == 'X'
|
||||||
|
return ast.NewTaskCheckBox(checked)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *taskCheckBoxParser) CloseBlock(parent gast.Node, pc parser.Context) {
|
||||||
|
// nothing to do
|
||||||
|
}
|
||||||
|
|
||||||
|
// TaskCheckBoxHTMLRenderer is a renderer.NodeRenderer implementation that
|
||||||
|
// renders checkboxes in list items.
|
||||||
|
type TaskCheckBoxHTMLRenderer struct {
|
||||||
|
html.Config
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewTaskCheckBoxHTMLRenderer returns a new TaskCheckBoxHTMLRenderer.
|
||||||
|
func NewTaskCheckBoxHTMLRenderer(opts ...html.Option) renderer.NodeRenderer {
|
||||||
|
r := &TaskCheckBoxHTMLRenderer{
|
||||||
|
Config: html.NewConfig(),
|
||||||
|
}
|
||||||
|
for _, opt := range opts {
|
||||||
|
opt.SetHTMLOption(&r.Config)
|
||||||
|
}
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render implements renderer.NodeRenderer.Render.
|
||||||
|
func (r *TaskCheckBoxHTMLRenderer) Render(writer util.BufWriter, source []byte, n gast.Node, entering bool) (gast.WalkStatus, error) {
|
||||||
|
switch node := n.(type) {
|
||||||
|
case *ast.TaskCheckBox:
|
||||||
|
return r.renderTaskCheckBox(writer, source, node, entering), nil
|
||||||
|
}
|
||||||
|
return gast.WalkContinue, renderer.NotSupported
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *TaskCheckBoxHTMLRenderer) renderTaskCheckBox(w util.BufWriter, source []byte, n *ast.TaskCheckBox, entering bool) gast.WalkStatus {
|
||||||
|
if !entering {
|
||||||
|
return gast.WalkContinue
|
||||||
|
}
|
||||||
|
|
||||||
|
if n.IsChecked {
|
||||||
|
w.WriteString(`<input checked="" disabled="" type="checkbox"`)
|
||||||
|
} else {
|
||||||
|
w.WriteString(`<input disabled="" type="checkbox"`)
|
||||||
|
}
|
||||||
|
if r.XHTML {
|
||||||
|
w.WriteString(" />")
|
||||||
|
} else {
|
||||||
|
w.WriteString(">")
|
||||||
|
}
|
||||||
|
return gast.WalkContinue
|
||||||
|
}
|
||||||
|
|
||||||
|
type taskList struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
// TaskList is an extension that allow you to use GFM task lists.
|
||||||
|
var TaskList = &taskList{}
|
||||||
|
|
||||||
|
func (e *taskList) Extend(m goldmark.Markdown) {
|
||||||
|
m.Parser().AddOption(parser.WithInlineParsers(
|
||||||
|
util.Prioritized(NewTaskCheckBoxParser(), 0),
|
||||||
|
))
|
||||||
|
m.Renderer().AddOption(renderer.WithNodeRenderers(
|
||||||
|
util.Prioritized(NewTaskCheckBoxHTMLRenderer(), 500),
|
||||||
|
))
|
||||||
|
}
|
||||||
1
go.mod
Normal file
1
go.mod
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
module github.com/yuin/goldmark
|
||||||
144
markdown.go
Normal file
144
markdown.go
Normal file
|
|
@ -0,0 +1,144 @@
|
||||||
|
// Package goldmark implements functions to convert markdown text to a desired format.
|
||||||
|
package goldmark
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/yuin/goldmark/parser"
|
||||||
|
"github.com/yuin/goldmark/renderer"
|
||||||
|
"github.com/yuin/goldmark/renderer/html"
|
||||||
|
"github.com/yuin/goldmark/text"
|
||||||
|
"github.com/yuin/goldmark/util"
|
||||||
|
"io"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DefaultParser returns a new Parser that is configured by default values.
|
||||||
|
func DefaultParser() parser.Parser {
|
||||||
|
return parser.NewParser(parser.WithBlockParsers(parser.DefaultBlockParsers()...),
|
||||||
|
parser.WithInlineParsers(parser.DefaultInlineParsers()...),
|
||||||
|
parser.WithParagraphTransformers(parser.DefaultParagraphTransformers()...),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DefaultRenderer returns a new Renderer that is configured by default values.
|
||||||
|
func DefaultRenderer() renderer.Renderer {
|
||||||
|
return renderer.NewRenderer(renderer.WithNodeRenderers(util.Prioritized(html.NewRenderer(), 1000)))
|
||||||
|
}
|
||||||
|
|
||||||
|
var defaultMarkdown = New()
|
||||||
|
|
||||||
|
// Convert interprets a UTF-8 bytes source in Markdown and
|
||||||
|
// write rendered contents to a writer w.
|
||||||
|
func Convert(source []byte, w io.Writer) error {
|
||||||
|
return defaultMarkdown.Convert(source, w)
|
||||||
|
}
|
||||||
|
|
||||||
|
// A Markdown interface offers functions to convert Markdown text to
|
||||||
|
// a desired format.
|
||||||
|
type Markdown interface {
|
||||||
|
// Convert interprets a UTF-8 bytes source in Markdown and write rendered
|
||||||
|
// contents to a writer w.
|
||||||
|
Convert(source []byte, writer io.Writer) error
|
||||||
|
|
||||||
|
// Parser returns a Parser that will be used for conversion.
|
||||||
|
Parser() parser.Parser
|
||||||
|
|
||||||
|
// SetParser sets a Parser to this object.
|
||||||
|
SetParser(parser.Parser)
|
||||||
|
|
||||||
|
// Parser returns a Renderer that will be used for conversion.
|
||||||
|
Renderer() renderer.Renderer
|
||||||
|
|
||||||
|
// SetRenderer sets a Renderer to this object.
|
||||||
|
SetRenderer(renderer.Renderer)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Option is a functional option type for Markdown objects.
|
||||||
|
type Option func(*markdown)
|
||||||
|
|
||||||
|
// WithExtensions adds extensions.
|
||||||
|
func WithExtensions(ext ...Extender) Option {
|
||||||
|
return func(m *markdown) {
|
||||||
|
m.extensions = append(m.extensions, ext...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithParser allows you to override the default parser.
|
||||||
|
func WithParser(p parser.Parser) Option {
|
||||||
|
return func(m *markdown) {
|
||||||
|
m.parser = p
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithParserOptions applies options for the parser.
|
||||||
|
func WithParserOptions(opts ...parser.Option) Option {
|
||||||
|
return func(m *markdown) {
|
||||||
|
for _, opt := range opts {
|
||||||
|
m.parser.AddOption(opt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithRenderer allows you to override the default renderer.
|
||||||
|
func WithRenderer(r renderer.Renderer) Option {
|
||||||
|
return func(m *markdown) {
|
||||||
|
m.renderer = r
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithRendererOptions applies options for the renderer.
|
||||||
|
func WithRendererOptions(opts ...renderer.Option) Option {
|
||||||
|
return func(m *markdown) {
|
||||||
|
for _, opt := range opts {
|
||||||
|
m.renderer.AddOption(opt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type markdown struct {
|
||||||
|
parser parser.Parser
|
||||||
|
renderer renderer.Renderer
|
||||||
|
extensions []Extender
|
||||||
|
}
|
||||||
|
|
||||||
|
// New returns a new Markdown with given options.
|
||||||
|
func New(options ...Option) Markdown {
|
||||||
|
md := &markdown{
|
||||||
|
parser: DefaultParser(),
|
||||||
|
renderer: DefaultRenderer(),
|
||||||
|
extensions: []Extender{},
|
||||||
|
}
|
||||||
|
for _, opt := range options {
|
||||||
|
opt(md)
|
||||||
|
}
|
||||||
|
for _, e := range md.extensions {
|
||||||
|
e.Extend(md)
|
||||||
|
}
|
||||||
|
return md
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *markdown) Convert(source []byte, writer io.Writer) error {
|
||||||
|
reader := text.NewReader(source)
|
||||||
|
doc, _ := m.parser.Parse(reader)
|
||||||
|
return m.renderer.Render(writer, reader.Source(), doc)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *markdown) Parser() parser.Parser {
|
||||||
|
return m.parser
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *markdown) SetParser(v parser.Parser) {
|
||||||
|
m.parser = v
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *markdown) Renderer() renderer.Renderer {
|
||||||
|
return m.renderer
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *markdown) SetRenderer(v renderer.Renderer) {
|
||||||
|
m.renderer = v
|
||||||
|
}
|
||||||
|
|
||||||
|
// An Extender interface is used for extending Markdown.
|
||||||
|
type Extender interface {
|
||||||
|
// Extend extends the Markdown.
|
||||||
|
Extend(Markdown)
|
||||||
|
}
|
||||||
146
parser/atx_heading.go
Normal file
146
parser/atx_heading.go
Normal file
|
|
@ -0,0 +1,146 @@
|
||||||
|
package parser
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/yuin/goldmark/ast"
|
||||||
|
"github.com/yuin/goldmark/text"
|
||||||
|
"github.com/yuin/goldmark/util"
|
||||||
|
"regexp"
|
||||||
|
)
|
||||||
|
|
||||||
|
// A HeadingConfig struct is a data structure that holds configuration of the renderers related to headings.
|
||||||
|
type HeadingConfig struct {
|
||||||
|
HeadingID bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetOption implements SetOptioner.
|
||||||
|
func (b *HeadingConfig) SetOption(name OptionName, value interface{}) {
|
||||||
|
switch name {
|
||||||
|
case HeadingID:
|
||||||
|
b.HeadingID = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A HeadingOption interface sets options for heading parsers.
|
||||||
|
type HeadingOption interface {
|
||||||
|
SetHeadingOption(*HeadingConfig)
|
||||||
|
}
|
||||||
|
|
||||||
|
// HeadingID is an option name that enables custom and auto IDs for headings.
|
||||||
|
var HeadingID OptionName = "HeadingID"
|
||||||
|
|
||||||
|
type withHeadingID struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *withHeadingID) SetConfig(c *Config) {
|
||||||
|
c.Options[HeadingID] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *withHeadingID) SetHeadingOption(p *HeadingConfig) {
|
||||||
|
p.HeadingID = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithHeadingID is a functional option that enables custom heading ids and
|
||||||
|
// auto generated heading ids.
|
||||||
|
func WithHeadingID() interface {
|
||||||
|
Option
|
||||||
|
HeadingOption
|
||||||
|
} {
|
||||||
|
return &withHeadingID{}
|
||||||
|
}
|
||||||
|
|
||||||
|
var atxHeadingRegexp = regexp.MustCompile(`^[ ]{0,3}(#{1,6})(?:\s+(.*?)\s*([\s]#+\s*)?)?\n?$`)
|
||||||
|
|
||||||
|
type atxHeadingParser struct {
|
||||||
|
HeadingConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewATXHeadingParser return a new BlockParser that can parse ATX headings.
|
||||||
|
func NewATXHeadingParser(opts ...HeadingOption) BlockParser {
|
||||||
|
p := &atxHeadingParser{}
|
||||||
|
for _, o := range opts {
|
||||||
|
o.SetHeadingOption(&p.HeadingConfig)
|
||||||
|
}
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *atxHeadingParser) Open(parent ast.Node, reader text.Reader, pc Context) (ast.Node, State) {
|
||||||
|
line, segment := reader.PeekLine()
|
||||||
|
pos := pc.BlockOffset()
|
||||||
|
i := pos
|
||||||
|
for ; i < len(line) && line[i] == '#'; i++ {
|
||||||
|
}
|
||||||
|
level := i - pos
|
||||||
|
if i == pos || level > 6 {
|
||||||
|
return nil, NoChildren
|
||||||
|
}
|
||||||
|
l := util.TrimLeftSpaceLength(line[i:])
|
||||||
|
if l == 0 {
|
||||||
|
return nil, NoChildren
|
||||||
|
}
|
||||||
|
start := i + l
|
||||||
|
stop := len(line) - util.TrimRightSpaceLength(line)
|
||||||
|
if stop <= start { // empty headings like '##[space]'
|
||||||
|
stop = start + 1
|
||||||
|
} else {
|
||||||
|
i = stop - 1
|
||||||
|
for ; line[i] == '#' && i >= start; i-- {
|
||||||
|
}
|
||||||
|
if i != stop-1 && !util.IsSpace(line[i]) {
|
||||||
|
i = stop - 1
|
||||||
|
}
|
||||||
|
i++
|
||||||
|
stop = i
|
||||||
|
}
|
||||||
|
|
||||||
|
node := ast.NewHeading(level)
|
||||||
|
if len(util.TrimRight(line[start:stop], []byte{'#'})) != 0 { // empty heading like '### ###'
|
||||||
|
node.Lines().Append(text.NewSegment(segment.Start+start, segment.Start+stop))
|
||||||
|
}
|
||||||
|
return node, NoChildren
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *atxHeadingParser) Continue(node ast.Node, reader text.Reader, pc Context) State {
|
||||||
|
return Close
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *atxHeadingParser) Close(node ast.Node, pc Context) {
|
||||||
|
if !b.HeadingID {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
parseOrGenerateHeadingID(node.(*ast.Heading), pc)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *atxHeadingParser) CanInterruptParagraph() bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *atxHeadingParser) CanAcceptIndentedLine() bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
var headingIDRegexp = regexp.MustCompile(`^(.*[^\\])({#([^}]+)}\s*)\n?$`)
|
||||||
|
var headingIDMap = NewContextKey()
|
||||||
|
|
||||||
|
func parseOrGenerateHeadingID(node *ast.Heading, pc Context) {
|
||||||
|
existsv := pc.Get(headingIDMap)
|
||||||
|
var exists map[string]bool
|
||||||
|
if existsv == nil {
|
||||||
|
exists = map[string]bool{}
|
||||||
|
pc.Set(headingIDMap, exists)
|
||||||
|
} else {
|
||||||
|
exists = existsv.(map[string]bool)
|
||||||
|
}
|
||||||
|
lastIndex := node.Lines().Len() - 1
|
||||||
|
lastLine := node.Lines().At(lastIndex)
|
||||||
|
line := lastLine.Value(pc.Source())
|
||||||
|
m := headingIDRegexp.FindSubmatchIndex(line)
|
||||||
|
var headingID []byte
|
||||||
|
if m != nil {
|
||||||
|
headingID = line[m[6]:m[7]]
|
||||||
|
lastLine.Stop -= m[5] - m[4]
|
||||||
|
node.Lines().Set(lastIndex, lastLine)
|
||||||
|
} else {
|
||||||
|
headingID = util.GenerateLinkID(line, exists)
|
||||||
|
}
|
||||||
|
node.ID = headingID
|
||||||
|
}
|
||||||
46
parser/auto_link.go
Normal file
46
parser/auto_link.go
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
package parser
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/yuin/goldmark/ast"
|
||||||
|
"github.com/yuin/goldmark/text"
|
||||||
|
"regexp"
|
||||||
|
)
|
||||||
|
|
||||||
|
type autoLinkParser struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
var defaultAutoLinkParser = &autoLinkParser{}
|
||||||
|
|
||||||
|
// NewAutoLinkParser returns a new InlineParser that parses autolinks
|
||||||
|
// surrounded by '<' and '>' .
|
||||||
|
func NewAutoLinkParser() InlineParser {
|
||||||
|
return defaultAutoLinkParser
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *autoLinkParser) Trigger() []byte {
|
||||||
|
return []byte{'<'}
|
||||||
|
}
|
||||||
|
|
||||||
|
var emailAutoLinkRegexp = regexp.MustCompile(`^<([a-zA-Z0-9.!#$%&'*+\/=?^_` + "`" + `{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*)>`)
|
||||||
|
|
||||||
|
var autoLinkRegexp = regexp.MustCompile(`(?i)^<[A-Za-z][A-Za-z0-9.+-]{1,31}:[^<>\x00-\x20]*>`)
|
||||||
|
|
||||||
|
func (s *autoLinkParser) Parse(parent ast.Node, block text.Reader, pc Context) ast.Node {
|
||||||
|
line, segment := block.PeekLine()
|
||||||
|
match := emailAutoLinkRegexp.FindSubmatchIndex(line)
|
||||||
|
typ := ast.AutoLinkType(ast.AutoLinkEmail)
|
||||||
|
if match == nil {
|
||||||
|
match = autoLinkRegexp.FindSubmatchIndex(line)
|
||||||
|
typ = ast.AutoLinkURL
|
||||||
|
}
|
||||||
|
if match == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
value := ast.NewTextSegment(text.NewSegment(segment.Start+1, segment.Start+match[1]-1))
|
||||||
|
block.Advance(match[1])
|
||||||
|
return ast.NewAutoLink(typ, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *autoLinkParser) CloseBlock(parent ast.Node, pc Context) {
|
||||||
|
// nothing to do
|
||||||
|
}
|
||||||
65
parser/blockquote.go
Normal file
65
parser/blockquote.go
Normal file
|
|
@ -0,0 +1,65 @@
|
||||||
|
package parser
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/yuin/goldmark/ast"
|
||||||
|
"github.com/yuin/goldmark/text"
|
||||||
|
"github.com/yuin/goldmark/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
type blockquoteParser struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
var defaultBlockquoteParser = &blockquoteParser{}
|
||||||
|
|
||||||
|
// NewBlockquoteParser returns a new BlockParser that
|
||||||
|
// parses blockquotes.
|
||||||
|
func NewBlockquoteParser() BlockParser {
|
||||||
|
return defaultBlockquoteParser
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *blockquoteParser) process(reader text.Reader) bool {
|
||||||
|
line, _ := reader.PeekLine()
|
||||||
|
w, pos := util.IndentWidth(line, 0)
|
||||||
|
if w > 3 || pos >= len(line) || line[pos] != '>' {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
pos++
|
||||||
|
if pos >= len(line) || line[pos] == '\n' {
|
||||||
|
reader.Advance(pos)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if line[pos] == ' ' || line[pos] == '\t' {
|
||||||
|
pos++
|
||||||
|
}
|
||||||
|
reader.Advance(pos)
|
||||||
|
if line[pos-1] == '\t' {
|
||||||
|
reader.SetPadding(2)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *blockquoteParser) Open(parent ast.Node, reader text.Reader, pc Context) (ast.Node, State) {
|
||||||
|
if b.process(reader) {
|
||||||
|
return ast.NewBlockquote(), HasChildren
|
||||||
|
}
|
||||||
|
return nil, NoChildren
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *blockquoteParser) Continue(node ast.Node, reader text.Reader, pc Context) State {
|
||||||
|
if b.process(reader) {
|
||||||
|
return Continue | HasChildren
|
||||||
|
}
|
||||||
|
return Close
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *blockquoteParser) Close(node ast.Node, pc Context) {
|
||||||
|
// nothing to do
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *blockquoteParser) CanInterruptParagraph() bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *blockquoteParser) CanAcceptIndentedLine() bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
75
parser/code_block.go
Normal file
75
parser/code_block.go
Normal file
|
|
@ -0,0 +1,75 @@
|
||||||
|
package parser
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/yuin/goldmark/ast"
|
||||||
|
"github.com/yuin/goldmark/text"
|
||||||
|
"github.com/yuin/goldmark/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
type codeBlockParser struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
// CodeBlockParser is a BlockParser implementation that parses indented code blocks.
|
||||||
|
var defaultCodeBlockParser = &codeBlockParser{}
|
||||||
|
|
||||||
|
// NewCodeBlockParser returns a new BlockParser that
|
||||||
|
// parses code blocks.
|
||||||
|
func NewCodeBlockParser() BlockParser {
|
||||||
|
return defaultCodeBlockParser
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *codeBlockParser) Open(parent ast.Node, reader text.Reader, pc Context) (ast.Node, State) {
|
||||||
|
line, segment := reader.PeekLine()
|
||||||
|
pos, padding := util.IndentPosition(line, reader.LineOffset(), 4)
|
||||||
|
if pos < 0 {
|
||||||
|
return nil, NoChildren
|
||||||
|
}
|
||||||
|
node := ast.NewCodeBlock()
|
||||||
|
reader.AdvanceAndSetPadding(pos, padding)
|
||||||
|
_, segment = reader.PeekLine()
|
||||||
|
node.Lines().Append(segment)
|
||||||
|
reader.Advance(segment.Len() - 1)
|
||||||
|
return node, NoChildren
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *codeBlockParser) Continue(node ast.Node, reader text.Reader, pc Context) State {
|
||||||
|
line, segment := reader.PeekLine()
|
||||||
|
if util.IsBlank(line) {
|
||||||
|
node.Lines().Append(segment.TrimLeftSpaceWidth(4, reader.Source()))
|
||||||
|
return Continue | NoChildren
|
||||||
|
}
|
||||||
|
pos, padding := util.IndentPosition(line, reader.LineOffset(), 4)
|
||||||
|
if pos < 0 {
|
||||||
|
return Close
|
||||||
|
}
|
||||||
|
reader.AdvanceAndSetPadding(pos, padding)
|
||||||
|
_, segment = reader.PeekLine()
|
||||||
|
node.Lines().Append(segment)
|
||||||
|
reader.Advance(segment.Len() - 1)
|
||||||
|
return Continue | NoChildren
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *codeBlockParser) Close(node ast.Node, pc Context) {
|
||||||
|
// trim trailing blank lines
|
||||||
|
lines := node.Lines()
|
||||||
|
length := lines.Len() - 1
|
||||||
|
source := pc.Source()
|
||||||
|
for {
|
||||||
|
line := lines.At(length)
|
||||||
|
if util.IsBlank(line.Value(source)) {
|
||||||
|
length--
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lines.SetSliced(0, length+1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *codeBlockParser) CanInterruptParagraph() bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *codeBlockParser) CanAcceptIndentedLine() bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
87
parser/code_span.go
Normal file
87
parser/code_span.go
Normal file
|
|
@ -0,0 +1,87 @@
|
||||||
|
package parser
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/yuin/goldmark/ast"
|
||||||
|
"github.com/yuin/goldmark/text"
|
||||||
|
"github.com/yuin/goldmark/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
type codeSpanParser struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
var defaultCodeSpanParser = &codeSpanParser{}
|
||||||
|
|
||||||
|
// NewCodeSpanParser return a new InlineParser that parses inline codes
|
||||||
|
// surrounded by '`' .
|
||||||
|
func NewCodeSpanParser() InlineParser {
|
||||||
|
return defaultCodeSpanParser
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *codeSpanParser) Trigger() []byte {
|
||||||
|
return []byte{'`'}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *codeSpanParser) Parse(parent ast.Node, block text.Reader, pc Context) ast.Node {
|
||||||
|
line, startSegment := block.PeekLine()
|
||||||
|
opener := 0
|
||||||
|
for ; opener < len(line) && line[opener] == '`'; opener++ {
|
||||||
|
}
|
||||||
|
block.Advance(opener)
|
||||||
|
l, pos := block.Position()
|
||||||
|
node := ast.NewCodeSpan()
|
||||||
|
for {
|
||||||
|
line, segment := block.PeekLine()
|
||||||
|
if line == nil {
|
||||||
|
block.SetPosition(l, pos)
|
||||||
|
return ast.NewTextSegment(startSegment.WithStop(startSegment.Start + opener))
|
||||||
|
}
|
||||||
|
for i := 0; i < len(line); i++ {
|
||||||
|
c := line[i]
|
||||||
|
if c == '`' {
|
||||||
|
oldi := i
|
||||||
|
for ; i < len(line) && line[i] == '`'; i++ {
|
||||||
|
}
|
||||||
|
closure := i - oldi
|
||||||
|
if closure == opener && (i+1 >= len(line) || line[i+1] != '`') {
|
||||||
|
segment := segment.WithStop(segment.Start + i - closure)
|
||||||
|
if !segment.IsEmpty() {
|
||||||
|
node.AppendChild(node, ast.NewRawTextSegment(segment))
|
||||||
|
}
|
||||||
|
block.Advance(i)
|
||||||
|
goto end
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !util.IsBlank(line) {
|
||||||
|
node.AppendChild(node, ast.NewRawTextSegment(segment))
|
||||||
|
}
|
||||||
|
block.AdvanceLine()
|
||||||
|
}
|
||||||
|
end:
|
||||||
|
if !node.IsBlank(pc.Source()) {
|
||||||
|
// trim first halfspace and last halfspace
|
||||||
|
segment := node.FirstChild().(*ast.Text).Segment
|
||||||
|
shouldTrimmed := true
|
||||||
|
if !(!segment.IsEmpty() && pc.Source()[segment.Start] == ' ') {
|
||||||
|
shouldTrimmed = false
|
||||||
|
}
|
||||||
|
segment = node.LastChild().(*ast.Text).Segment
|
||||||
|
if !(!segment.IsEmpty() && pc.Source()[segment.Stop-1] == ' ') {
|
||||||
|
shouldTrimmed = false
|
||||||
|
}
|
||||||
|
if shouldTrimmed {
|
||||||
|
t := node.FirstChild().(*ast.Text)
|
||||||
|
segment := t.Segment
|
||||||
|
t.Segment = segment.WithStart(segment.Start + 1)
|
||||||
|
t = node.LastChild().(*ast.Text)
|
||||||
|
segment = node.LastChild().(*ast.Text).Segment
|
||||||
|
t.Segment = segment.WithStop(segment.Stop - 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
return node
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *codeSpanParser) CloseBlock(parent ast.Node, pc Context) {
|
||||||
|
// nothing to do
|
||||||
|
}
|
||||||
232
parser/delimiter.go
Normal file
232
parser/delimiter.go
Normal file
|
|
@ -0,0 +1,232 @@
|
||||||
|
package parser
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"unicode"
|
||||||
|
|
||||||
|
"github.com/yuin/goldmark/ast"
|
||||||
|
"github.com/yuin/goldmark/text"
|
||||||
|
"github.com/yuin/goldmark/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
// A DelimiterProcessor interface provides a set of functions about
|
||||||
|
// Deliiter nodes.
|
||||||
|
type DelimiterProcessor interface {
|
||||||
|
// IsDelimiter returns true if given character is a delimiter, otherwise false.
|
||||||
|
IsDelimiter(byte) bool
|
||||||
|
|
||||||
|
// CanOpenCloser returns true if given opener can close given closer, otherwise false.
|
||||||
|
CanOpenCloser(opener, closer *Delimiter) bool
|
||||||
|
|
||||||
|
// OnMatch will be called when new matched delimiter found.
|
||||||
|
// OnMatch should return a new Node correspond to the matched delimiter.
|
||||||
|
OnMatch(consumes int) ast.Node
|
||||||
|
}
|
||||||
|
|
||||||
|
// A Delimiter struct represents a delimiter like '*' of the Markdown text.
|
||||||
|
type Delimiter struct {
|
||||||
|
ast.BaseInline
|
||||||
|
|
||||||
|
Segment text.Segment
|
||||||
|
|
||||||
|
// CanOpen is set true if this delimiter can open a span for a new node.
|
||||||
|
// See https://spec.commonmark.org/0.29/#can-open-emphasis for details.
|
||||||
|
CanOpen bool
|
||||||
|
|
||||||
|
// CanClose is set true if this delimiter can close a span for a new node.
|
||||||
|
// See https://spec.commonmark.org/0.29/#can-open-emphasis for details.
|
||||||
|
CanClose bool
|
||||||
|
|
||||||
|
// Length is a remaining length of this delmiter.
|
||||||
|
Length int
|
||||||
|
|
||||||
|
// OriginalLength is a original length of this delimiter.
|
||||||
|
OriginalLength int
|
||||||
|
|
||||||
|
// Char is a character of this delimiter.
|
||||||
|
Char byte
|
||||||
|
|
||||||
|
// PreviousDelimiter is a previous sibling delimiter node of this delimiter.
|
||||||
|
PreviousDelimiter *Delimiter
|
||||||
|
|
||||||
|
// NextDelimiter is a next sibling delimiter node of this delimiter.
|
||||||
|
NextDelimiter *Delimiter
|
||||||
|
|
||||||
|
// Processor is a DelimiterProcessor associated with this delimiter.
|
||||||
|
Processor DelimiterProcessor
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inline implements Inline.Inline.
|
||||||
|
func (d *Delimiter) Inline() {}
|
||||||
|
|
||||||
|
// Dump implements Node.Dump.
|
||||||
|
func (d *Delimiter) Dump(source []byte, level int) {
|
||||||
|
fmt.Printf("%sDelimiter: \"%s\"\n", strings.Repeat(" ", level), string(d.Text(source)))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Text implements Node.Text
|
||||||
|
func (d *Delimiter) Text(source []byte) []byte {
|
||||||
|
return d.Segment.Value(source)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConsumeCharacters consumes delimiters.
|
||||||
|
func (d *Delimiter) ConsumeCharacters(n int) {
|
||||||
|
d.Length -= n
|
||||||
|
d.Segment = d.Segment.WithStop(d.Segment.Start + d.Length)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CalcComsumption calculates how many characters should be used for opening
|
||||||
|
// a new span correspond to given closer.
|
||||||
|
func (d *Delimiter) CalcComsumption(closer *Delimiter) int {
|
||||||
|
if (d.CanClose || closer.CanOpen) && (d.OriginalLength+closer.OriginalLength)%3 == 0 && closer.OriginalLength%3 != 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
if d.Length >= 2 && closer.Length >= 2 {
|
||||||
|
return 2
|
||||||
|
}
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewDelimiter returns a new Delimiter node.
|
||||||
|
func NewDelimiter(canOpen, canClose bool, length int, char byte, processor DelimiterProcessor) *Delimiter {
|
||||||
|
c := &Delimiter{
|
||||||
|
BaseInline: ast.BaseInline{},
|
||||||
|
CanOpen: canOpen,
|
||||||
|
CanClose: canClose,
|
||||||
|
Length: length,
|
||||||
|
OriginalLength: length,
|
||||||
|
Char: char,
|
||||||
|
PreviousDelimiter: nil,
|
||||||
|
NextDelimiter: nil,
|
||||||
|
Processor: processor,
|
||||||
|
}
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// ScanDelimiter scans a delimiter by given DelimiterProcessor.
|
||||||
|
func ScanDelimiter(line []byte, before rune, min int, processor DelimiterProcessor) *Delimiter {
|
||||||
|
i := 0
|
||||||
|
c := line[i]
|
||||||
|
j := i
|
||||||
|
if !processor.IsDelimiter(c) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
for ; j < len(line) && c == line[j]; j++ {
|
||||||
|
}
|
||||||
|
if (j - i) >= min {
|
||||||
|
after := rune(' ')
|
||||||
|
if j != len(line) {
|
||||||
|
after = util.ToRune(line, j)
|
||||||
|
}
|
||||||
|
|
||||||
|
isLeft, isRight, canOpen, canClose := false, false, false, false
|
||||||
|
beforeIsPunctuation := unicode.IsPunct(before)
|
||||||
|
beforeIsWhitespace := unicode.IsSpace(before)
|
||||||
|
afterIsPunctuation := unicode.IsPunct(after)
|
||||||
|
afterIsWhitespace := unicode.IsSpace(after)
|
||||||
|
|
||||||
|
isLeft = !afterIsWhitespace &&
|
||||||
|
(!afterIsPunctuation || beforeIsWhitespace || beforeIsPunctuation)
|
||||||
|
isRight = !beforeIsWhitespace &&
|
||||||
|
(!beforeIsPunctuation || afterIsWhitespace || afterIsPunctuation)
|
||||||
|
|
||||||
|
if line[i] == '_' {
|
||||||
|
canOpen = isLeft && (!isRight || beforeIsPunctuation)
|
||||||
|
canClose = isRight && (!isLeft || afterIsPunctuation)
|
||||||
|
} else {
|
||||||
|
canOpen = isLeft
|
||||||
|
canClose = isRight
|
||||||
|
}
|
||||||
|
return NewDelimiter(canOpen, canClose, j-i, c, processor)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProcessDelimiters processes the delimiter list in the context.
|
||||||
|
// Processing will be stop when reaching the bottom.
|
||||||
|
//
|
||||||
|
// If you implement an inline parser that can have other inline nodes as
|
||||||
|
// children, you should call this function when nesting span has closed.
|
||||||
|
func ProcessDelimiters(bottom ast.Node, pc Context) {
|
||||||
|
if pc.LastDelimiter() == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var closer *Delimiter
|
||||||
|
if bottom != nil {
|
||||||
|
for c := pc.LastDelimiter().PreviousSibling(); c != nil; {
|
||||||
|
if d, ok := c.(*Delimiter); ok {
|
||||||
|
closer = d
|
||||||
|
}
|
||||||
|
prev := c.PreviousSibling()
|
||||||
|
if prev == bottom {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
c = prev
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
closer = pc.FirstDelimiter()
|
||||||
|
}
|
||||||
|
if closer == nil {
|
||||||
|
pc.ClearDelimiters(bottom)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for closer != nil {
|
||||||
|
if !closer.CanClose {
|
||||||
|
closer = closer.NextDelimiter
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
consume := 0
|
||||||
|
found := false
|
||||||
|
maybeOpener := false
|
||||||
|
var opener *Delimiter
|
||||||
|
for opener = closer.PreviousDelimiter; opener != nil; opener = opener.PreviousDelimiter {
|
||||||
|
if opener.CanOpen && opener.Processor.CanOpenCloser(opener, closer) {
|
||||||
|
maybeOpener = true
|
||||||
|
consume = opener.CalcComsumption(closer)
|
||||||
|
if consume > 0 {
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
if !maybeOpener && !closer.CanOpen {
|
||||||
|
pc.RemoveDelimiter(closer)
|
||||||
|
}
|
||||||
|
closer = closer.NextDelimiter
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
opener.ConsumeCharacters(consume)
|
||||||
|
closer.ConsumeCharacters(consume)
|
||||||
|
|
||||||
|
node := opener.Processor.OnMatch(consume)
|
||||||
|
|
||||||
|
parent := opener.Parent()
|
||||||
|
child := opener.NextSibling()
|
||||||
|
|
||||||
|
for child != nil && child != closer {
|
||||||
|
next := child.NextSibling()
|
||||||
|
node.AppendChild(node, child)
|
||||||
|
child = next
|
||||||
|
}
|
||||||
|
parent.InsertAfter(parent, opener, node)
|
||||||
|
|
||||||
|
for c := opener.NextDelimiter; c != nil && c != closer; {
|
||||||
|
next := c.NextDelimiter
|
||||||
|
pc.RemoveDelimiter(c)
|
||||||
|
c = next
|
||||||
|
}
|
||||||
|
|
||||||
|
if opener.Length == 0 {
|
||||||
|
pc.RemoveDelimiter(opener)
|
||||||
|
}
|
||||||
|
|
||||||
|
if closer.Length == 0 {
|
||||||
|
next := closer.NextDelimiter
|
||||||
|
pc.RemoveDelimiter(closer)
|
||||||
|
closer = next
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pc.ClearDelimiters(bottom)
|
||||||
|
}
|
||||||
54
parser/emphasis.go
Normal file
54
parser/emphasis.go
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
package parser
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/yuin/goldmark/ast"
|
||||||
|
"github.com/yuin/goldmark/text"
|
||||||
|
)
|
||||||
|
|
||||||
|
type emphasisDelimiterProcessor struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *emphasisDelimiterProcessor) IsDelimiter(b byte) bool {
|
||||||
|
return b == '*' || b == '_'
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *emphasisDelimiterProcessor) CanOpenCloser(opener, closer *Delimiter) bool {
|
||||||
|
return opener.Char == closer.Char
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *emphasisDelimiterProcessor) OnMatch(consumes int) ast.Node {
|
||||||
|
return ast.NewEmphasis(consumes)
|
||||||
|
}
|
||||||
|
|
||||||
|
var defaultEmphasisDelimiterProcessor = &emphasisDelimiterProcessor{}
|
||||||
|
|
||||||
|
type emphasisParser struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
var defaultEmphasisParser = &emphasisParser{}
|
||||||
|
|
||||||
|
// NewEmphasisParser return a new InlineParser that parses emphasises.
|
||||||
|
func NewEmphasisParser() InlineParser {
|
||||||
|
return defaultEmphasisParser
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *emphasisParser) Trigger() []byte {
|
||||||
|
return []byte{'*', '_'}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *emphasisParser) Parse(parent ast.Node, block text.Reader, pc Context) ast.Node {
|
||||||
|
before := block.PrecendingCharacter()
|
||||||
|
line, segment := block.PeekLine()
|
||||||
|
node := ScanDelimiter(line, before, 1, defaultEmphasisDelimiterProcessor)
|
||||||
|
if node == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
node.Segment = segment.WithStop(segment.Start + node.OriginalLength)
|
||||||
|
block.Advance(node.OriginalLength)
|
||||||
|
pc.PushDelimiter(node)
|
||||||
|
return node
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *emphasisParser) CloseBlock(parent ast.Node, pc Context) {
|
||||||
|
// nothing to do
|
||||||
|
}
|
||||||
96
parser/fcode_block.go
Normal file
96
parser/fcode_block.go
Normal file
|
|
@ -0,0 +1,96 @@
|
||||||
|
package parser
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
|
||||||
|
"github.com/yuin/goldmark/ast"
|
||||||
|
"github.com/yuin/goldmark/text"
|
||||||
|
"github.com/yuin/goldmark/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
type fencedCodeBlockParser struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
var defaultFencedCodeBlockParser = &fencedCodeBlockParser{}
|
||||||
|
|
||||||
|
// NewFencedCodeBlockParser returns a new BlockParser that
|
||||||
|
// parses fenced code blocks.
|
||||||
|
func NewFencedCodeBlockParser() BlockParser {
|
||||||
|
return defaultFencedCodeBlockParser
|
||||||
|
}
|
||||||
|
|
||||||
|
type fenceData struct {
|
||||||
|
char byte
|
||||||
|
indent int
|
||||||
|
length int
|
||||||
|
}
|
||||||
|
|
||||||
|
var fencedCodeBlockInfoKey = NewContextKey()
|
||||||
|
|
||||||
|
func (b *fencedCodeBlockParser) Open(parent ast.Node, reader text.Reader, pc Context) (ast.Node, State) {
|
||||||
|
line, segment := reader.PeekLine()
|
||||||
|
pos := pc.BlockOffset()
|
||||||
|
if line[pos] != '`' && line[pos] != '~' {
|
||||||
|
return nil, NoChildren
|
||||||
|
}
|
||||||
|
findent := pos
|
||||||
|
fenceChar := line[pos]
|
||||||
|
i := pos
|
||||||
|
for ; i < len(line) && line[i] == fenceChar; i++ {
|
||||||
|
}
|
||||||
|
oFenceLength := i - pos
|
||||||
|
if oFenceLength < 3 {
|
||||||
|
return nil, NoChildren
|
||||||
|
}
|
||||||
|
var info *ast.Text
|
||||||
|
if i < len(line)-1 {
|
||||||
|
rest := line[i:]
|
||||||
|
left := util.TrimLeftSpaceLength(rest)
|
||||||
|
right := util.TrimRightSpaceLength(rest)
|
||||||
|
infoStart, infoStop := segment.Start+i+left, segment.Stop-right
|
||||||
|
value := rest[left : len(rest)-right]
|
||||||
|
if fenceChar == '`' && bytes.IndexByte(value, '`') > -1 {
|
||||||
|
return nil, NoChildren
|
||||||
|
} else if infoStart != infoStop {
|
||||||
|
info = ast.NewTextSegment(text.NewSegment(infoStart, infoStop))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pc.Set(fencedCodeBlockInfoKey, &fenceData{fenceChar, findent, oFenceLength})
|
||||||
|
node := ast.NewFencedCodeBlock(info)
|
||||||
|
return node, NoChildren
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *fencedCodeBlockParser) Continue(node ast.Node, reader text.Reader, pc Context) State {
|
||||||
|
line, segment := reader.PeekLine()
|
||||||
|
fdata := pc.Get(fencedCodeBlockInfoKey).(*fenceData)
|
||||||
|
w, pos := util.IndentWidth(line, 0)
|
||||||
|
if w < 4 {
|
||||||
|
i := pos
|
||||||
|
for ; i < len(line) && line[i] == fdata.char; i++ {
|
||||||
|
}
|
||||||
|
length := i - pos
|
||||||
|
if length >= fdata.length && util.IsBlank(line[i:]) {
|
||||||
|
reader.Advance(segment.Stop - segment.Start - 1 - segment.Padding)
|
||||||
|
return Close
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pos, padding := util.DedentPosition(line, fdata.indent)
|
||||||
|
seg := text.NewSegmentPadding(segment.Start+pos, segment.Stop, padding)
|
||||||
|
node.Lines().Append(seg)
|
||||||
|
reader.AdvanceAndSetPadding(segment.Stop-segment.Start-pos-1, padding)
|
||||||
|
return Continue | NoChildren
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *fencedCodeBlockParser) Close(node ast.Node, pc Context) {
|
||||||
|
pc.Set(fencedCodeBlockInfoKey, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *fencedCodeBlockParser) CanInterruptParagraph() bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *fencedCodeBlockParser) CanAcceptIndentedLine() bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
278
parser/html_block.go
Normal file
278
parser/html_block.go
Normal file
|
|
@ -0,0 +1,278 @@
|
||||||
|
package parser
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"github.com/yuin/goldmark/ast"
|
||||||
|
"github.com/yuin/goldmark/text"
|
||||||
|
"github.com/yuin/goldmark/util"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// An HTMLConfig struct is a data structure that holds configuration of the renderers related to raw htmls.
|
||||||
|
type HTMLConfig struct {
|
||||||
|
FilterTags map[string]bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetOption implements SetOptioner.
|
||||||
|
func (b *HTMLConfig) SetOption(name OptionName, value interface{}) {
|
||||||
|
switch name {
|
||||||
|
case FilterTags:
|
||||||
|
b.FilterTags = value.(map[string]bool)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A HTMLOption interface sets options for the raw HTML parsers.
|
||||||
|
type HTMLOption interface {
|
||||||
|
SetHTMLOption(*HTMLConfig)
|
||||||
|
}
|
||||||
|
|
||||||
|
// FilterTags is an otpion name that specify forbidden tag names.
|
||||||
|
const FilterTags OptionName = "FilterTags"
|
||||||
|
|
||||||
|
type withFilterTags struct {
|
||||||
|
value map[string]bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *withFilterTags) SetConfig(c *Config) {
|
||||||
|
c.Options[FilterTags] = o.value
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *withFilterTags) SetHTMLOption(p *HTMLConfig) {
|
||||||
|
p.FilterTags = o.value
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithFilterTags is a functional otpion that specify forbidden tag names.
|
||||||
|
func WithFilterTags(names ...string) interface {
|
||||||
|
Option
|
||||||
|
HTMLOption
|
||||||
|
} {
|
||||||
|
m := map[string]bool{}
|
||||||
|
for _, name := range names {
|
||||||
|
m[name] = true
|
||||||
|
}
|
||||||
|
return &withFilterTags{m}
|
||||||
|
}
|
||||||
|
|
||||||
|
var allowedBlockTags = map[string]bool{
|
||||||
|
"address": true,
|
||||||
|
"article": true,
|
||||||
|
"aside": true,
|
||||||
|
"base": true,
|
||||||
|
"basefont": true,
|
||||||
|
"blockquote": true,
|
||||||
|
"body": true,
|
||||||
|
"caption": true,
|
||||||
|
"center": true,
|
||||||
|
"col": true,
|
||||||
|
"colgroup": true,
|
||||||
|
"dd": true,
|
||||||
|
"details": true,
|
||||||
|
"dialog": true,
|
||||||
|
"dir": true,
|
||||||
|
"div": true,
|
||||||
|
"dl": true,
|
||||||
|
"dt": true,
|
||||||
|
"fieldset": true,
|
||||||
|
"figcaption": true,
|
||||||
|
"figure": true,
|
||||||
|
"footer": true,
|
||||||
|
"form": true,
|
||||||
|
"frame": true,
|
||||||
|
"frameset": true,
|
||||||
|
"h1": true,
|
||||||
|
"h2": true,
|
||||||
|
"h3": true,
|
||||||
|
"h4": true,
|
||||||
|
"h5": true,
|
||||||
|
"h6": true,
|
||||||
|
"head": true,
|
||||||
|
"header": true,
|
||||||
|
"hr": true,
|
||||||
|
"html": true,
|
||||||
|
"iframe": true,
|
||||||
|
"legend": true,
|
||||||
|
"li": true,
|
||||||
|
"link": true,
|
||||||
|
"main": true,
|
||||||
|
"menu": true,
|
||||||
|
"menuitem": true,
|
||||||
|
"meta": true,
|
||||||
|
"nav": true,
|
||||||
|
"noframes": true,
|
||||||
|
"ol": true,
|
||||||
|
"optgroup": true,
|
||||||
|
"option": true,
|
||||||
|
"p": true,
|
||||||
|
"param": true,
|
||||||
|
"section": true,
|
||||||
|
"source": true,
|
||||||
|
"summary": true,
|
||||||
|
"table": true,
|
||||||
|
"tbody": true,
|
||||||
|
"td": true,
|
||||||
|
"tfoot": true,
|
||||||
|
"th": true,
|
||||||
|
"thead": true,
|
||||||
|
"title": true,
|
||||||
|
"tr": true,
|
||||||
|
"track": true,
|
||||||
|
"ul": true,
|
||||||
|
}
|
||||||
|
|
||||||
|
var htmlBlockType1OpenRegexp = regexp.MustCompile(`(?i)^[ ]{0,3}<(script|pre|style)(?:\s.*|>.*|/>.*|)\n?$`)
|
||||||
|
var htmlBlockType1CloseRegexp = regexp.MustCompile(`(?i)^[ ]{0,3}(?:[^ ].*|)</(?:script|pre|style)>.*`)
|
||||||
|
|
||||||
|
var htmlBlockType2OpenRegexp = regexp.MustCompile(`^[ ]{0,3}<!\-\-`)
|
||||||
|
var htmlBlockType2Close = []byte{'-', '-', '>'}
|
||||||
|
|
||||||
|
var htmlBlockType3OpenRegexp = regexp.MustCompile(`^[ ]{0,3}<\?`)
|
||||||
|
var htmlBlockType3Close = []byte{'?', '>'}
|
||||||
|
|
||||||
|
var htmlBlockType4OpenRegexp = regexp.MustCompile(`^[ ]{0,3}<![A-Z]+.*\n?$`)
|
||||||
|
var htmlBlockType4Close = []byte{'>'}
|
||||||
|
|
||||||
|
var htmlBlockType5OpenRegexp = regexp.MustCompile(`^[ ]{0,3}<\!\[CDATA\[`)
|
||||||
|
var htmlBlockType5Close = []byte{']', ']', '>'}
|
||||||
|
|
||||||
|
var htmlBlockType6Regexp = regexp.MustCompile(`^[ ]{0,3}</?([a-zA-Z0-9]+)(?:\s.*|>.*|/>.*|)\n?$`)
|
||||||
|
|
||||||
|
var htmlBlockType7Regexp = regexp.MustCompile(`^[ ]{0,3}<(/)?([a-zA-Z0-9]+)(` + attributePattern + `*)(:?>|/>)\s*\n?$`)
|
||||||
|
|
||||||
|
type htmlBlockParser struct {
|
||||||
|
HTMLConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewHTMLBlockParser return a new BlockParser that can parse html
|
||||||
|
// blocks.
|
||||||
|
func NewHTMLBlockParser(opts ...HTMLOption) BlockParser {
|
||||||
|
p := &htmlBlockParser{}
|
||||||
|
for _, o := range opts {
|
||||||
|
o.SetHTMLOption(&p.HTMLConfig)
|
||||||
|
}
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *htmlBlockParser) Open(parent ast.Node, reader text.Reader, pc Context) (ast.Node, State) {
|
||||||
|
var node *ast.HTMLBlock
|
||||||
|
line, segment := reader.PeekLine()
|
||||||
|
last := pc.LastOpenedBlock().Node
|
||||||
|
if pos := pc.BlockOffset(); line[pos] != '<' {
|
||||||
|
return nil, NoChildren
|
||||||
|
}
|
||||||
|
|
||||||
|
tagName := ""
|
||||||
|
if m := htmlBlockType1OpenRegexp.FindSubmatchIndex(line); m != nil {
|
||||||
|
tagName = string(line[m[2]:m[3]])
|
||||||
|
node = ast.NewHTMLBlock(ast.HTMLBlockType1)
|
||||||
|
} else if htmlBlockType2OpenRegexp.Match(line) {
|
||||||
|
node = ast.NewHTMLBlock(ast.HTMLBlockType2)
|
||||||
|
} else if htmlBlockType3OpenRegexp.Match(line) {
|
||||||
|
node = ast.NewHTMLBlock(ast.HTMLBlockType3)
|
||||||
|
} else if htmlBlockType4OpenRegexp.Match(line) {
|
||||||
|
node = ast.NewHTMLBlock(ast.HTMLBlockType4)
|
||||||
|
} else if htmlBlockType5OpenRegexp.Match(line) {
|
||||||
|
node = ast.NewHTMLBlock(ast.HTMLBlockType5)
|
||||||
|
} else if match := htmlBlockType7Regexp.FindSubmatchIndex(line); match != nil {
|
||||||
|
isCloseTag := match[2] > -1 && bytes.Equal(line[match[2]:match[3]], []byte("/"))
|
||||||
|
hasAttr := match[6] != match[7]
|
||||||
|
tagName = strings.ToLower(string(line[match[4]:match[5]]))
|
||||||
|
_, ok := allowedBlockTags[strings.ToLower(string(tagName))]
|
||||||
|
if ok {
|
||||||
|
node = ast.NewHTMLBlock(ast.HTMLBlockType6)
|
||||||
|
} else if tagName != "script" && tagName != "style" && tagName != "pre" && !ast.IsParagraph(last) && !(isCloseTag && hasAttr) { // type 7 can not interrupt paragraph
|
||||||
|
node = ast.NewHTMLBlock(ast.HTMLBlockType7)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if node == nil {
|
||||||
|
if match := htmlBlockType6Regexp.FindSubmatchIndex(line); match != nil {
|
||||||
|
tagName = string(line[match[2]:match[3]])
|
||||||
|
_, ok := allowedBlockTags[strings.ToLower(tagName)]
|
||||||
|
if ok {
|
||||||
|
node = ast.NewHTMLBlock(ast.HTMLBlockType6)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if node != nil {
|
||||||
|
if b.FilterTags != nil {
|
||||||
|
if _, ok := b.FilterTags[tagName]; ok {
|
||||||
|
return nil, NoChildren
|
||||||
|
}
|
||||||
|
}
|
||||||
|
reader.Advance(segment.Len() - 1)
|
||||||
|
node.Lines().Append(segment)
|
||||||
|
return node, NoChildren
|
||||||
|
}
|
||||||
|
return nil, NoChildren
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *htmlBlockParser) Continue(node ast.Node, reader text.Reader, pc Context) State {
|
||||||
|
htmlBlock := node.(*ast.HTMLBlock)
|
||||||
|
lines := htmlBlock.Lines()
|
||||||
|
line, segment := reader.PeekLine()
|
||||||
|
var closurePattern []byte
|
||||||
|
|
||||||
|
switch htmlBlock.HTMLBlockType {
|
||||||
|
case ast.HTMLBlockType1:
|
||||||
|
if lines.Len() == 1 {
|
||||||
|
firstLine := lines.At(0)
|
||||||
|
if htmlBlockType1CloseRegexp.Match(firstLine.Value(reader.Source())) {
|
||||||
|
return Close
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if htmlBlockType1CloseRegexp.Match(line) {
|
||||||
|
htmlBlock.ClosureLine = segment
|
||||||
|
reader.Advance(segment.Len() - 1)
|
||||||
|
return Close
|
||||||
|
}
|
||||||
|
case ast.HTMLBlockType2:
|
||||||
|
closurePattern = htmlBlockType2Close
|
||||||
|
fallthrough
|
||||||
|
case ast.HTMLBlockType3:
|
||||||
|
if closurePattern == nil {
|
||||||
|
closurePattern = htmlBlockType3Close
|
||||||
|
}
|
||||||
|
fallthrough
|
||||||
|
case ast.HTMLBlockType4:
|
||||||
|
if closurePattern == nil {
|
||||||
|
closurePattern = htmlBlockType4Close
|
||||||
|
}
|
||||||
|
fallthrough
|
||||||
|
case ast.HTMLBlockType5:
|
||||||
|
if closurePattern == nil {
|
||||||
|
closurePattern = htmlBlockType5Close
|
||||||
|
}
|
||||||
|
|
||||||
|
if lines.Len() == 1 {
|
||||||
|
firstLine := lines.At(0)
|
||||||
|
if bytes.Contains(firstLine.Value(reader.Source()), closurePattern) {
|
||||||
|
return Close
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if bytes.Contains(line, closurePattern) {
|
||||||
|
htmlBlock.ClosureLine = segment
|
||||||
|
reader.Advance(segment.Len() - 1)
|
||||||
|
return Close
|
||||||
|
}
|
||||||
|
|
||||||
|
case ast.HTMLBlockType6, ast.HTMLBlockType7:
|
||||||
|
if util.IsBlank(line) {
|
||||||
|
return Close
|
||||||
|
}
|
||||||
|
}
|
||||||
|
node.Lines().Append(segment)
|
||||||
|
reader.Advance(segment.Len() - 1)
|
||||||
|
return Continue | NoChildren
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *htmlBlockParser) Close(node ast.Node, pc Context) {
|
||||||
|
// nothing to do
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *htmlBlockParser) CanInterruptParagraph() bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *htmlBlockParser) CanAcceptIndentedLine() bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
366
parser/link.go
Normal file
366
parser/link.go
Normal file
|
|
@ -0,0 +1,366 @@
|
||||||
|
package parser
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/yuin/goldmark/ast"
|
||||||
|
"github.com/yuin/goldmark/text"
|
||||||
|
"github.com/yuin/goldmark/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
var linkLabelStateKey = NewContextKey()
|
||||||
|
|
||||||
|
type linkLabelState struct {
|
||||||
|
ast.BaseInline
|
||||||
|
|
||||||
|
Segment text.Segment
|
||||||
|
|
||||||
|
IsImage bool
|
||||||
|
|
||||||
|
Prev *linkLabelState
|
||||||
|
|
||||||
|
Next *linkLabelState
|
||||||
|
|
||||||
|
First *linkLabelState
|
||||||
|
|
||||||
|
Last *linkLabelState
|
||||||
|
}
|
||||||
|
|
||||||
|
func newLinkLabelState(segment text.Segment, isImage bool) *linkLabelState {
|
||||||
|
return &linkLabelState{
|
||||||
|
Segment: segment,
|
||||||
|
IsImage: isImage,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *linkLabelState) Text(source []byte) []byte {
|
||||||
|
return s.Segment.Value(source)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *linkLabelState) Dump(source []byte, level int) {
|
||||||
|
fmt.Printf("%slinkLabelState: \"%s\"\n", strings.Repeat(" ", level), s.Text(source))
|
||||||
|
}
|
||||||
|
|
||||||
|
func pushLinkLabelState(pc Context, v *linkLabelState) {
|
||||||
|
tlist := pc.Get(linkLabelStateKey)
|
||||||
|
var list *linkLabelState
|
||||||
|
if tlist == nil {
|
||||||
|
list = v
|
||||||
|
v.First = v
|
||||||
|
v.Last = v
|
||||||
|
pc.Set(linkLabelStateKey, list)
|
||||||
|
} else {
|
||||||
|
list = tlist.(*linkLabelState)
|
||||||
|
l := list.Last
|
||||||
|
list.Last = v
|
||||||
|
l.Next = v
|
||||||
|
v.Prev = l
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func removeLinkLabelState(pc Context, d *linkLabelState) {
|
||||||
|
tlist := pc.Get(linkLabelStateKey)
|
||||||
|
var list *linkLabelState
|
||||||
|
if tlist == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
list = tlist.(*linkLabelState)
|
||||||
|
|
||||||
|
if d.Prev == nil {
|
||||||
|
list = d.Next
|
||||||
|
if list != nil {
|
||||||
|
list.First = d
|
||||||
|
list.Last = d.Last
|
||||||
|
list.Prev = nil
|
||||||
|
pc.Set(linkLabelStateKey, list)
|
||||||
|
} else {
|
||||||
|
pc.Set(linkLabelStateKey, nil)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
d.Prev.Next = d.Next
|
||||||
|
if d.Next != nil {
|
||||||
|
d.Next.Prev = d.Prev
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if list != nil && d.Next == nil {
|
||||||
|
list.Last = d.Prev
|
||||||
|
}
|
||||||
|
d.Next = nil
|
||||||
|
d.Prev = nil
|
||||||
|
d.First = nil
|
||||||
|
d.Last = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type linkParser struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
var defaultLinkParser = &linkParser{}
|
||||||
|
|
||||||
|
// NewLinkParser return a new InlineParser that parses links.
|
||||||
|
func NewLinkParser() InlineParser {
|
||||||
|
return defaultLinkParser
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *linkParser) Trigger() []byte {
|
||||||
|
return []byte{'!', '[', ']'}
|
||||||
|
}
|
||||||
|
|
||||||
|
var linkDestinationRegexp = regexp.MustCompile(`\s*([^\s].+)`)
|
||||||
|
var linkTitleRegexp = regexp.MustCompile(`\s+(\)|["'\(].+)`)
|
||||||
|
var linkBottom = NewContextKey()
|
||||||
|
|
||||||
|
func (s *linkParser) Parse(parent ast.Node, block text.Reader, pc Context) ast.Node {
|
||||||
|
line, segment := block.PeekLine()
|
||||||
|
if line[0] == '!' && len(line) > 1 && line[1] == '[' {
|
||||||
|
block.Advance(1)
|
||||||
|
pc.Set(linkBottom, pc.LastDelimiter())
|
||||||
|
return processLinkLabelOpen(block, segment.Start+1, true, pc)
|
||||||
|
}
|
||||||
|
if line[0] == '[' {
|
||||||
|
pc.Set(linkBottom, pc.LastDelimiter())
|
||||||
|
return processLinkLabelOpen(block, segment.Start, false, pc)
|
||||||
|
}
|
||||||
|
|
||||||
|
// line[0] == ']'
|
||||||
|
tlist := pc.Get(linkLabelStateKey)
|
||||||
|
if tlist == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
last := tlist.(*linkLabelState).Last
|
||||||
|
if last == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
block.Advance(1)
|
||||||
|
removeLinkLabelState(pc, last)
|
||||||
|
if s.containsLink(last) { // a link in a link text is not allowed
|
||||||
|
ast.MergeOrReplaceTextSegment(last.Parent(), last, last.Segment)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
labelValue := block.Value(text.NewSegment(last.Segment.Start+1, segment.Start))
|
||||||
|
if util.IsBlank(labelValue) && !last.IsImage {
|
||||||
|
ast.MergeOrReplaceTextSegment(last.Parent(), last, last.Segment)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
c := block.Peek()
|
||||||
|
l, pos := block.Position()
|
||||||
|
var link *ast.Link
|
||||||
|
var hasValue bool
|
||||||
|
if c == '(' { // normal link
|
||||||
|
link = s.parseLink(parent, last, block, pc)
|
||||||
|
} else if c == '[' { // reference link
|
||||||
|
link, hasValue = s.parseReferenceLink(parent, last, block, pc)
|
||||||
|
if link == nil && hasValue {
|
||||||
|
ast.MergeOrReplaceTextSegment(last.Parent(), last, last.Segment)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if link == nil {
|
||||||
|
// maybe shortcut reference link
|
||||||
|
block.SetPosition(l, pos)
|
||||||
|
ssegment := text.NewSegment(last.Segment.Stop, segment.Start)
|
||||||
|
maybeReference := block.Value(ssegment)
|
||||||
|
ref, ok := pc.Reference(util.ToLinkReference(maybeReference))
|
||||||
|
if !ok {
|
||||||
|
ast.MergeOrReplaceTextSegment(last.Parent(), last, last.Segment)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
link = ast.NewLink()
|
||||||
|
s.processLinkLabel(parent, link, last, pc)
|
||||||
|
link.Title = ref.Title()
|
||||||
|
link.Destination = ref.Destination()
|
||||||
|
}
|
||||||
|
if last.IsImage {
|
||||||
|
last.Parent().RemoveChild(last.Parent(), last)
|
||||||
|
return ast.NewImage(link)
|
||||||
|
}
|
||||||
|
last.Parent().RemoveChild(last.Parent(), last)
|
||||||
|
return link
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *linkParser) containsLink(last *linkLabelState) bool {
|
||||||
|
if last.IsImage {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
var c ast.Node
|
||||||
|
for c = last; c != nil; c = c.NextSibling() {
|
||||||
|
if _, ok := c.(*ast.Link); ok {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func processLinkLabelOpen(block text.Reader, pos int, isImage bool, pc Context) *linkLabelState {
|
||||||
|
start := pos
|
||||||
|
if isImage {
|
||||||
|
start--
|
||||||
|
}
|
||||||
|
state := newLinkLabelState(text.NewSegment(start, pos+1), isImage)
|
||||||
|
pushLinkLabelState(pc, state)
|
||||||
|
block.Advance(1)
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *linkParser) processLinkLabel(parent ast.Node, link *ast.Link, last *linkLabelState, pc Context) {
|
||||||
|
var bottom ast.Node
|
||||||
|
if v := pc.Get(linkBottom); v != nil {
|
||||||
|
bottom = v.(ast.Node)
|
||||||
|
}
|
||||||
|
pc.Set(linkBottom, nil)
|
||||||
|
ProcessDelimiters(bottom, pc)
|
||||||
|
for c := last.NextSibling(); c != nil; {
|
||||||
|
next := c.NextSibling()
|
||||||
|
parent.RemoveChild(parent, c)
|
||||||
|
link.AppendChild(link, c)
|
||||||
|
c = next
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *linkParser) parseReferenceLink(parent ast.Node, last *linkLabelState, block text.Reader, pc Context) (*ast.Link, bool) {
|
||||||
|
_, orgpos := block.Position()
|
||||||
|
block.Advance(1) // skip '['
|
||||||
|
line, segment := block.PeekLine()
|
||||||
|
endIndex := util.FindClosure(line, '[', ']', false, true)
|
||||||
|
if endIndex < 0 {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
block.Advance(endIndex + 1)
|
||||||
|
ssegment := segment.WithStop(segment.Start + endIndex)
|
||||||
|
maybeReference := block.Value(ssegment)
|
||||||
|
if util.IsBlank(maybeReference) { // collapsed reference link
|
||||||
|
ssegment = text.NewSegment(last.Segment.Stop, orgpos.Start-1)
|
||||||
|
maybeReference = block.Value(ssegment)
|
||||||
|
}
|
||||||
|
|
||||||
|
ref, ok := pc.Reference(util.ToLinkReference(maybeReference))
|
||||||
|
if !ok {
|
||||||
|
return nil, true
|
||||||
|
}
|
||||||
|
|
||||||
|
link := ast.NewLink()
|
||||||
|
s.processLinkLabel(parent, link, last, pc)
|
||||||
|
link.Title = ref.Title()
|
||||||
|
link.Destination = ref.Destination()
|
||||||
|
return link, true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *linkParser) parseLink(parent ast.Node, last *linkLabelState, block text.Reader, pc Context) *ast.Link {
|
||||||
|
block.Advance(1) // skip '('
|
||||||
|
block.SkipSpaces()
|
||||||
|
var title []byte
|
||||||
|
var destination []byte
|
||||||
|
var ok bool
|
||||||
|
if block.Peek() == ')' { // empty link like '[link]()'
|
||||||
|
block.Advance(1)
|
||||||
|
} else {
|
||||||
|
destination, ok = parseLinkDestination(block)
|
||||||
|
if !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
block.SkipSpaces()
|
||||||
|
if block.Peek() == ')' {
|
||||||
|
block.Advance(1)
|
||||||
|
} else {
|
||||||
|
title, ok = parseLinkTitle(block)
|
||||||
|
if !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
block.SkipSpaces()
|
||||||
|
if block.Peek() == ')' {
|
||||||
|
block.Advance(1)
|
||||||
|
} else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
link := ast.NewLink()
|
||||||
|
s.processLinkLabel(parent, link, last, pc)
|
||||||
|
link.Destination = destination
|
||||||
|
link.Title = title
|
||||||
|
return link
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseLinkDestination(block text.Reader) ([]byte, bool) {
|
||||||
|
block.SkipSpaces()
|
||||||
|
line, _ := block.PeekLine()
|
||||||
|
buf := []byte{}
|
||||||
|
if block.Peek() == '<' {
|
||||||
|
i := 1
|
||||||
|
for i < len(line) {
|
||||||
|
c := line[i]
|
||||||
|
if c == '\\' && i < len(line)-1 && util.IsPunct(line[i+1]) {
|
||||||
|
buf = append(buf, '\\', line[i+1])
|
||||||
|
i += 2
|
||||||
|
continue
|
||||||
|
} else if c == '>' {
|
||||||
|
block.Advance(i + 1)
|
||||||
|
return line[1:i], true
|
||||||
|
}
|
||||||
|
buf = append(buf, c)
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
opened := 0
|
||||||
|
i := 0
|
||||||
|
for i < len(line) {
|
||||||
|
c := line[i]
|
||||||
|
if c == '\\' && i < len(line)-1 && util.IsPunct(line[i+1]) {
|
||||||
|
buf = append(buf, '\\', line[i+1])
|
||||||
|
i += 2
|
||||||
|
continue
|
||||||
|
} else if c == '(' {
|
||||||
|
opened++
|
||||||
|
} else if c == ')' {
|
||||||
|
opened--
|
||||||
|
if opened < 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
} else if util.IsSpace(c) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
buf = append(buf, c)
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
block.Advance(i)
|
||||||
|
return line[:i], len(line[:i]) != 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseLinkTitle(block text.Reader) ([]byte, bool) {
|
||||||
|
block.SkipSpaces()
|
||||||
|
opener := block.Peek()
|
||||||
|
if opener != '"' && opener != '\'' && opener != '(' {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
closer := opener
|
||||||
|
if opener == '(' {
|
||||||
|
closer = ')'
|
||||||
|
}
|
||||||
|
line, _ := block.PeekLine()
|
||||||
|
pos := util.FindClosure(line[1:], opener, closer, false, true)
|
||||||
|
if pos < 0 {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
pos += 2 // opener + closer
|
||||||
|
block.Advance(pos)
|
||||||
|
return line[1 : pos-1], true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *linkParser) CloseBlock(parent ast.Node, pc Context) {
|
||||||
|
tlist := pc.Get(linkLabelStateKey)
|
||||||
|
if tlist == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for s := tlist.(*linkLabelState); s != nil; {
|
||||||
|
next := s.Next
|
||||||
|
removeLinkLabelState(pc, s)
|
||||||
|
s.Parent().ReplaceChild(s.Parent(), s, ast.NewTextSegment(s.Segment))
|
||||||
|
s = next
|
||||||
|
}
|
||||||
|
}
|
||||||
163
parser/link_ref.go
Normal file
163
parser/link_ref.go
Normal file
|
|
@ -0,0 +1,163 @@
|
||||||
|
package parser
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/yuin/goldmark/ast"
|
||||||
|
"github.com/yuin/goldmark/text"
|
||||||
|
"github.com/yuin/goldmark/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
type linkReferenceParagraphTransformer struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
// LinkReferenceParagraphTransformer is a ParagraphTransformer implementation
|
||||||
|
// that parses and extracts link reference from paragraphs.
|
||||||
|
var LinkReferenceParagraphTransformer = &linkReferenceParagraphTransformer{}
|
||||||
|
|
||||||
|
func (p *linkReferenceParagraphTransformer) Transform(node *ast.Paragraph, pc Context) {
|
||||||
|
lines := node.Lines()
|
||||||
|
block := text.NewBlockReader(pc.Source(), lines)
|
||||||
|
removes := [][2]int{}
|
||||||
|
for {
|
||||||
|
start, end := parseLinkReferenceDefinition(block, pc)
|
||||||
|
if start > -1 {
|
||||||
|
if start == end {
|
||||||
|
end++
|
||||||
|
}
|
||||||
|
removes = append(removes, [2]int{start, end})
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
offset := 0
|
||||||
|
for _, remove := range removes {
|
||||||
|
if lines.Len() == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
s := lines.Sliced(remove[1]-offset, lines.Len())
|
||||||
|
lines.SetSliced(0, remove[0]-offset)
|
||||||
|
lines.AppendAll(s)
|
||||||
|
offset = remove[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
if lines.Len() == 0 {
|
||||||
|
t := ast.NewTextBlock()
|
||||||
|
t.SetBlankPreviousLines(node.HasBlankPreviousLines())
|
||||||
|
node.Parent().ReplaceChild(node.Parent(), node, t)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
node.SetLines(lines)
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseLinkReferenceDefinition(block text.Reader, pc Context) (int, int) {
|
||||||
|
block.SkipSpaces()
|
||||||
|
line, segment := block.PeekLine()
|
||||||
|
if line == nil {
|
||||||
|
return -1, -1
|
||||||
|
}
|
||||||
|
startLine, _ := block.Position()
|
||||||
|
width, pos := util.IndentWidth(line, 0)
|
||||||
|
if width > 3 {
|
||||||
|
return -1, -1
|
||||||
|
}
|
||||||
|
if width != 0 {
|
||||||
|
pos++
|
||||||
|
}
|
||||||
|
if line[pos] != '[' {
|
||||||
|
return -1, -1
|
||||||
|
}
|
||||||
|
open := segment.Start + pos + 1
|
||||||
|
closes := -1
|
||||||
|
block.Advance(pos + 1)
|
||||||
|
for {
|
||||||
|
line, segment = block.PeekLine()
|
||||||
|
if line == nil {
|
||||||
|
return -1, -1
|
||||||
|
}
|
||||||
|
closure := util.FindClosure(line, '[', ']', false, false)
|
||||||
|
if closure > -1 {
|
||||||
|
closes = segment.Start + closure
|
||||||
|
next := closure + 1
|
||||||
|
if next >= len(line) || line[next] != ':' {
|
||||||
|
return -1, -1
|
||||||
|
}
|
||||||
|
block.Advance(next + 1)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
block.AdvanceLine()
|
||||||
|
}
|
||||||
|
if closes < 0 {
|
||||||
|
return -1, -1
|
||||||
|
}
|
||||||
|
label := block.Value(text.NewSegment(open, closes))
|
||||||
|
if util.IsBlank(label) {
|
||||||
|
return -1, -1
|
||||||
|
}
|
||||||
|
block.SkipSpaces()
|
||||||
|
destination, ok := parseLinkDestination(block)
|
||||||
|
if !ok {
|
||||||
|
return -1, -1
|
||||||
|
}
|
||||||
|
line, segment = block.PeekLine()
|
||||||
|
isNewLine := line == nil || util.IsBlank(line)
|
||||||
|
|
||||||
|
endLine, _ := block.Position()
|
||||||
|
_, spaces, _ := block.SkipSpaces()
|
||||||
|
opener := block.Peek()
|
||||||
|
if opener != '"' && opener != '\'' && opener != '(' {
|
||||||
|
if !isNewLine {
|
||||||
|
return -1, -1
|
||||||
|
}
|
||||||
|
ref := NewReference(label, destination, nil)
|
||||||
|
pc.AddReference(ref)
|
||||||
|
return startLine, endLine + 1
|
||||||
|
}
|
||||||
|
if spaces == 0 {
|
||||||
|
return -1, -1
|
||||||
|
}
|
||||||
|
block.Advance(1)
|
||||||
|
open = -1
|
||||||
|
closes = -1
|
||||||
|
closer := opener
|
||||||
|
if opener == '(' {
|
||||||
|
closer = ')'
|
||||||
|
}
|
||||||
|
for {
|
||||||
|
line, segment = block.PeekLine()
|
||||||
|
if line == nil {
|
||||||
|
return -1, -1
|
||||||
|
}
|
||||||
|
if open < 0 {
|
||||||
|
open = segment.Start
|
||||||
|
}
|
||||||
|
closure := util.FindClosure(line, opener, closer, false, true)
|
||||||
|
if closure > -1 {
|
||||||
|
closes = segment.Start + closure
|
||||||
|
block.Advance(closure + 1)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
block.AdvanceLine()
|
||||||
|
}
|
||||||
|
if closes < 0 {
|
||||||
|
return -1, -1
|
||||||
|
}
|
||||||
|
|
||||||
|
line, segment = block.PeekLine()
|
||||||
|
if line != nil && !util.IsBlank(line) {
|
||||||
|
if !isNewLine {
|
||||||
|
return -1, -1
|
||||||
|
}
|
||||||
|
title := block.Value(text.NewSegment(open, closes))
|
||||||
|
ref := NewReference(label, destination, title)
|
||||||
|
pc.AddReference(ref)
|
||||||
|
return startLine, endLine
|
||||||
|
}
|
||||||
|
|
||||||
|
title := block.Value(text.NewSegment(open, closes))
|
||||||
|
|
||||||
|
endLine, _ = block.Position()
|
||||||
|
ref := NewReference(label, destination, title)
|
||||||
|
pc.AddReference(ref)
|
||||||
|
return startLine, endLine + 1
|
||||||
|
}
|
||||||
241
parser/list.go
Normal file
241
parser/list.go
Normal file
|
|
@ -0,0 +1,241 @@
|
||||||
|
package parser
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/yuin/goldmark/ast"
|
||||||
|
"github.com/yuin/goldmark/text"
|
||||||
|
"github.com/yuin/goldmark/util"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
type listItemType int
|
||||||
|
|
||||||
|
const (
|
||||||
|
notList listItemType = iota
|
||||||
|
bulletList
|
||||||
|
orderedList
|
||||||
|
)
|
||||||
|
|
||||||
|
// Same as
|
||||||
|
// `^(([ ]*)([\-\*\+]))(\s+.*)?\n?$`.FindSubmatchIndex or
|
||||||
|
// `^(([ ]*)(\d{1,9}[\.\)]))(\s+.*)?\n?$`.FindSubmatchIndex
|
||||||
|
func parseListItem(line []byte) ([6]int, listItemType) {
|
||||||
|
i := 0
|
||||||
|
l := len(line)
|
||||||
|
ret := [6]int{}
|
||||||
|
for ; i < l && line[i] == ' '; i++ {
|
||||||
|
c := line[i]
|
||||||
|
if c == '\t' {
|
||||||
|
return ret, notList
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if i > 3 {
|
||||||
|
return ret, notList
|
||||||
|
}
|
||||||
|
ret[0] = 0
|
||||||
|
ret[1] = i
|
||||||
|
ret[2] = i
|
||||||
|
var typ listItemType
|
||||||
|
if i < l && line[i] == '-' || line[i] == '*' || line[i] == '+' {
|
||||||
|
i++
|
||||||
|
ret[3] = i
|
||||||
|
typ = bulletList
|
||||||
|
} else if i < l {
|
||||||
|
for ; i < l && util.IsNumeric(line[i]); i++ {
|
||||||
|
}
|
||||||
|
ret[3] = i
|
||||||
|
if ret[3] == ret[2] || ret[3]-ret[2] > 9 {
|
||||||
|
return ret, notList
|
||||||
|
}
|
||||||
|
if i < l && line[i] == '.' || line[i] == ')' {
|
||||||
|
i++
|
||||||
|
ret[3] = i
|
||||||
|
} else {
|
||||||
|
return ret, notList
|
||||||
|
}
|
||||||
|
typ = orderedList
|
||||||
|
} else {
|
||||||
|
return ret, notList
|
||||||
|
}
|
||||||
|
if line[i] != '\n' {
|
||||||
|
w, _ := util.IndentWidth(line[i:], 0)
|
||||||
|
if w == 0 {
|
||||||
|
return ret, notList
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ret[4] = i
|
||||||
|
ret[5] = len(line)
|
||||||
|
if line[ret[5]-1] == '\n' && line[i] != '\n' {
|
||||||
|
ret[5]--
|
||||||
|
}
|
||||||
|
return ret, typ
|
||||||
|
}
|
||||||
|
|
||||||
|
func matchesListItem(source []byte, strict bool) ([6]int, listItemType) {
|
||||||
|
m, typ := parseListItem(source)
|
||||||
|
if typ != notList && (!strict || strict && m[1] < 4) {
|
||||||
|
return m, typ
|
||||||
|
}
|
||||||
|
return m, notList
|
||||||
|
}
|
||||||
|
|
||||||
|
func calcListOffset(source []byte, match [6]int) int {
|
||||||
|
offset := 0
|
||||||
|
if util.IsBlank(source[match[4]:]) { // list item starts with a blank line
|
||||||
|
offset = 1
|
||||||
|
} else {
|
||||||
|
offset, _ = util.IndentWidth(source[match[4]:], match[2])
|
||||||
|
if offset > 4 { // offseted codeblock
|
||||||
|
offset = 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return offset
|
||||||
|
}
|
||||||
|
|
||||||
|
func lastOffset(node ast.Node) int {
|
||||||
|
lastChild := node.LastChild()
|
||||||
|
if lastChild != nil {
|
||||||
|
return lastChild.(*ast.ListItem).Offset
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
type listParser struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
var defaultListParser = &listParser{}
|
||||||
|
|
||||||
|
// NewListParser returns a new BlockParser that
|
||||||
|
// parses lists.
|
||||||
|
// This parser must take predecence over the ListItemParser.
|
||||||
|
func NewListParser() BlockParser {
|
||||||
|
return defaultListParser
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *listParser) Open(parent ast.Node, reader text.Reader, pc Context) (ast.Node, State) {
|
||||||
|
last := pc.LastOpenedBlock().Node
|
||||||
|
if _, lok := last.(*ast.List); lok || pc.Get(skipListParser) != nil {
|
||||||
|
pc.Set(skipListParser, nil)
|
||||||
|
return nil, NoChildren
|
||||||
|
}
|
||||||
|
line, _ := reader.PeekLine()
|
||||||
|
match, typ := matchesListItem(line, true)
|
||||||
|
if typ == notList {
|
||||||
|
return nil, NoChildren
|
||||||
|
}
|
||||||
|
start := -1
|
||||||
|
if typ == orderedList {
|
||||||
|
number := line[match[2] : match[3]-1]
|
||||||
|
start, _ = strconv.Atoi(string(number))
|
||||||
|
}
|
||||||
|
|
||||||
|
if ast.IsParagraph(last) && last.Parent() == parent {
|
||||||
|
// we allow only lists starting with 1 to interrupt paragraphs.
|
||||||
|
if typ == orderedList && start != 1 {
|
||||||
|
return nil, NoChildren
|
||||||
|
}
|
||||||
|
//an empty list item cannot interrupt a paragraph:
|
||||||
|
if match[5]-match[4] == 1 {
|
||||||
|
return nil, NoChildren
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
marker := line[match[3]-1]
|
||||||
|
node := ast.NewList(marker)
|
||||||
|
if start > -1 {
|
||||||
|
node.Start = start
|
||||||
|
}
|
||||||
|
return node, HasChildren
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *listParser) Continue(node ast.Node, reader text.Reader, pc Context) State {
|
||||||
|
list := node.(*ast.List)
|
||||||
|
line, _ := reader.PeekLine()
|
||||||
|
if util.IsBlank(line) {
|
||||||
|
// A list item can begin with at most one blank line
|
||||||
|
if node.ChildCount() == 1 && node.LastChild().ChildCount() == 0 {
|
||||||
|
return Close
|
||||||
|
}
|
||||||
|
return Continue | HasChildren
|
||||||
|
}
|
||||||
|
// Themantic Breaks take predecence over lists
|
||||||
|
if isThemanticBreak(line) {
|
||||||
|
isHeading := false
|
||||||
|
last := pc.LastOpenedBlock().Node
|
||||||
|
if ast.IsParagraph(last) {
|
||||||
|
c, ok := matchesSetextHeadingBar(line)
|
||||||
|
if ok && c == '-' {
|
||||||
|
isHeading = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !isHeading {
|
||||||
|
return Close
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// "offset" means a width that bar indicates.
|
||||||
|
// - aaaaaaaa
|
||||||
|
// |----|
|
||||||
|
//
|
||||||
|
// If the indent is less than the last offset like
|
||||||
|
// - a
|
||||||
|
// - b <--- current line
|
||||||
|
// it maybe a new child of the list.
|
||||||
|
offset := lastOffset(node)
|
||||||
|
indent, _ := util.IndentWidth(line, 0)
|
||||||
|
|
||||||
|
if indent < offset {
|
||||||
|
if indent < 4 {
|
||||||
|
match, typ := matchesListItem(line, false) // may have a leading spaces more than 3
|
||||||
|
if typ != notList && match[1]-offset < 4 {
|
||||||
|
marker := line[match[3]-1]
|
||||||
|
if !list.CanContinue(marker, typ == orderedList) {
|
||||||
|
return Close
|
||||||
|
}
|
||||||
|
return Continue | HasChildren
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Close
|
||||||
|
}
|
||||||
|
return Continue | HasChildren
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *listParser) Close(node ast.Node, pc Context) {
|
||||||
|
list := node.(*ast.List)
|
||||||
|
|
||||||
|
for c := node.FirstChild(); c != nil && list.IsTight; c = c.NextSibling() {
|
||||||
|
if c.FirstChild() != nil && c.FirstChild() != c.LastChild() {
|
||||||
|
for c1 := c.FirstChild().NextSibling(); c1 != nil; c1 = c1.NextSibling() {
|
||||||
|
if bl, ok := c1.(ast.Node); ok && bl.HasBlankPreviousLines() {
|
||||||
|
list.IsTight = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if c != node.FirstChild() {
|
||||||
|
if bl, ok := c.(ast.Node); ok && bl.HasBlankPreviousLines() {
|
||||||
|
list.IsTight = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if list.IsTight {
|
||||||
|
for child := node.FirstChild(); child != nil; child = child.NextSibling() {
|
||||||
|
for gc := child.FirstChild(); gc != nil; gc = gc.NextSibling() {
|
||||||
|
paragraph, ok := gc.(*ast.Paragraph)
|
||||||
|
if ok {
|
||||||
|
textBlock := ast.NewTextBlock()
|
||||||
|
textBlock.SetLines(paragraph.Lines())
|
||||||
|
child.ReplaceChild(child, paragraph, textBlock)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *listParser) CanInterruptParagraph() bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *listParser) CanAcceptIndentedLine() bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
81
parser/list_item.go
Normal file
81
parser/list_item.go
Normal file
|
|
@ -0,0 +1,81 @@
|
||||||
|
package parser
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/yuin/goldmark/ast"
|
||||||
|
"github.com/yuin/goldmark/text"
|
||||||
|
"github.com/yuin/goldmark/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
type listItemParser struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
var defaultListItemParser = &listItemParser{}
|
||||||
|
|
||||||
|
// NewListItemParser returns a new BlockParser that
|
||||||
|
// parses list items.
|
||||||
|
func NewListItemParser() BlockParser {
|
||||||
|
return defaultListItemParser
|
||||||
|
}
|
||||||
|
|
||||||
|
var skipListParser = NewContextKey()
|
||||||
|
var skipListParserValue interface{} = true
|
||||||
|
|
||||||
|
func (b *listItemParser) Open(parent ast.Node, reader text.Reader, pc Context) (ast.Node, State) {
|
||||||
|
list, lok := parent.(*ast.List)
|
||||||
|
if !lok { // list item must be a child of a list
|
||||||
|
return nil, NoChildren
|
||||||
|
}
|
||||||
|
offset := lastOffset(list)
|
||||||
|
line, _ := reader.PeekLine()
|
||||||
|
match, typ := matchesListItem(line, false)
|
||||||
|
if typ == notList {
|
||||||
|
return nil, NoChildren
|
||||||
|
}
|
||||||
|
if match[1]-offset > 3 {
|
||||||
|
return nil, NoChildren
|
||||||
|
}
|
||||||
|
itemOffset := calcListOffset(line, match)
|
||||||
|
node := ast.NewListItem(match[3] + itemOffset)
|
||||||
|
if match[5]-match[4] == 1 {
|
||||||
|
return node, NoChildren
|
||||||
|
}
|
||||||
|
|
||||||
|
pos, padding := util.IndentPosition(line[match[4]:], match[4], itemOffset)
|
||||||
|
child := match[3] + pos
|
||||||
|
reader.AdvanceAndSetPadding(child, padding)
|
||||||
|
return node, HasChildren
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *listItemParser) Continue(node ast.Node, reader text.Reader, pc Context) State {
|
||||||
|
line, _ := reader.PeekLine()
|
||||||
|
if util.IsBlank(line) {
|
||||||
|
return Continue | HasChildren
|
||||||
|
}
|
||||||
|
|
||||||
|
indent, _ := util.IndentWidth(line, reader.LineOffset())
|
||||||
|
offset := lastOffset(node.Parent())
|
||||||
|
if indent < offset && indent < 4 {
|
||||||
|
_, typ := matchesListItem(line, true)
|
||||||
|
// new list item found
|
||||||
|
if typ != notList {
|
||||||
|
pc.Set(skipListParser, skipListParserValue)
|
||||||
|
}
|
||||||
|
return Close
|
||||||
|
}
|
||||||
|
pos, padding := util.IndentPosition(line, reader.LineOffset(), offset)
|
||||||
|
reader.AdvanceAndSetPadding(pos, padding)
|
||||||
|
|
||||||
|
return Continue | HasChildren
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *listItemParser) Close(node ast.Node, pc Context) {
|
||||||
|
// nothing to do
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *listItemParser) CanInterruptParagraph() bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *listItemParser) CanAcceptIndentedLine() bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
62
parser/paragraph.go
Normal file
62
parser/paragraph.go
Normal file
|
|
@ -0,0 +1,62 @@
|
||||||
|
package parser
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/yuin/goldmark/ast"
|
||||||
|
"github.com/yuin/goldmark/text"
|
||||||
|
)
|
||||||
|
|
||||||
|
type paragraphParser struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
var defaultParagraphParser = ¶graphParser{}
|
||||||
|
|
||||||
|
// NewParagraphParser returns a new BlockParser that
|
||||||
|
// parses paragraphs.
|
||||||
|
func NewParagraphParser() BlockParser {
|
||||||
|
return defaultParagraphParser
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *paragraphParser) Open(parent ast.Node, reader text.Reader, pc Context) (ast.Node, State) {
|
||||||
|
_, segment := reader.PeekLine()
|
||||||
|
segment = segment.TrimLeftSpace(reader.Source())
|
||||||
|
if segment.IsEmpty() {
|
||||||
|
return nil, NoChildren
|
||||||
|
}
|
||||||
|
node := ast.NewParagraph()
|
||||||
|
node.Lines().Append(segment)
|
||||||
|
reader.Advance(segment.Len() - 1)
|
||||||
|
return node, NoChildren
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *paragraphParser) Continue(node ast.Node, reader text.Reader, pc Context) State {
|
||||||
|
_, segment := reader.PeekLine()
|
||||||
|
segment = segment.TrimLeftSpace(reader.Source())
|
||||||
|
if segment.IsEmpty() {
|
||||||
|
return Close
|
||||||
|
}
|
||||||
|
node.Lines().Append(segment)
|
||||||
|
reader.Advance(segment.Len() - 1)
|
||||||
|
return Continue | NoChildren
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *paragraphParser) Close(node ast.Node, pc Context) {
|
||||||
|
lines := node.Lines()
|
||||||
|
if lines.Len() != 0 {
|
||||||
|
// trim trailing spaces
|
||||||
|
length := lines.Len()
|
||||||
|
lastLine := node.Lines().At(length - 1)
|
||||||
|
node.Lines().Set(length-1, lastLine.TrimRightSpace(pc.Source()))
|
||||||
|
}
|
||||||
|
if lines.Len() == 0 {
|
||||||
|
node.Parent().RemoveChild(node.Parent(), node)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *paragraphParser) CanInterruptParagraph() bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *paragraphParser) CanAcceptIndentedLine() bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
987
parser/parser.go
Normal file
987
parser/parser.go
Normal file
|
|
@ -0,0 +1,987 @@
|
||||||
|
// Package parser contains stuff that are related to parsing a Markdown text.
|
||||||
|
package parser
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
|
|
||||||
|
"github.com/yuin/goldmark/ast"
|
||||||
|
"github.com/yuin/goldmark/text"
|
||||||
|
"github.com/yuin/goldmark/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
// A Reference interface represents a link reference in Markdown text.
|
||||||
|
type Reference interface {
|
||||||
|
// String implements Stringer.
|
||||||
|
String() string
|
||||||
|
|
||||||
|
// Label returns a label of the reference.
|
||||||
|
Label() []byte
|
||||||
|
|
||||||
|
// Destination returns a destination(URL) of the reference.
|
||||||
|
Destination() []byte
|
||||||
|
|
||||||
|
// Title returns a title of the reference.
|
||||||
|
Title() []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
type reference struct {
|
||||||
|
label []byte
|
||||||
|
destination []byte
|
||||||
|
title []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewReference returns a new Reference.
|
||||||
|
func NewReference(label, destination, title []byte) Reference {
|
||||||
|
return &reference{label, destination, title}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *reference) Label() []byte {
|
||||||
|
return r.label
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *reference) Destination() []byte {
|
||||||
|
return r.destination
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *reference) Title() []byte {
|
||||||
|
return r.title
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *reference) String() string {
|
||||||
|
return fmt.Sprintf("Reference{Label:%s, Destination:%s, Title:%s}", r.label, r.destination, r.title)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ContextKey is a key that is used to set arbitary values to the context.
|
||||||
|
type ContextKey int32
|
||||||
|
|
||||||
|
// New returns a new ContextKey value.
|
||||||
|
func (c *ContextKey) New() ContextKey {
|
||||||
|
return ContextKey(atomic.AddInt32((*int32)(c), 1))
|
||||||
|
}
|
||||||
|
|
||||||
|
var contextKey ContextKey
|
||||||
|
|
||||||
|
// NewContextKey return a new ContextKey value.
|
||||||
|
func NewContextKey() ContextKey {
|
||||||
|
return contextKey.New()
|
||||||
|
}
|
||||||
|
|
||||||
|
// A Context interface holds a information that are necessary to parse
|
||||||
|
// Markdown text.
|
||||||
|
type Context interface {
|
||||||
|
// String implements Stringer.
|
||||||
|
String() string
|
||||||
|
|
||||||
|
// Source returns a source of Markdown text.
|
||||||
|
Source() []byte
|
||||||
|
|
||||||
|
// Get returns a value associated with given key.
|
||||||
|
Get(ContextKey) interface{}
|
||||||
|
|
||||||
|
// Set sets given value to the context.
|
||||||
|
Set(ContextKey, interface{})
|
||||||
|
|
||||||
|
// AddReference adds given reference to this context.
|
||||||
|
AddReference(Reference)
|
||||||
|
|
||||||
|
// Reference returns (a reference, true) if a reference associated with
|
||||||
|
// given label exists, otherwise (nil, false).
|
||||||
|
Reference(label string) (Reference, bool)
|
||||||
|
|
||||||
|
// References returns a list of references.
|
||||||
|
References() []Reference
|
||||||
|
|
||||||
|
// BlockOffset returns a first non-space character position on current line.
|
||||||
|
// This value is valid only for BlockParser.Open.
|
||||||
|
BlockOffset() int
|
||||||
|
|
||||||
|
// BlockOffset sets a first non-space character position on current line.
|
||||||
|
// This value is valid only for BlockParser.Open.
|
||||||
|
SetBlockOffset(int)
|
||||||
|
|
||||||
|
// FirstDelimiter returns a first delimiter of the current delimiter list.
|
||||||
|
FirstDelimiter() *Delimiter
|
||||||
|
|
||||||
|
// LastDelimiter returns a last delimiter of the current delimiter list.
|
||||||
|
LastDelimiter() *Delimiter
|
||||||
|
|
||||||
|
// PushDelimiter appends given delimiter to the tail of the current
|
||||||
|
// delimiter list.
|
||||||
|
PushDelimiter(delimiter *Delimiter)
|
||||||
|
|
||||||
|
// RemoveDelimiter removes given delimiter from the current delimiter list.
|
||||||
|
RemoveDelimiter(d *Delimiter)
|
||||||
|
|
||||||
|
// ClearDelimiters clears the current delimiter list.
|
||||||
|
ClearDelimiters(bottom ast.Node)
|
||||||
|
|
||||||
|
// OpenedBlocks returns a list of nodes that are currently in parsing.
|
||||||
|
OpenedBlocks() []Block
|
||||||
|
|
||||||
|
// SetOpenedBlocks sets a list of nodes that are currently in parsing.
|
||||||
|
SetOpenedBlocks([]Block)
|
||||||
|
|
||||||
|
// LastOpenedBlock returns a last node that is currently in parsing.
|
||||||
|
LastOpenedBlock() Block
|
||||||
|
|
||||||
|
// SetLastOpenedBlock sets a last node that is currently in parsing.
|
||||||
|
SetLastOpenedBlock(Block)
|
||||||
|
}
|
||||||
|
|
||||||
|
// A Result interface holds a result of parsing Markdown text.
|
||||||
|
type Result interface {
|
||||||
|
// Reference returns (a reference, true) if a reference associated with
|
||||||
|
// given label exists, otherwise (nil, false).
|
||||||
|
Reference(label string) (Reference, bool)
|
||||||
|
}
|
||||||
|
|
||||||
|
type parseContext struct {
|
||||||
|
store []interface{}
|
||||||
|
source []byte
|
||||||
|
refs map[string]Reference
|
||||||
|
blockOffset int
|
||||||
|
delimiters *Delimiter
|
||||||
|
lastDelimiter *Delimiter
|
||||||
|
openedBlocks []Block
|
||||||
|
lastOpenedBlock Block
|
||||||
|
}
|
||||||
|
|
||||||
|
func newContext(source []byte) Context {
|
||||||
|
return &parseContext{
|
||||||
|
store: make([]interface{}, contextKey+1),
|
||||||
|
source: source,
|
||||||
|
refs: map[string]Reference{},
|
||||||
|
blockOffset: 0,
|
||||||
|
delimiters: nil,
|
||||||
|
lastDelimiter: nil,
|
||||||
|
openedBlocks: []Block{},
|
||||||
|
lastOpenedBlock: Block{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *parseContext) Get(key ContextKey) interface{} {
|
||||||
|
return p.store[key]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *parseContext) Set(key ContextKey, value interface{}) {
|
||||||
|
p.store[key] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *parseContext) BlockOffset() int {
|
||||||
|
return p.blockOffset
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *parseContext) SetBlockOffset(v int) {
|
||||||
|
p.blockOffset = v
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *parseContext) Source() []byte {
|
||||||
|
return p.source
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *parseContext) LastDelimiter() *Delimiter {
|
||||||
|
return p.lastDelimiter
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *parseContext) FirstDelimiter() *Delimiter {
|
||||||
|
return p.delimiters
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *parseContext) PushDelimiter(d *Delimiter) {
|
||||||
|
if p.delimiters == nil {
|
||||||
|
p.delimiters = d
|
||||||
|
p.lastDelimiter = d
|
||||||
|
} else {
|
||||||
|
l := p.lastDelimiter
|
||||||
|
p.lastDelimiter = d
|
||||||
|
l.NextDelimiter = d
|
||||||
|
d.PreviousDelimiter = l
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *parseContext) RemoveDelimiter(d *Delimiter) {
|
||||||
|
if d.PreviousDelimiter == nil {
|
||||||
|
p.delimiters = d.NextDelimiter
|
||||||
|
} else {
|
||||||
|
d.PreviousDelimiter.NextDelimiter = d.NextDelimiter
|
||||||
|
if d.NextDelimiter != nil {
|
||||||
|
d.NextDelimiter.PreviousDelimiter = d.PreviousDelimiter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if d.NextDelimiter == nil {
|
||||||
|
p.lastDelimiter = d.PreviousDelimiter
|
||||||
|
}
|
||||||
|
if p.delimiters != nil {
|
||||||
|
p.delimiters.PreviousDelimiter = nil
|
||||||
|
}
|
||||||
|
if p.lastDelimiter != nil {
|
||||||
|
p.lastDelimiter.NextDelimiter = nil
|
||||||
|
}
|
||||||
|
d.NextDelimiter = nil
|
||||||
|
d.PreviousDelimiter = nil
|
||||||
|
if d.Length != 0 {
|
||||||
|
ast.MergeOrReplaceTextSegment(d.Parent(), d, d.Segment)
|
||||||
|
} else {
|
||||||
|
d.Parent().RemoveChild(d.Parent(), d)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *parseContext) ClearDelimiters(bottom ast.Node) {
|
||||||
|
if p.lastDelimiter == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var c ast.Node
|
||||||
|
for c = p.lastDelimiter; c != nil && c != bottom; {
|
||||||
|
prev := c.PreviousSibling()
|
||||||
|
if d, ok := c.(*Delimiter); ok {
|
||||||
|
p.RemoveDelimiter(d)
|
||||||
|
}
|
||||||
|
c = prev
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *parseContext) AddReference(ref Reference) {
|
||||||
|
key := util.ToLinkReference(ref.Label())
|
||||||
|
if _, ok := p.refs[key]; !ok {
|
||||||
|
p.refs[key] = ref
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *parseContext) Reference(label string) (Reference, bool) {
|
||||||
|
v, ok := p.refs[label]
|
||||||
|
return v, ok
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *parseContext) References() []Reference {
|
||||||
|
ret := make([]Reference, 0, len(p.refs))
|
||||||
|
for _, v := range p.refs {
|
||||||
|
ret = append(ret, v)
|
||||||
|
}
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *parseContext) String() string {
|
||||||
|
refs := []string{}
|
||||||
|
for _, r := range p.refs {
|
||||||
|
refs = append(refs, r.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf("Context{Store:%#v, Refs:%s}", p.store, strings.Join(refs, ","))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *parseContext) OpenedBlocks() []Block {
|
||||||
|
return p.openedBlocks
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *parseContext) SetOpenedBlocks(v []Block) {
|
||||||
|
p.openedBlocks = v
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *parseContext) LastOpenedBlock() Block {
|
||||||
|
return p.lastOpenedBlock
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *parseContext) SetLastOpenedBlock(v Block) {
|
||||||
|
p.lastOpenedBlock = v
|
||||||
|
}
|
||||||
|
|
||||||
|
// State represents parser's state.
|
||||||
|
// State is designed to use as a bit flag.
|
||||||
|
type State int
|
||||||
|
|
||||||
|
const (
|
||||||
|
none State = 1 << iota
|
||||||
|
|
||||||
|
// Continue indicates parser can continue parsing.
|
||||||
|
Continue
|
||||||
|
|
||||||
|
// Close indicates parser cannot parse anymore.
|
||||||
|
Close
|
||||||
|
|
||||||
|
// HasChildren indicates parser may have child blocks.
|
||||||
|
HasChildren
|
||||||
|
|
||||||
|
// NoChildren indicates parser does not have child blocks.
|
||||||
|
NoChildren
|
||||||
|
)
|
||||||
|
|
||||||
|
// A Config struct is a data structure that holds configuration of the Parser.
|
||||||
|
type Config struct {
|
||||||
|
Options map[OptionName]interface{}
|
||||||
|
BlockParsers util.PrioritizedSlice /*<BlockParser>*/
|
||||||
|
InlineParsers util.PrioritizedSlice /*<InlineParser>*/
|
||||||
|
ParagraphTransformers util.PrioritizedSlice /*<ParagraphTransformer>*/
|
||||||
|
ASTTransformers util.PrioritizedSlice /*<ASTTransformer>*/
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewConfig returns a new Config.
|
||||||
|
func NewConfig() *Config {
|
||||||
|
return &Config{
|
||||||
|
Options: map[OptionName]interface{}{},
|
||||||
|
BlockParsers: util.PrioritizedSlice{},
|
||||||
|
InlineParsers: util.PrioritizedSlice{},
|
||||||
|
ParagraphTransformers: util.PrioritizedSlice{},
|
||||||
|
ASTTransformers: util.PrioritizedSlice{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// An Option interface is a functional option type for the Parser.
|
||||||
|
type Option interface {
|
||||||
|
SetConfig(*Config)
|
||||||
|
}
|
||||||
|
|
||||||
|
// OptionName is a name of parser options.
|
||||||
|
type OptionName string
|
||||||
|
|
||||||
|
// A Parser interface parses Markdown text into AST nodes.
|
||||||
|
type Parser interface {
|
||||||
|
// Parse parses given Markdown text into AST nodes.
|
||||||
|
Parse(reader text.Reader) (ast.Node, Result)
|
||||||
|
|
||||||
|
// AddOption adds given option to thie parser.
|
||||||
|
AddOption(Option)
|
||||||
|
}
|
||||||
|
|
||||||
|
// A SetOptioner interface sets given option to the object.
|
||||||
|
type SetOptioner interface {
|
||||||
|
// SetOption sets given option to the object.
|
||||||
|
// Unacceptable options may be passed.
|
||||||
|
// Thus implementations must ignore unacceptable options.
|
||||||
|
SetOption(name OptionName, value interface{})
|
||||||
|
}
|
||||||
|
|
||||||
|
// A BlockParser interface parses a block level element like Paragraph, List,
|
||||||
|
// Blockquote etc.
|
||||||
|
type BlockParser interface {
|
||||||
|
// Open parses the current line and returns a result of parsing.
|
||||||
|
//
|
||||||
|
// Open must not parse beyond the current line.
|
||||||
|
// If Open has been able to parse the current line, Open must advance a reader
|
||||||
|
// position by consumed byte length.
|
||||||
|
//
|
||||||
|
// If Open has not been able to parse the current line, Open should returns
|
||||||
|
// (nil, NoChildren). If Open has been able to parse the current line, Open
|
||||||
|
// should returns a new Block node and returns HasChildren or NoChildren.
|
||||||
|
Open(parent ast.Node, reader text.Reader, pc Context) (ast.Node, State)
|
||||||
|
|
||||||
|
// Continue parses the current line and returns a result of parsing.
|
||||||
|
//
|
||||||
|
// Continue must not parse beyond the current line.
|
||||||
|
// If Continue has been able to parse the current line, Continue must advance
|
||||||
|
// a reader position by consumed byte length.
|
||||||
|
//
|
||||||
|
// If Continue has not been able to parse the current line, Continue should
|
||||||
|
// returns Close. If Continue has been able to parse the current line,
|
||||||
|
// Continue should returns (Continue | NoChildren) or
|
||||||
|
// (Continue | HasChildren)
|
||||||
|
Continue(node ast.Node, reader text.Reader, pc Context) State
|
||||||
|
|
||||||
|
// Close will be called when the parser returns Close.
|
||||||
|
Close(node ast.Node, pc Context)
|
||||||
|
|
||||||
|
// CanInterruptParagraph returns true if the parser can interrupt pargraphs,
|
||||||
|
// otherwise false.
|
||||||
|
CanInterruptParagraph() bool
|
||||||
|
|
||||||
|
// CanAcceptIndentedLine returns true if the parser can open new node when
|
||||||
|
// given line is being indented more than 3 spaces.
|
||||||
|
CanAcceptIndentedLine() bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// An InlineParser interface parses an inline level element like CodeSpan, Link etc.
|
||||||
|
type InlineParser interface {
|
||||||
|
// Trigger returns a list of characters that triggers Parse method of
|
||||||
|
// this parser.
|
||||||
|
// Trigger characters must be a punctuation or a halfspace.
|
||||||
|
// Halfspaces triggers this parser when character is any spaces characters or
|
||||||
|
// a head of line
|
||||||
|
Trigger() []byte
|
||||||
|
|
||||||
|
// Parse parse given block into an inline node.
|
||||||
|
//
|
||||||
|
// Parse can parse beyond the current line.
|
||||||
|
// If Parse has been able to parse the current line, it must advance a reader
|
||||||
|
// position by consumed byte length.
|
||||||
|
Parse(parent ast.Node, block text.Reader, pc Context) ast.Node
|
||||||
|
|
||||||
|
// CloseBlock will be called when a block is closed.
|
||||||
|
CloseBlock(parent ast.Node, pc Context)
|
||||||
|
}
|
||||||
|
|
||||||
|
// A ParagraphTransformer transforms parsed Paragraph nodes.
|
||||||
|
// For example, link references are searched in parsed Paragraphs.
|
||||||
|
type ParagraphTransformer interface {
|
||||||
|
// Transform transforms given paragraph.
|
||||||
|
Transform(node *ast.Paragraph, pc Context)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ASTTransformer transforms entire Markdown document AST tree.
|
||||||
|
type ASTTransformer interface {
|
||||||
|
// Transform transforms given AST tree.
|
||||||
|
Transform(node *ast.Document, pc Context)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DefaultBlockParsers returns a new list of default BlockParsers.
|
||||||
|
// Priorities of default BlockParsers are:
|
||||||
|
//
|
||||||
|
// SetextHeadingParser, 100
|
||||||
|
// ThemanticBreakParser, 200
|
||||||
|
// ListParser, 300
|
||||||
|
// ListItemParser, 400
|
||||||
|
// CodeBlockParser, 500
|
||||||
|
// ATXHeadingParser, 600
|
||||||
|
// FencedCodeBlockParser, 700
|
||||||
|
// BlockquoteParser, 800
|
||||||
|
// HTMLBlockParser, 900
|
||||||
|
// ParagraphParser, 1000
|
||||||
|
func DefaultBlockParsers() []util.PrioritizedValue {
|
||||||
|
return []util.PrioritizedValue{
|
||||||
|
util.Prioritized(NewSetextHeadingParser(), 100),
|
||||||
|
util.Prioritized(NewThemanticBreakParser(), 200),
|
||||||
|
util.Prioritized(NewListParser(), 300),
|
||||||
|
util.Prioritized(NewListItemParser(), 400),
|
||||||
|
util.Prioritized(NewCodeBlockParser(), 500),
|
||||||
|
util.Prioritized(NewATXHeadingParser(), 600),
|
||||||
|
util.Prioritized(NewFencedCodeBlockParser(), 700),
|
||||||
|
util.Prioritized(NewBlockquoteParser(), 800),
|
||||||
|
util.Prioritized(NewHTMLBlockParser(), 900),
|
||||||
|
util.Prioritized(NewParagraphParser(), 1000),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DefaultInlineParsers returns a new list of default InlineParsers.
|
||||||
|
// Priorities of default InlineParsers are:
|
||||||
|
//
|
||||||
|
// CodeSpanParser, 100
|
||||||
|
// LinkParser, 200
|
||||||
|
// AutoLinkParser, 300
|
||||||
|
// RawHTMLParser, 400
|
||||||
|
// EmphasisParser, 500
|
||||||
|
func DefaultInlineParsers() []util.PrioritizedValue {
|
||||||
|
return []util.PrioritizedValue{
|
||||||
|
util.Prioritized(NewCodeSpanParser(), 100),
|
||||||
|
util.Prioritized(NewLinkParser(), 200),
|
||||||
|
util.Prioritized(NewAutoLinkParser(), 300),
|
||||||
|
util.Prioritized(NewRawHTMLParser(), 400),
|
||||||
|
util.Prioritized(NewEmphasisParser(), 500),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DefaultParagraphTransformers returns a new list of default ParagraphTransformers.
|
||||||
|
// Priorities of default ParagraphTransformers are:
|
||||||
|
//
|
||||||
|
// LinkReferenceParagraphTransformer, 100
|
||||||
|
func DefaultParagraphTransformers() []util.PrioritizedValue {
|
||||||
|
return []util.PrioritizedValue{
|
||||||
|
util.Prioritized(LinkReferenceParagraphTransformer, 100),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A Block struct holds a node and correspond parser pair.
|
||||||
|
type Block struct {
|
||||||
|
// Node is a BlockNode.
|
||||||
|
Node ast.Node
|
||||||
|
// Parser is a BlockParser.
|
||||||
|
Parser BlockParser
|
||||||
|
}
|
||||||
|
|
||||||
|
type parser struct {
|
||||||
|
options map[OptionName]interface{}
|
||||||
|
blockParsers []BlockParser
|
||||||
|
inlineParsers [256][]InlineParser
|
||||||
|
inlineParsersList []InlineParser
|
||||||
|
paragraphTransformers []ParagraphTransformer
|
||||||
|
astTransformers []ASTTransformer
|
||||||
|
config *Config
|
||||||
|
initSync sync.Once
|
||||||
|
}
|
||||||
|
|
||||||
|
type withBlockParsers struct {
|
||||||
|
value []util.PrioritizedValue
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *withBlockParsers) SetConfig(c *Config) {
|
||||||
|
c.BlockParsers = append(c.BlockParsers, o.value...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithBlockParsers is a functional option that allow you to add
|
||||||
|
// BlockParsers to the parser.
|
||||||
|
func WithBlockParsers(bs ...util.PrioritizedValue) Option {
|
||||||
|
return &withBlockParsers{bs}
|
||||||
|
}
|
||||||
|
|
||||||
|
type withInlineParsers struct {
|
||||||
|
value []util.PrioritizedValue
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *withInlineParsers) SetConfig(c *Config) {
|
||||||
|
c.InlineParsers = append(c.InlineParsers, o.value...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithInlineParsers is a functional option that allow you to add
|
||||||
|
// InlineParsers to the parser.
|
||||||
|
func WithInlineParsers(bs ...util.PrioritizedValue) Option {
|
||||||
|
return &withInlineParsers{bs}
|
||||||
|
}
|
||||||
|
|
||||||
|
type withParagraphTransformers struct {
|
||||||
|
value []util.PrioritizedValue
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *withParagraphTransformers) SetConfig(c *Config) {
|
||||||
|
c.ParagraphTransformers = append(c.ParagraphTransformers, o.value...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithParagraphTransformers is a functional option that allow you to add
|
||||||
|
// ParagraphTransformers to the parser.
|
||||||
|
func WithParagraphTransformers(ps ...util.PrioritizedValue) Option {
|
||||||
|
return &withParagraphTransformers{ps}
|
||||||
|
}
|
||||||
|
|
||||||
|
type withASTTransformers struct {
|
||||||
|
value []util.PrioritizedValue
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *withASTTransformers) SetConfig(c *Config) {
|
||||||
|
c.ASTTransformers = append(c.ASTTransformers, o.value...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithASTTransformers is a functional option that allow you to add
|
||||||
|
// ASTTransformers to the parser.
|
||||||
|
func WithASTTransformers(ps ...util.PrioritizedValue) Option {
|
||||||
|
return &withASTTransformers{ps}
|
||||||
|
}
|
||||||
|
|
||||||
|
type withOption struct {
|
||||||
|
name OptionName
|
||||||
|
value interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *withOption) SetConfig(c *Config) {
|
||||||
|
c.Options[o.name] = o.value
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithOption is a functional option that allow you to set
|
||||||
|
// an arbitary option to the parser.
|
||||||
|
func WithOption(name OptionName, value interface{}) Option {
|
||||||
|
return &withOption{name, value}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewParser returns a new Parser with given options.
|
||||||
|
func NewParser(options ...Option) Parser {
|
||||||
|
config := NewConfig()
|
||||||
|
for _, opt := range options {
|
||||||
|
opt.SetConfig(config)
|
||||||
|
}
|
||||||
|
|
||||||
|
p := &parser{
|
||||||
|
options: map[OptionName]interface{}{},
|
||||||
|
config: config,
|
||||||
|
}
|
||||||
|
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *parser) AddOption(o Option) {
|
||||||
|
o.SetConfig(p.config)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *parser) addBlockParser(v util.PrioritizedValue, options map[OptionName]interface{}) {
|
||||||
|
bp, ok := v.Value.(BlockParser)
|
||||||
|
if !ok {
|
||||||
|
panic(fmt.Sprintf("%v is not a BlockParser", v.Value))
|
||||||
|
}
|
||||||
|
so, ok := v.Value.(SetOptioner)
|
||||||
|
if ok {
|
||||||
|
for oname, ovalue := range options {
|
||||||
|
so.SetOption(oname, ovalue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
p.blockParsers = append(p.blockParsers, bp)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *parser) addInlineParser(v util.PrioritizedValue, options map[OptionName]interface{}) {
|
||||||
|
ip, ok := v.Value.(InlineParser)
|
||||||
|
if !ok {
|
||||||
|
panic(fmt.Sprintf("%v is not a InlineParser", v.Value))
|
||||||
|
}
|
||||||
|
tcs := ip.Trigger()
|
||||||
|
so, ok := v.Value.(SetOptioner)
|
||||||
|
if ok {
|
||||||
|
for oname, ovalue := range options {
|
||||||
|
so.SetOption(oname, ovalue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
p.inlineParsersList = append(p.inlineParsersList, ip)
|
||||||
|
for _, tc := range tcs {
|
||||||
|
if p.inlineParsers[tc] == nil {
|
||||||
|
p.inlineParsers[tc] = []InlineParser{}
|
||||||
|
}
|
||||||
|
p.inlineParsers[tc] = append(p.inlineParsers[tc], ip)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *parser) addParagraphTransformer(v util.PrioritizedValue, options map[OptionName]interface{}) {
|
||||||
|
pt, ok := v.Value.(ParagraphTransformer)
|
||||||
|
if !ok {
|
||||||
|
panic(fmt.Sprintf("%v is not a ParagraphTransformer", v.Value))
|
||||||
|
}
|
||||||
|
so, ok := v.Value.(SetOptioner)
|
||||||
|
if ok {
|
||||||
|
for oname, ovalue := range options {
|
||||||
|
so.SetOption(oname, ovalue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
p.paragraphTransformers = append(p.paragraphTransformers, pt)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *parser) addASTTransformer(v util.PrioritizedValue, options map[OptionName]interface{}) {
|
||||||
|
at, ok := v.Value.(ASTTransformer)
|
||||||
|
if !ok {
|
||||||
|
panic(fmt.Sprintf("%v is not a ASTTransformer", v.Value))
|
||||||
|
}
|
||||||
|
so, ok := v.Value.(SetOptioner)
|
||||||
|
if ok {
|
||||||
|
for oname, ovalue := range options {
|
||||||
|
so.SetOption(oname, ovalue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
p.astTransformers = append(p.astTransformers, at)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *parser) Parse(reader text.Reader) (ast.Node, Result) {
|
||||||
|
p.initSync.Do(func() {
|
||||||
|
p.config.BlockParsers.Sort()
|
||||||
|
for _, v := range p.config.BlockParsers {
|
||||||
|
p.addBlockParser(v, p.config.Options)
|
||||||
|
}
|
||||||
|
p.config.InlineParsers.Sort()
|
||||||
|
for _, v := range p.config.InlineParsers {
|
||||||
|
p.addInlineParser(v, p.config.Options)
|
||||||
|
}
|
||||||
|
p.config.ParagraphTransformers.Sort()
|
||||||
|
for _, v := range p.config.ParagraphTransformers {
|
||||||
|
p.addParagraphTransformer(v, p.config.Options)
|
||||||
|
}
|
||||||
|
p.config.ASTTransformers.Sort()
|
||||||
|
for _, v := range p.config.ASTTransformers {
|
||||||
|
p.addASTTransformer(v, p.config.Options)
|
||||||
|
}
|
||||||
|
p.config = nil
|
||||||
|
})
|
||||||
|
root := ast.NewDocument()
|
||||||
|
pc := newContext(reader.Source())
|
||||||
|
p.parseBlocks(root, reader, pc)
|
||||||
|
blockReader := text.NewBlockReader(reader.Source(), nil)
|
||||||
|
p.walkBlock(root, func(node ast.Node) {
|
||||||
|
p.parseBlock(blockReader, node, pc)
|
||||||
|
})
|
||||||
|
for _, at := range p.astTransformers {
|
||||||
|
at.Transform(root, pc)
|
||||||
|
}
|
||||||
|
//root.Dump(reader.Source(), 0)
|
||||||
|
return root, pc
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *parser) transformParagraph(node *ast.Paragraph, pc Context) {
|
||||||
|
for _, pt := range p.paragraphTransformers {
|
||||||
|
pt.Transform(node, pc)
|
||||||
|
if node.Parent() == nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *parser) closeBlocks(from, to int, pc Context) {
|
||||||
|
blocks := pc.OpenedBlocks()
|
||||||
|
last := pc.LastOpenedBlock()
|
||||||
|
for i := from; i >= to; i-- {
|
||||||
|
node := blocks[i].Node
|
||||||
|
if node.Parent() != nil {
|
||||||
|
blocks[i].Parser.Close(blocks[i].Node, pc)
|
||||||
|
paragraph, ok := node.(*ast.Paragraph)
|
||||||
|
if ok && node.Parent() != nil {
|
||||||
|
p.transformParagraph(paragraph, pc)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if from == len(blocks)-1 {
|
||||||
|
blocks = blocks[0:to]
|
||||||
|
} else {
|
||||||
|
blocks = append(blocks[0:to], blocks[from+1:]...)
|
||||||
|
}
|
||||||
|
l := len(blocks)
|
||||||
|
if l == 0 {
|
||||||
|
last.Node = nil
|
||||||
|
} else {
|
||||||
|
last = blocks[l-1]
|
||||||
|
}
|
||||||
|
pc.SetOpenedBlocks(blocks)
|
||||||
|
pc.SetLastOpenedBlock(last)
|
||||||
|
}
|
||||||
|
|
||||||
|
type blockOpenResult int
|
||||||
|
|
||||||
|
const (
|
||||||
|
paragraphContinuation blockOpenResult = iota + 1
|
||||||
|
newBlocksOpened
|
||||||
|
noBlocksOpened
|
||||||
|
)
|
||||||
|
|
||||||
|
func (p *parser) openBlocks(parent ast.Node, blankLine bool, reader text.Reader, pc Context) blockOpenResult {
|
||||||
|
result := blockOpenResult(noBlocksOpened)
|
||||||
|
continuable := false
|
||||||
|
lastBlock := pc.LastOpenedBlock()
|
||||||
|
if lastBlock.Node != nil {
|
||||||
|
continuable = ast.IsParagraph(lastBlock.Node)
|
||||||
|
}
|
||||||
|
retry:
|
||||||
|
shouldPeek := true
|
||||||
|
var currentLineNum int
|
||||||
|
var w int
|
||||||
|
var pos int
|
||||||
|
var line []byte
|
||||||
|
for _, bp := range p.blockParsers {
|
||||||
|
if shouldPeek {
|
||||||
|
currentLineNum, _ = reader.Position()
|
||||||
|
line, _ = reader.PeekLine()
|
||||||
|
w, pos = util.IndentWidth(line, 0)
|
||||||
|
pc.SetBlockOffset(pos)
|
||||||
|
shouldPeek = false
|
||||||
|
if line == nil || line[0] == '\n' {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if continuable && result == noBlocksOpened && !bp.CanInterruptParagraph() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if w > 3 && !bp.CanAcceptIndentedLine() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
last := pc.LastOpenedBlock().Node
|
||||||
|
node, state := bp.Open(parent, reader, pc)
|
||||||
|
if l, _ := reader.Position(); l != currentLineNum {
|
||||||
|
panic("BlockParser.Open must not advance position beyond the current line")
|
||||||
|
}
|
||||||
|
if node != nil {
|
||||||
|
shouldPeek = true
|
||||||
|
node.SetBlankPreviousLines(blankLine)
|
||||||
|
if last != nil && last.Parent() == nil {
|
||||||
|
lastPos := len(pc.OpenedBlocks()) - 1
|
||||||
|
p.closeBlocks(lastPos, lastPos, pc)
|
||||||
|
}
|
||||||
|
parent.AppendChild(parent, node)
|
||||||
|
result = newBlocksOpened
|
||||||
|
be := Block{node, bp}
|
||||||
|
pc.SetOpenedBlocks(append(pc.OpenedBlocks(), be))
|
||||||
|
pc.SetLastOpenedBlock(be)
|
||||||
|
if state == HasChildren {
|
||||||
|
parent = node
|
||||||
|
goto retry // try child block
|
||||||
|
}
|
||||||
|
break // no children, can not open more blocks on this line
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if result == noBlocksOpened && continuable {
|
||||||
|
state := lastBlock.Parser.Continue(lastBlock.Node, reader, pc)
|
||||||
|
if state&Continue != 0 {
|
||||||
|
result = paragraphContinuation
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
type lineStat struct {
|
||||||
|
lineNum int
|
||||||
|
level int
|
||||||
|
isBlank bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func isBlankLine(lineNum, level int, stats []lineStat) ([]lineStat, bool) {
|
||||||
|
ret := false
|
||||||
|
for i := len(stats) - 1 - level; i >= 0; i-- {
|
||||||
|
s := stats[i]
|
||||||
|
if s.lineNum == lineNum && s.level == level {
|
||||||
|
ret = s.isBlank
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if s.lineNum < lineNum {
|
||||||
|
return stats[i:], ret
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return stats[0:0], ret
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *parser) parseBlocks(parent ast.Node, reader text.Reader, pc Context) {
|
||||||
|
pc.SetLastOpenedBlock(Block{})
|
||||||
|
pc.SetOpenedBlocks([]Block{})
|
||||||
|
blankLines := make([]lineStat, 0, 64)
|
||||||
|
isBlank := false
|
||||||
|
for { // process blocks separated by blank lines
|
||||||
|
_, lines, ok := reader.SkipBlankLines()
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// first, we try to open blocks
|
||||||
|
if p.openBlocks(parent, lines != 0, reader, pc) != newBlocksOpened {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
lineNum, _ := reader.Position()
|
||||||
|
for i := 0; i < len(pc.OpenedBlocks()); i++ {
|
||||||
|
blankLines = append(blankLines, lineStat{lineNum - 1, i, lines != 0})
|
||||||
|
}
|
||||||
|
reader.AdvanceLine()
|
||||||
|
for len(pc.OpenedBlocks()) != 0 { // process opened blocks line by line
|
||||||
|
lastIndex := len(pc.OpenedBlocks()) - 1
|
||||||
|
for i := 0; i < len(pc.OpenedBlocks()); i++ {
|
||||||
|
be := pc.OpenedBlocks()[i]
|
||||||
|
line, _ := reader.PeekLine()
|
||||||
|
if line == nil {
|
||||||
|
p.closeBlocks(lastIndex, 0, pc)
|
||||||
|
reader.AdvanceLine()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
lineNum, _ := reader.Position()
|
||||||
|
blankLines = append(blankLines, lineStat{lineNum, i, util.IsBlank(line)})
|
||||||
|
// If node is a paragraph, p.openBlocks determines whether it is continuable.
|
||||||
|
// So we do not process paragraphs here.
|
||||||
|
if !ast.IsParagraph(be.Node) {
|
||||||
|
state := be.Parser.Continue(be.Node, reader, pc)
|
||||||
|
if state&Continue != 0 {
|
||||||
|
// When current node is a container block and has no children,
|
||||||
|
// we try to open new child nodes
|
||||||
|
if state&HasChildren != 0 && i == lastIndex {
|
||||||
|
blankLines, isBlank = isBlankLine(lineNum-1, i, blankLines)
|
||||||
|
p.openBlocks(be.Node, isBlank, reader, pc)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// current node may be closed or lazy continuation
|
||||||
|
blankLines, isBlank = isBlankLine(lineNum-1, i, blankLines)
|
||||||
|
thisParent := parent
|
||||||
|
if i != 0 {
|
||||||
|
thisParent = pc.OpenedBlocks()[i-1].Node
|
||||||
|
}
|
||||||
|
result := p.openBlocks(thisParent, isBlank, reader, pc)
|
||||||
|
if result != paragraphContinuation {
|
||||||
|
p.closeBlocks(lastIndex, i, pc)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
reader.AdvanceLine()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *parser) walkBlock(block ast.Node, cb func(node ast.Node)) {
|
||||||
|
for c := block.FirstChild(); c != nil; c = c.NextSibling() {
|
||||||
|
p.walkBlock(c, cb)
|
||||||
|
}
|
||||||
|
cb(block)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *parser) parseBlock(block text.BlockReader, parent ast.Node, pc Context) {
|
||||||
|
if parent.IsRaw() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
escaped := false
|
||||||
|
source := block.Source()
|
||||||
|
block.Reset(parent.Lines())
|
||||||
|
for {
|
||||||
|
retry:
|
||||||
|
line, _ := block.PeekLine()
|
||||||
|
if line == nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
lineLength := len(line)
|
||||||
|
l, startPosition := block.Position()
|
||||||
|
n := 0
|
||||||
|
softLinebreak := false
|
||||||
|
for i := 0; i < lineLength; i++ {
|
||||||
|
c := line[i]
|
||||||
|
if c == '\n' {
|
||||||
|
softLinebreak = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
isSpace := util.IsSpace(c)
|
||||||
|
isPunct := util.IsPunct(c)
|
||||||
|
if (isPunct && !escaped) || isSpace || i == 0 {
|
||||||
|
parserChar := c
|
||||||
|
if isSpace || (i == 0 && !isPunct) {
|
||||||
|
parserChar = ' '
|
||||||
|
}
|
||||||
|
ips := p.inlineParsers[parserChar]
|
||||||
|
if ips != nil {
|
||||||
|
block.Advance(n)
|
||||||
|
n = 0
|
||||||
|
savedLine, savedPosition := block.Position()
|
||||||
|
if i != 0 {
|
||||||
|
_, currentPosition := block.Position()
|
||||||
|
ast.MergeOrAppendTextSegment(parent, startPosition.Between(currentPosition))
|
||||||
|
_, startPosition = block.Position()
|
||||||
|
}
|
||||||
|
var inlineNode ast.Node
|
||||||
|
for _, ip := range ips {
|
||||||
|
inlineNode = ip.Parse(parent, block, pc)
|
||||||
|
if inlineNode != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
block.SetPosition(savedLine, savedPosition)
|
||||||
|
}
|
||||||
|
if inlineNode != nil {
|
||||||
|
parent.AppendChild(parent, inlineNode)
|
||||||
|
goto retry
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if escaped {
|
||||||
|
escaped = false
|
||||||
|
n++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if c == '\\' {
|
||||||
|
escaped = true
|
||||||
|
n++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
escaped = false
|
||||||
|
n++
|
||||||
|
}
|
||||||
|
if n != 0 {
|
||||||
|
block.Advance(n)
|
||||||
|
}
|
||||||
|
currentL, currentPosition := block.Position()
|
||||||
|
if l != currentL {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
diff := startPosition.Between(currentPosition)
|
||||||
|
stop := diff.Stop
|
||||||
|
hardlineBreak := false
|
||||||
|
if lineLength > 2 && line[lineLength-2] == '\\' && softLinebreak { // ends with \\n
|
||||||
|
stop--
|
||||||
|
hardlineBreak = true
|
||||||
|
} else if lineLength > 3 && line[lineLength-3] == ' ' && line[lineLength-2] == ' ' && softLinebreak { // ends with [space][space]\n
|
||||||
|
hardlineBreak = true
|
||||||
|
}
|
||||||
|
rest := diff.WithStop(stop)
|
||||||
|
text := ast.NewTextSegment(rest.TrimRightSpace(source))
|
||||||
|
text.SetSoftLineBreak(softLinebreak)
|
||||||
|
text.SetHardLineBreak(hardlineBreak)
|
||||||
|
parent.AppendChild(parent, text)
|
||||||
|
block.AdvanceLine()
|
||||||
|
}
|
||||||
|
|
||||||
|
ProcessDelimiters(nil, pc)
|
||||||
|
for _, ip := range p.inlineParsersList {
|
||||||
|
ip.CloseBlock(parent, pc)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
126
parser/raw_html.go
Normal file
126
parser/raw_html.go
Normal file
|
|
@ -0,0 +1,126 @@
|
||||||
|
package parser
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"github.com/yuin/goldmark/ast"
|
||||||
|
"github.com/yuin/goldmark/text"
|
||||||
|
"github.com/yuin/goldmark/util"
|
||||||
|
"regexp"
|
||||||
|
)
|
||||||
|
|
||||||
|
type rawHTMLParser struct {
|
||||||
|
HTMLConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewRawHTMLParser return a new InlineParser that can parse
|
||||||
|
// inline htmls
|
||||||
|
func NewRawHTMLParser(opts ...HTMLOption) InlineParser {
|
||||||
|
p := &rawHTMLParser{}
|
||||||
|
for _, o := range opts {
|
||||||
|
o.SetHTMLOption(&p.HTMLConfig)
|
||||||
|
}
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *rawHTMLParser) Trigger() []byte {
|
||||||
|
return []byte{'<'}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *rawHTMLParser) Parse(parent ast.Node, block text.Reader, pc Context) ast.Node {
|
||||||
|
line, _ := block.PeekLine()
|
||||||
|
if len(line) > 1 && util.IsAlphaNumeric(line[1]) {
|
||||||
|
return s.parseMultiLineRegexp(openTagRegexp, block, pc)
|
||||||
|
}
|
||||||
|
if len(line) > 2 && line[1] == '/' && util.IsAlphaNumeric(line[2]) {
|
||||||
|
return s.parseMultiLineRegexp(closeTagRegexp, block, pc)
|
||||||
|
}
|
||||||
|
if bytes.HasPrefix(line, []byte("<!--")) {
|
||||||
|
return s.parseMultiLineRegexp(commentRegexp, block, pc)
|
||||||
|
}
|
||||||
|
if bytes.HasPrefix(line, []byte("<?")) {
|
||||||
|
return s.parseSingleLineRegexp(processingInstructionRegexp, block, pc)
|
||||||
|
}
|
||||||
|
if len(line) > 2 && line[1] == '!' && line[2] >= 'A' && line[2] <= 'Z' {
|
||||||
|
return s.parseSingleLineRegexp(declRegexp, block, pc)
|
||||||
|
}
|
||||||
|
if bytes.HasPrefix(line, []byte("<![CDATA[")) {
|
||||||
|
return s.parseMultiLineRegexp(cdataRegexp, block, pc)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var tagnamePattern = `([A-Za-z][A-Za-z0-9-]*)`
|
||||||
|
var attributePattern = `(?:\s+[a-zA-Z_:][a-zA-Z0-9:._-]*(?:\s*=\s*(?:[^\"'=<>` + "`" + `\x00-\x20]+|'[^']*'|"[^"]*"))?)`
|
||||||
|
var openTagRegexp = regexp.MustCompile("^<" + tagnamePattern + attributePattern + `*\s*/?>`)
|
||||||
|
var closeTagRegexp = regexp.MustCompile("^</" + tagnamePattern + `\s*>`)
|
||||||
|
var commentRegexp = regexp.MustCompile(`^<!---->|<!--(?:-?[^>-])(?:-?[^-])*-->`)
|
||||||
|
var processingInstructionRegexp = regexp.MustCompile(`^(?:<\?).*?(?:\?>)`)
|
||||||
|
var declRegexp = regexp.MustCompile(`^<![A-Z]+\s+[^>]*>`)
|
||||||
|
var cdataRegexp = regexp.MustCompile(`<!\[CDATA\[[\s\S]*?\]\]>`)
|
||||||
|
|
||||||
|
func (s *rawHTMLParser) parseSingleLineRegexp(reg *regexp.Regexp, block text.Reader, pc Context) ast.Node {
|
||||||
|
line, segment := block.PeekLine()
|
||||||
|
match := reg.FindSubmatchIndex(line)
|
||||||
|
if match == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
node := ast.NewRawHTML()
|
||||||
|
node.AppendChild(node, ast.NewRawTextSegment(segment.WithStop(segment.Start+match[1])))
|
||||||
|
block.Advance(match[1])
|
||||||
|
return node
|
||||||
|
}
|
||||||
|
|
||||||
|
var dummyMatch = [][]byte{}
|
||||||
|
|
||||||
|
func (s *rawHTMLParser) parseMultiLineRegexp(reg *regexp.Regexp, block text.Reader, pc Context) ast.Node {
|
||||||
|
sline, ssegment := block.Position()
|
||||||
|
var m [][]byte
|
||||||
|
if s.FilterTags != nil {
|
||||||
|
m = block.FindSubMatch(reg)
|
||||||
|
} else {
|
||||||
|
if block.Match(reg) {
|
||||||
|
m = dummyMatch
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if m != nil {
|
||||||
|
if s.FilterTags != nil {
|
||||||
|
tagName := string(m[1])
|
||||||
|
if _, ok := s.FilterTags[tagName]; ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
node := ast.NewRawHTML()
|
||||||
|
eline, esegment := block.Position()
|
||||||
|
block.SetPosition(sline, ssegment)
|
||||||
|
for {
|
||||||
|
line, segment := block.PeekLine()
|
||||||
|
if line == nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
l, _ := block.Position()
|
||||||
|
start := segment.Start
|
||||||
|
if l == sline {
|
||||||
|
start = ssegment.Start
|
||||||
|
}
|
||||||
|
end := segment.Stop
|
||||||
|
if l == eline {
|
||||||
|
end = esegment.Start
|
||||||
|
}
|
||||||
|
|
||||||
|
node.AppendChild(node, ast.NewRawTextSegment(text.NewSegment(start, end)))
|
||||||
|
if l == eline {
|
||||||
|
block.Advance(end - start)
|
||||||
|
break
|
||||||
|
} else {
|
||||||
|
block.AdvanceLine()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return node
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *rawHTMLParser) CloseBlock(parent ast.Node, pc Context) {
|
||||||
|
// nothing to do
|
||||||
|
}
|
||||||
109
parser/setext_headings.go
Normal file
109
parser/setext_headings.go
Normal file
|
|
@ -0,0 +1,109 @@
|
||||||
|
package parser
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/yuin/goldmark/ast"
|
||||||
|
"github.com/yuin/goldmark/text"
|
||||||
|
"github.com/yuin/goldmark/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
var temporaryParagraphKey = NewContextKey()
|
||||||
|
|
||||||
|
type setextHeadingParser struct {
|
||||||
|
HeadingConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
func matchesSetextHeadingBar(line []byte) (byte, bool) {
|
||||||
|
start := 0
|
||||||
|
end := len(line)
|
||||||
|
space := util.TrimLeftLength(line, []byte{' '})
|
||||||
|
if space > 3 {
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
start += space
|
||||||
|
level1 := util.TrimLeftLength(line[start:end], []byte{'='})
|
||||||
|
c := byte('=')
|
||||||
|
var level2 int
|
||||||
|
if level1 == 0 {
|
||||||
|
level2 = util.TrimLeftLength(line[start:end], []byte{'-'})
|
||||||
|
c = '-'
|
||||||
|
}
|
||||||
|
end -= util.TrimRightSpaceLength(line[start:end])
|
||||||
|
if !((level1 > 0 && start+level1 == end) || (level2 > 0 && start+level2 == end)) {
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
return c, true
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewSetextHeadingParser return a new BlockParser that can parse Setext headings.
|
||||||
|
func NewSetextHeadingParser(opts ...HeadingOption) BlockParser {
|
||||||
|
p := &setextHeadingParser{}
|
||||||
|
for _, o := range opts {
|
||||||
|
o.SetHeadingOption(&p.HeadingConfig)
|
||||||
|
}
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *setextHeadingParser) Open(parent ast.Node, reader text.Reader, pc Context) (ast.Node, State) {
|
||||||
|
last := pc.LastOpenedBlock().Node
|
||||||
|
if last == nil {
|
||||||
|
return nil, NoChildren
|
||||||
|
}
|
||||||
|
paragraph, ok := last.(*ast.Paragraph)
|
||||||
|
if !ok || paragraph.Parent() != parent {
|
||||||
|
return nil, NoChildren
|
||||||
|
}
|
||||||
|
line, segment := reader.PeekLine()
|
||||||
|
c, ok := matchesSetextHeadingBar(line)
|
||||||
|
if !ok {
|
||||||
|
return nil, NoChildren
|
||||||
|
}
|
||||||
|
level := 1
|
||||||
|
if c == '-' {
|
||||||
|
level = 2
|
||||||
|
}
|
||||||
|
node := ast.NewHeading(level)
|
||||||
|
node.Lines().Append(segment)
|
||||||
|
pc.Set(temporaryParagraphKey, paragraph)
|
||||||
|
return node, NoChildren
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *setextHeadingParser) Continue(node ast.Node, reader text.Reader, pc Context) State {
|
||||||
|
return Close
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *setextHeadingParser) Close(node ast.Node, pc Context) {
|
||||||
|
heading := node.(*ast.Heading)
|
||||||
|
segment := node.Lines().At(0)
|
||||||
|
heading.Lines().Clear()
|
||||||
|
tmp := pc.Get(temporaryParagraphKey).(*ast.Paragraph)
|
||||||
|
pc.Set(temporaryParagraphKey, nil)
|
||||||
|
if tmp.Lines().Len() == 0 {
|
||||||
|
next := heading.NextSibling()
|
||||||
|
segment = segment.TrimLeftSpace(pc.Source())
|
||||||
|
if next == nil || !ast.IsParagraph(next) {
|
||||||
|
para := ast.NewParagraph()
|
||||||
|
para.Lines().Append(segment)
|
||||||
|
heading.Parent().InsertAfter(heading.Parent(), heading, para)
|
||||||
|
} else {
|
||||||
|
next.(ast.Node).Lines().Unshift(segment)
|
||||||
|
}
|
||||||
|
heading.Parent().RemoveChild(heading.Parent(), heading)
|
||||||
|
} else {
|
||||||
|
heading.SetLines(tmp.Lines())
|
||||||
|
heading.SetBlankPreviousLines(tmp.HasBlankPreviousLines())
|
||||||
|
tmp.Parent().RemoveChild(tmp.Parent(), tmp)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !b.HeadingID {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
parseOrGenerateHeadingID(heading, pc)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *setextHeadingParser) CanInterruptParagraph() bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *setextHeadingParser) CanAcceptIndentedLine() bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
71
parser/themantic_break.go
Normal file
71
parser/themantic_break.go
Normal file
|
|
@ -0,0 +1,71 @@
|
||||||
|
package parser
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/yuin/goldmark/ast"
|
||||||
|
"github.com/yuin/goldmark/text"
|
||||||
|
"github.com/yuin/goldmark/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
type themanticBreakParser struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
var defaultThemanticBreakParser = &themanticBreakParser{}
|
||||||
|
|
||||||
|
// NewThemanticBreakParser returns a new BlockParser that
|
||||||
|
// parses themantic breaks.
|
||||||
|
func NewThemanticBreakParser() BlockParser {
|
||||||
|
return defaultThemanticBreakParser
|
||||||
|
}
|
||||||
|
|
||||||
|
func isThemanticBreak(line []byte) bool {
|
||||||
|
w, pos := util.IndentWidth(line, 0)
|
||||||
|
if w > 3 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
mark := byte(0)
|
||||||
|
count := 0
|
||||||
|
for i := pos; i < len(line); i++ {
|
||||||
|
c := line[i]
|
||||||
|
if util.IsSpace(c) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if mark == 0 {
|
||||||
|
mark = c
|
||||||
|
count = 1
|
||||||
|
if mark == '*' || mark == '-' || mark == '_' {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if c != mark {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
count++
|
||||||
|
}
|
||||||
|
return count > 2
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *themanticBreakParser) Open(parent ast.Node, reader text.Reader, pc Context) (ast.Node, State) {
|
||||||
|
line, segment := reader.PeekLine()
|
||||||
|
if isThemanticBreak(line) {
|
||||||
|
reader.Advance(segment.Len() - 1)
|
||||||
|
return ast.NewThemanticBreak(), NoChildren
|
||||||
|
}
|
||||||
|
return nil, NoChildren
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *themanticBreakParser) Continue(node ast.Node, reader text.Reader, pc Context) State {
|
||||||
|
return Close
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *themanticBreakParser) Close(node ast.Node, pc Context) {
|
||||||
|
// nothing to do
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *themanticBreakParser) CanInterruptParagraph() bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *themanticBreakParser) CanAcceptIndentedLine() bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
588
renderer/html/html.go
Normal file
588
renderer/html/html.go
Normal file
|
|
@ -0,0 +1,588 @@
|
||||||
|
package html
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/yuin/goldmark/ast"
|
||||||
|
"github.com/yuin/goldmark/renderer"
|
||||||
|
"github.com/yuin/goldmark/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
// A Config struct has configurations for the HTML based renderers.
|
||||||
|
type Config struct {
|
||||||
|
Writer Writer
|
||||||
|
SoftLineBreak bool
|
||||||
|
XHTML bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewConfig returns a new Config with defaults.
|
||||||
|
func NewConfig() Config {
|
||||||
|
return Config{
|
||||||
|
Writer: DefaultWriter,
|
||||||
|
SoftLineBreak: false,
|
||||||
|
XHTML: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetOption implements renderer.NodeRenderer.SetOption.
|
||||||
|
func (c *Config) SetOption(name renderer.OptionName, value interface{}) {
|
||||||
|
switch name {
|
||||||
|
case SoftLineBreak:
|
||||||
|
c.SoftLineBreak = value.(bool)
|
||||||
|
case XHTML:
|
||||||
|
c.XHTML = value.(bool)
|
||||||
|
case TextWriter:
|
||||||
|
c.Writer = value.(Writer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// An Option interface sets options for HTML based renderers.
|
||||||
|
type Option interface {
|
||||||
|
SetHTMLOption(*Config)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TextWriter is an option name used in WithWriter.
|
||||||
|
const TextWriter renderer.OptionName = "Writer"
|
||||||
|
|
||||||
|
type withWriter struct {
|
||||||
|
value Writer
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *withWriter) SetConfig(c *renderer.Config) {
|
||||||
|
c.Options[TextWriter] = o.value
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *withWriter) SetHTMLOption(c *Config) {
|
||||||
|
c.Writer = o.value
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithWriter is a functional option that allow you to set given writer to
|
||||||
|
// the renderer.
|
||||||
|
func WithWriter(writer Writer) interface {
|
||||||
|
renderer.Option
|
||||||
|
Option
|
||||||
|
} {
|
||||||
|
return &withWriter{writer}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SoftLineBreak is an option name used in WithSoftLineBreak.
|
||||||
|
const SoftLineBreak renderer.OptionName = "SoftLineBreak"
|
||||||
|
|
||||||
|
type withSoftLineBreak struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *withSoftLineBreak) SetConfig(c *renderer.Config) {
|
||||||
|
c.Options[SoftLineBreak] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *withSoftLineBreak) SetHTMLOption(c *Config) {
|
||||||
|
c.SoftLineBreak = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithSoftLineBreak is a functional option that indicates whether softline breaks
|
||||||
|
// should be rendered as '<br>'.
|
||||||
|
func WithSoftLineBreak() interface {
|
||||||
|
renderer.Option
|
||||||
|
Option
|
||||||
|
} {
|
||||||
|
return &withSoftLineBreak{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// XHTML is an option name used in WithXHTML.
|
||||||
|
const XHTML renderer.OptionName = "XHTML"
|
||||||
|
|
||||||
|
type withXHTML struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *withXHTML) SetConfig(c *renderer.Config) {
|
||||||
|
c.Options[XHTML] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *withXHTML) SetHTMLOption(c *Config) {
|
||||||
|
c.XHTML = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithXHTML is a functional option indicates that nodes should be rendered in
|
||||||
|
// xhtml instead of HTML5.
|
||||||
|
func WithXHTML() interface {
|
||||||
|
Option
|
||||||
|
renderer.Option
|
||||||
|
} {
|
||||||
|
return &withXHTML{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A Renderer struct is an implementation of renderer.NodeRenderer that renders
|
||||||
|
// nodes as (X)HTML.
|
||||||
|
type Renderer struct {
|
||||||
|
Config
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewRenderer returns a new Renderer with given options.
|
||||||
|
func NewRenderer(opts ...Option) renderer.NodeRenderer {
|
||||||
|
r := &Renderer{
|
||||||
|
Config: NewConfig(),
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, opt := range opts {
|
||||||
|
opt.SetHTMLOption(&r.Config)
|
||||||
|
}
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render implements renderer.NodeRenderer.Render.
|
||||||
|
func (r *Renderer) Render(writer util.BufWriter, source []byte, n ast.Node, entering bool) (ast.WalkStatus, error) {
|
||||||
|
switch node := n.(type) {
|
||||||
|
|
||||||
|
// blocks
|
||||||
|
|
||||||
|
case *ast.Document:
|
||||||
|
return r.renderDocument(writer, source, node, entering), nil
|
||||||
|
case *ast.Heading:
|
||||||
|
return r.renderHeading(writer, source, node, entering), nil
|
||||||
|
case *ast.Blockquote:
|
||||||
|
return r.renderBlockquote(writer, source, node, entering), nil
|
||||||
|
case *ast.CodeBlock:
|
||||||
|
return r.renderCodeBlock(writer, source, node, entering), nil
|
||||||
|
case *ast.FencedCodeBlock:
|
||||||
|
return r.renderFencedCodeBlock(writer, source, node, entering), nil
|
||||||
|
case *ast.HTMLBlock:
|
||||||
|
return r.renderHTMLBlock(writer, source, node, entering), nil
|
||||||
|
case *ast.List:
|
||||||
|
return r.renderList(writer, source, node, entering), nil
|
||||||
|
case *ast.ListItem:
|
||||||
|
return r.renderListItem(writer, source, node, entering), nil
|
||||||
|
case *ast.Paragraph:
|
||||||
|
return r.renderParagraph(writer, source, node, entering), nil
|
||||||
|
case *ast.TextBlock:
|
||||||
|
return r.renderTextBlock(writer, source, node, entering), nil
|
||||||
|
case *ast.ThemanticBreak:
|
||||||
|
return r.renderThemanticBreak(writer, source, node, entering), nil
|
||||||
|
// inlines
|
||||||
|
|
||||||
|
case *ast.AutoLink:
|
||||||
|
return r.renderAutoLink(writer, source, node, entering), nil
|
||||||
|
case *ast.CodeSpan:
|
||||||
|
return r.renderCodeSpan(writer, source, node, entering), nil
|
||||||
|
case *ast.Emphasis:
|
||||||
|
return r.renderEmphasis(writer, source, node, entering), nil
|
||||||
|
case *ast.Image:
|
||||||
|
return r.renderImage(writer, source, node, entering), nil
|
||||||
|
case *ast.Link:
|
||||||
|
return r.renderLink(writer, source, node, entering), nil
|
||||||
|
case *ast.RawHTML:
|
||||||
|
return r.renderRawHTML(writer, source, node, entering), nil
|
||||||
|
case *ast.Text:
|
||||||
|
return r.renderText(writer, source, node, entering), nil
|
||||||
|
}
|
||||||
|
return ast.WalkContinue, renderer.NotSupported
|
||||||
|
}
|
||||||
|
func (r *Renderer) writeLines(w util.BufWriter, source []byte, n ast.Node) {
|
||||||
|
l := n.Lines().Len()
|
||||||
|
for i := 0; i < l; i++ {
|
||||||
|
line := n.Lines().At(i)
|
||||||
|
r.Writer.RawWrite(w, line.Value(source))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Renderer) renderDocument(w util.BufWriter, source []byte, n *ast.Document, entering bool) ast.WalkStatus {
|
||||||
|
// nothing to do
|
||||||
|
return ast.WalkContinue
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Renderer) renderHeading(w util.BufWriter, source []byte, n *ast.Heading, entering bool) ast.WalkStatus {
|
||||||
|
if entering {
|
||||||
|
w.WriteString("<h")
|
||||||
|
w.WriteByte("0123456"[n.Level])
|
||||||
|
if n.ID != nil {
|
||||||
|
w.WriteString(` id="`)
|
||||||
|
w.Write(n.ID)
|
||||||
|
w.WriteByte('"')
|
||||||
|
}
|
||||||
|
w.WriteByte('>')
|
||||||
|
} else {
|
||||||
|
w.WriteString("</h")
|
||||||
|
w.WriteByte("0123456"[n.Level])
|
||||||
|
w.WriteString(">\n")
|
||||||
|
}
|
||||||
|
return ast.WalkContinue
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Renderer) renderBlockquote(w util.BufWriter, source []byte, n *ast.Blockquote, entering bool) ast.WalkStatus {
|
||||||
|
if entering {
|
||||||
|
w.WriteString("<blockquote>\n")
|
||||||
|
} else {
|
||||||
|
w.WriteString("</blockquote>\n")
|
||||||
|
}
|
||||||
|
return ast.WalkContinue
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Renderer) renderCodeBlock(w util.BufWriter, source []byte, n *ast.CodeBlock, entering bool) ast.WalkStatus {
|
||||||
|
if entering {
|
||||||
|
w.WriteString("<pre><code>")
|
||||||
|
r.writeLines(w, source, n)
|
||||||
|
} else {
|
||||||
|
w.WriteString("</code></pre>\n")
|
||||||
|
}
|
||||||
|
return ast.WalkContinue
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Renderer) renderFencedCodeBlock(w util.BufWriter, source []byte, n *ast.FencedCodeBlock, entering bool) ast.WalkStatus {
|
||||||
|
if entering {
|
||||||
|
w.WriteString("<pre><code")
|
||||||
|
if n.Info != nil {
|
||||||
|
segment := n.Info.Segment
|
||||||
|
info := segment.Value(source)
|
||||||
|
i := 0
|
||||||
|
for ; i < len(info); i++ {
|
||||||
|
if info[i] == ' ' {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
language := info[:i]
|
||||||
|
w.WriteString(" class=\"language-")
|
||||||
|
r.Writer.Write(w, language)
|
||||||
|
w.WriteString("\"")
|
||||||
|
}
|
||||||
|
w.WriteByte('>')
|
||||||
|
r.writeLines(w, source, n)
|
||||||
|
} else {
|
||||||
|
w.WriteString("</code></pre>\n")
|
||||||
|
}
|
||||||
|
return ast.WalkContinue
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Renderer) renderHTMLBlock(w util.BufWriter, source []byte, n *ast.HTMLBlock, entering bool) ast.WalkStatus {
|
||||||
|
if entering {
|
||||||
|
l := n.Lines().Len()
|
||||||
|
for i := 0; i < l; i++ {
|
||||||
|
line := n.Lines().At(i)
|
||||||
|
w.Write(line.Value(source))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if n.HasClosure() {
|
||||||
|
closure := n.ClosureLine
|
||||||
|
w.Write(closure.Value(source))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ast.WalkContinue
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Renderer) renderList(w util.BufWriter, source []byte, n *ast.List, entering bool) ast.WalkStatus {
|
||||||
|
tag := "ul"
|
||||||
|
if n.IsOrdered() {
|
||||||
|
tag = "ol"
|
||||||
|
}
|
||||||
|
if entering {
|
||||||
|
w.WriteByte('<')
|
||||||
|
w.WriteString(tag)
|
||||||
|
if n.IsOrdered() && n.Start != 1 {
|
||||||
|
fmt.Fprintf(w, " start=\"%d\">\n", n.Start)
|
||||||
|
} else {
|
||||||
|
w.WriteString(">\n")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
w.WriteString("</")
|
||||||
|
w.WriteString(tag)
|
||||||
|
w.WriteString(">\n")
|
||||||
|
}
|
||||||
|
return ast.WalkContinue
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Renderer) renderListItem(w util.BufWriter, source []byte, n *ast.ListItem, entering bool) ast.WalkStatus {
|
||||||
|
if entering {
|
||||||
|
w.WriteString("<li>")
|
||||||
|
fc := n.FirstChild()
|
||||||
|
if fc != nil {
|
||||||
|
if _, ok := fc.(*ast.TextBlock); !ok {
|
||||||
|
w.WriteByte('\n')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
w.WriteString("</li>\n")
|
||||||
|
}
|
||||||
|
return ast.WalkContinue
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Renderer) renderParagraph(w util.BufWriter, source []byte, n *ast.Paragraph, entering bool) ast.WalkStatus {
|
||||||
|
if entering {
|
||||||
|
w.WriteString("<p>")
|
||||||
|
} else {
|
||||||
|
w.WriteString("</p>\n")
|
||||||
|
}
|
||||||
|
return ast.WalkContinue
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Renderer) renderTextBlock(w util.BufWriter, source []byte, n *ast.TextBlock, entering bool) ast.WalkStatus {
|
||||||
|
if !entering {
|
||||||
|
if _, ok := n.NextSibling().(ast.Node); ok && n.FirstChild() != nil {
|
||||||
|
w.WriteByte('\n')
|
||||||
|
}
|
||||||
|
return ast.WalkContinue
|
||||||
|
}
|
||||||
|
return ast.WalkContinue
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Renderer) renderThemanticBreak(w util.BufWriter, source []byte, n *ast.ThemanticBreak, entering bool) ast.WalkStatus {
|
||||||
|
if !entering {
|
||||||
|
return ast.WalkContinue
|
||||||
|
}
|
||||||
|
if r.XHTML {
|
||||||
|
w.WriteString("<hr />\n")
|
||||||
|
} else {
|
||||||
|
w.WriteString("<hr>\n")
|
||||||
|
}
|
||||||
|
return ast.WalkContinue
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Renderer) renderAutoLink(w util.BufWriter, source []byte, n *ast.AutoLink, entering bool) ast.WalkStatus {
|
||||||
|
if !entering {
|
||||||
|
return ast.WalkContinue
|
||||||
|
}
|
||||||
|
w.WriteString(`<a href="`)
|
||||||
|
segment := n.Value.Segment
|
||||||
|
value := segment.Value(source)
|
||||||
|
if n.AutoLinkType == ast.AutoLinkEmail && !bytes.HasPrefix(bytes.ToLower(value), []byte("mailto:")) {
|
||||||
|
w.WriteString("mailto:")
|
||||||
|
}
|
||||||
|
w.Write(util.EscapeHTML(util.URLEscape(value, false)))
|
||||||
|
w.WriteString(`">`)
|
||||||
|
w.Write(util.EscapeHTML(value))
|
||||||
|
w.WriteString(`</a>`)
|
||||||
|
return ast.WalkContinue
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Renderer) renderCodeSpan(w util.BufWriter, source []byte, n *ast.CodeSpan, entering bool) ast.WalkStatus {
|
||||||
|
if entering {
|
||||||
|
w.WriteString("<code>")
|
||||||
|
for c := n.FirstChild(); c != nil; c = c.NextSibling() {
|
||||||
|
segment := c.(*ast.Text).Segment
|
||||||
|
value := segment.Value(source)
|
||||||
|
if bytes.HasSuffix(value, []byte("\n")) {
|
||||||
|
r.Writer.RawWrite(w, value[:len(value)-1])
|
||||||
|
if c != n.LastChild() {
|
||||||
|
r.Writer.RawWrite(w, []byte(" "))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
r.Writer.RawWrite(w, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ast.WalkSkipChildren
|
||||||
|
}
|
||||||
|
w.WriteString("</code>")
|
||||||
|
return ast.WalkContinue
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Renderer) renderEmphasis(w util.BufWriter, source []byte, n *ast.Emphasis, entering bool) ast.WalkStatus {
|
||||||
|
tag := "em"
|
||||||
|
if n.Level == 2 {
|
||||||
|
tag = "strong"
|
||||||
|
}
|
||||||
|
if entering {
|
||||||
|
w.WriteByte('<')
|
||||||
|
w.WriteString(tag)
|
||||||
|
w.WriteByte('>')
|
||||||
|
} else {
|
||||||
|
w.WriteString("</")
|
||||||
|
w.WriteString(tag)
|
||||||
|
w.WriteByte('>')
|
||||||
|
}
|
||||||
|
return ast.WalkContinue
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Renderer) renderLink(w util.BufWriter, source []byte, n *ast.Link, entering bool) ast.WalkStatus {
|
||||||
|
if entering {
|
||||||
|
w.WriteString("<a href=\"")
|
||||||
|
w.Write(util.EscapeHTML(util.URLEscape(n.Destination, true)))
|
||||||
|
w.WriteByte('"')
|
||||||
|
if n.Title != nil {
|
||||||
|
w.WriteString(` title="`)
|
||||||
|
r.Writer.Write(w, n.Title)
|
||||||
|
w.WriteByte('"')
|
||||||
|
}
|
||||||
|
w.WriteByte('>')
|
||||||
|
} else {
|
||||||
|
w.WriteString("</a>")
|
||||||
|
}
|
||||||
|
return ast.WalkContinue
|
||||||
|
}
|
||||||
|
func (r *Renderer) renderImage(w util.BufWriter, source []byte, n *ast.Image, entering bool) ast.WalkStatus {
|
||||||
|
if !entering {
|
||||||
|
return ast.WalkContinue
|
||||||
|
}
|
||||||
|
w.WriteString("<img src=\"")
|
||||||
|
w.Write(util.EscapeHTML(util.URLEscape(n.Destination, true)))
|
||||||
|
w.WriteString(`" alt="`)
|
||||||
|
w.Write(n.Text(source))
|
||||||
|
w.WriteByte('"')
|
||||||
|
if n.Title != nil {
|
||||||
|
w.WriteString(` title="`)
|
||||||
|
r.Writer.Write(w, n.Title)
|
||||||
|
w.WriteByte('"')
|
||||||
|
}
|
||||||
|
if r.XHTML {
|
||||||
|
w.WriteString(" />")
|
||||||
|
} else {
|
||||||
|
w.WriteString(">")
|
||||||
|
}
|
||||||
|
return ast.WalkSkipChildren
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Renderer) renderRawHTML(w util.BufWriter, source []byte, n *ast.RawHTML, entering bool) ast.WalkStatus {
|
||||||
|
return ast.WalkContinue
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Renderer) renderText(w util.BufWriter, source []byte, n *ast.Text, entering bool) ast.WalkStatus {
|
||||||
|
if !entering {
|
||||||
|
return ast.WalkContinue
|
||||||
|
}
|
||||||
|
segment := n.Segment
|
||||||
|
if n.IsRaw() {
|
||||||
|
w.Write(segment.Value(source))
|
||||||
|
} else {
|
||||||
|
r.Writer.Write(w, segment.Value(source))
|
||||||
|
if n.HardLineBreak() || (n.SoftLineBreak() && r.SoftLineBreak) {
|
||||||
|
if r.XHTML {
|
||||||
|
w.WriteString("<br />\n")
|
||||||
|
} else {
|
||||||
|
w.WriteString("<br>\n")
|
||||||
|
}
|
||||||
|
} else if n.SoftLineBreak() {
|
||||||
|
w.WriteByte('\n')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ast.WalkContinue
|
||||||
|
}
|
||||||
|
|
||||||
|
func readWhile(source []byte, index [2]int, pred func(byte) bool) (int, bool) {
|
||||||
|
j := index[0]
|
||||||
|
ok := false
|
||||||
|
for ; j < index[1]; j++ {
|
||||||
|
c1 := source[j]
|
||||||
|
if pred(c1) {
|
||||||
|
ok = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
return j, ok
|
||||||
|
}
|
||||||
|
|
||||||
|
// A Writer interface wirtes textual contents to a writer.
|
||||||
|
type Writer interface {
|
||||||
|
// Write writes given source to writer with resolving references and unescaping
|
||||||
|
// backslash escaped characters.
|
||||||
|
Write(writer util.BufWriter, source []byte)
|
||||||
|
|
||||||
|
// RawWrite wirtes given source to writer without resolving references and
|
||||||
|
// unescaping backslash escaped characters.
|
||||||
|
RawWrite(writer util.BufWriter, source []byte)
|
||||||
|
}
|
||||||
|
|
||||||
|
type defaultWriter struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
var htmlEscaleTable = [256][]byte{nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, []byte("""), nil, nil, nil, []byte("&"), nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, []byte("<"), nil, []byte(">"), nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil}
|
||||||
|
|
||||||
|
func escapeRune(writer util.BufWriter, r rune) {
|
||||||
|
if r < 256 {
|
||||||
|
v := htmlEscaleTable[byte(r)]
|
||||||
|
if v != nil {
|
||||||
|
writer.Write(v)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
writer.WriteRune(util.ToValidRune(r))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *defaultWriter) RawWrite(writer util.BufWriter, source []byte) {
|
||||||
|
n := 0
|
||||||
|
l := len(source)
|
||||||
|
for i := 0; i < l; i++ {
|
||||||
|
v := htmlEscaleTable[source[i]]
|
||||||
|
if v != nil {
|
||||||
|
writer.Write(source[i-n : i])
|
||||||
|
n = 0
|
||||||
|
writer.Write(v)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
n++
|
||||||
|
}
|
||||||
|
if n != 0 {
|
||||||
|
writer.Write(source[l-n:])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *defaultWriter) Write(writer util.BufWriter, source []byte) {
|
||||||
|
escaped := false
|
||||||
|
ok := false
|
||||||
|
limit := len(source)
|
||||||
|
n := 0
|
||||||
|
for i := 0; i < limit; i++ {
|
||||||
|
c := source[i]
|
||||||
|
if escaped {
|
||||||
|
if util.IsPunct(c) {
|
||||||
|
d.RawWrite(writer, source[n:i-1])
|
||||||
|
n = i
|
||||||
|
escaped = false
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if c == '&' {
|
||||||
|
pos := i
|
||||||
|
next := i + 1
|
||||||
|
if next < limit && source[next] == '#' {
|
||||||
|
nnext := next + 1
|
||||||
|
nc := source[nnext]
|
||||||
|
// code point like #x22;
|
||||||
|
if nnext < limit && nc == 'x' || nc == 'X' {
|
||||||
|
start := nnext + 1
|
||||||
|
i, ok = readWhile(source, [2]int{start, limit}, util.IsHexDecimal)
|
||||||
|
if ok && i < limit && source[i] == ';' {
|
||||||
|
v, _ := strconv.ParseUint(util.BytesToReadOnlyString(source[start:i]), 16, 32)
|
||||||
|
d.RawWrite(writer, source[n:pos])
|
||||||
|
n = i + 1
|
||||||
|
escapeRune(writer, rune(v))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// code point like #1234;
|
||||||
|
} else if nc >= '0' && nc <= '9' {
|
||||||
|
start := nnext
|
||||||
|
i, ok = readWhile(source, [2]int{start, limit}, util.IsNumeric)
|
||||||
|
if ok && i < limit && i-start < 8 && source[i] == ';' {
|
||||||
|
v, _ := strconv.ParseUint(util.BytesToReadOnlyString(source[start:i]), 0, 32)
|
||||||
|
d.RawWrite(writer, source[n:pos])
|
||||||
|
n = i + 1
|
||||||
|
escapeRune(writer, rune(v))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
start := next
|
||||||
|
i, ok = readWhile(source, [2]int{start, limit}, util.IsAlphaNumeric)
|
||||||
|
// entity reference
|
||||||
|
if ok && i < limit && source[i] == ';' {
|
||||||
|
name := util.BytesToReadOnlyString(source[start:i])
|
||||||
|
entity, ok := util.LookUpHTML5EntityByName(name)
|
||||||
|
if ok {
|
||||||
|
d.RawWrite(writer, source[n:pos])
|
||||||
|
n = i + 1
|
||||||
|
d.RawWrite(writer, entity.Characters)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
i = next - 1
|
||||||
|
}
|
||||||
|
if c == '\\' {
|
||||||
|
escaped = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
escaped = false
|
||||||
|
}
|
||||||
|
d.RawWrite(writer, source[n:len(source)])
|
||||||
|
}
|
||||||
|
|
||||||
|
// DefaultWriter is a default implementation of the Writer.
|
||||||
|
var DefaultWriter = &defaultWriter{}
|
||||||
161
renderer/renderer.go
Normal file
161
renderer/renderer.go
Normal file
|
|
@ -0,0 +1,161 @@
|
||||||
|
// Package renderer renders given AST to certain formats.
|
||||||
|
package renderer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"io"
|
||||||
|
|
||||||
|
"github.com/yuin/goldmark/ast"
|
||||||
|
"github.com/yuin/goldmark/util"
|
||||||
|
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
// A Config struct is a data structure that holds configuration of the Renderer.
|
||||||
|
type Config struct {
|
||||||
|
Options map[OptionName]interface{}
|
||||||
|
NodeRenderers util.PrioritizedSlice
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewConfig returns a new Config
|
||||||
|
func NewConfig() *Config {
|
||||||
|
return &Config{
|
||||||
|
Options: map[OptionName]interface{}{},
|
||||||
|
NodeRenderers: util.PrioritizedSlice{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type notSupported struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *notSupported) Error() string {
|
||||||
|
return "not supported by this parser"
|
||||||
|
}
|
||||||
|
|
||||||
|
// NotSupported indicates given node can not be rendered by this NodeRenderer.
|
||||||
|
var NotSupported = ¬Supported{}
|
||||||
|
|
||||||
|
// An OptionName is a name of the option.
|
||||||
|
type OptionName string
|
||||||
|
|
||||||
|
// An Option interface is a functional option type for the Renderer.
|
||||||
|
type Option interface {
|
||||||
|
SetConfig(*Config)
|
||||||
|
}
|
||||||
|
|
||||||
|
type withNodeRenderers struct {
|
||||||
|
value []util.PrioritizedValue
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *withNodeRenderers) SetConfig(c *Config) {
|
||||||
|
c.NodeRenderers = append(c.NodeRenderers, o.value...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithNodeRenderers is a functional option that allow you to add
|
||||||
|
// NodeRenderers to the renderer.
|
||||||
|
func WithNodeRenderers(ps ...util.PrioritizedValue) Option {
|
||||||
|
return &withNodeRenderers{ps}
|
||||||
|
}
|
||||||
|
|
||||||
|
type withOption struct {
|
||||||
|
name OptionName
|
||||||
|
value interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *withOption) SetConfig(c *Config) {
|
||||||
|
c.Options[o.name] = o.value
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithOption is a functional option that allow you to set
|
||||||
|
// an arbitary option to the parser.
|
||||||
|
func WithOption(name OptionName, value interface{}) Option {
|
||||||
|
return &withOption{name, value}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A SetOptioner interface sets given option to the object.
|
||||||
|
type SetOptioner interface {
|
||||||
|
// SetOption sets given option to the object.
|
||||||
|
// Unacceptable options may be passed.
|
||||||
|
// Thus implementations must ignore unacceptable options.
|
||||||
|
SetOption(name OptionName, value interface{})
|
||||||
|
}
|
||||||
|
|
||||||
|
// A NodeRenderer interface renders given AST node to given writer.
|
||||||
|
type NodeRenderer interface {
|
||||||
|
// Render renders given AST node to given writer.
|
||||||
|
Render(writer util.BufWriter, source []byte, n ast.Node, entering bool) (ast.WalkStatus, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// A Renderer interface renders given AST node to given
|
||||||
|
// writer with given Renderer.
|
||||||
|
type Renderer interface {
|
||||||
|
Render(w io.Writer, source []byte, n ast.Node) error
|
||||||
|
|
||||||
|
// AddOption adds given option to thie parser.
|
||||||
|
AddOption(Option)
|
||||||
|
}
|
||||||
|
|
||||||
|
type renderer struct {
|
||||||
|
config *Config
|
||||||
|
options map[OptionName]interface{}
|
||||||
|
nodeRenderers []NodeRenderer
|
||||||
|
initSync sync.Once
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewRenderer returns a new Renderer with given options.
|
||||||
|
func NewRenderer(options ...Option) Renderer {
|
||||||
|
config := NewConfig()
|
||||||
|
for _, opt := range options {
|
||||||
|
opt.SetConfig(config)
|
||||||
|
}
|
||||||
|
|
||||||
|
r := &renderer{
|
||||||
|
options: map[OptionName]interface{}{},
|
||||||
|
config: config,
|
||||||
|
}
|
||||||
|
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *renderer) AddOption(o Option) {
|
||||||
|
o.SetConfig(r.config)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render renders given AST node to given writer with given Renderer.
|
||||||
|
func (r *renderer) Render(w io.Writer, source []byte, n ast.Node) error {
|
||||||
|
r.initSync.Do(func() {
|
||||||
|
r.options = r.config.Options
|
||||||
|
r.config.NodeRenderers.Sort()
|
||||||
|
r.nodeRenderers = make([]NodeRenderer, 0, len(r.config.NodeRenderers))
|
||||||
|
for _, v := range r.config.NodeRenderers {
|
||||||
|
nr, _ := v.Value.(NodeRenderer)
|
||||||
|
if se, ok := v.Value.(SetOptioner); ok {
|
||||||
|
for oname, ovalue := range r.options {
|
||||||
|
se.SetOption(oname, ovalue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
r.nodeRenderers = append(r.nodeRenderers, nr)
|
||||||
|
}
|
||||||
|
r.config = nil
|
||||||
|
})
|
||||||
|
writer, ok := w.(util.BufWriter)
|
||||||
|
if !ok {
|
||||||
|
writer = bufio.NewWriter(w)
|
||||||
|
}
|
||||||
|
err := ast.Walk(n, func(n ast.Node, entering bool) (ast.WalkStatus, error) {
|
||||||
|
var s ast.WalkStatus
|
||||||
|
var err error
|
||||||
|
for _, nr := range r.nodeRenderers {
|
||||||
|
s, err = nr.Render(writer, source, n, entering)
|
||||||
|
if err == NotSupported {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
return s, err
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return writer.Flush()
|
||||||
|
}
|
||||||
492
text/reader.go
Normal file
492
text/reader.go
Normal file
|
|
@ -0,0 +1,492 @@
|
||||||
|
package text
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/yuin/goldmark/util"
|
||||||
|
"io"
|
||||||
|
"regexp"
|
||||||
|
"unicode/utf8"
|
||||||
|
)
|
||||||
|
|
||||||
|
const invalidValue = -1
|
||||||
|
|
||||||
|
// EOF indicates the end of file.
|
||||||
|
const EOF = byte(0xff)
|
||||||
|
|
||||||
|
// A Reader interface provides abstracted method for reading text.
|
||||||
|
type Reader interface {
|
||||||
|
io.RuneReader
|
||||||
|
|
||||||
|
// Source returns a source of the reader.
|
||||||
|
Source() []byte
|
||||||
|
|
||||||
|
// Peek returns a byte at current position without advancing the internal pointer.
|
||||||
|
Peek() byte
|
||||||
|
|
||||||
|
// PeekLine returns the current line without advancing the internal pointer.
|
||||||
|
PeekLine() ([]byte, Segment)
|
||||||
|
|
||||||
|
// PrecendingCharacter returns a character just before current internal pointer.
|
||||||
|
PrecendingCharacter() rune
|
||||||
|
|
||||||
|
// Value returns a value of given segment.
|
||||||
|
Value(Segment) []byte
|
||||||
|
|
||||||
|
// LineOffset returns a distance from the line head to current position.
|
||||||
|
LineOffset() int
|
||||||
|
|
||||||
|
// Position returns current line number and position.
|
||||||
|
Position() (int, Segment)
|
||||||
|
|
||||||
|
// SetPosition sets current line number and position.
|
||||||
|
SetPosition(int, Segment)
|
||||||
|
|
||||||
|
// SetPadding sets padding to the reader.
|
||||||
|
SetPadding(int)
|
||||||
|
|
||||||
|
// Advance advances the internal pointer.
|
||||||
|
Advance(int)
|
||||||
|
|
||||||
|
// AdvanceAndSetPadding advances the internal pointer and add padding to the
|
||||||
|
// reader.
|
||||||
|
AdvanceAndSetPadding(int, int)
|
||||||
|
|
||||||
|
// AdvanceLine advances the internal pointer to the next line head.
|
||||||
|
AdvanceLine()
|
||||||
|
|
||||||
|
// SkipSpaces skips space characters and returns a non-blank line.
|
||||||
|
// If it reaches EOF, returns false.
|
||||||
|
SkipSpaces() (Segment, int, bool)
|
||||||
|
|
||||||
|
// SkipSpaces skips blank lines and returns a non-blank line.
|
||||||
|
// If it reaches EOF, returns false.
|
||||||
|
SkipBlankLines() (Segment, int, bool)
|
||||||
|
|
||||||
|
// Match performs regular expression matching to current line.
|
||||||
|
Match(reg *regexp.Regexp) bool
|
||||||
|
|
||||||
|
// Match performs regular expression searching to current line.
|
||||||
|
FindSubMatch(reg *regexp.Regexp) [][]byte
|
||||||
|
}
|
||||||
|
|
||||||
|
type reader struct {
|
||||||
|
source []byte
|
||||||
|
sourceLength int
|
||||||
|
line int
|
||||||
|
peekedLine []byte
|
||||||
|
pos Segment
|
||||||
|
head int
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewReader return a new Reader that can read UTF-8 bytes .
|
||||||
|
func NewReader(source []byte) Reader {
|
||||||
|
r := &reader{
|
||||||
|
source: source,
|
||||||
|
sourceLength: len(source),
|
||||||
|
line: -1,
|
||||||
|
head: 0,
|
||||||
|
}
|
||||||
|
r.AdvanceLine()
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *reader) Source() []byte {
|
||||||
|
return r.source
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *reader) Value(seg Segment) []byte {
|
||||||
|
return seg.Value(r.source)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *reader) Peek() byte {
|
||||||
|
if r.pos.Start >= 0 && r.pos.Start < r.sourceLength {
|
||||||
|
if r.pos.Padding != 0 {
|
||||||
|
return space[0]
|
||||||
|
}
|
||||||
|
return r.source[r.pos.Start]
|
||||||
|
}
|
||||||
|
return EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *reader) PeekLine() ([]byte, Segment) {
|
||||||
|
if r.pos.Start >= 0 && r.pos.Start < r.sourceLength {
|
||||||
|
if r.peekedLine == nil {
|
||||||
|
r.peekedLine = r.pos.Value(r.Source())
|
||||||
|
}
|
||||||
|
return r.peekedLine, r.pos
|
||||||
|
}
|
||||||
|
return nil, r.pos
|
||||||
|
}
|
||||||
|
|
||||||
|
// io.RuneReader interface
|
||||||
|
func (r *reader) ReadRune() (rune, int, error) {
|
||||||
|
return readRuneReader(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *reader) LineOffset() int {
|
||||||
|
v := r.pos.Start - r.head
|
||||||
|
if r.pos.Padding > 0 {
|
||||||
|
v += util.TabWidth(v) - r.pos.Padding
|
||||||
|
}
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *reader) PrecendingCharacter() rune {
|
||||||
|
if r.pos.Start <= 0 {
|
||||||
|
if r.pos.Padding != 0 {
|
||||||
|
return rune(' ')
|
||||||
|
}
|
||||||
|
return rune('\n')
|
||||||
|
}
|
||||||
|
i := r.pos.Start - 1
|
||||||
|
for ; i >= 0; i-- {
|
||||||
|
if utf8.RuneStart(r.source[i]) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rn, _ := utf8.DecodeRune(r.source[i:])
|
||||||
|
return rn
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *reader) Advance(n int) {
|
||||||
|
if n < len(r.peekedLine) && r.pos.Padding == 0 {
|
||||||
|
r.pos.Start += n
|
||||||
|
r.peekedLine = nil
|
||||||
|
return
|
||||||
|
}
|
||||||
|
r.peekedLine = nil
|
||||||
|
l := r.sourceLength
|
||||||
|
for ; n > 0 && r.pos.Start < l; n-- {
|
||||||
|
if r.pos.Padding != 0 {
|
||||||
|
r.pos.Padding--
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if r.source[r.pos.Start] == '\n' {
|
||||||
|
r.AdvanceLine()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
r.pos.Start++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *reader) AdvanceAndSetPadding(n, padding int) {
|
||||||
|
r.Advance(n)
|
||||||
|
if padding > r.pos.Padding {
|
||||||
|
r.SetPadding(padding)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *reader) AdvanceLine() {
|
||||||
|
r.peekedLine = nil
|
||||||
|
r.pos.Start = r.pos.Stop
|
||||||
|
r.head = r.pos.Start
|
||||||
|
if r.pos.Start < 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
r.pos.Stop = invalidValue
|
||||||
|
for i := r.pos.Start; i < r.sourceLength; i++ {
|
||||||
|
c := r.source[i]
|
||||||
|
if c == '\n' {
|
||||||
|
r.pos.Stop = i + 1
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
r.line++
|
||||||
|
r.pos.Padding = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *reader) Position() (int, Segment) {
|
||||||
|
return r.line, r.pos
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *reader) SetPosition(line int, pos Segment) {
|
||||||
|
r.line = line
|
||||||
|
r.pos = pos
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *reader) SetPadding(v int) {
|
||||||
|
r.pos.Padding = v
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *reader) SkipSpaces() (Segment, int, bool) {
|
||||||
|
return skipSpacesReader(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *reader) SkipBlankLines() (Segment, int, bool) {
|
||||||
|
return skipBlankLinesReader(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *reader) Match(reg *regexp.Regexp) bool {
|
||||||
|
return matchReader(r, reg)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *reader) FindSubMatch(reg *regexp.Regexp) [][]byte {
|
||||||
|
return findSubMatchReader(r, reg)
|
||||||
|
}
|
||||||
|
|
||||||
|
// A BlockReader interface is a reader that is optimized for Blocks.
|
||||||
|
type BlockReader interface {
|
||||||
|
Reader
|
||||||
|
Reset(segment *Segments)
|
||||||
|
}
|
||||||
|
|
||||||
|
type blockReader struct {
|
||||||
|
source []byte
|
||||||
|
segments *Segments
|
||||||
|
segmentsLength int
|
||||||
|
line int
|
||||||
|
pos Segment
|
||||||
|
head int
|
||||||
|
last int
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewBlockReader returns a new BlockReader.
|
||||||
|
func NewBlockReader(source []byte, segments *Segments) BlockReader {
|
||||||
|
r := &blockReader{
|
||||||
|
source: source,
|
||||||
|
}
|
||||||
|
if segments != nil {
|
||||||
|
r.Reset(segments)
|
||||||
|
}
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset resets current state and sets new segments to the reader.
|
||||||
|
func (r *blockReader) Reset(segments *Segments) {
|
||||||
|
r.segments = segments
|
||||||
|
r.segmentsLength = segments.Len()
|
||||||
|
r.line = -1
|
||||||
|
r.head = 0
|
||||||
|
r.last = 0
|
||||||
|
r.pos.Start = -1
|
||||||
|
r.pos.Stop = -1
|
||||||
|
r.pos.Padding = 0
|
||||||
|
if r.segmentsLength > 0 {
|
||||||
|
last := r.segments.At(r.segmentsLength - 1)
|
||||||
|
r.last = last.Stop
|
||||||
|
}
|
||||||
|
r.AdvanceLine()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *blockReader) Source() []byte {
|
||||||
|
return r.source
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *blockReader) Value(seg Segment) []byte {
|
||||||
|
line := r.segmentsLength - 1
|
||||||
|
ret := make([]byte, 0, seg.Stop-seg.Start+1)
|
||||||
|
for ; line >= 0; line-- {
|
||||||
|
if seg.Start >= r.segments.At(line).Start {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
i := seg.Start
|
||||||
|
for ; line < r.segmentsLength; line++ {
|
||||||
|
s := r.segments.At(line)
|
||||||
|
if i < 0 {
|
||||||
|
i = s.Start
|
||||||
|
}
|
||||||
|
ret = s.ConcatPadding(ret)
|
||||||
|
for ; i < seg.Stop && i < s.Stop; i++ {
|
||||||
|
ret = append(ret, r.source[i])
|
||||||
|
}
|
||||||
|
i = -1
|
||||||
|
if s.Stop > seg.Stop {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
// io.RuneReader interface
|
||||||
|
func (r *blockReader) ReadRune() (rune, int, error) {
|
||||||
|
return readRuneReader(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *blockReader) PrecendingCharacter() rune {
|
||||||
|
if r.pos.Padding != 0 {
|
||||||
|
return rune(' ')
|
||||||
|
}
|
||||||
|
if r.pos.Start <= 0 {
|
||||||
|
return rune('\n')
|
||||||
|
}
|
||||||
|
i := r.pos.Start - 1
|
||||||
|
for ; i >= 0; i-- {
|
||||||
|
if utf8.RuneStart(r.source[i]) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rn, _ := utf8.DecodeRune(r.source[i:])
|
||||||
|
return rn
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *blockReader) LineOffset() int {
|
||||||
|
v := r.pos.Start - r.head
|
||||||
|
if r.pos.Padding > 0 {
|
||||||
|
v += util.TabWidth(v) - r.pos.Padding
|
||||||
|
}
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *blockReader) Peek() byte {
|
||||||
|
if r.line < r.segmentsLength && r.pos.Start >= 0 && r.pos.Start < r.last {
|
||||||
|
if r.pos.Padding != 0 {
|
||||||
|
return space[0]
|
||||||
|
}
|
||||||
|
return r.source[r.pos.Start]
|
||||||
|
}
|
||||||
|
return EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *blockReader) PeekLine() ([]byte, Segment) {
|
||||||
|
if r.line < r.segmentsLength && r.pos.Start >= 0 && r.pos.Start < r.last {
|
||||||
|
return r.pos.Value(r.source), r.pos
|
||||||
|
}
|
||||||
|
return nil, r.pos
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *blockReader) Advance(n int) {
|
||||||
|
if n < r.pos.Stop-r.pos.Start && r.pos.Padding == 0 {
|
||||||
|
r.pos.Start += n
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for ; n > 0; n-- {
|
||||||
|
if r.pos.Padding != 0 {
|
||||||
|
r.pos.Padding--
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if r.pos.Start >= r.pos.Stop-1 && r.pos.Stop < r.last {
|
||||||
|
r.AdvanceLine()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
r.pos.Start++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *blockReader) AdvanceAndSetPadding(n, padding int) {
|
||||||
|
r.Advance(n)
|
||||||
|
if padding > r.pos.Padding {
|
||||||
|
r.SetPadding(padding)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *blockReader) AdvanceLine() {
|
||||||
|
r.SetPosition(r.line+1, NewSegment(invalidValue, invalidValue))
|
||||||
|
r.head = r.pos.Start
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *blockReader) Position() (int, Segment) {
|
||||||
|
return r.line, r.pos
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *blockReader) SetPosition(line int, pos Segment) {
|
||||||
|
r.line = line
|
||||||
|
if pos.Start == invalidValue {
|
||||||
|
if r.line < r.segmentsLength {
|
||||||
|
r.pos = r.segments.At(line)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
r.pos = pos
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *blockReader) SetPadding(v int) {
|
||||||
|
r.pos.Padding = v
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *blockReader) SkipSpaces() (Segment, int, bool) {
|
||||||
|
return skipSpacesReader(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *blockReader) SkipBlankLines() (Segment, int, bool) {
|
||||||
|
return skipBlankLinesReader(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *blockReader) Match(reg *regexp.Regexp) bool {
|
||||||
|
return matchReader(r, reg)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *blockReader) FindSubMatch(reg *regexp.Regexp) [][]byte {
|
||||||
|
return findSubMatchReader(r, reg)
|
||||||
|
}
|
||||||
|
|
||||||
|
func skipBlankLinesReader(r Reader) (Segment, int, bool) {
|
||||||
|
lines := 0
|
||||||
|
for {
|
||||||
|
line, seg := r.PeekLine()
|
||||||
|
if line == nil {
|
||||||
|
return seg, lines, false
|
||||||
|
}
|
||||||
|
if util.IsBlank(line) {
|
||||||
|
lines++
|
||||||
|
r.AdvanceLine()
|
||||||
|
} else {
|
||||||
|
return seg, lines, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func skipSpacesReader(r Reader) (Segment, int, bool) {
|
||||||
|
chars := 0
|
||||||
|
for {
|
||||||
|
line, segment := r.PeekLine()
|
||||||
|
if line == nil {
|
||||||
|
return segment, chars, false
|
||||||
|
}
|
||||||
|
for i, c := range line {
|
||||||
|
if util.IsSpace(c) {
|
||||||
|
chars++
|
||||||
|
r.Advance(1)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return segment.WithStart(segment.Start + i + 1), chars, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func matchReader(r Reader, reg *regexp.Regexp) bool {
|
||||||
|
oldline, oldseg := r.Position()
|
||||||
|
match := reg.FindReaderSubmatchIndex(r)
|
||||||
|
r.SetPosition(oldline, oldseg)
|
||||||
|
if match == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
r.Advance(match[1] - match[0])
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func findSubMatchReader(r Reader, reg *regexp.Regexp) [][]byte {
|
||||||
|
oldline, oldseg := r.Position()
|
||||||
|
match := reg.FindReaderSubmatchIndex(r)
|
||||||
|
r.SetPosition(oldline, oldseg)
|
||||||
|
if match == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
runes := make([]rune, 0, match[1]-match[0])
|
||||||
|
for i := 0; i < match[1]; {
|
||||||
|
r, size, _ := readRuneReader(r)
|
||||||
|
i += size
|
||||||
|
runes = append(runes, r)
|
||||||
|
}
|
||||||
|
result := [][]byte{}
|
||||||
|
for i := 0; i < len(match); i += 2 {
|
||||||
|
result = append(result, []byte(string(runes[match[i]:match[i+1]])))
|
||||||
|
}
|
||||||
|
|
||||||
|
r.SetPosition(oldline, oldseg)
|
||||||
|
r.Advance(match[1] - match[0])
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func readRuneReader(r Reader) (rune, int, error) {
|
||||||
|
line, _ := r.PeekLine()
|
||||||
|
if line == nil {
|
||||||
|
return 0, 0, io.EOF
|
||||||
|
}
|
||||||
|
rn, size := utf8.DecodeRune(line)
|
||||||
|
if rn == utf8.RuneError {
|
||||||
|
return 0, 0, io.EOF
|
||||||
|
}
|
||||||
|
r.Advance(size)
|
||||||
|
return rn, size, nil
|
||||||
|
}
|
||||||
209
text/segment.go
Normal file
209
text/segment.go
Normal file
|
|
@ -0,0 +1,209 @@
|
||||||
|
package text
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"github.com/yuin/goldmark/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
var space = []byte(" ")
|
||||||
|
|
||||||
|
// A Segment struct holds information about source potisions.
|
||||||
|
type Segment struct {
|
||||||
|
// Start is a start position of the segment.
|
||||||
|
Start int
|
||||||
|
|
||||||
|
// Stop is a stop position of the segment.
|
||||||
|
// This value should be excluded.
|
||||||
|
Stop int
|
||||||
|
|
||||||
|
// Padding is a padding length of the segment.
|
||||||
|
Padding int
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewSegment return a new Segment.
|
||||||
|
func NewSegment(start, stop int) Segment {
|
||||||
|
return Segment{
|
||||||
|
Start: start,
|
||||||
|
Stop: stop,
|
||||||
|
Padding: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewSegmentPadding returns a new Segment with given padding.
|
||||||
|
func NewSegmentPadding(start, stop, n int) Segment {
|
||||||
|
return Segment{
|
||||||
|
Start: start,
|
||||||
|
Stop: stop,
|
||||||
|
Padding: n,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Value returns a value of the segment.
|
||||||
|
func (t *Segment) Value(buffer []byte) []byte {
|
||||||
|
if t.Padding == 0 {
|
||||||
|
return buffer[t.Start:t.Stop]
|
||||||
|
}
|
||||||
|
result := make([]byte, 0, t.Padding+t.Stop-t.Start+1)
|
||||||
|
result = append(result, bytes.Repeat(space, t.Padding)...)
|
||||||
|
return append(result, buffer[t.Start:t.Stop]...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Len returns a length of the segment.
|
||||||
|
func (t *Segment) Len() int {
|
||||||
|
return t.Stop - t.Start + t.Padding
|
||||||
|
}
|
||||||
|
|
||||||
|
// Between returns a segment between this segment and given segment.
|
||||||
|
func (t *Segment) Between(other Segment) Segment {
|
||||||
|
if t.Stop != other.Stop {
|
||||||
|
panic("invalid state")
|
||||||
|
}
|
||||||
|
return NewSegmentPadding(
|
||||||
|
t.Start,
|
||||||
|
other.Start,
|
||||||
|
t.Padding-other.Padding,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsEmpty returns true if this segment is empty, otherwise false.
|
||||||
|
func (t *Segment) IsEmpty() bool {
|
||||||
|
return t.Start >= t.Stop && t.Padding == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// TrimRightSpace returns a new segment by slicing off all trailing
|
||||||
|
// space characters.
|
||||||
|
func (t *Segment) TrimRightSpace(buffer []byte) Segment {
|
||||||
|
v := buffer[t.Start:t.Stop]
|
||||||
|
l := util.TrimRightSpaceLength(v)
|
||||||
|
if l == len(v) {
|
||||||
|
return NewSegment(t.Start, t.Start)
|
||||||
|
}
|
||||||
|
return NewSegmentPadding(t.Start, t.Stop-l, t.Padding)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TrimLeftSpace returns a new segment by slicing off all leading
|
||||||
|
// space characters including padding.
|
||||||
|
func (t *Segment) TrimLeftSpace(buffer []byte) Segment {
|
||||||
|
v := buffer[t.Start:t.Stop]
|
||||||
|
l := util.TrimLeftSpaceLength(v)
|
||||||
|
return NewSegment(t.Start+l, t.Stop)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TrimLeftSpaceWidth returns a new segment by slicing off leading space
|
||||||
|
// characters until given width.
|
||||||
|
func (t *Segment) TrimLeftSpaceWidth(width int, buffer []byte) Segment {
|
||||||
|
padding := t.Padding
|
||||||
|
for ; width > 0; width-- {
|
||||||
|
if padding == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
padding--
|
||||||
|
}
|
||||||
|
if width == 0 {
|
||||||
|
return NewSegmentPadding(t.Start, t.Stop, padding)
|
||||||
|
}
|
||||||
|
text := buffer[t.Start:t.Stop]
|
||||||
|
start := t.Start
|
||||||
|
for _, c := range text {
|
||||||
|
if start >= t.Stop-1 || width <= 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if c == ' ' {
|
||||||
|
width--
|
||||||
|
} else if c == '\t' {
|
||||||
|
width -= 4
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
start++
|
||||||
|
}
|
||||||
|
if width < 0 {
|
||||||
|
padding = width * -1
|
||||||
|
}
|
||||||
|
return NewSegmentPadding(start, t.Stop, padding)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithStart returns a new Segment with same value except Start.
|
||||||
|
func (t *Segment) WithStart(v int) Segment {
|
||||||
|
return NewSegmentPadding(v, t.Stop, t.Padding)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithStop returns a new Segment with same value except Stop.
|
||||||
|
func (t *Segment) WithStop(v int) Segment {
|
||||||
|
return NewSegmentPadding(t.Start, v, t.Padding)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConcatPadding concats the padding to given slice.
|
||||||
|
func (t *Segment) ConcatPadding(v []byte) []byte {
|
||||||
|
if t.Padding > 0 {
|
||||||
|
return append(v, bytes.Repeat(space, t.Padding)...)
|
||||||
|
}
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
// Segments is a collection of the Segment.
|
||||||
|
type Segments struct {
|
||||||
|
values []Segment
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewSegments return a new Segments.
|
||||||
|
func NewSegments() *Segments {
|
||||||
|
return &Segments{
|
||||||
|
values: nil,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Append appends given segment after the tail of the collection.
|
||||||
|
func (s *Segments) Append(t Segment) {
|
||||||
|
if s.values == nil {
|
||||||
|
s.values = make([]Segment, 0, 20)
|
||||||
|
}
|
||||||
|
s.values = append(s.values, t)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AppendAll appends all elements of given segments after the tail of the collection.
|
||||||
|
func (s *Segments) AppendAll(t []Segment) {
|
||||||
|
if s.values == nil {
|
||||||
|
s.values = make([]Segment, 0, 20)
|
||||||
|
}
|
||||||
|
s.values = append(s.values, t...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Len returns the length of the collection.
|
||||||
|
func (s *Segments) Len() int {
|
||||||
|
if s.values == nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return len(s.values)
|
||||||
|
}
|
||||||
|
|
||||||
|
// At returns a segment at given index.
|
||||||
|
func (s *Segments) At(i int) Segment {
|
||||||
|
return s.values[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set sets given Segment.
|
||||||
|
func (s *Segments) Set(i int, v Segment) {
|
||||||
|
s.values[i] = v
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetSliced replace the collection with a subsliced value.
|
||||||
|
func (s *Segments) SetSliced(lo, hi int) {
|
||||||
|
s.values = s.values[lo:hi]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sliced returns a subslice of the collection.
|
||||||
|
func (s *Segments) Sliced(lo, hi int) []Segment {
|
||||||
|
return s.values[lo:hi]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear delete all element of the collction.
|
||||||
|
func (s *Segments) Clear() {
|
||||||
|
s.values = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unshift insert given Segment to head of the collection.
|
||||||
|
func (s *Segments) Unshift(v Segment) {
|
||||||
|
s.values = append(s.values[0:1], s.values[0:]...)
|
||||||
|
s.values[0] = v
|
||||||
|
}
|
||||||
2142
util/html5entities.go
Normal file
2142
util/html5entities.go
Normal file
File diff suppressed because it is too large
Load diff
538
util/util.go
Normal file
538
util/util.go
Normal file
|
|
@ -0,0 +1,538 @@
|
||||||
|
// Package util provides utility functions for the goldmark.
|
||||||
|
package util
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/url"
|
||||||
|
"regexp"
|
||||||
|
"sort"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"unicode/utf8"
|
||||||
|
)
|
||||||
|
|
||||||
|
// IsBlank returns true if given string is all space characters.
|
||||||
|
func IsBlank(bs []byte) bool {
|
||||||
|
for _, b := range bs {
|
||||||
|
if IsSpace(b) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// DedentPosition dedents lines by given width.
|
||||||
|
func DedentPosition(bs []byte, width int) (pos, padding int) {
|
||||||
|
i := 0
|
||||||
|
l := len(bs)
|
||||||
|
w := 0
|
||||||
|
for ; i < l && w < width; i++ {
|
||||||
|
b := bs[i]
|
||||||
|
if b == ' ' {
|
||||||
|
w++
|
||||||
|
} else if b == '\t' {
|
||||||
|
w += 4
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
padding = w - width
|
||||||
|
if padding < 0 {
|
||||||
|
padding = 0
|
||||||
|
}
|
||||||
|
return i, padding
|
||||||
|
}
|
||||||
|
|
||||||
|
// VisualizeSpaces visualize invisible space characters.
|
||||||
|
func VisualizeSpaces(bs []byte) []byte {
|
||||||
|
bs = bytes.Replace(bs, []byte(" "), []byte("[SPACE]"), -1)
|
||||||
|
bs = bytes.Replace(bs, []byte("\t"), []byte("[TAB]"), -1)
|
||||||
|
bs = bytes.Replace(bs, []byte("\n"), []byte("[NEWLINE]\n"), -1)
|
||||||
|
return bs
|
||||||
|
}
|
||||||
|
|
||||||
|
// TabWidth calculates actual width of a tab at given position.
|
||||||
|
func TabWidth(currentPos int) int {
|
||||||
|
return 4 - currentPos%4
|
||||||
|
}
|
||||||
|
|
||||||
|
// IndentPosition searches an indent position with given width for given line.
|
||||||
|
// If the line contains tab characters, paddings may be not zero.
|
||||||
|
// currentPos==0 and width==2:
|
||||||
|
//
|
||||||
|
// position: 0 1
|
||||||
|
// [TAB]aaaa
|
||||||
|
// width: 1234 5678
|
||||||
|
//
|
||||||
|
// width=2 is in the tab character. In this case, IndentPosition returns
|
||||||
|
// (pos=1, padding=2)
|
||||||
|
func IndentPosition(bs []byte, currentPos, width int) (pos, padding int) {
|
||||||
|
w := 0
|
||||||
|
l := len(bs)
|
||||||
|
for i := 0; i < l; i++ {
|
||||||
|
b := bs[i]
|
||||||
|
if b == ' ' {
|
||||||
|
w++
|
||||||
|
} else if b == '\t' {
|
||||||
|
w += TabWidth(currentPos + w)
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if w >= width {
|
||||||
|
return i + 1, w - width
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1, -1
|
||||||
|
}
|
||||||
|
|
||||||
|
// IndentWidth calculate an indent width for given line.
|
||||||
|
func IndentWidth(bs []byte, currentPos int) (width, pos int) {
|
||||||
|
l := len(bs)
|
||||||
|
for i := 0; i < l; i++ {
|
||||||
|
b := bs[i]
|
||||||
|
if b == ' ' {
|
||||||
|
width++
|
||||||
|
pos++
|
||||||
|
} else if b == '\t' {
|
||||||
|
width += TabWidth(currentPos + width)
|
||||||
|
pos++
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// FirstNonSpacePosition returns a potisoin line that is a first nonspace
|
||||||
|
// character.
|
||||||
|
func FirstNonSpacePosition(bs []byte) int {
|
||||||
|
i := 0
|
||||||
|
for ; i < len(bs); i++ {
|
||||||
|
c := bs[i]
|
||||||
|
if c == ' ' || c == '\t' {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if c == '\n' {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
|
||||||
|
// FindClosure returns a position that closes given opener.
|
||||||
|
// If codeSpan is set true, it ignores characters in code spans.
|
||||||
|
// If allowNesting is set true, closures correspond to nested opener will be
|
||||||
|
// ignored.
|
||||||
|
func FindClosure(bs []byte, opener, closure byte, codeSpan, allowNesting bool) int {
|
||||||
|
i := 0
|
||||||
|
opened := 1
|
||||||
|
codeSpanOpener := 0
|
||||||
|
for i < len(bs) {
|
||||||
|
c := bs[i]
|
||||||
|
if codeSpan && codeSpanOpener != 0 && c == '`' {
|
||||||
|
codeSpanCloser := 0
|
||||||
|
for ; i < len(bs); i++ {
|
||||||
|
if bs[i] == '`' {
|
||||||
|
codeSpanCloser++
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if codeSpanCloser == codeSpanOpener {
|
||||||
|
codeSpanOpener = 0
|
||||||
|
}
|
||||||
|
} else if c == '\\' && i < len(bs)-1 && IsPunct(bs[i+1]) {
|
||||||
|
i += 2
|
||||||
|
continue
|
||||||
|
} else if codeSpan && codeSpanOpener == 0 && c == '`' {
|
||||||
|
for ; i < len(bs); i++ {
|
||||||
|
if bs[i] == '`' {
|
||||||
|
codeSpanOpener++
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (codeSpan && codeSpanOpener == 0) || !codeSpan {
|
||||||
|
if c == closure {
|
||||||
|
opened--
|
||||||
|
if opened == 0 {
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
} else if c == opener {
|
||||||
|
if !allowNesting {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
opened++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
|
||||||
|
// TrimLeft trims characters in given s from head of the source.
|
||||||
|
// bytes.TrimLeft offers same functionalities, but bytes.TrimLeft
|
||||||
|
// allocates new buffer for the result.
|
||||||
|
func TrimLeft(source, b []byte) []byte {
|
||||||
|
i := 0
|
||||||
|
for ; i < len(source); i++ {
|
||||||
|
c := source[i]
|
||||||
|
found := false
|
||||||
|
for j := 0; j < len(b); j++ {
|
||||||
|
if c == b[j] {
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return source[i:]
|
||||||
|
}
|
||||||
|
|
||||||
|
// TrimRight trims characters in given s from tail of the source.
|
||||||
|
func TrimRight(source, b []byte) []byte {
|
||||||
|
i := len(source) - 1
|
||||||
|
for ; i >= 0; i-- {
|
||||||
|
c := source[i]
|
||||||
|
found := false
|
||||||
|
for j := 0; j < len(b); j++ {
|
||||||
|
if c == b[j] {
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return source[:i+1]
|
||||||
|
}
|
||||||
|
|
||||||
|
// TrimLeftLength returns a length of leading specified characters.
|
||||||
|
func TrimLeftLength(source, s []byte) int {
|
||||||
|
return len(source) - len(TrimLeft(source, s))
|
||||||
|
}
|
||||||
|
|
||||||
|
// TrimRightLength returns a length of trailing specified characters.
|
||||||
|
func TrimRightLength(source, s []byte) int {
|
||||||
|
return len(source) - len(TrimRight(source, s))
|
||||||
|
}
|
||||||
|
|
||||||
|
// TrimLeftSpaceLength returns a length of leading space characters.
|
||||||
|
func TrimLeftSpaceLength(source []byte) int {
|
||||||
|
return TrimLeftLength(source, spaces)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TrimRightSpaceLength returns a length of trailing space characters.
|
||||||
|
func TrimRightSpaceLength(source []byte) int {
|
||||||
|
return TrimRightLength(source, spaces)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TrimLeftSpace returns a subslice of given string by slicing off all leading
|
||||||
|
// space characters.
|
||||||
|
func TrimLeftSpace(source []byte) []byte {
|
||||||
|
return TrimLeft(source, spaces)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TrimRightSpace returns a subslice of given string by slicing off all trailing
|
||||||
|
// space characters.
|
||||||
|
func TrimRightSpace(source []byte) []byte {
|
||||||
|
return TrimRight(source, spaces)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReplaceSpaces replaces sequence of spaces with given repl.
|
||||||
|
func ReplaceSpaces(source []byte, repl byte) []byte {
|
||||||
|
var ret []byte
|
||||||
|
start := -1
|
||||||
|
for i, c := range source {
|
||||||
|
iss := IsSpace(c)
|
||||||
|
if start < 0 && iss {
|
||||||
|
start = i
|
||||||
|
continue
|
||||||
|
} else if start >= 0 && iss {
|
||||||
|
continue
|
||||||
|
} else if start >= 0 {
|
||||||
|
if ret == nil {
|
||||||
|
ret = make([]byte, 0, len(source))
|
||||||
|
ret = append(ret, source[:start]...)
|
||||||
|
}
|
||||||
|
ret = append(ret, repl)
|
||||||
|
start = -1
|
||||||
|
}
|
||||||
|
if ret != nil {
|
||||||
|
ret = append(ret, c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if start >= 0 && ret != nil {
|
||||||
|
ret = append(ret, repl)
|
||||||
|
}
|
||||||
|
if ret == nil {
|
||||||
|
return source
|
||||||
|
}
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToRune decode given bytes start at pos and returns a rune.
|
||||||
|
func ToRune(source []byte, pos int) rune {
|
||||||
|
i := pos
|
||||||
|
for ; i >= 0; i-- {
|
||||||
|
if utf8.RuneStart(source[i]) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
r, _ := utf8.DecodeRune(source[i:])
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToValidRune returns 0xFFFD if given rune is invalid, otherwise v.
|
||||||
|
func ToValidRune(v rune) rune {
|
||||||
|
if v == 0 || !utf8.ValidRune(v) {
|
||||||
|
return rune(0xFFFD)
|
||||||
|
}
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToLinkReference convert given bytes into a valid link reference string.
|
||||||
|
// ToLinkReference trims leading and trailing spaces and convert into lower
|
||||||
|
// case and replace spaces with a single space character.
|
||||||
|
func ToLinkReference(v []byte) string {
|
||||||
|
v = TrimLeftSpace(v)
|
||||||
|
v = TrimRightSpace(v)
|
||||||
|
return strings.ToLower(string(ReplaceSpaces(v, ' ')))
|
||||||
|
}
|
||||||
|
|
||||||
|
var escapeRegex = regexp.MustCompile(`\\.`)
|
||||||
|
var hexRefRegex = regexp.MustCompile(`#[xX][\da-fA-F]+;`)
|
||||||
|
var numRefRegex = regexp.MustCompile(`#\d{1,7};`)
|
||||||
|
var entityRefRegex = regexp.MustCompile(`&([a-zA-Z\d]+);`)
|
||||||
|
|
||||||
|
var entityLt = []byte("<")
|
||||||
|
var entityGt = []byte(">")
|
||||||
|
var entityAmp = []byte("&")
|
||||||
|
var entityQuot = []byte(""")
|
||||||
|
|
||||||
|
// EscapeHTML escapes characters that should be escaped in HTML text.
|
||||||
|
func EscapeHTML(v []byte) []byte {
|
||||||
|
result := make([]byte, 0, len(v)+10)
|
||||||
|
for _, c := range v {
|
||||||
|
switch c {
|
||||||
|
case '<':
|
||||||
|
result = append(result, entityLt...)
|
||||||
|
case '>':
|
||||||
|
result = append(result, entityGt...)
|
||||||
|
case '&':
|
||||||
|
result = append(result, entityAmp...)
|
||||||
|
case '"':
|
||||||
|
result = append(result, entityQuot...)
|
||||||
|
default:
|
||||||
|
result = append(result, c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnescapePunctuations unescapes blackslash escaped punctuations.
|
||||||
|
func UnescapePunctuations(v []byte) []byte {
|
||||||
|
return escapeRegex.ReplaceAllFunc(v, func(match []byte) []byte {
|
||||||
|
if IsPunct(match[1]) {
|
||||||
|
return []byte{match[1]}
|
||||||
|
}
|
||||||
|
return match
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResolveNumericReferences resolve numeric references like 'Ӓ" .
|
||||||
|
func ResolveNumericReferences(v []byte) []byte {
|
||||||
|
buf := make([]byte, 6, 6)
|
||||||
|
v = hexRefRegex.ReplaceAllFunc(v, func(match []byte) []byte {
|
||||||
|
v, _ := strconv.ParseUint(string(match[2:len(match)-1]), 16, 32)
|
||||||
|
n := utf8.EncodeRune(buf, ToValidRune(rune(v)))
|
||||||
|
return buf[:n]
|
||||||
|
})
|
||||||
|
return numRefRegex.ReplaceAllFunc(v, func(match []byte) []byte {
|
||||||
|
v, _ := strconv.ParseUint(string(match[1:len(match)-1]), 0, 32)
|
||||||
|
n := utf8.EncodeRune(buf, ToValidRune(rune(v)))
|
||||||
|
return buf[:n]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResolveEntityNames resolve entity references like 'ö" .
|
||||||
|
func ResolveEntityNames(v []byte) []byte {
|
||||||
|
return entityRefRegex.ReplaceAllFunc(v, func(match []byte) []byte {
|
||||||
|
entity, ok := LookUpHTML5EntityByName(string(match[1 : len(match)-1]))
|
||||||
|
if ok {
|
||||||
|
return entity.Characters
|
||||||
|
}
|
||||||
|
return match
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// URLEscape escape given URL.
|
||||||
|
// If resolveReference is set true:
|
||||||
|
// 1. unescape punctuations
|
||||||
|
// 2. resolve numeric references
|
||||||
|
// 3. resolve entity references
|
||||||
|
//
|
||||||
|
// URL encoded values (%xx) are keeped as is.
|
||||||
|
func URLEscape(v []byte, resolveReference bool) []byte {
|
||||||
|
if resolveReference {
|
||||||
|
v = UnescapePunctuations(v)
|
||||||
|
v = ResolveNumericReferences(v)
|
||||||
|
v = ResolveEntityNames(v)
|
||||||
|
}
|
||||||
|
result := make([]byte, 0, len(v)+10)
|
||||||
|
for i := 0; i < len(v); {
|
||||||
|
c := v[i]
|
||||||
|
if urlEscapeTable[c] == 1 {
|
||||||
|
result = append(result, c)
|
||||||
|
i++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if c == '%' && i+2 < len(v) && IsHexDecimal(v[i+1]) && IsHexDecimal(v[i+1]) {
|
||||||
|
result = append(result, c, v[i+1], v[i+2])
|
||||||
|
i += 3
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
u8len := utf8lenTable[c]
|
||||||
|
if u8len == 99 { // invalid utf8 leading byte, skip it
|
||||||
|
result = append(result, c)
|
||||||
|
i++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if c == ' ' {
|
||||||
|
result = append(result, '%', '2', '0')
|
||||||
|
i++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
result = append(result, []byte(url.QueryEscape(string(v[i:i+int(u8len)])))...)
|
||||||
|
i += int(u8len)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenerateLinkID generates an ID for links.
|
||||||
|
func GenerateLinkID(value []byte, exists map[string]bool) []byte {
|
||||||
|
value = TrimLeftSpace(value)
|
||||||
|
value = TrimRightSpace(value)
|
||||||
|
result := []byte{}
|
||||||
|
for i := 0; i < len(value); {
|
||||||
|
v := value[i]
|
||||||
|
l := utf8lenTable[v]
|
||||||
|
i += int(l)
|
||||||
|
if l != 1 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if IsAlphaNumeric(v) {
|
||||||
|
result = append(result, v)
|
||||||
|
} else if v == ' ' {
|
||||||
|
result = append(result, '-')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(result) == 0 {
|
||||||
|
result = []byte("id")
|
||||||
|
}
|
||||||
|
if _, ok := exists[string(result)]; !ok {
|
||||||
|
exists[string(result)] = true
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
for i := 1; ; i++ {
|
||||||
|
newResult := fmt.Sprintf("%s%d", result, i)
|
||||||
|
if _, ok := exists[newResult]; !ok {
|
||||||
|
exists[newResult] = true
|
||||||
|
return []byte(newResult)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var spaces = []byte(" \t\n\x0b\x0c\x0d")
|
||||||
|
|
||||||
|
var spaceTable = [256]int8{0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
|
||||||
|
|
||||||
|
var punctTable = [256]int8{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
|
||||||
|
|
||||||
|
// a-zA-Z0-9, ;/?:@&=+$,-_.!~*'()#
|
||||||
|
var urlEscapeTable = [256]int8{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
|
||||||
|
|
||||||
|
var utf8lenTable = [256]int8{1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 4, 99, 99, 99, 99, 99, 99, 99, 99}
|
||||||
|
|
||||||
|
// IsPunct returns true if given character is a punctuation, otherwise false.
|
||||||
|
func IsPunct(c byte) bool {
|
||||||
|
return punctTable[c] == 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsSpace returns true if given character is a space, otherwise false.
|
||||||
|
func IsSpace(c byte) bool {
|
||||||
|
return spaceTable[c] == 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsNumeric returns true if given character is a numeric, otherwise false.
|
||||||
|
func IsNumeric(c byte) bool {
|
||||||
|
return c >= '0' && c <= '9'
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsHexDecimal returns true if given character is a hexdecimal, otherwise false.
|
||||||
|
func IsHexDecimal(c byte) bool {
|
||||||
|
return c >= '0' && c <= '9' || c >= 'a' && c <= 'f' || c >= 'A' && c <= 'F'
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsAlphaNumeric returns true if given character is a alphabet or a numeric, otherwise false.
|
||||||
|
func IsAlphaNumeric(c byte) bool {
|
||||||
|
return c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z' || c >= '0' && c <= '9'
|
||||||
|
}
|
||||||
|
|
||||||
|
// A BufWriter is a subset of the bufio.Writer .
|
||||||
|
type BufWriter interface {
|
||||||
|
io.Writer
|
||||||
|
Available() int
|
||||||
|
Buffered() int
|
||||||
|
Flush() error
|
||||||
|
WriteByte(c byte) error
|
||||||
|
WriteRune(r rune) (size int, err error)
|
||||||
|
WriteString(s string) (int, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// A PrioritizedValue struct holds pair of an arbitary value and a priority.
|
||||||
|
type PrioritizedValue struct {
|
||||||
|
// Value is an arbitary value that you want to prioritize.
|
||||||
|
Value interface{}
|
||||||
|
// Priority is a priority of the value.
|
||||||
|
Priority int
|
||||||
|
}
|
||||||
|
|
||||||
|
// PrioritizedSlice is a slice of the PrioritizedValues
|
||||||
|
type PrioritizedSlice []PrioritizedValue
|
||||||
|
|
||||||
|
// Sort sorts the PrioritizedSlice in ascending order.
|
||||||
|
func (s PrioritizedSlice) Sort() {
|
||||||
|
sort.Slice(s, func(i, j int) bool {
|
||||||
|
return s[i].Priority < s[j].Priority
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove removes given value from this slice.
|
||||||
|
func (s PrioritizedSlice) Remove(v interface{}) PrioritizedSlice {
|
||||||
|
i := 0
|
||||||
|
found := false
|
||||||
|
for ; i < len(s); i++ {
|
||||||
|
if s[i].Value == v {
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
return append(s[:i], s[i+1:]...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prioritized returns a new PrioritizedValue.
|
||||||
|
func Prioritized(v interface{}, priority int) PrioritizedValue {
|
||||||
|
return PrioritizedValue{v, priority}
|
||||||
|
}
|
||||||
13
util/util_safe.go
Normal file
13
util/util_safe.go
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
// +build appengine,js
|
||||||
|
|
||||||
|
package util
|
||||||
|
|
||||||
|
// BytesToReadOnlyString returns a string converted from given bytes.
|
||||||
|
func BytesToReadOnlyString(b []byte) string {
|
||||||
|
return string(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
// StringToReadOnlyBytes returns bytes converted from given string.
|
||||||
|
func StringToReadOnlyBytes(s string) []byte {
|
||||||
|
return []byte(s)
|
||||||
|
}
|
||||||
20
util/util_unsafe.go
Normal file
20
util/util_unsafe.go
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
// +build !appengine,!js
|
||||||
|
|
||||||
|
package util
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"unsafe"
|
||||||
|
)
|
||||||
|
|
||||||
|
// BytesToReadOnlyString returns a string converted from given bytes.
|
||||||
|
func BytesToReadOnlyString(b []byte) string {
|
||||||
|
return *(*string)(unsafe.Pointer(&b))
|
||||||
|
}
|
||||||
|
|
||||||
|
// StringToReadOnlyBytes returns bytes converted from given string.
|
||||||
|
func StringToReadOnlyBytes(s string) []byte {
|
||||||
|
sh := (*reflect.StringHeader)(unsafe.Pointer(&s))
|
||||||
|
bh := reflect.SliceHeader{Data: sh.Data, Len: sh.Len, Cap: sh.Len}
|
||||||
|
return *(*[]byte)(unsafe.Pointer(&bh))
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue