Reviewed-on: https://git.openi.org.cn/OpenI/aiforge/pulls/2654 Reviewed-by: lewis <747342561@qq.com>fix-2666
@@ -120,7 +120,7 @@ require ( | |||
github.com/urfave/cli v1.22.1 | |||
github.com/xanzy/go-gitlab v0.31.0 | |||
github.com/yohcop/openid-go v1.0.0 | |||
github.com/yuin/goldmark v1.1.30 | |||
github.com/yuin/goldmark v1.4.13 | |||
github.com/yuin/goldmark-meta v0.0.0-20191126180153-f0638e958b60 | |||
golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37 | |||
golang.org/x/mod v0.3.0 // indirect | |||
@@ -804,6 +804,8 @@ github.com/yuin/goldmark v1.1.27 h1:nqDD4MMMQA0lmWq03Z2/myGPYLQoXtmi0rGVs95ntbo= | |||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= | |||
github.com/yuin/goldmark v1.1.30 h1:j4d4Lw3zqZelDhBksEo3BnWg9xhXRQGJPPSL6OApZjI= | |||
github.com/yuin/goldmark v1.1.30/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= | |||
github.com/yuin/goldmark v1.4.13 h1:fVcFKWvrslecOb/tg+Cc05dkeYx540o0FuFt3nUVDoE= | |||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= | |||
github.com/yuin/goldmark-meta v0.0.0-20191126180153-f0638e958b60 h1:gZucqLjL1eDzVWrXj4uiWeMbAopJlBR2mKQAsTGdPwo= | |||
github.com/yuin/goldmark-meta v0.0.0-20191126180153-f0638e958b60/go.mod h1:i9VhcIHN2PxXMbQrKqXNueok6QNONoPjNMoj9MygVL0= | |||
github.com/ziutek/mymysql v1.5.4 h1:GB0qdRGsTwQSBVYuVShFBKaXSnSnYYC2d9knnE1LHFs= | |||
@@ -12,5 +12,5 @@ fuzz: | |||
rm -rf ./fuzz/crashers | |||
rm -rf ./fuzz/suppressions | |||
rm -f ./fuzz/fuzz-fuzz.zip | |||
cd ./fuzz && go-fuzz-build | |||
cd ./fuzz && GO111MODULE=off go-fuzz-build | |||
cd ./fuzz && go-fuzz |
@@ -1,14 +1,14 @@ | |||
goldmark | |||
========================================== | |||
[](http://godoc.org/github.com/yuin/goldmark) | |||
[](https://pkg.go.dev/github.com/yuin/goldmark) | |||
[](https://github.com/yuin/goldmark/actions?query=workflow:test) | |||
[](https://coveralls.io/github/yuin/goldmark) | |||
[](https://goreportcard.com/report/github.com/yuin/goldmark) | |||
> A Markdown parser written in Go. Easy to extend, standards-compliant, well-structured. | |||
goldmark is compliant with CommonMark 0.29. | |||
goldmark is compliant with CommonMark 0.30. | |||
Motivation | |||
---------------------- | |||
@@ -173,6 +173,7 @@ Parser and Renderer options | |||
- This extension enables Table, Strikethrough, Linkify and TaskList. | |||
- This extension does not filter tags defined in [6.11: Disallowed Raw HTML (extension)](https://github.github.com/gfm/#disallowed-raw-html-extension-). | |||
If you need to filter HTML tags, see [Security](#security). | |||
- If you need to parse github emojis, you can use [goldmark-emoji](https://github.com/yuin/goldmark-emoji) extension. | |||
- `extension.DefinitionList` | |||
- [PHP Markdown Extra: Definition lists](https://michelf.ca/projects/php-markdown/extra/#def-list) | |||
- `extension.Footnote` | |||
@@ -203,6 +204,18 @@ heading {#id .className attrName=attrValue} | |||
============ | |||
``` | |||
### Table extension | |||
The Table extension implements [Table(extension)](https://github.github.com/gfm/#tables-extension-), as | |||
defined in [GitHub Flavored Markdown Spec](https://github.github.com/gfm/). | |||
Specs are defined for XHTML, so specs use some deprecated attributes for HTML5. | |||
You can override alignment rendering method via options. | |||
| Functional option | Type | Description | | |||
| ----------------- | ---- | ----------- | | |||
| `extension.WithTableCellAlignMethod` | `extension.TableCellAlignMethod` | Option indicates how are table cells aligned. | | |||
### Typographer extension | |||
The Typographer extension translates plain ASCII punctuation characters into typographic-punctuation HTML entities. | |||
@@ -219,7 +232,7 @@ Default substitutions are: | |||
| `<<` | `«` | | |||
| `>>` | `»` | | |||
You can override the defualt substitutions via `extensions.WithTypographicSubstitutions`: | |||
You can override the default substitutions via `extensions.WithTypographicSubstitutions`: | |||
```go | |||
markdown := goldmark.New( | |||
@@ -267,13 +280,96 @@ markdown := goldmark.New( | |||
[]byte("https:"), | |||
}), | |||
extension.WithLinkifyURLRegexp( | |||
xurls.Strict(), | |||
xurls.Strict, | |||
), | |||
), | |||
), | |||
) | |||
``` | |||
### Footnotes extension | |||
The Footnote extension implements [PHP Markdown Extra: Footnotes](https://michelf.ca/projects/php-markdown/extra/#footnotes). | |||
This extension has some options: | |||
| Functional option | Type | Description | | |||
| ----------------- | ---- | ----------- | | |||
| `extension.WithFootnoteIDPrefix` | `[]byte` | a prefix for the id attributes.| | |||
| `extension.WithFootnoteIDPrefixFunction` | `func(gast.Node) []byte` | a function that determines the id attribute for given Node.| | |||
| `extension.WithFootnoteLinkTitle` | `[]byte` | an optional title attribute for footnote links.| | |||
| `extension.WithFootnoteBacklinkTitle` | `[]byte` | an optional title attribute for footnote backlinks. | | |||
| `extension.WithFootnoteLinkClass` | `[]byte` | a class for footnote links. This defaults to `footnote-ref`. | | |||
| `extension.WithFootnoteBacklinkClass` | `[]byte` | a class for footnote backlinks. This defaults to `footnote-backref`. | | |||
| `extension.WithFootnoteBacklinkHTML` | `[]byte` | a class for footnote backlinks. This defaults to `↩︎`. | | |||
Some options can have special substitutions. Occurrences of “^^” in the string will be replaced by the corresponding footnote number in the HTML output. Occurrences of “%%” will be replaced by a number for the reference (footnotes can have multiple references). | |||
`extension.WithFootnoteIDPrefix` and `extension.WithFootnoteIDPrefixFunction` are useful if you have multiple Markdown documents displayed inside one HTML document to avoid footnote ids to clash each other. | |||
`extension.WithFootnoteIDPrefix` sets fixed id prefix, so you may write codes like the following: | |||
```go | |||
for _, path := range files { | |||
source := readAll(path) | |||
prefix := getPrefix(path) | |||
markdown := goldmark.New( | |||
goldmark.WithExtensions( | |||
NewFootnote( | |||
WithFootnoteIDPrefix([]byte(path)), | |||
), | |||
), | |||
) | |||
var b bytes.Buffer | |||
err := markdown.Convert(source, &b) | |||
if err != nil { | |||
t.Error(err.Error()) | |||
} | |||
} | |||
``` | |||
`extension.WithFootnoteIDPrefixFunction` determines an id prefix by calling given function, so you may write codes like the following: | |||
```go | |||
markdown := goldmark.New( | |||
goldmark.WithExtensions( | |||
NewFootnote( | |||
WithFootnoteIDPrefixFunction(func(n gast.Node) []byte { | |||
v, ok := n.OwnerDocument().Meta()["footnote-prefix"] | |||
if ok { | |||
return util.StringToReadOnlyBytes(v.(string)) | |||
} | |||
return nil | |||
}), | |||
), | |||
), | |||
) | |||
for _, path := range files { | |||
source := readAll(path) | |||
var b bytes.Buffer | |||
doc := markdown.Parser().Parse(text.NewReader(source)) | |||
doc.Meta()["footnote-prefix"] = getPrefix(path) | |||
err := markdown.Renderer().Render(&b, source, doc) | |||
} | |||
``` | |||
You can use [goldmark-meta](https://github.com/yuin/goldmark-meta) to define a id prefix in the markdown document: | |||
```markdown | |||
--- | |||
title: document title | |||
slug: article1 | |||
footnote-prefix: article1 | |||
--- | |||
# My article | |||
``` | |||
Security | |||
-------------------- | |||
By default, goldmark does not render raw HTML or potentially-dangerous URLs. | |||
@@ -291,28 +387,29 @@ blackfriday v2 seems to be the fastest, but as it is not CommonMark compliant, i | |||
goldmark, meanwhile, builds a clean, extensible AST structure, achieves full compliance with | |||
CommonMark, and consumes less memory, all while being reasonably fast. | |||
- MBP 2019 13″(i5, 16GB), Go1.17 | |||
``` | |||
goos: darwin | |||
goarch: amd64 | |||
BenchmarkMarkdown/Blackfriday-v2-12 326 3465240 ns/op 3298861 B/op 20047 allocs/op | |||
BenchmarkMarkdown/GoldMark-12 303 3927494 ns/op 2574809 B/op 13853 allocs/op | |||
BenchmarkMarkdown/CommonMark-12 244 4900853 ns/op 2753851 B/op 20527 allocs/op | |||
BenchmarkMarkdown/Lute-12 130 9195245 ns/op 9175030 B/op 123534 allocs/op | |||
BenchmarkMarkdown/GoMarkdown-12 9 113541994 ns/op 2187472 B/op 22173 allocs/op | |||
BenchmarkMarkdown/Blackfriday-v2-8 302 3743747 ns/op 3290445 B/op 20050 allocs/op | |||
BenchmarkMarkdown/GoldMark-8 280 4200974 ns/op 2559738 B/op 13435 allocs/op | |||
BenchmarkMarkdown/CommonMark-8 226 5283686 ns/op 2702490 B/op 20792 allocs/op | |||
BenchmarkMarkdown/Lute-8 12 92652857 ns/op 10602649 B/op 40555 allocs/op | |||
BenchmarkMarkdown/GoMarkdown-8 13 81380167 ns/op 2245002 B/op 22889 allocs/op | |||
``` | |||
### against cmark (CommonMark reference implementation written in C) | |||
- MBP 2019 13″(i5, 16GB), Go1.17 | |||
``` | |||
----------- cmark ----------- | |||
file: _data.md | |||
iteration: 50 | |||
average: 0.0037760639 sec | |||
go run ./goldmark_benchmark.go | |||
average: 0.0044073057 sec | |||
------- goldmark ------- | |||
file: _data.md | |||
iteration: 50 | |||
average: 0.0040964230 sec | |||
average: 0.0041611990 sec | |||
``` | |||
As you can see, goldmark's performance is on par with cmark's. | |||
@@ -324,7 +421,17 @@ Extensions | |||
extension for the goldmark Markdown parser. | |||
- [goldmark-highlighting](https://github.com/yuin/goldmark-highlighting): A syntax-highlighting extension | |||
for the goldmark markdown parser. | |||
- [goldmark-emoji](https://github.com/yuin/goldmark-emoji): An emoji | |||
extension for the goldmark Markdown parser. | |||
- [goldmark-mathjax](https://github.com/litao91/goldmark-mathjax): Mathjax support for the goldmark markdown parser | |||
- [goldmark-pdf](https://github.com/stephenafamo/goldmark-pdf): A PDF renderer that can be passed to `goldmark.WithRenderer()`. | |||
- [goldmark-hashtag](https://github.com/abhinav/goldmark-hashtag): Adds support for `#hashtag`-based tagging to goldmark. | |||
- [goldmark-wikilink](https://github.com/abhinav/goldmark-wikilink): Adds support for `[[wiki]]`-style links to goldmark. | |||
- [goldmark-toc](https://github.com/abhinav/goldmark-toc): Adds support for generating tables-of-contents for goldmark documents. | |||
- [goldmark-mermaid](https://github.com/abhinav/goldmark-mermaid): Adds support for rendering [Mermaid](https://mermaid-js.github.io/mermaid/) diagrams in goldmark documents. | |||
- [goldmark-pikchr](https://github.com/jchenry/goldmark-pikchr): Adds support for rendering [Pikchr](https://pikchr.org/home/doc/trunk/homepage.md) diagrams in goldmark documents. | |||
- [goldmark-embed](https://github.com/13rac1/goldmark-embed): Adds support for rendering embeds from YouTube links. | |||
goldmark internal(for extension developers) | |||
---------------------------------------------- | |||
@@ -45,11 +45,6 @@ type Attribute struct { | |||
Value interface{} | |||
} | |||
var attrNameIDS = []byte("#") | |||
var attrNameID = []byte("id") | |||
var attrNameClassS = []byte(".") | |||
var attrNameClass = []byte("class") | |||
// A Node interface defines basic AST node functionalities. | |||
type Node interface { | |||
// Type returns a type of this node. | |||
@@ -116,6 +111,11 @@ type Node interface { | |||
// tail of the children. | |||
InsertAfter(self, v1, insertee Node) | |||
// OwnerDocument returns this node's owner document. | |||
// If this node is not a child of the Document node, OwnerDocument | |||
// returns nil. | |||
OwnerDocument() *Document | |||
// Dump dumps an AST tree structure to stdout. | |||
// This function completely aimed for debugging. | |||
// level is a indent level. Implementer should indent informations with | |||
@@ -169,7 +169,7 @@ type Node interface { | |||
RemoveAttributes() | |||
} | |||
// A BaseNode struct implements the Node interface. | |||
// A BaseNode struct implements the Node interface partialliy. | |||
type BaseNode struct { | |||
firstChild Node | |||
lastChild Node | |||
@@ -358,6 +358,22 @@ func (n *BaseNode) InsertBefore(self, v1, insertee Node) { | |||
} | |||
} | |||
// OwnerDocument implements Node.OwnerDocument | |||
func (n *BaseNode) OwnerDocument() *Document { | |||
d := n.Parent() | |||
for { | |||
p := d.Parent() | |||
if p == nil { | |||
if v, ok := d.(*Document); ok { | |||
return v | |||
} | |||
break | |||
} | |||
d = p | |||
} | |||
return nil | |||
} | |||
// Text implements Node.Text . | |||
func (n *BaseNode) Text(source []byte) []byte { | |||
var buf bytes.Buffer | |||
@@ -7,7 +7,7 @@ import ( | |||
textm "github.com/yuin/goldmark/text" | |||
) | |||
// A BaseBlock struct implements the Node interface. | |||
// A BaseBlock struct implements the Node interface partialliy. | |||
type BaseBlock struct { | |||
BaseNode | |||
blankPreviousLines bool | |||
@@ -50,6 +50,8 @@ func (b *BaseBlock) SetLines(v *textm.Segments) { | |||
// A Document struct is a root node of Markdown text. | |||
type Document struct { | |||
BaseBlock | |||
meta map[string]interface{} | |||
} | |||
// KindDocument is a NodeKind of the Document node. | |||
@@ -70,10 +72,42 @@ func (n *Document) Kind() NodeKind { | |||
return KindDocument | |||
} | |||
// OwnerDocument implements Node.OwnerDocument | |||
func (n *Document) OwnerDocument() *Document { | |||
return n | |||
} | |||
// Meta returns metadata of this document. | |||
func (n *Document) Meta() map[string]interface{} { | |||
if n.meta == nil { | |||
n.meta = map[string]interface{}{} | |||
} | |||
return n.meta | |||
} | |||
// SetMeta sets given metadata to this document. | |||
func (n *Document) SetMeta(meta map[string]interface{}) { | |||
if n.meta == nil { | |||
n.meta = map[string]interface{}{} | |||
} | |||
for k, v := range meta { | |||
n.meta[k] = v | |||
} | |||
} | |||
// AddMeta adds given metadata to this document. | |||
func (n *Document) AddMeta(key string, value interface{}) { | |||
if n.meta == nil { | |||
n.meta = map[string]interface{}{} | |||
} | |||
n.meta[key] = value | |||
} | |||
// NewDocument returns a new Document node. | |||
func NewDocument() *Document { | |||
return &Document{ | |||
BaseBlock: BaseBlock{}, | |||
meta: nil, | |||
} | |||
} | |||
@@ -311,7 +345,7 @@ type List struct { | |||
Marker byte | |||
// IsTight is a true if this list is a 'tight' list. | |||
// See https://spec.commonmark.org/0.29/#loose for details. | |||
// See https://spec.commonmark.org/0.30/#loose for details. | |||
IsTight bool | |||
// Start is an initial number of this ordered list. | |||
@@ -393,7 +427,7 @@ func NewListItem(offset int) *ListItem { | |||
} | |||
// HTMLBlockType represents kinds of an html blocks. | |||
// See https://spec.commonmark.org/0.29/#html-blocks | |||
// See https://spec.commonmark.org/0.30/#html-blocks | |||
type HTMLBlockType int | |||
const ( | |||
@@ -8,7 +8,7 @@ import ( | |||
"github.com/yuin/goldmark/util" | |||
) | |||
// A BaseInline struct implements the Node interface. | |||
// A BaseInline struct implements the Node interface partialliy. | |||
type BaseInline struct { | |||
BaseNode | |||
} | |||
@@ -111,7 +111,7 @@ func (n *Text) SetRaw(v bool) { | |||
} | |||
// HardLineBreak returns true if this node ends with a hard line break. | |||
// See https://spec.commonmark.org/0.29/#hard-line-breaks for details. | |||
// See https://spec.commonmark.org/0.30/#hard-line-breaks for details. | |||
func (n *Text) HardLineBreak() bool { | |||
return n.flags&textHardLineBreak != 0 | |||
} | |||
@@ -2,6 +2,7 @@ package ast | |||
import ( | |||
"fmt" | |||
gast "github.com/yuin/goldmark/ast" | |||
) | |||
@@ -9,13 +10,17 @@ import ( | |||
// (PHP Markdown Extra) text. | |||
type FootnoteLink struct { | |||
gast.BaseInline | |||
Index int | |||
Index int | |||
RefCount int | |||
RefIndex int | |||
} | |||
// Dump implements Node.Dump. | |||
func (n *FootnoteLink) Dump(source []byte, level int) { | |||
m := map[string]string{} | |||
m["Index"] = fmt.Sprintf("%v", n.Index) | |||
m["RefCount"] = fmt.Sprintf("%v", n.RefCount) | |||
m["RefIndex"] = fmt.Sprintf("%v", n.RefIndex) | |||
gast.DumpHelper(n, source, level, m, nil) | |||
} | |||
@@ -30,36 +35,44 @@ func (n *FootnoteLink) Kind() gast.NodeKind { | |||
// NewFootnoteLink returns a new FootnoteLink node. | |||
func NewFootnoteLink(index int) *FootnoteLink { | |||
return &FootnoteLink{ | |||
Index: index, | |||
Index: index, | |||
RefCount: 0, | |||
RefIndex: 0, | |||
} | |||
} | |||
// A FootnoteBackLink struct represents a link to a footnote of Markdown | |||
// A FootnoteBacklink struct represents a link to a footnote of Markdown | |||
// (PHP Markdown Extra) text. | |||
type FootnoteBackLink struct { | |||
type FootnoteBacklink struct { | |||
gast.BaseInline | |||
Index int | |||
Index int | |||
RefCount int | |||
RefIndex int | |||
} | |||
// Dump implements Node.Dump. | |||
func (n *FootnoteBackLink) Dump(source []byte, level int) { | |||
func (n *FootnoteBacklink) Dump(source []byte, level int) { | |||
m := map[string]string{} | |||
m["Index"] = fmt.Sprintf("%v", n.Index) | |||
m["RefCount"] = fmt.Sprintf("%v", n.RefCount) | |||
m["RefIndex"] = fmt.Sprintf("%v", n.RefIndex) | |||
gast.DumpHelper(n, source, level, m, nil) | |||
} | |||
// KindFootnoteBackLink is a NodeKind of the FootnoteBackLink node. | |||
var KindFootnoteBackLink = gast.NewNodeKind("FootnoteBackLink") | |||
// KindFootnoteBacklink is a NodeKind of the FootnoteBacklink node. | |||
var KindFootnoteBacklink = gast.NewNodeKind("FootnoteBacklink") | |||
// Kind implements Node.Kind. | |||
func (n *FootnoteBackLink) Kind() gast.NodeKind { | |||
return KindFootnoteBackLink | |||
func (n *FootnoteBacklink) Kind() gast.NodeKind { | |||
return KindFootnoteBacklink | |||
} | |||
// NewFootnoteBackLink returns a new FootnoteBackLink node. | |||
func NewFootnoteBackLink(index int) *FootnoteBackLink { | |||
return &FootnoteBackLink{ | |||
Index: index, | |||
// NewFootnoteBacklink returns a new FootnoteBacklink node. | |||
func NewFootnoteBacklink(index int) *FootnoteBacklink { | |||
return &FootnoteBacklink{ | |||
Index: index, | |||
RefCount: 0, | |||
RefIndex: 0, | |||
} | |||
} | |||
@@ -138,7 +138,7 @@ func (b *definitionDescriptionParser) Open(parent gast.Node, reader text.Reader, | |||
para.Parent().RemoveChild(para.Parent(), para) | |||
} | |||
cpos, padding := util.IndentPosition(line[pos+1:], pos+1, list.Offset-pos-1) | |||
reader.AdvanceAndSetPadding(cpos, padding) | |||
reader.AdvanceAndSetPadding(cpos+1, padding) | |||
return ast.NewDefinitionDescription(), parser.HasChildren | |||
} | |||
@@ -2,6 +2,9 @@ package extension | |||
import ( | |||
"bytes" | |||
"fmt" | |||
"strconv" | |||
"github.com/yuin/goldmark" | |||
gast "github.com/yuin/goldmark/ast" | |||
"github.com/yuin/goldmark/extension/ast" | |||
@@ -10,10 +13,10 @@ import ( | |||
"github.com/yuin/goldmark/renderer/html" | |||
"github.com/yuin/goldmark/text" | |||
"github.com/yuin/goldmark/util" | |||
"strconv" | |||
) | |||
var footnoteListKey = parser.NewContextKey() | |||
var footnoteLinkListKey = parser.NewContextKey() | |||
type footnoteBlockParser struct { | |||
} | |||
@@ -164,7 +167,20 @@ func (s *footnoteParser) Parse(parent gast.Node, block text.Reader, pc parser.Co | |||
return nil | |||
} | |||
return ast.NewFootnoteLink(index) | |||
fnlink := ast.NewFootnoteLink(index) | |||
var fnlist []*ast.FootnoteLink | |||
if tmp := pc.Get(footnoteLinkListKey); tmp != nil { | |||
fnlist = tmp.([]*ast.FootnoteLink) | |||
} else { | |||
fnlist = []*ast.FootnoteLink{} | |||
pc.Set(footnoteLinkListKey, fnlist) | |||
} | |||
pc.Set(footnoteLinkListKey, append(fnlist, fnlink)) | |||
if line[0] == '!' { | |||
parent.AppendChild(parent, gast.NewTextSegment(text.NewSegment(segment.Start, segment.Start+1))) | |||
} | |||
return fnlink | |||
} | |||
type footnoteASTTransformer struct { | |||
@@ -180,23 +196,62 @@ func NewFootnoteASTTransformer() parser.ASTTransformer { | |||
func (a *footnoteASTTransformer) Transform(node *gast.Document, reader text.Reader, pc parser.Context) { | |||
var list *ast.FootnoteList | |||
if tlist := pc.Get(footnoteListKey); tlist != nil { | |||
list = tlist.(*ast.FootnoteList) | |||
} else { | |||
return | |||
var fnlist []*ast.FootnoteLink | |||
if tmp := pc.Get(footnoteListKey); tmp != nil { | |||
list = tmp.(*ast.FootnoteList) | |||
} | |||
if tmp := pc.Get(footnoteLinkListKey); tmp != nil { | |||
fnlist = tmp.([]*ast.FootnoteLink) | |||
} | |||
pc.Set(footnoteListKey, nil) | |||
pc.Set(footnoteLinkListKey, nil) | |||
if list == nil { | |||
return | |||
} | |||
counter := map[int]int{} | |||
if fnlist != nil { | |||
for _, fnlink := range fnlist { | |||
if fnlink.Index >= 0 { | |||
counter[fnlink.Index]++ | |||
} | |||
} | |||
refCounter := map[int]int{} | |||
for _, fnlink := range fnlist { | |||
fnlink.RefCount = counter[fnlink.Index] | |||
if _, ok := refCounter[fnlink.Index]; !ok { | |||
refCounter[fnlink.Index] = 0 | |||
} | |||
fnlink.RefIndex = refCounter[fnlink.Index] | |||
refCounter[fnlink.Index]++ | |||
} | |||
} | |||
for footnote := list.FirstChild(); footnote != nil; { | |||
var container gast.Node = footnote | |||
next := footnote.NextSibling() | |||
if fc := container.LastChild(); fc != nil && gast.IsParagraph(fc) { | |||
container = fc | |||
} | |||
index := footnote.(*ast.Footnote).Index | |||
fn := footnote.(*ast.Footnote) | |||
index := fn.Index | |||
if index < 0 { | |||
list.RemoveChild(list, footnote) | |||
} else { | |||
container.AppendChild(container, ast.NewFootnoteBackLink(index)) | |||
refCount := counter[index] | |||
backLink := ast.NewFootnoteBacklink(index) | |||
backLink.RefCount = refCount | |||
backLink.RefIndex = 0 | |||
container.AppendChild(container, backLink) | |||
if refCount > 1 { | |||
for i := 1; i < refCount; i++ { | |||
backLink := ast.NewFootnoteBacklink(index) | |||
backLink.RefCount = refCount | |||
backLink.RefIndex = i | |||
container.AppendChild(container, backLink) | |||
} | |||
} | |||
} | |||
footnote = next | |||
} | |||
@@ -214,19 +269,250 @@ func (a *footnoteASTTransformer) Transform(node *gast.Document, reader text.Read | |||
node.AppendChild(node, list) | |||
} | |||
// FootnoteConfig holds configuration values for the footnote extension. | |||
// | |||
// Link* and Backlink* configurations have some variables: | |||
// Occurrances of “^^” in the string will be replaced by the | |||
// corresponding footnote number in the HTML output. | |||
// Occurrances of “%%” will be replaced by a number for the | |||
// reference (footnotes can have multiple references). | |||
type FootnoteConfig struct { | |||
html.Config | |||
// IDPrefix is a prefix for the id attributes generated by footnotes. | |||
IDPrefix []byte | |||
// IDPrefix is a function that determines the id attribute for given Node. | |||
IDPrefixFunction func(gast.Node) []byte | |||
// LinkTitle is an optional title attribute for footnote links. | |||
LinkTitle []byte | |||
// BacklinkTitle is an optional title attribute for footnote backlinks. | |||
BacklinkTitle []byte | |||
// LinkClass is a class for footnote links. | |||
LinkClass []byte | |||
// BacklinkClass is a class for footnote backlinks. | |||
BacklinkClass []byte | |||
// BacklinkHTML is an HTML content for footnote backlinks. | |||
BacklinkHTML []byte | |||
} | |||
// FootnoteOption interface is a functional option interface for the extension. | |||
type FootnoteOption interface { | |||
renderer.Option | |||
// SetFootnoteOption sets given option to the extension. | |||
SetFootnoteOption(*FootnoteConfig) | |||
} | |||
// NewFootnoteConfig returns a new Config with defaults. | |||
func NewFootnoteConfig() FootnoteConfig { | |||
return FootnoteConfig{ | |||
Config: html.NewConfig(), | |||
LinkTitle: []byte(""), | |||
BacklinkTitle: []byte(""), | |||
LinkClass: []byte("footnote-ref"), | |||
BacklinkClass: []byte("footnote-backref"), | |||
BacklinkHTML: []byte("↩︎"), | |||
} | |||
} | |||
// SetOption implements renderer.SetOptioner. | |||
func (c *FootnoteConfig) SetOption(name renderer.OptionName, value interface{}) { | |||
switch name { | |||
case optFootnoteIDPrefixFunction: | |||
c.IDPrefixFunction = value.(func(gast.Node) []byte) | |||
case optFootnoteIDPrefix: | |||
c.IDPrefix = value.([]byte) | |||
case optFootnoteLinkTitle: | |||
c.LinkTitle = value.([]byte) | |||
case optFootnoteBacklinkTitle: | |||
c.BacklinkTitle = value.([]byte) | |||
case optFootnoteLinkClass: | |||
c.LinkClass = value.([]byte) | |||
case optFootnoteBacklinkClass: | |||
c.BacklinkClass = value.([]byte) | |||
case optFootnoteBacklinkHTML: | |||
c.BacklinkHTML = value.([]byte) | |||
default: | |||
c.Config.SetOption(name, value) | |||
} | |||
} | |||
type withFootnoteHTMLOptions struct { | |||
value []html.Option | |||
} | |||
func (o *withFootnoteHTMLOptions) SetConfig(c *renderer.Config) { | |||
if o.value != nil { | |||
for _, v := range o.value { | |||
v.(renderer.Option).SetConfig(c) | |||
} | |||
} | |||
} | |||
func (o *withFootnoteHTMLOptions) SetFootnoteOption(c *FootnoteConfig) { | |||
if o.value != nil { | |||
for _, v := range o.value { | |||
v.SetHTMLOption(&c.Config) | |||
} | |||
} | |||
} | |||
// WithFootnoteHTMLOptions is functional option that wraps goldmark HTMLRenderer options. | |||
func WithFootnoteHTMLOptions(opts ...html.Option) FootnoteOption { | |||
return &withFootnoteHTMLOptions{opts} | |||
} | |||
const optFootnoteIDPrefix renderer.OptionName = "FootnoteIDPrefix" | |||
type withFootnoteIDPrefix struct { | |||
value []byte | |||
} | |||
func (o *withFootnoteIDPrefix) SetConfig(c *renderer.Config) { | |||
c.Options[optFootnoteIDPrefix] = o.value | |||
} | |||
func (o *withFootnoteIDPrefix) SetFootnoteOption(c *FootnoteConfig) { | |||
c.IDPrefix = o.value | |||
} | |||
// WithFootnoteIDPrefix is a functional option that is a prefix for the id attributes generated by footnotes. | |||
func WithFootnoteIDPrefix(a []byte) FootnoteOption { | |||
return &withFootnoteIDPrefix{a} | |||
} | |||
const optFootnoteIDPrefixFunction renderer.OptionName = "FootnoteIDPrefixFunction" | |||
type withFootnoteIDPrefixFunction struct { | |||
value func(gast.Node) []byte | |||
} | |||
func (o *withFootnoteIDPrefixFunction) SetConfig(c *renderer.Config) { | |||
c.Options[optFootnoteIDPrefixFunction] = o.value | |||
} | |||
func (o *withFootnoteIDPrefixFunction) SetFootnoteOption(c *FootnoteConfig) { | |||
c.IDPrefixFunction = o.value | |||
} | |||
// WithFootnoteIDPrefixFunction is a functional option that is a prefix for the id attributes generated by footnotes. | |||
func WithFootnoteIDPrefixFunction(a func(gast.Node) []byte) FootnoteOption { | |||
return &withFootnoteIDPrefixFunction{a} | |||
} | |||
const optFootnoteLinkTitle renderer.OptionName = "FootnoteLinkTitle" | |||
type withFootnoteLinkTitle struct { | |||
value []byte | |||
} | |||
func (o *withFootnoteLinkTitle) SetConfig(c *renderer.Config) { | |||
c.Options[optFootnoteLinkTitle] = o.value | |||
} | |||
func (o *withFootnoteLinkTitle) SetFootnoteOption(c *FootnoteConfig) { | |||
c.LinkTitle = o.value | |||
} | |||
// WithFootnoteLinkTitle is a functional option that is an optional title attribute for footnote links. | |||
func WithFootnoteLinkTitle(a []byte) FootnoteOption { | |||
return &withFootnoteLinkTitle{a} | |||
} | |||
const optFootnoteBacklinkTitle renderer.OptionName = "FootnoteBacklinkTitle" | |||
type withFootnoteBacklinkTitle struct { | |||
value []byte | |||
} | |||
func (o *withFootnoteBacklinkTitle) SetConfig(c *renderer.Config) { | |||
c.Options[optFootnoteBacklinkTitle] = o.value | |||
} | |||
func (o *withFootnoteBacklinkTitle) SetFootnoteOption(c *FootnoteConfig) { | |||
c.BacklinkTitle = o.value | |||
} | |||
// WithFootnoteBacklinkTitle is a functional option that is an optional title attribute for footnote backlinks. | |||
func WithFootnoteBacklinkTitle(a []byte) FootnoteOption { | |||
return &withFootnoteBacklinkTitle{a} | |||
} | |||
const optFootnoteLinkClass renderer.OptionName = "FootnoteLinkClass" | |||
type withFootnoteLinkClass struct { | |||
value []byte | |||
} | |||
func (o *withFootnoteLinkClass) SetConfig(c *renderer.Config) { | |||
c.Options[optFootnoteLinkClass] = o.value | |||
} | |||
func (o *withFootnoteLinkClass) SetFootnoteOption(c *FootnoteConfig) { | |||
c.LinkClass = o.value | |||
} | |||
// WithFootnoteLinkClass is a functional option that is a class for footnote links. | |||
func WithFootnoteLinkClass(a []byte) FootnoteOption { | |||
return &withFootnoteLinkClass{a} | |||
} | |||
const optFootnoteBacklinkClass renderer.OptionName = "FootnoteBacklinkClass" | |||
type withFootnoteBacklinkClass struct { | |||
value []byte | |||
} | |||
func (o *withFootnoteBacklinkClass) SetConfig(c *renderer.Config) { | |||
c.Options[optFootnoteBacklinkClass] = o.value | |||
} | |||
func (o *withFootnoteBacklinkClass) SetFootnoteOption(c *FootnoteConfig) { | |||
c.BacklinkClass = o.value | |||
} | |||
// WithFootnoteBacklinkClass is a functional option that is a class for footnote backlinks. | |||
func WithFootnoteBacklinkClass(a []byte) FootnoteOption { | |||
return &withFootnoteBacklinkClass{a} | |||
} | |||
const optFootnoteBacklinkHTML renderer.OptionName = "FootnoteBacklinkHTML" | |||
type withFootnoteBacklinkHTML struct { | |||
value []byte | |||
} | |||
func (o *withFootnoteBacklinkHTML) SetConfig(c *renderer.Config) { | |||
c.Options[optFootnoteBacklinkHTML] = o.value | |||
} | |||
func (o *withFootnoteBacklinkHTML) SetFootnoteOption(c *FootnoteConfig) { | |||
c.BacklinkHTML = o.value | |||
} | |||
// WithFootnoteBacklinkHTML is an HTML content for footnote backlinks. | |||
func WithFootnoteBacklinkHTML(a []byte) FootnoteOption { | |||
return &withFootnoteBacklinkHTML{a} | |||
} | |||
// FootnoteHTMLRenderer is a renderer.NodeRenderer implementation that | |||
// renders FootnoteLink nodes. | |||
type FootnoteHTMLRenderer struct { | |||
html.Config | |||
FootnoteConfig | |||
} | |||
// NewFootnoteHTMLRenderer returns a new FootnoteHTMLRenderer. | |||
func NewFootnoteHTMLRenderer(opts ...html.Option) renderer.NodeRenderer { | |||
func NewFootnoteHTMLRenderer(opts ...FootnoteOption) renderer.NodeRenderer { | |||
r := &FootnoteHTMLRenderer{ | |||
Config: html.NewConfig(), | |||
FootnoteConfig: NewFootnoteConfig(), | |||
} | |||
for _, opt := range opts { | |||
opt.SetHTMLOption(&r.Config) | |||
opt.SetFootnoteOption(&r.FootnoteConfig) | |||
} | |||
return r | |||
} | |||
@@ -234,7 +520,7 @@ func NewFootnoteHTMLRenderer(opts ...html.Option) renderer.NodeRenderer { | |||
// RegisterFuncs implements renderer.NodeRenderer.RegisterFuncs. | |||
func (r *FootnoteHTMLRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) { | |||
reg.Register(ast.KindFootnoteLink, r.renderFootnoteLink) | |||
reg.Register(ast.KindFootnoteBackLink, r.renderFootnoteBackLink) | |||
reg.Register(ast.KindFootnoteBacklink, r.renderFootnoteBacklink) | |||
reg.Register(ast.KindFootnote, r.renderFootnote) | |||
reg.Register(ast.KindFootnoteList, r.renderFootnoteList) | |||
} | |||
@@ -243,25 +529,53 @@ func (r *FootnoteHTMLRenderer) renderFootnoteLink(w util.BufWriter, source []byt | |||
if entering { | |||
n := node.(*ast.FootnoteLink) | |||
is := strconv.Itoa(n.Index) | |||
_, _ = w.WriteString(`<sup id="fnref:`) | |||
_, _ = w.WriteString(`<sup id="`) | |||
_, _ = w.Write(r.idPrefix(node)) | |||
_, _ = w.WriteString(`fnref`) | |||
if n.RefIndex > 0 { | |||
_, _ = w.WriteString(fmt.Sprintf("%v", n.RefIndex)) | |||
} | |||
_ = w.WriteByte(':') | |||
_, _ = w.WriteString(is) | |||
_, _ = w.WriteString(`"><a href="#fn:`) | |||
_, _ = w.WriteString(`"><a href="#`) | |||
_, _ = w.Write(r.idPrefix(node)) | |||
_, _ = w.WriteString(`fn:`) | |||
_, _ = w.WriteString(is) | |||
_, _ = w.WriteString(`" class="footnote-ref" role="doc-noteref">`) | |||
_, _ = w.WriteString(`" class="`) | |||
_, _ = w.Write(applyFootnoteTemplate(r.FootnoteConfig.LinkClass, | |||
n.Index, n.RefCount)) | |||
if len(r.FootnoteConfig.LinkTitle) > 0 { | |||
_, _ = w.WriteString(`" title="`) | |||
_, _ = w.Write(util.EscapeHTML(applyFootnoteTemplate(r.FootnoteConfig.LinkTitle, n.Index, n.RefCount))) | |||
} | |||
_, _ = w.WriteString(`" role="doc-noteref">`) | |||
_, _ = w.WriteString(is) | |||
_, _ = w.WriteString(`</a></sup>`) | |||
} | |||
return gast.WalkContinue, nil | |||
} | |||
func (r *FootnoteHTMLRenderer) renderFootnoteBackLink(w util.BufWriter, source []byte, node gast.Node, entering bool) (gast.WalkStatus, error) { | |||
func (r *FootnoteHTMLRenderer) renderFootnoteBacklink(w util.BufWriter, source []byte, node gast.Node, entering bool) (gast.WalkStatus, error) { | |||
if entering { | |||
n := node.(*ast.FootnoteBackLink) | |||
n := node.(*ast.FootnoteBacklink) | |||
is := strconv.Itoa(n.Index) | |||
_, _ = w.WriteString(` <a href="#fnref:`) | |||
_, _ = w.WriteString(` <a href="#`) | |||
_, _ = w.Write(r.idPrefix(node)) | |||
_, _ = w.WriteString(`fnref`) | |||
if n.RefIndex > 0 { | |||
_, _ = w.WriteString(fmt.Sprintf("%v", n.RefIndex)) | |||
} | |||
_ = w.WriteByte(':') | |||
_, _ = w.WriteString(is) | |||
_, _ = w.WriteString(`" class="footnote-backref" role="doc-backlink">`) | |||
_, _ = w.WriteString("↩︎") | |||
_, _ = w.WriteString(`" class="`) | |||
_, _ = w.Write(applyFootnoteTemplate(r.FootnoteConfig.BacklinkClass, n.Index, n.RefCount)) | |||
if len(r.FootnoteConfig.BacklinkTitle) > 0 { | |||
_, _ = w.WriteString(`" title="`) | |||
_, _ = w.Write(util.EscapeHTML(applyFootnoteTemplate(r.FootnoteConfig.BacklinkTitle, n.Index, n.RefCount))) | |||
} | |||
_, _ = w.WriteString(`" role="doc-backlink">`) | |||
_, _ = w.Write(applyFootnoteTemplate(r.FootnoteConfig.BacklinkHTML, n.Index, n.RefCount)) | |||
_, _ = w.WriteString(`</a>`) | |||
} | |||
return gast.WalkContinue, nil | |||
@@ -271,9 +585,11 @@ func (r *FootnoteHTMLRenderer) renderFootnote(w util.BufWriter, source []byte, n | |||
n := node.(*ast.Footnote) | |||
is := strconv.Itoa(n.Index) | |||
if entering { | |||
_, _ = w.WriteString(`<li id="fn:`) | |||
_, _ = w.WriteString(`<li id="`) | |||
_, _ = w.Write(r.idPrefix(node)) | |||
_, _ = w.WriteString(`fn:`) | |||
_, _ = w.WriteString(is) | |||
_, _ = w.WriteString(`" role="doc-endnote"`) | |||
_, _ = w.WriteString(`"`) | |||
if node.Attributes() != nil { | |||
html.RenderAttributes(w, node, html.ListItemAttributeFilter) | |||
} | |||
@@ -285,14 +601,8 @@ func (r *FootnoteHTMLRenderer) renderFootnote(w util.BufWriter, source []byte, n | |||
} | |||
func (r *FootnoteHTMLRenderer) renderFootnoteList(w util.BufWriter, source []byte, node gast.Node, entering bool) (gast.WalkStatus, error) { | |||
tag := "section" | |||
if r.Config.XHTML { | |||
tag = "div" | |||
} | |||
if entering { | |||
_, _ = w.WriteString("<") | |||
_, _ = w.WriteString(tag) | |||
_, _ = w.WriteString(` class="footnotes" role="doc-endnotes"`) | |||
_, _ = w.WriteString(`<div class="footnotes" role="doc-endnotes"`) | |||
if node.Attributes() != nil { | |||
html.RenderAttributes(w, node, html.GlobalAttributeFilter) | |||
} | |||
@@ -305,18 +615,59 @@ func (r *FootnoteHTMLRenderer) renderFootnoteList(w util.BufWriter, source []byt | |||
_, _ = w.WriteString("<ol>\n") | |||
} else { | |||
_, _ = w.WriteString("</ol>\n") | |||
_, _ = w.WriteString("</") | |||
_, _ = w.WriteString(tag) | |||
_, _ = w.WriteString(">\n") | |||
_, _ = w.WriteString("</div>\n") | |||
} | |||
return gast.WalkContinue, nil | |||
} | |||
func (r *FootnoteHTMLRenderer) idPrefix(node gast.Node) []byte { | |||
if r.FootnoteConfig.IDPrefix != nil { | |||
return r.FootnoteConfig.IDPrefix | |||
} | |||
if r.FootnoteConfig.IDPrefixFunction != nil { | |||
return r.FootnoteConfig.IDPrefixFunction(node) | |||
} | |||
return []byte("") | |||
} | |||
func applyFootnoteTemplate(b []byte, index, refCount int) []byte { | |||
fast := true | |||
for i, c := range b { | |||
if i != 0 { | |||
if b[i-1] == '^' && c == '^' { | |||
fast = false | |||
break | |||
} | |||
if b[i-1] == '%' && c == '%' { | |||
fast = false | |||
break | |||
} | |||
} | |||
} | |||
if fast { | |||
return b | |||
} | |||
is := []byte(strconv.Itoa(index)) | |||
rs := []byte(strconv.Itoa(refCount)) | |||
ret := bytes.Replace(b, []byte("^^"), is, -1) | |||
return bytes.Replace(ret, []byte("%%"), rs, -1) | |||
} | |||
type footnote struct { | |||
options []FootnoteOption | |||
} | |||
// Footnote is an extension that allow you to use PHP Markdown Extra Footnotes. | |||
var Footnote = &footnote{} | |||
var Footnote = &footnote{ | |||
options: []FootnoteOption{}, | |||
} | |||
// NewFootnote returns a new extension with given options. | |||
func NewFootnote(opts ...FootnoteOption) goldmark.Extender { | |||
return &footnote{ | |||
options: opts, | |||
} | |||
} | |||
func (e *footnote) Extend(m goldmark.Markdown) { | |||
m.Parser().AddOptions( | |||
@@ -331,6 +682,6 @@ func (e *footnote) Extend(m goldmark.Markdown) { | |||
), | |||
) | |||
m.Renderer().AddOptions(renderer.WithNodeRenderers( | |||
util.Prioritized(NewFootnoteHTMLRenderer(), 500), | |||
util.Prioritized(NewFootnoteHTMLRenderer(e.options...), 500), | |||
)) | |||
} |
@@ -11,9 +11,9 @@ import ( | |||
"github.com/yuin/goldmark/util" | |||
) | |||
var wwwURLRegxp = regexp.MustCompile(`^www\.[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]+(?:(?:/|[#?])[-a-zA-Z0-9@:%_\+.~#!?&//=\(\);,'">\^{}\[\]` + "`" + `]*)?`) | |||
var wwwURLRegxp = regexp.MustCompile(`^www\.[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-z]+(?:[/#?][-a-zA-Z0-9@:%_\+.~#!?&/=\(\);,'">\^{}\[\]` + "`" + `]*)?`) | |||
var urlRegexp = regexp.MustCompile(`^(?:http|https|ftp):\/\/(?:www\.)?[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]+(?:(?:/|[#?])[-a-zA-Z0-9@:%_+.~#$!?&//=\(\);,'">\^{}\[\]` + "`" + `]*)?`) | |||
var urlRegexp = regexp.MustCompile(`^(?:http|https|ftp)://[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-z]+(?::\d+)?(?:[/#?][-a-zA-Z0-9@:%_+.~#$!?&/=\(\);,'">\^{}\[\]` + "`" + `]*)?`) | |||
// An LinkifyConfig struct is a data structure that holds configuration of the | |||
// Linkify extension. | |||
@@ -24,10 +24,12 @@ type LinkifyConfig struct { | |||
EmailRegexp *regexp.Regexp | |||
} | |||
const optLinkifyAllowedProtocols parser.OptionName = "LinkifyAllowedProtocols" | |||
const optLinkifyURLRegexp parser.OptionName = "LinkifyURLRegexp" | |||
const optLinkifyWWWRegexp parser.OptionName = "LinkifyWWWRegexp" | |||
const optLinkifyEmailRegexp parser.OptionName = "LinkifyEmailRegexp" | |||
const ( | |||
optLinkifyAllowedProtocols parser.OptionName = "LinkifyAllowedProtocols" | |||
optLinkifyURLRegexp parser.OptionName = "LinkifyURLRegexp" | |||
optLinkifyWWWRegexp parser.OptionName = "LinkifyWWWRegexp" | |||
optLinkifyEmailRegexp parser.OptionName = "LinkifyEmailRegexp" | |||
) | |||
// SetOption implements SetOptioner. | |||
func (c *LinkifyConfig) SetOption(name parser.OptionName, value interface{}) { | |||
@@ -156,10 +158,12 @@ func (s *linkifyParser) Trigger() []byte { | |||
return []byte{' ', '*', '_', '~', '('} | |||
} | |||
var protoHTTP = []byte("http:") | |||
var protoHTTPS = []byte("https:") | |||
var protoFTP = []byte("ftp:") | |||
var domainWWW = []byte("www.") | |||
var ( | |||
protoHTTP = []byte("http:") | |||
protoHTTPS = []byte("https:") | |||
protoFTP = []byte("ftp:") | |||
domainWWW = []byte("www.") | |||
) | |||
func (s *linkifyParser) Parse(parent ast.Node, block text.Reader, pc parser.Context) ast.Node { | |||
if pc.IsInLinkLabel() { | |||
@@ -269,9 +273,20 @@ func (s *linkifyParser) Parse(parent ast.Node, block text.Reader, pc parser.Cont | |||
s := segment.WithStop(segment.Start + 1) | |||
ast.MergeOrAppendTextSegment(parent, s) | |||
} | |||
consumes += m[1] | |||
i := m[1] - 1 | |||
for ; i > 0; i-- { | |||
c := line[i] | |||
switch c { | |||
case '?', '!', '.', ',', ':', '*', '_', '~': | |||
default: | |||
goto endfor | |||
} | |||
} | |||
endfor: | |||
i++ | |||
consumes += i | |||
block.Advance(consumes) | |||
n := ast.NewTextSegment(text.NewSegment(start, start+m[1])) | |||
n := ast.NewTextSegment(text.NewSegment(start, start+i)) | |||
link := ast.NewAutoLink(typ, n) | |||
link.Protocol = protocol | |||
return link | |||
@@ -15,7 +15,121 @@ import ( | |||
"github.com/yuin/goldmark/util" | |||
) | |||
var tableDelimRegexp = regexp.MustCompile(`^[\s\-\|\:]+$`) | |||
var escapedPipeCellListKey = parser.NewContextKey() | |||
type escapedPipeCell struct { | |||
Cell *ast.TableCell | |||
Pos []int | |||
Transformed bool | |||
} | |||
// TableCellAlignMethod indicates how are table cells aligned in HTML format.indicates how are table cells aligned in HTML format. | |||
type TableCellAlignMethod int | |||
const ( | |||
// TableCellAlignDefault renders alignments by default method. | |||
// With XHTML, alignments are rendered as an align attribute. | |||
// With HTML5, alignments are rendered as a style attribute. | |||
TableCellAlignDefault TableCellAlignMethod = iota | |||
// TableCellAlignAttribute renders alignments as an align attribute. | |||
TableCellAlignAttribute | |||
// TableCellAlignStyle renders alignments as a style attribute. | |||
TableCellAlignStyle | |||
// TableCellAlignNone does not care about alignments. | |||
// If you using classes or other styles, you can add these attributes | |||
// in an ASTTransformer. | |||
TableCellAlignNone | |||
) | |||
// TableConfig struct holds options for the extension. | |||
type TableConfig struct { | |||
html.Config | |||
// TableCellAlignMethod indicates how are table celss aligned. | |||
TableCellAlignMethod TableCellAlignMethod | |||
} | |||
// TableOption interface is a functional option interface for the extension. | |||
type TableOption interface { | |||
renderer.Option | |||
// SetTableOption sets given option to the extension. | |||
SetTableOption(*TableConfig) | |||
} | |||
// NewTableConfig returns a new Config with defaults. | |||
func NewTableConfig() TableConfig { | |||
return TableConfig{ | |||
Config: html.NewConfig(), | |||
TableCellAlignMethod: TableCellAlignDefault, | |||
} | |||
} | |||
// SetOption implements renderer.SetOptioner. | |||
func (c *TableConfig) SetOption(name renderer.OptionName, value interface{}) { | |||
switch name { | |||
case optTableCellAlignMethod: | |||
c.TableCellAlignMethod = value.(TableCellAlignMethod) | |||
default: | |||
c.Config.SetOption(name, value) | |||
} | |||
} | |||
type withTableHTMLOptions struct { | |||
value []html.Option | |||
} | |||
func (o *withTableHTMLOptions) SetConfig(c *renderer.Config) { | |||
if o.value != nil { | |||
for _, v := range o.value { | |||
v.(renderer.Option).SetConfig(c) | |||
} | |||
} | |||
} | |||
func (o *withTableHTMLOptions) SetTableOption(c *TableConfig) { | |||
if o.value != nil { | |||
for _, v := range o.value { | |||
v.SetHTMLOption(&c.Config) | |||
} | |||
} | |||
} | |||
// WithTableHTMLOptions is functional option that wraps goldmark HTMLRenderer options. | |||
func WithTableHTMLOptions(opts ...html.Option) TableOption { | |||
return &withTableHTMLOptions{opts} | |||
} | |||
const optTableCellAlignMethod renderer.OptionName = "TableTableCellAlignMethod" | |||
type withTableCellAlignMethod struct { | |||
value TableCellAlignMethod | |||
} | |||
func (o *withTableCellAlignMethod) SetConfig(c *renderer.Config) { | |||
c.Options[optTableCellAlignMethod] = o.value | |||
} | |||
func (o *withTableCellAlignMethod) SetTableOption(c *TableConfig) { | |||
c.TableCellAlignMethod = o.value | |||
} | |||
// WithTableCellAlignMethod is a functional option that indicates how are table cells aligned in HTML format. | |||
func WithTableCellAlignMethod(a TableCellAlignMethod) TableOption { | |||
return &withTableCellAlignMethod{a} | |||
} | |||
func isTableDelim(bs []byte) bool { | |||
for _, b := range bs { | |||
if !(util.IsSpace(b) || b == '-' || b == '|' || b == ':') { | |||
return false | |||
} | |||
} | |||
return true | |||
} | |||
var tableDelimLeft = regexp.MustCompile(`^\s*\:\-+\s*$`) | |||
var tableDelimRight = regexp.MustCompile(`^\s*\-+\:\s*$`) | |||
var tableDelimCenter = regexp.MustCompile(`^\s*\:\-+\:\s*$`) | |||
@@ -37,25 +151,34 @@ func (b *tableParagraphTransformer) Transform(node *gast.Paragraph, reader text. | |||
if lines.Len() < 2 { | |||
return | |||
} | |||
alignments := b.parseDelimiter(lines.At(1), reader) | |||
if alignments == nil { | |||
return | |||
} | |||
header := b.parseRow(lines.At(0), alignments, true, reader) | |||
if header == nil || len(alignments) != header.ChildCount() { | |||
return | |||
} | |||
table := ast.NewTable() | |||
table.Alignments = alignments | |||
table.AppendChild(table, ast.NewTableHeader(header)) | |||
for i := 2; i < lines.Len(); i++ { | |||
table.AppendChild(table, b.parseRow(lines.At(i), alignments, false, reader)) | |||
for i := 1; i < lines.Len(); i++ { | |||
alignments := b.parseDelimiter(lines.At(i), reader) | |||
if alignments == nil { | |||
continue | |||
} | |||
header := b.parseRow(lines.At(i-1), alignments, true, reader, pc) | |||
if header == nil || len(alignments) != header.ChildCount() { | |||
return | |||
} | |||
table := ast.NewTable() | |||
table.Alignments = alignments | |||
table.AppendChild(table, ast.NewTableHeader(header)) | |||
for j := i + 1; j < lines.Len(); j++ { | |||
table.AppendChild(table, b.parseRow(lines.At(j), alignments, false, reader, pc)) | |||
} | |||
node.Lines().SetSliced(0, i-1) | |||
node.Parent().InsertAfter(node.Parent(), node, table) | |||
if node.Lines().Len() == 0 { | |||
node.Parent().RemoveChild(node.Parent(), node) | |||
} else { | |||
last := node.Lines().At(i - 2) | |||
last.Stop = last.Stop - 1 // trim last newline(\n) | |||
node.Lines().Set(i-2, last) | |||
} | |||
} | |||
node.Parent().InsertBefore(node.Parent(), node, table) | |||
node.Parent().RemoveChild(node.Parent(), node) | |||
} | |||
func (b *tableParagraphTransformer) parseRow(segment text.Segment, alignments []ast.Alignment, isHeader bool, reader text.Reader) *ast.TableRow { | |||
func (b *tableParagraphTransformer) parseRow(segment text.Segment, alignments []ast.Alignment, isHeader bool, reader text.Reader, pc parser.Context) *ast.TableRow { | |||
source := reader.Source() | |||
line := segment.Value(source) | |||
pos := 0 | |||
@@ -79,18 +202,39 @@ func (b *tableParagraphTransformer) parseRow(segment text.Segment, alignments [] | |||
} else { | |||
alignment = alignments[i] | |||
} | |||
closure := util.FindClosure(line[pos:], byte(0), '|', true, false) | |||
if closure < 0 { | |||
closure = len(line[pos:]) | |||
} | |||
var escapedCell *escapedPipeCell | |||
node := ast.NewTableCell() | |||
seg := text.NewSegment(segment.Start+pos, segment.Start+pos+closure) | |||
node.Alignment = alignment | |||
hasBacktick := false | |||
closure := pos | |||
for ; closure < limit; closure++ { | |||
if line[closure] == '`' { | |||
hasBacktick = true | |||
} | |||
if line[closure] == '|' { | |||
if closure == 0 || line[closure-1] != '\\' { | |||
break | |||
} else if hasBacktick { | |||
if escapedCell == nil { | |||
escapedCell = &escapedPipeCell{node, []int{}, false} | |||
escapedList := pc.ComputeIfAbsent(escapedPipeCellListKey, | |||
func() interface{} { | |||
return []*escapedPipeCell{} | |||
}).([]*escapedPipeCell) | |||
escapedList = append(escapedList, escapedCell) | |||
pc.Set(escapedPipeCellListKey, escapedList) | |||
} | |||
escapedCell.Pos = append(escapedCell.Pos, segment.Start+closure-1) | |||
} | |||
} | |||
} | |||
seg := text.NewSegment(segment.Start+pos, segment.Start+closure) | |||
seg = seg.TrimLeftSpace(source) | |||
seg = seg.TrimRightSpace(source) | |||
node.Lines().Append(seg) | |||
node.Alignment = alignment | |||
row.AppendChild(row, node) | |||
pos += closure + 1 | |||
pos = closure + 1 | |||
} | |||
for ; i < len(alignments); i++ { | |||
row.AppendChild(row, ast.NewTableCell()) | |||
@@ -100,7 +244,7 @@ func (b *tableParagraphTransformer) parseRow(segment text.Segment, alignments [] | |||
func (b *tableParagraphTransformer) parseDelimiter(segment text.Segment, reader text.Reader) []ast.Alignment { | |||
line := segment.Value(reader.Source()) | |||
if !tableDelimRegexp.Match(line) { | |||
if !isTableDelim(line) { | |||
return nil | |||
} | |||
cols := bytes.Split(line, []byte{'|'}) | |||
@@ -128,19 +272,74 @@ func (b *tableParagraphTransformer) parseDelimiter(segment text.Segment, reader | |||
return alignments | |||
} | |||
type tableASTTransformer struct { | |||
} | |||
var defaultTableASTTransformer = &tableASTTransformer{} | |||
// NewTableASTTransformer returns a parser.ASTTransformer for tables. | |||
func NewTableASTTransformer() parser.ASTTransformer { | |||
return defaultTableASTTransformer | |||
} | |||
func (a *tableASTTransformer) Transform(node *gast.Document, reader text.Reader, pc parser.Context) { | |||
lst := pc.Get(escapedPipeCellListKey) | |||
if lst == nil { | |||
return | |||
} | |||
pc.Set(escapedPipeCellListKey, nil) | |||
for _, v := range lst.([]*escapedPipeCell) { | |||
if v.Transformed { | |||
continue | |||
} | |||
_ = gast.Walk(v.Cell, func(n gast.Node, entering bool) (gast.WalkStatus, error) { | |||
if !entering || n.Kind() != gast.KindCodeSpan { | |||
return gast.WalkContinue, nil | |||
} | |||
for c := n.FirstChild(); c != nil; { | |||
next := c.NextSibling() | |||
if c.Kind() != gast.KindText { | |||
c = next | |||
continue | |||
} | |||
parent := c.Parent() | |||
ts := &c.(*gast.Text).Segment | |||
n := c | |||
for _, v := range lst.([]*escapedPipeCell) { | |||
for _, pos := range v.Pos { | |||
if ts.Start <= pos && pos < ts.Stop { | |||
segment := n.(*gast.Text).Segment | |||
n1 := gast.NewRawTextSegment(segment.WithStop(pos)) | |||
n2 := gast.NewRawTextSegment(segment.WithStart(pos + 1)) | |||
parent.InsertAfter(parent, n, n1) | |||
parent.InsertAfter(parent, n1, n2) | |||
parent.RemoveChild(parent, n) | |||
n = n2 | |||
v.Transformed = true | |||
} | |||
} | |||
} | |||
c = next | |||
} | |||
return gast.WalkContinue, nil | |||
}) | |||
} | |||
} | |||
// TableHTMLRenderer is a renderer.NodeRenderer implementation that | |||
// renders Table nodes. | |||
type TableHTMLRenderer struct { | |||
html.Config | |||
TableConfig | |||
} | |||
// NewTableHTMLRenderer returns a new TableHTMLRenderer. | |||
func NewTableHTMLRenderer(opts ...html.Option) renderer.NodeRenderer { | |||
func NewTableHTMLRenderer(opts ...TableOption) renderer.NodeRenderer { | |||
r := &TableHTMLRenderer{ | |||
Config: html.NewConfig(), | |||
TableConfig: NewTableConfig(), | |||
} | |||
for _, opt := range opts { | |||
opt.SetHTMLOption(&r.Config) | |||
opt.SetTableOption(&r.TableConfig) | |||
} | |||
return r | |||
} | |||
@@ -281,14 +480,33 @@ func (r *TableHTMLRenderer) renderTableCell(w util.BufWriter, source []byte, nod | |||
tag = "th" | |||
} | |||
if entering { | |||
align := "" | |||
fmt.Fprintf(w, "<%s", tag) | |||
if n.Alignment != ast.AlignNone { | |||
if _, ok := n.AttributeString("align"); !ok { // Skip align render if overridden | |||
// TODO: "align" is deprecated. style="text-align:%s" instead? | |||
align = fmt.Sprintf(` align="%s"`, n.Alignment.String()) | |||
amethod := r.TableConfig.TableCellAlignMethod | |||
if amethod == TableCellAlignDefault { | |||
if r.Config.XHTML { | |||
amethod = TableCellAlignAttribute | |||
} else { | |||
amethod = TableCellAlignStyle | |||
} | |||
} | |||
switch amethod { | |||
case TableCellAlignAttribute: | |||
if _, ok := n.AttributeString("align"); !ok { // Skip align render if overridden | |||
fmt.Fprintf(w, ` align="%s"`, n.Alignment.String()) | |||
} | |||
case TableCellAlignStyle: | |||
v, ok := n.AttributeString("style") | |||
var cob util.CopyOnWriteBuffer | |||
if ok { | |||
cob = util.NewCopyOnWriteBuffer(v.([]byte)) | |||
cob.AppendByte(';') | |||
} | |||
style := fmt.Sprintf("text-align:%s", n.Alignment.String()) | |||
cob.AppendString(style) | |||
n.SetAttributeString("style", cob.Bytes()) | |||
} | |||
} | |||
fmt.Fprintf(w, "<%s", tag) | |||
if n.Attributes() != nil { | |||
if tag == "td" { | |||
html.RenderAttributes(w, n, TableTdCellAttributeFilter) // <td> | |||
@@ -296,7 +514,7 @@ func (r *TableHTMLRenderer) renderTableCell(w util.BufWriter, source []byte, nod | |||
html.RenderAttributes(w, n, TableThCellAttributeFilter) // <th> | |||
} | |||
} | |||
fmt.Fprintf(w, "%s>", align) | |||
_ = w.WriteByte('>') | |||
} else { | |||
fmt.Fprintf(w, "</%s>\n", tag) | |||
} | |||
@@ -304,16 +522,31 @@ func (r *TableHTMLRenderer) renderTableCell(w util.BufWriter, source []byte, nod | |||
} | |||
type table struct { | |||
options []TableOption | |||
} | |||
// Table is an extension that allow you to use GFM tables . | |||
var Table = &table{} | |||
var Table = &table{ | |||
options: []TableOption{}, | |||
} | |||
// NewTable returns a new extension with given options. | |||
func NewTable(opts ...TableOption) goldmark.Extender { | |||
return &table{ | |||
options: opts, | |||
} | |||
} | |||
func (e *table) Extend(m goldmark.Markdown) { | |||
m.Parser().AddOptions(parser.WithParagraphTransformers( | |||
util.Prioritized(NewTableParagraphTransformer(), 200), | |||
)) | |||
m.Parser().AddOptions( | |||
parser.WithParagraphTransformers( | |||
util.Prioritized(NewTableParagraphTransformer(), 200), | |||
), | |||
parser.WithASTTransformers( | |||
util.Prioritized(defaultTableASTTransformer, 0), | |||
), | |||
) | |||
m.Renderer().AddOptions(renderer.WithNodeRenderers( | |||
util.Prioritized(NewTableHTMLRenderer(), 500), | |||
util.Prioritized(NewTableHTMLRenderer(e.options...), 500), | |||
)) | |||
} |
@@ -10,6 +10,27 @@ import ( | |||
"github.com/yuin/goldmark/util" | |||
) | |||
var uncloseCounterKey = parser.NewContextKey() | |||
type unclosedCounter struct { | |||
Single int | |||
Double int | |||
} | |||
func (u *unclosedCounter) Reset() { | |||
u.Single = 0 | |||
u.Double = 0 | |||
} | |||
func getUnclosedCounter(pc parser.Context) *unclosedCounter { | |||
v := pc.Get(uncloseCounterKey) | |||
if v == nil { | |||
v = &unclosedCounter{} | |||
pc.Set(uncloseCounterKey, v) | |||
} | |||
return v.(*unclosedCounter) | |||
} | |||
// TypographicPunctuation is a key of the punctuations that can be replaced with | |||
// typographic entities. | |||
type TypographicPunctuation int | |||
@@ -139,11 +160,10 @@ func NewTypographerParser(opts ...TypographerOption) parser.InlineParser { | |||
} | |||
func (s *typographerParser) Trigger() []byte { | |||
return []byte{'\'', '"', '-', '.', '<', '>'} | |||
return []byte{'\'', '"', '-', '.', ',', '<', '>', '*', '['} | |||
} | |||
func (s *typographerParser) Parse(parent gast.Node, block text.Reader, pc parser.Context) gast.Node { | |||
before := block.PrecendingCharacter() | |||
line, _ := block.PeekLine() | |||
c := line[0] | |||
if len(line) > 2 { | |||
@@ -189,10 +209,12 @@ func (s *typographerParser) Parse(parent gast.Node, block text.Reader, pc parser | |||
} | |||
} | |||
if c == '\'' || c == '"' { | |||
before := block.PrecendingCharacter() | |||
d := parser.ScanDelimiter(line, before, 1, defaultTypographerDelimiterProcessor) | |||
if d == nil { | |||
return nil | |||
} | |||
counter := getUnclosedCounter(pc) | |||
if c == '\'' { | |||
if s.Substitutions[Apostrophe] != nil { | |||
// Handle decade abbrevations such as '90s | |||
@@ -201,13 +223,20 @@ func (s *typographerParser) Parse(parent gast.Node, block text.Reader, pc parser | |||
if len(line) > 4 { | |||
after = util.ToRune(line, 4) | |||
} | |||
if len(line) == 3 || unicode.IsSpace(after) || unicode.IsPunct(after) { | |||
if len(line) == 3 || util.IsSpaceRune(after) || util.IsPunctRune(after) { | |||
node := gast.NewString(s.Substitutions[Apostrophe]) | |||
node.SetCode(true) | |||
block.Advance(1) | |||
return node | |||
} | |||
} | |||
// special cases: 'twas, 'em, 'net | |||
if len(line) > 1 && (unicode.IsPunct(before) || unicode.IsSpace(before)) && (line[1] == 't' || line[1] == 'e' || line[1] == 'n' || line[1] == 'l') { | |||
node := gast.NewString(s.Substitutions[Apostrophe]) | |||
node.SetCode(true) | |||
block.Advance(1) | |||
return node | |||
} | |||
// Convert normal apostrophes. This is probably more flexible than necessary but | |||
// converts any apostrophe in between two alphanumerics. | |||
if len(line) > 1 && (unicode.IsDigit(before) || unicode.IsLetter(before)) && (unicode.IsLetter(util.ToRune(line, 1))) { | |||
@@ -218,16 +247,43 @@ func (s *typographerParser) Parse(parent gast.Node, block text.Reader, pc parser | |||
} | |||
} | |||
if s.Substitutions[LeftSingleQuote] != nil && d.CanOpen && !d.CanClose { | |||
node := gast.NewString(s.Substitutions[LeftSingleQuote]) | |||
nt := LeftSingleQuote | |||
// special cases: Alice's, I'm, Don't, You'd | |||
if len(line) > 1 && (line[1] == 's' || line[1] == 'm' || line[1] == 't' || line[1] == 'd') && (len(line) < 3 || util.IsPunct(line[2]) || util.IsSpace(line[2])) { | |||
nt = RightSingleQuote | |||
} | |||
// special cases: I've, I'll, You're | |||
if len(line) > 2 && ((line[1] == 'v' && line[2] == 'e') || (line[1] == 'l' && line[2] == 'l') || (line[1] == 'r' && line[2] == 'e')) && (len(line) < 4 || util.IsPunct(line[3]) || util.IsSpace(line[3])) { | |||
nt = RightSingleQuote | |||
} | |||
if nt == LeftSingleQuote { | |||
counter.Single++ | |||
} | |||
node := gast.NewString(s.Substitutions[nt]) | |||
node.SetCode(true) | |||
block.Advance(1) | |||
return node | |||
} | |||
if s.Substitutions[RightSingleQuote] != nil && d.CanClose && !d.CanOpen { | |||
node := gast.NewString(s.Substitutions[RightSingleQuote]) | |||
node.SetCode(true) | |||
block.Advance(1) | |||
return node | |||
if s.Substitutions[RightSingleQuote] != nil { | |||
// plural possesives and abbreviations: Smiths', doin' | |||
if len(line) > 1 && unicode.IsSpace(util.ToRune(line, 0)) || unicode.IsPunct(util.ToRune(line, 0)) && (len(line) > 2 && !unicode.IsDigit(util.ToRune(line, 1))) { | |||
node := gast.NewString(s.Substitutions[RightSingleQuote]) | |||
node.SetCode(true) | |||
block.Advance(1) | |||
return node | |||
} | |||
} | |||
if s.Substitutions[RightSingleQuote] != nil && counter.Single > 0 { | |||
isClose := d.CanClose && !d.CanOpen | |||
maybeClose := d.CanClose && d.CanOpen && len(line) > 1 && unicode.IsPunct(util.ToRune(line, 1)) && (len(line) == 2 || (len(line) > 2 && util.IsPunct(line[2]) || util.IsSpace(line[2]))) | |||
if isClose || maybeClose { | |||
node := gast.NewString(s.Substitutions[RightSingleQuote]) | |||
node.SetCode(true) | |||
block.Advance(1) | |||
counter.Single-- | |||
return node | |||
} | |||
} | |||
} | |||
if c == '"' { | |||
@@ -235,13 +291,23 @@ func (s *typographerParser) Parse(parent gast.Node, block text.Reader, pc parser | |||
node := gast.NewString(s.Substitutions[LeftDoubleQuote]) | |||
node.SetCode(true) | |||
block.Advance(1) | |||
counter.Double++ | |||
return node | |||
} | |||
if s.Substitutions[RightDoubleQuote] != nil && d.CanClose && !d.CanOpen { | |||
node := gast.NewString(s.Substitutions[RightDoubleQuote]) | |||
node.SetCode(true) | |||
block.Advance(1) | |||
return node | |||
if s.Substitutions[RightDoubleQuote] != nil && counter.Double > 0 { | |||
isClose := d.CanClose && !d.CanOpen | |||
maybeClose := d.CanClose && d.CanOpen && len(line) > 1 && (unicode.IsPunct(util.ToRune(line, 1))) && (len(line) == 2 || (len(line) > 2 && util.IsPunct(line[2]) || util.IsSpace(line[2]))) | |||
if isClose || maybeClose { | |||
// special case: "Monitor 21"" | |||
if len(line) > 1 && line[1] == '"' && unicode.IsDigit(before) { | |||
return nil | |||
} | |||
node := gast.NewString(s.Substitutions[RightDoubleQuote]) | |||
node.SetCode(true) | |||
block.Advance(1) | |||
counter.Double-- | |||
return node | |||
} | |||
} | |||
} | |||
} | |||
@@ -249,7 +315,7 @@ func (s *typographerParser) Parse(parent gast.Node, block text.Reader, pc parser | |||
} | |||
func (s *typographerParser) CloseBlock(parent gast.Node, pc parser.Context) { | |||
// nothing to do | |||
getUnclosedCounter(pc).Reset() | |||
} | |||
type typographer struct { | |||
@@ -1,3 +1,3 @@ | |||
module github.com/yuin/goldmark | |||
go 1.13 | |||
go 1.18 |
@@ -89,7 +89,11 @@ func parseAttribute(reader text.Reader) (Attribute, bool) { | |||
reader.Advance(1) | |||
line, _ := reader.PeekLine() | |||
i := 0 | |||
for ; i < len(line) && !util.IsSpace(line[i]) && (!util.IsPunct(line[i]) || line[i] == '_' || line[i] == '-'); i++ { | |||
// HTML5 allows any kind of characters as id, but XHTML restricts characters for id. | |||
// CommonMark is basically defined for XHTML(even though it is legacy). | |||
// So we restrict id characters. | |||
for ; i < len(line) && !util.IsSpace(line[i]) && | |||
(!util.IsPunct(line[i]) || line[i] == '_' || line[i] == '-' || line[i] == ':' || line[i] == '.'); i++ { | |||
} | |||
name := attrNameClass | |||
if c == '#' { | |||
@@ -129,6 +133,11 @@ func parseAttribute(reader text.Reader) (Attribute, bool) { | |||
if !ok { | |||
return Attribute{}, false | |||
} | |||
if bytes.Equal(name, attrNameClass) { | |||
if _, ok = value.([]byte); !ok { | |||
return Attribute{}, false | |||
} | |||
} | |||
return Attribute{Name: name, Value: value}, true | |||
} | |||
@@ -91,6 +91,9 @@ func (b *atxHeadingParser) Open(parent ast.Node, reader text.Reader, pc Context) | |||
if i == pos || level > 6 { | |||
return nil, NoChildren | |||
} | |||
if i == len(line) { // alone '#' (without a new line character) | |||
return ast.NewHeading(level), NoChildren | |||
} | |||
l := util.TrimLeftSpaceLength(line[i:]) | |||
if l == 0 { | |||
return nil, NoChildren | |||
@@ -126,7 +129,8 @@ func (b *atxHeadingParser) Open(parent ast.Node, reader text.Reader, pc Context) | |||
if closureClose > 0 { | |||
reader.Advance(closureClose) | |||
attrs, ok := ParseAttributes(reader) | |||
parsed = ok | |||
rest, _ := reader.PeekLine() | |||
parsed = ok && util.IsBlank(rest) | |||
if parsed { | |||
for _, attr := range attrs { | |||
node.SetAttribute(attr.Name, attr.Value) | |||
@@ -31,6 +31,10 @@ func (b *codeBlockParser) Open(parent ast.Node, reader text.Reader, pc Context) | |||
node := ast.NewCodeBlock() | |||
reader.AdvanceAndSetPadding(pos, padding) | |||
_, segment = reader.PeekLine() | |||
// if code block line starts with a tab, keep a tab as it is. | |||
if segment.Padding != 0 { | |||
preserveLeadingTabInCodeBlock(&segment, reader, 0) | |||
} | |||
node.Lines().Append(segment) | |||
reader.Advance(segment.Len() - 1) | |||
return node, NoChildren | |||
@@ -49,6 +53,12 @@ func (b *codeBlockParser) Continue(node ast.Node, reader text.Reader, pc Context | |||
} | |||
reader.AdvanceAndSetPadding(pos, padding) | |||
_, segment = reader.PeekLine() | |||
// if code block line starts with a tab, keep a tab as it is. | |||
if segment.Padding != 0 { | |||
preserveLeadingTabInCodeBlock(&segment, reader, 0) | |||
} | |||
node.Lines().Append(segment) | |||
reader.Advance(segment.Len() - 1) | |||
return Continue | NoChildren | |||
@@ -77,3 +87,14 @@ func (b *codeBlockParser) CanInterruptParagraph() bool { | |||
func (b *codeBlockParser) CanAcceptIndentedLine() bool { | |||
return true | |||
} | |||
func preserveLeadingTabInCodeBlock(segment *text.Segment, reader text.Reader, indent int) { | |||
offsetWithPadding := reader.LineOffset() + indent | |||
sl, ss := reader.Position() | |||
reader.SetPosition(sl, text.NewSegment(ss.Start-1, ss.Stop)) | |||
if offsetWithPadding == reader.LineOffset() { | |||
segment.Padding = 0 | |||
segment.Start-- | |||
} | |||
reader.SetPosition(sl, ss) | |||
} |
@@ -3,7 +3,6 @@ package parser | |||
import ( | |||
"github.com/yuin/goldmark/ast" | |||
"github.com/yuin/goldmark/text" | |||
"github.com/yuin/goldmark/util" | |||
) | |||
type codeSpanParser struct { | |||
@@ -52,9 +51,7 @@ func (s *codeSpanParser) Parse(parent ast.Node, block text.Reader, pc Context) a | |||
} | |||
} | |||
} | |||
if !util.IsBlank(line) { | |||
node.AppendChild(node, ast.NewRawTextSegment(segment)) | |||
} | |||
node.AppendChild(node, ast.NewRawTextSegment(segment)) | |||
block.AdvanceLine() | |||
} | |||
end: | |||
@@ -62,11 +59,11 @@ end: | |||
// trim first halfspace and last halfspace | |||
segment := node.FirstChild().(*ast.Text).Segment | |||
shouldTrimmed := true | |||
if !(!segment.IsEmpty() && block.Source()[segment.Start] == ' ') { | |||
if !(!segment.IsEmpty() && isSpaceOrNewline(block.Source()[segment.Start])) { | |||
shouldTrimmed = false | |||
} | |||
segment = node.LastChild().(*ast.Text).Segment | |||
if !(!segment.IsEmpty() && block.Source()[segment.Stop-1] == ' ') { | |||
if !(!segment.IsEmpty() && isSpaceOrNewline(block.Source()[segment.Stop-1])) { | |||
shouldTrimmed = false | |||
} | |||
if shouldTrimmed { | |||
@@ -81,3 +78,7 @@ end: | |||
} | |||
return node | |||
} | |||
func isSpaceOrNewline(c byte) bool { | |||
return c == ' ' || c == '\n' | |||
} |
@@ -3,7 +3,6 @@ package parser | |||
import ( | |||
"fmt" | |||
"strings" | |||
"unicode" | |||
"github.com/yuin/goldmark/ast" | |||
"github.com/yuin/goldmark/text" | |||
@@ -31,11 +30,11 @@ type Delimiter struct { | |||
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. | |||
// See https://spec.commonmark.org/0.30/#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. | |||
// See https://spec.commonmark.org/0.30/#can-open-emphasis for details. | |||
CanClose bool | |||
// Length is a remaining length of this delimiter. | |||
@@ -128,10 +127,10 @@ func ScanDelimiter(line []byte, before rune, min int, processor DelimiterProcess | |||
} | |||
canOpen, canClose := false, false | |||
beforeIsPunctuation := unicode.IsPunct(before) | |||
beforeIsWhitespace := unicode.IsSpace(before) | |||
afterIsPunctuation := unicode.IsPunct(after) | |||
afterIsWhitespace := unicode.IsSpace(after) | |||
beforeIsPunctuation := util.IsPunctRune(before) | |||
beforeIsWhitespace := util.IsSpaceRune(before) | |||
afterIsPunctuation := util.IsPunctRune(after) | |||
afterIsWhitespace := util.IsSpaceRune(after) | |||
isLeft := !afterIsWhitespace && | |||
(!afterIsPunctuation || beforeIsWhitespace || beforeIsPunctuation) | |||
@@ -163,15 +162,11 @@ func ProcessDelimiters(bottom ast.Node, pc Context) { | |||
var closer *Delimiter | |||
if bottom != nil { | |||
if bottom != lastDelimiter { | |||
for c := lastDelimiter.PreviousSibling(); c != nil; { | |||
for c := lastDelimiter.PreviousSibling(); c != nil && c != bottom; { | |||
if d, ok := c.(*Delimiter); ok { | |||
closer = d | |||
} | |||
prev := c.PreviousSibling() | |||
if prev == bottom { | |||
break | |||
} | |||
c = prev | |||
c = c.PreviousSibling() | |||
} | |||
} | |||
} else { | |||
@@ -190,7 +185,7 @@ func ProcessDelimiters(bottom ast.Node, pc Context) { | |||
found := false | |||
maybeOpener := false | |||
var opener *Delimiter | |||
for opener = closer.PreviousDelimiter; opener != nil; opener = opener.PreviousDelimiter { | |||
for opener = closer.PreviousDelimiter; opener != nil && opener != bottom; opener = opener.PreviousDelimiter { | |||
if opener.CanOpen && opener.Processor.CanOpenCloser(opener, closer) { | |||
maybeOpener = true | |||
consume = opener.CalcComsumption(closer) | |||
@@ -201,10 +196,11 @@ func ProcessDelimiters(bottom ast.Node, pc Context) { | |||
} | |||
} | |||
if !found { | |||
next := closer.NextDelimiter | |||
if !maybeOpener && !closer.CanOpen { | |||
pc.RemoveDelimiter(closer) | |||
} | |||
closer = closer.NextDelimiter | |||
closer = next | |||
continue | |||
} | |||
opener.ConsumeCharacters(consume) | |||
@@ -71,6 +71,7 @@ func (b *fencedCodeBlockParser) Open(parent ast.Node, reader text.Reader, pc Con | |||
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, reader.LineOffset()) | |||
if w < 4 { | |||
i := pos | |||
@@ -86,9 +87,19 @@ func (b *fencedCodeBlockParser) Continue(node ast.Node, reader text.Reader, pc C | |||
return Close | |||
} | |||
} | |||
pos, padding := util.DedentPositionPadding(line, reader.LineOffset(), segment.Padding, fdata.indent) | |||
pos, padding := util.IndentPositionPadding(line, reader.LineOffset(), segment.Padding, fdata.indent) | |||
if pos < 0 { | |||
pos = util.FirstNonSpacePosition(line) | |||
if pos < 0 { | |||
pos = 0 | |||
} | |||
padding = 0 | |||
} | |||
seg := text.NewSegmentPadding(segment.Start+pos, segment.Stop, padding) | |||
// if code block line starts with a tab, keep a tab as it is. | |||
if padding != 0 { | |||
preserveLeadingTabInCodeBlock(&seg, reader, fdata.indent) | |||
} | |||
node.Lines().Append(seg) | |||
reader.AdvanceAndSetPadding(segment.Stop-segment.Start-pos-1, padding) | |||
return Continue | NoChildren | |||
@@ -76,8 +76,8 @@ var allowedBlockTags = map[string]bool{ | |||
"ul": true, | |||
} | |||
var htmlBlockType1OpenRegexp = regexp.MustCompile(`(?i)^[ ]{0,3}<(script|pre|style)(?:\s.*|>.*|/>.*|)\n?$`) | |||
var htmlBlockType1CloseRegexp = regexp.MustCompile(`(?i)^.*</(?:script|pre|style)>.*`) | |||
var htmlBlockType1OpenRegexp = regexp.MustCompile(`(?i)^[ ]{0,3}<(script|pre|style|textarea)(?:\s.*|>.*|/>.*|)(?:\r\n|\n)?$`) | |||
var htmlBlockType1CloseRegexp = regexp.MustCompile(`(?i)^.*</(?:script|pre|style|textarea)>.*`) | |||
var htmlBlockType2OpenRegexp = regexp.MustCompile(`^[ ]{0,3}<!\-\-`) | |||
var htmlBlockType2Close = []byte{'-', '-', '>'} | |||
@@ -85,15 +85,15 @@ var htmlBlockType2Close = []byte{'-', '-', '>'} | |||
var htmlBlockType3OpenRegexp = regexp.MustCompile(`^[ ]{0,3}<\?`) | |||
var htmlBlockType3Close = []byte{'?', '>'} | |||
var htmlBlockType4OpenRegexp = regexp.MustCompile(`^[ ]{0,3}<![A-Z]+.*\n?$`) | |||
var htmlBlockType4OpenRegexp = regexp.MustCompile(`^[ ]{0,3}<![A-Z]+.*(?:\r\n|\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 htmlBlockType6Regexp = regexp.MustCompile(`^[ ]{0,3}<(?:/[ ]*)?([a-zA-Z]+[a-zA-Z0-9\-]*)(?:[ ].*|>.*|/>.*|)(?:\r\n|\n)?$`) | |||
var htmlBlockType7Regexp = regexp.MustCompile(`^[ ]{0,3}<(/)?([a-zA-Z0-9]+)(` + attributePattern + `*)(:?>|/>)\s*\n?$`) | |||
var htmlBlockType7Regexp = regexp.MustCompile(`^[ ]{0,3}<(/[ ]*)?([a-zA-Z]+[a-zA-Z0-9\-]*)(` + attributePattern + `*)[ ]*(?:>|/>)[ ]*(?:\r\n|\n)?$`) | |||
type htmlBlockParser struct { | |||
} | |||
@@ -201,7 +201,7 @@ func (b *htmlBlockParser) Continue(node ast.Node, reader text.Reader, pc Context | |||
} | |||
if bytes.Contains(line, closurePattern) { | |||
htmlBlock.ClosureLine = segment | |||
reader.Advance(segment.Len() - 1) | |||
reader.Advance(segment.Len()) | |||
return Close | |||
} | |||
@@ -2,7 +2,6 @@ package parser | |||
import ( | |||
"fmt" | |||
"regexp" | |||
"strings" | |||
"github.com/yuin/goldmark/ast" | |||
@@ -49,6 +48,13 @@ func (s *linkLabelState) Kind() ast.NodeKind { | |||
return kindLinkLabelState | |||
} | |||
func linkLabelStateLength(v *linkLabelState) int { | |||
if v == nil || v.Last == nil || v.First == nil { | |||
return 0 | |||
} | |||
return v.Last.Segment.Stop - v.First.Segment.Start | |||
} | |||
func pushLinkLabelState(pc Context, v *linkLabelState) { | |||
tlist := pc.Get(linkLabelStateKey) | |||
var list *linkLabelState | |||
@@ -113,8 +119,6 @@ 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 { | |||
@@ -143,7 +147,14 @@ func (s *linkParser) Parse(parent ast.Node, block text.Reader, pc Context) ast.N | |||
} | |||
block.Advance(1) | |||
removeLinkLabelState(pc, last) | |||
if s.containsLink(last) { // a link in a link text is not allowed | |||
// CommonMark spec says: | |||
// > A link label can have at most 999 characters inside the square brackets. | |||
if linkLabelStateLength(tlist.(*linkLabelState)) > 998 { | |||
ast.MergeOrReplaceTextSegment(last.Parent(), last, last.Segment) | |||
return nil | |||
} | |||
if !last.IsImage && s.containsLink(last) { // a link in a link text is not allowed | |||
ast.MergeOrReplaceTextSegment(last.Parent(), last, last.Segment) | |||
return nil | |||
} | |||
@@ -167,6 +178,13 @@ func (s *linkParser) Parse(parent ast.Node, block text.Reader, pc Context) ast.N | |||
block.SetPosition(l, pos) | |||
ssegment := text.NewSegment(last.Segment.Stop, segment.Start) | |||
maybeReference := block.Value(ssegment) | |||
// CommonMark spec says: | |||
// > A link label can have at most 999 characters inside the square brackets. | |||
if len(maybeReference) > 999 { | |||
ast.MergeOrReplaceTextSegment(last.Parent(), last, last.Segment) | |||
return nil | |||
} | |||
ref, ok := pc.Reference(util.ToLinkReference(maybeReference)) | |||
if !ok { | |||
ast.MergeOrReplaceTextSegment(last.Parent(), last, last.Segment) | |||
@@ -185,15 +203,17 @@ func (s *linkParser) Parse(parent ast.Node, block text.Reader, pc Context) ast.N | |||
return link | |||
} | |||
func (s *linkParser) containsLink(last *linkLabelState) bool { | |||
if last.IsImage { | |||
func (s *linkParser) containsLink(n ast.Node) bool { | |||
if n == nil { | |||
return false | |||
} | |||
var c ast.Node | |||
for c = last; c != nil; c = c.NextSibling() { | |||
for c := n; c != nil; c = c.NextSibling() { | |||
if _, ok := c.(*ast.Link); ok { | |||
return true | |||
} | |||
if s.containsLink(c.FirstChild()) { | |||
return true | |||
} | |||
} | |||
return false | |||
} | |||
@@ -224,21 +244,38 @@ func (s *linkParser) processLinkLabel(parent ast.Node, link *ast.Link, last *lin | |||
} | |||
} | |||
var linkFindClosureOptions text.FindClosureOptions = text.FindClosureOptions{ | |||
Nesting: false, | |||
Newline: true, | |||
Advance: true, | |||
} | |||
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 { | |||
segments, found := block.FindClosure('[', ']', linkFindClosureOptions) | |||
if !found { | |||
return nil, false | |||
} | |||
block.Advance(endIndex + 1) | |||
ssegment := segment.WithStop(segment.Start + endIndex) | |||
maybeReference := block.Value(ssegment) | |||
var maybeReference []byte | |||
if segments.Len() == 1 { // avoid allocate a new byte slice | |||
maybeReference = block.Value(segments.At(0)) | |||
} else { | |||
maybeReference = []byte{} | |||
for i := 0; i < segments.Len(); i++ { | |||
s := segments.At(i) | |||
maybeReference = append(maybeReference, block.Value(s)...) | |||
} | |||
} | |||
if util.IsBlank(maybeReference) { // collapsed reference link | |||
ssegment = text.NewSegment(last.Segment.Stop, orgpos.Start-1) | |||
maybeReference = block.Value(ssegment) | |||
s := text.NewSegment(last.Segment.Stop, orgpos.Start-1) | |||
maybeReference = block.Value(s) | |||
} | |||
// CommonMark spec says: | |||
// > A link label can have at most 999 characters inside the square brackets. | |||
if len(maybeReference) > 999 { | |||
return nil, true | |||
} | |||
ref, ok := pc.Reference(util.ToLinkReference(maybeReference)) | |||
@@ -293,20 +330,17 @@ func (s *linkParser) parseLink(parent ast.Node, last *linkLabelState, block text | |||
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 | |||
@@ -316,7 +350,6 @@ func parseLinkDestination(block text.Reader) ([]byte, bool) { | |||
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 == '(' { | |||
@@ -329,7 +362,6 @@ func parseLinkDestination(block text.Reader) ([]byte, bool) { | |||
} else if util.IsSpace(c) { | |||
break | |||
} | |||
buf = append(buf, c) | |||
i++ | |||
} | |||
block.Advance(i) | |||
@@ -346,34 +378,24 @@ func parseLinkTitle(block text.Reader) ([]byte, bool) { | |||
if opener == '(' { | |||
closer = ')' | |||
} | |||
savedLine, savedPosition := block.Position() | |||
var title []byte | |||
for i := 0; ; i++ { | |||
line, _ := block.PeekLine() | |||
if line == nil { | |||
block.SetPosition(savedLine, savedPosition) | |||
return nil, false | |||
} | |||
offset := 0 | |||
if i == 0 { | |||
offset = 1 | |||
} | |||
pos := util.FindClosure(line[offset:], opener, closer, false, true) | |||
if pos < 0 { | |||
title = append(title, line[offset:]...) | |||
block.AdvanceLine() | |||
continue | |||
block.Advance(1) | |||
segments, found := block.FindClosure(opener, closer, linkFindClosureOptions) | |||
if found { | |||
if segments.Len() == 1 { | |||
return block.Value(segments.At(0)), true | |||
} | |||
pos += offset + 1 // 1: closer | |||
block.Advance(pos) | |||
if i == 0 { // avoid allocating new slice | |||
return line[offset : pos-1], true | |||
var title []byte | |||
for i := 0; i < segments.Len(); i++ { | |||
s := segments.At(i) | |||
title = append(title, block.Value(s)...) | |||
} | |||
return append(title, line[offset:pos-1]...), true | |||
return title, true | |||
} | |||
return nil, false | |||
} | |||
func (s *linkParser) CloseBlock(parent ast.Node, block text.Reader, pc Context) { | |||
pc.Set(linkBottom, nil) | |||
tlist := pc.Get(linkLabelStateKey) | |||
if tlist == nil { | |||
return | |||
@@ -52,7 +52,7 @@ func (p *linkReferenceParagraphTransformer) Transform(node *ast.Paragraph, reade | |||
func parseLinkReferenceDefinition(block text.Reader, pc Context) (int, int) { | |||
block.SkipSpaces() | |||
line, segment := block.PeekLine() | |||
line, _ := block.PeekLine() | |||
if line == nil { | |||
return -1, -1 | |||
} | |||
@@ -67,39 +67,33 @@ func parseLinkReferenceDefinition(block text.Reader, pc Context) (int, int) { | |||
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 | |||
segments, found := block.FindClosure('[', ']', linkFindClosureOptions) | |||
if !found { | |||
return -1, -1 | |||
} | |||
var label []byte | |||
if segments.Len() == 1 { | |||
label = block.Value(segments.At(0)) | |||
} else { | |||
for i := 0; i < segments.Len(); i++ { | |||
s := segments.At(i) | |||
label = append(label, block.Value(s)...) | |||
} | |||
block.AdvanceLine() | |||
} | |||
if closes < 0 { | |||
if util.IsBlank(label) { | |||
return -1, -1 | |||
} | |||
label := block.Value(text.NewSegment(open, closes)) | |||
if util.IsBlank(label) { | |||
if block.Peek() != ':' { | |||
return -1, -1 | |||
} | |||
block.Advance(1) | |||
block.SkipSpaces() | |||
destination, ok := parseLinkDestination(block) | |||
if !ok { | |||
return -1, -1 | |||
} | |||
line, segment = block.PeekLine() | |||
line, _ = block.PeekLine() | |||
isNewLine := line == nil || util.IsBlank(line) | |||
endLine, _ := block.Position() | |||
@@ -117,45 +111,40 @@ func parseLinkReferenceDefinition(block text.Reader, pc Context) (int, int) { | |||
return -1, -1 | |||
} | |||
block.Advance(1) | |||
open = -1 | |||
closes = -1 | |||
closer := opener | |||
if opener == '(' { | |||
closer = ')' | |||
} | |||
for { | |||
line, segment = block.PeekLine() | |||
if line == nil { | |||
segments, found = block.FindClosure(opener, closer, linkFindClosureOptions) | |||
if !found { | |||
if !isNewLine { | |||
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 | |||
} | |||
ref := NewReference(label, destination, nil) | |||
pc.AddReference(ref) | |||
block.AdvanceLine() | |||
return startLine, endLine + 1 | |||
} | |||
if closes < 0 { | |||
return -1, -1 | |||
var title []byte | |||
if segments.Len() == 1 { | |||
title = block.Value(segments.At(0)) | |||
} else { | |||
for i := 0; i < segments.Len(); i++ { | |||
s := segments.At(i) | |||
title = append(title, block.Value(s)...) | |||
} | |||
} | |||
line, segment = block.PeekLine() | |||
line, _ = 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) | |||
@@ -1,10 +1,11 @@ | |||
package parser | |||
import ( | |||
"strconv" | |||
"github.com/yuin/goldmark/ast" | |||
"github.com/yuin/goldmark/text" | |||
"github.com/yuin/goldmark/util" | |||
"strconv" | |||
) | |||
type listItemType int | |||
@@ -15,6 +16,10 @@ const ( | |||
orderedList | |||
) | |||
var skipListParserKey = NewContextKey() | |||
var emptyListItemWithBlankLines = NewContextKey() | |||
var listItemFlagValue interface{} = true | |||
// Same as | |||
// `^(([ ]*)([\-\*\+]))(\s+.*)?\n?$`.FindSubmatchIndex or | |||
// `^(([ ]*)(\d{1,9}[\.\)]))(\s+.*)?\n?$`.FindSubmatchIndex | |||
@@ -122,8 +127,8 @@ func (b *listParser) Trigger() []byte { | |||
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) | |||
if _, lok := last.(*ast.List); lok || pc.Get(skipListParserKey) != nil { | |||
pc.Set(skipListParserKey, nil) | |||
return nil, NoChildren | |||
} | |||
line, _ := reader.PeekLine() | |||
@@ -143,7 +148,7 @@ func (b *listParser) Open(parent ast.Node, reader text.Reader, pc Context) (ast. | |||
return nil, NoChildren | |||
} | |||
//an empty list item cannot interrupt a paragraph: | |||
if match[5]-match[4] == 1 { | |||
if match[4] < 0 || util.IsBlank(line[match[4]:match[5]]) { | |||
return nil, NoChildren | |||
} | |||
} | |||
@@ -153,6 +158,7 @@ func (b *listParser) Open(parent ast.Node, reader text.Reader, pc Context) (ast. | |||
if start > -1 { | |||
node.Start = start | |||
} | |||
pc.Set(emptyListItemWithBlankLines, nil) | |||
return node, HasChildren | |||
} | |||
@@ -160,9 +166,8 @@ func (b *listParser) Continue(node ast.Node, reader text.Reader, pc Context) Sta | |||
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 | |||
if node.LastChild().ChildCount() == 0 { | |||
pc.Set(emptyListItemWithBlankLines, listItemFlagValue) | |||
} | |||
return Continue | HasChildren | |||
} | |||
@@ -175,10 +180,23 @@ func (b *listParser) Continue(node ast.Node, reader text.Reader, pc Context) Sta | |||
// - a | |||
// - b <--- current line | |||
// it maybe a new child of the list. | |||
// | |||
// Empty list items can have multiple blanklines | |||
// | |||
// - <--- 1st item is an empty thus "offset" is unknown | |||
// | |||
// | |||
// - <--- current line | |||
// | |||
// -> 1 list with 2 blank items | |||
// | |||
// So if the last item is an empty, it maybe a new child of the list. | |||
// | |||
offset := lastOffset(node) | |||
lastIsEmpty := node.LastChild().ChildCount() == 0 | |||
indent, _ := util.IndentWidth(line, reader.LineOffset()) | |||
if indent < offset { | |||
if indent < offset || lastIsEmpty { | |||
if indent < 4 { | |||
match, typ := matchesListItem(line, false) // may have a leading spaces more than 3 | |||
if typ != notList && match[1]-offset < 4 { | |||
@@ -200,10 +218,27 @@ func (b *listParser) Continue(node ast.Node, reader text.Reader, pc Context) Sta | |||
return Close | |||
} | |||
} | |||
return Continue | HasChildren | |||
} | |||
} | |||
if !lastIsEmpty { | |||
return Close | |||
} | |||
} | |||
if lastIsEmpty && indent < offset { | |||
return Close | |||
} | |||
// Non empty items can not exist next to an empty list item | |||
// with blank lines. So we need to close the current list | |||
// | |||
// - | |||
// | |||
// foo | |||
// | |||
// -> 1 list with 1 blank items and 1 paragraph | |||
if pc.Get(emptyListItemWithBlankLines) != nil { | |||
return Close | |||
} | |||
return Continue | HasChildren | |||
@@ -230,8 +265,9 @@ func (b *listParser) Close(node ast.Node, reader text.Reader, pc Context) { | |||
if list.IsTight { | |||
for child := node.FirstChild(); child != nil; child = child.NextSibling() { | |||
for gc := child.FirstChild(); gc != nil; gc = gc.NextSibling() { | |||
for gc := child.FirstChild(); gc != nil; { | |||
paragraph, ok := gc.(*ast.Paragraph) | |||
gc = gc.NextSibling() | |||
if ok { | |||
textBlock := ast.NewTextBlock() | |||
textBlock.SetLines(paragraph.Lines()) | |||
@@ -17,9 +17,6 @@ func NewListItemParser() BlockParser { | |||
return defaultListItemParser | |||
} | |||
var skipListParser = NewContextKey() | |||
var skipListParserValue interface{} = true | |||
func (b *listItemParser) Trigger() []byte { | |||
return []byte{'-', '+', '*', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9'} | |||
} | |||
@@ -38,9 +35,12 @@ func (b *listItemParser) Open(parent ast.Node, reader text.Reader, pc Context) ( | |||
if match[1]-offset > 3 { | |||
return nil, NoChildren | |||
} | |||
pc.Set(emptyListItemWithBlankLines, nil) | |||
itemOffset := calcListOffset(line, match) | |||
node := ast.NewListItem(match[3] + itemOffset) | |||
if match[4] < 0 || match[5]-match[4] == 1 { | |||
if match[4] < 0 || util.IsBlank(line[match[4]:match[5]]) { | |||
return node, NoChildren | |||
} | |||
@@ -53,18 +53,23 @@ func (b *listItemParser) Open(parent ast.Node, reader text.Reader, pc Context) ( | |||
func (b *listItemParser) Continue(node ast.Node, reader text.Reader, pc Context) State { | |||
line, _ := reader.PeekLine() | |||
if util.IsBlank(line) { | |||
reader.Advance(len(line) - 1) | |||
return Continue | HasChildren | |||
} | |||
indent, _ := util.IndentWidth(line, reader.LineOffset()) | |||
offset := lastOffset(node.Parent()) | |||
if indent < offset && indent < 4 { | |||
isEmpty := node.ChildCount() == 0 | |||
indent, _ := util.IndentWidth(line, reader.LineOffset()) | |||
if (isEmpty || indent < offset) && indent < 4 { | |||
_, typ := matchesListItem(line, true) | |||
// new list item found | |||
if typ != notList { | |||
pc.Set(skipListParser, skipListParserValue) | |||
pc.Set(skipListParserKey, listItemFlagValue) | |||
return Close | |||
} | |||
if !isEmpty { | |||
return Close | |||
} | |||
return Close | |||
} | |||
pos, padding := util.IndentPosition(line, reader.LineOffset(), offset) | |||
reader.AdvanceAndSetPadding(pos, padding) | |||
@@ -138,6 +138,9 @@ type Context interface { | |||
// Get returns a value associated with the given key. | |||
Get(ContextKey) interface{} | |||
// ComputeIfAbsent computes a value if a value associated with the given key is absent and returns the value. | |||
ComputeIfAbsent(ContextKey, func() interface{}) interface{} | |||
// Set sets the given value to the context. | |||
Set(ContextKey, interface{}) | |||
@@ -252,6 +255,15 @@ func (p *parseContext) Get(key ContextKey) interface{} { | |||
return p.store[key] | |||
} | |||
func (p *parseContext) ComputeIfAbsent(key ContextKey, f func() interface{}) interface{} { | |||
v := p.store[key] | |||
if v == nil { | |||
v = f() | |||
p.store[key] = v | |||
} | |||
return v | |||
} | |||
func (p *parseContext) Set(key ContextKey, value interface{}) { | |||
p.store[key] = value | |||
} | |||
@@ -1103,6 +1115,12 @@ func (p *parser) walkBlock(block ast.Node, cb func(node ast.Node)) { | |||
cb(block) | |||
} | |||
const ( | |||
lineBreakHard uint8 = 1 << iota | |||
lineBreakSoft | |||
lineBreakVisible | |||
) | |||
func (p *parser) parseBlock(block text.BlockReader, parent ast.Node, pc Context) { | |||
if parent.IsRaw() { | |||
return | |||
@@ -1117,21 +1135,25 @@ func (p *parser) parseBlock(block text.BlockReader, parent ast.Node, pc Context) | |||
break | |||
} | |||
lineLength := len(line) | |||
hardlineBreak := false | |||
softLinebreak := line[lineLength-1] == '\n' | |||
if lineLength >= 2 && line[lineLength-2] == '\\' && softLinebreak { // ends with \\n | |||
var lineBreakFlags uint8 = 0 | |||
hasNewLine := line[lineLength-1] == '\n' | |||
if ((lineLength >= 3 && line[lineLength-2] == '\\' && line[lineLength-3] != '\\') || (lineLength == 2 && line[lineLength-2] == '\\')) && hasNewLine { // ends with \\n | |||
lineLength -= 2 | |||
hardlineBreak = true | |||
} else if lineLength >= 3 && line[lineLength-3] == '\\' && line[lineLength-2] == '\r' && softLinebreak { // ends with \\r\n | |||
lineBreakFlags |= lineBreakHard | lineBreakVisible | |||
} else if ((lineLength >= 4 && line[lineLength-3] == '\\' && line[lineLength-2] == '\r' && line[lineLength-4] != '\\') || (lineLength == 3 && line[lineLength-3] == '\\' && line[lineLength-2] == '\r')) && hasNewLine { // ends with \\r\n | |||
lineLength -= 3 | |||
hardlineBreak = true | |||
} else if lineLength >= 3 && line[lineLength-3] == ' ' && line[lineLength-2] == ' ' && softLinebreak { // ends with [space][space]\n | |||
lineBreakFlags |= lineBreakHard | lineBreakVisible | |||
} else if lineLength >= 3 && line[lineLength-3] == ' ' && line[lineLength-2] == ' ' && hasNewLine { // ends with [space][space]\n | |||
lineLength -= 3 | |||
hardlineBreak = true | |||
} else if lineLength >= 4 && line[lineLength-4] == ' ' && line[lineLength-3] == ' ' && line[lineLength-2] == '\r' && softLinebreak { // ends with [space][space]\r\n | |||
lineBreakFlags |= lineBreakHard | |||
} else if lineLength >= 4 && line[lineLength-4] == ' ' && line[lineLength-3] == ' ' && line[lineLength-2] == '\r' && hasNewLine { // ends with [space][space]\r\n | |||
lineLength -= 4 | |||
hardlineBreak = true | |||
lineBreakFlags |= lineBreakHard | |||
} else if hasNewLine { | |||
// If the line ends with a newline character, but it is not a hardlineBreak, then it is a softLinebreak | |||
// If the line ends with a hardlineBreak, then it cannot end with a softLinebreak | |||
// See https://spec.commonmark.org/0.30/#soft-line-breaks | |||
lineBreakFlags |= lineBreakSoft | |||
} | |||
l, startPosition := block.Position() | |||
@@ -1195,11 +1217,14 @@ func (p *parser) parseBlock(block text.BlockReader, parent ast.Node, pc Context) | |||
continue | |||
} | |||
diff := startPosition.Between(currentPosition) | |||
stop := diff.Stop | |||
rest := diff.WithStop(stop) | |||
text := ast.NewTextSegment(rest.TrimRightSpace(source)) | |||
text.SetSoftLineBreak(softLinebreak) | |||
text.SetHardLineBreak(hardlineBreak) | |||
var text *ast.Text | |||
if lineBreakFlags&(lineBreakHard|lineBreakVisible) == lineBreakHard|lineBreakVisible { | |||
text = ast.NewTextSegment(diff) | |||
} else { | |||
text = ast.NewTextSegment(diff.TrimRightSpace(source)) | |||
} | |||
text.SetSoftLineBreak(lineBreakFlags&lineBreakSoft != 0) | |||
text.SetHardLineBreak(lineBreakFlags&lineBreakHard != 0) | |||
parent.AppendChild(parent, text) | |||
block.AdvanceLine() | |||
} | |||
@@ -2,10 +2,11 @@ package parser | |||
import ( | |||
"bytes" | |||
"regexp" | |||
"github.com/yuin/goldmark/ast" | |||
"github.com/yuin/goldmark/text" | |||
"github.com/yuin/goldmark/util" | |||
"regexp" | |||
) | |||
type rawHTMLParser struct { | |||
@@ -31,43 +32,101 @@ func (s *rawHTMLParser) Parse(parent ast.Node, block text.Reader, pc Context) as | |||
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, openComment) { | |||
return s.parseComment(block, pc) | |||
} | |||
if bytes.HasPrefix(line, []byte("<?")) { | |||
return s.parseSingleLineRegexp(processingInstructionRegexp, block, pc) | |||
if bytes.HasPrefix(line, openProcessingInstruction) { | |||
return s.parseUntil(block, closeProcessingInstruction, pc) | |||
} | |||
if len(line) > 2 && line[1] == '!' && line[2] >= 'A' && line[2] <= 'Z' { | |||
return s.parseSingleLineRegexp(declRegexp, block, pc) | |||
return s.parseUntil(block, closeDecl, pc) | |||
} | |||
if bytes.HasPrefix(line, []byte("<![CDATA[")) { | |||
return s.parseMultiLineRegexp(cdataRegexp, block, pc) | |||
if bytes.HasPrefix(line, openCDATA) { | |||
return s.parseUntil(block, closeCDATA, 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 attributePattern = `(?:[\r\n \t]+[a-zA-Z_:][a-zA-Z0-9:._-]*(?:[\r\n \t]*=[\r\n \t]*(?:[^\"'=<>` + "`" + `\x00-\x20]+|'[^']*'|"[^"]*"))?)` | |||
var openTagRegexp = regexp.MustCompile("^<" + tagnamePattern + attributePattern + `*[ \t]*/?>`) | |||
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 { | |||
var openProcessingInstruction = []byte("<?") | |||
var closeProcessingInstruction = []byte("?>") | |||
var openCDATA = []byte("<![CDATA[") | |||
var closeCDATA = []byte("]]>") | |||
var closeDecl = []byte(">") | |||
var emptyComment = []byte("<!---->") | |||
var invalidComment1 = []byte("<!-->") | |||
var invalidComment2 = []byte("<!--->") | |||
var openComment = []byte("<!--") | |||
var closeComment = []byte("-->") | |||
var doubleHyphen = []byte("--") | |||
func (s *rawHTMLParser) parseComment(block text.Reader, pc Context) ast.Node { | |||
savedLine, savedSegment := block.Position() | |||
node := ast.NewRawHTML() | |||
line, segment := block.PeekLine() | |||
match := reg.FindSubmatchIndex(line) | |||
if match == nil { | |||
if bytes.HasPrefix(line, emptyComment) { | |||
node.Segments.Append(segment.WithStop(segment.Start + len(emptyComment))) | |||
block.Advance(len(emptyComment)) | |||
return node | |||
} | |||
if bytes.HasPrefix(line, invalidComment1) || bytes.HasPrefix(line, invalidComment2) { | |||
return nil | |||
} | |||
node := ast.NewRawHTML() | |||
node.Segments.Append(segment.WithStop(segment.Start + match[1])) | |||
block.Advance(match[1]) | |||
return node | |||
offset := len(openComment) | |||
line = line[offset:] | |||
for { | |||
hindex := bytes.Index(line, doubleHyphen) | |||
if hindex > -1 { | |||
hindex += offset | |||
} | |||
index := bytes.Index(line, closeComment) + offset | |||
if index > -1 && hindex == index { | |||
if index == 0 || len(line) < 2 || line[index-offset-1] != '-' { | |||
node.Segments.Append(segment.WithStop(segment.Start + index + len(closeComment))) | |||
block.Advance(index + len(closeComment)) | |||
return node | |||
} | |||
} | |||
if hindex > 0 { | |||
break | |||
} | |||
node.Segments.Append(segment) | |||
block.AdvanceLine() | |||
line, segment = block.PeekLine() | |||
offset = 0 | |||
if line == nil { | |||
break | |||
} | |||
} | |||
block.SetPosition(savedLine, savedSegment) | |||
return nil | |||
} | |||
var dummyMatch = [][]byte{} | |||
func (s *rawHTMLParser) parseUntil(block text.Reader, closer []byte, pc Context) ast.Node { | |||
savedLine, savedSegment := block.Position() | |||
node := ast.NewRawHTML() | |||
for { | |||
line, segment := block.PeekLine() | |||
if line == nil { | |||
break | |||
} | |||
index := bytes.Index(line, closer) | |||
if index > -1 { | |||
node.Segments.Append(segment.WithStop(segment.Start + index + len(closer))) | |||
block.Advance(index + len(closer)) | |||
return node | |||
} | |||
node.Segments.Append(segment) | |||
block.AdvanceLine() | |||
} | |||
block.SetPosition(savedLine, savedSegment) | |||
return nil | |||
} | |||
func (s *rawHTMLParser) parseMultiLineRegexp(reg *regexp.Regexp, block text.Reader, pc Context) ast.Node { | |||
sline, ssegment := block.Position() | |||
@@ -102,7 +161,3 @@ func (s *rawHTMLParser) parseMultiLineRegexp(reg *regexp.Regexp, block text.Read | |||
} | |||
return nil | |||
} | |||
func (s *rawHTMLParser) CloseBlock(parent ast.Node, pc Context) { | |||
// nothing to do | |||
} |
@@ -198,16 +198,24 @@ func (r *Renderer) writeLines(w util.BufWriter, source []byte, n ast.Node) { | |||
var GlobalAttributeFilter = util.NewBytesFilter( | |||
[]byte("accesskey"), | |||
[]byte("autocapitalize"), | |||
[]byte("autofocus"), | |||
[]byte("class"), | |||
[]byte("contenteditable"), | |||
[]byte("contextmenu"), | |||
[]byte("dir"), | |||
[]byte("draggable"), | |||
[]byte("dropzone"), | |||
[]byte("enterkeyhint"), | |||
[]byte("hidden"), | |||
[]byte("id"), | |||
[]byte("inert"), | |||
[]byte("inputmode"), | |||
[]byte("is"), | |||
[]byte("itemid"), | |||
[]byte("itemprop"), | |||
[]byte("itemref"), | |||
[]byte("itemscope"), | |||
[]byte("itemtype"), | |||
[]byte("lang"), | |||
[]byte("part"), | |||
[]byte("slot"), | |||
[]byte("spellcheck"), | |||
[]byte("style"), | |||
@@ -296,7 +304,7 @@ func (r *Renderer) renderHTMLBlock(w util.BufWriter, source []byte, node ast.Nod | |||
l := n.Lines().Len() | |||
for i := 0; i < l; i++ { | |||
line := n.Lines().At(i) | |||
_, _ = w.Write(line.Value(source)) | |||
r.Writer.SecureWrite(w, line.Value(source)) | |||
} | |||
} else { | |||
_, _ = w.WriteString("<!-- raw HTML omitted -->\n") | |||
@@ -305,7 +313,7 @@ func (r *Renderer) renderHTMLBlock(w util.BufWriter, source []byte, node ast.Nod | |||
if n.HasClosure() { | |||
if r.Unsafe { | |||
closure := n.ClosureLine | |||
_, _ = w.Write(closure.Value(source)) | |||
r.Writer.SecureWrite(w, closure.Value(source)) | |||
} else { | |||
_, _ = w.WriteString("<!-- raw HTML omitted -->\n") | |||
} | |||
@@ -318,6 +326,7 @@ func (r *Renderer) renderHTMLBlock(w util.BufWriter, source []byte, node ast.Nod | |||
var ListAttributeFilter = GlobalAttributeFilter.Extend( | |||
[]byte("start"), | |||
[]byte("reversed"), | |||
[]byte("type"), | |||
) | |||
func (r *Renderer) renderList(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) { | |||
@@ -476,9 +485,7 @@ func (r *Renderer) renderCodeSpan(w util.BufWriter, source []byte, n ast.Node, e | |||
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(" ")) | |||
} | |||
r.Writer.RawWrite(w, []byte(" ")) | |||
} else { | |||
r.Writer.RawWrite(w, value) | |||
} | |||
@@ -564,7 +571,7 @@ func (r *Renderer) renderImage(w util.BufWriter, source []byte, node ast.Node, e | |||
_, _ = w.Write(util.EscapeHTML(util.URLEscape(n.Destination, true))) | |||
} | |||
_, _ = w.WriteString(`" alt="`) | |||
_, _ = w.Write(n.Text(source)) | |||
_, _ = w.Write(util.EscapeHTML(n.Text(source))) | |||
_ = w.WriteByte('"') | |||
if n.Title != nil { | |||
_, _ = w.WriteString(` title="`) | |||
@@ -669,8 +676,13 @@ type Writer interface { | |||
// RawWrite writes the given source to writer without resolving references and | |||
// unescaping backslash escaped characters. | |||
RawWrite(writer util.BufWriter, source []byte) | |||
// SecureWrite writes the given source to writer with replacing insecure characters. | |||
SecureWrite(writer util.BufWriter, source []byte) | |||
} | |||
var replacementCharacter = []byte("\ufffd") | |||
type defaultWriter struct { | |||
} | |||
@@ -685,6 +697,23 @@ func escapeRune(writer util.BufWriter, r rune) { | |||
_, _ = writer.WriteRune(util.ToValidRune(r)) | |||
} | |||
func (d *defaultWriter) SecureWrite(writer util.BufWriter, source []byte) { | |||
n := 0 | |||
l := len(source) | |||
for i := 0; i < l; i++ { | |||
if source[i] == '\u0000' { | |||
_, _ = writer.Write(source[i-n : i]) | |||
n = 0 | |||
_, _ = writer.Write(replacementCharacter) | |||
continue | |||
} | |||
n++ | |||
} | |||
if n != 0 { | |||
_, _ = writer.Write(source[l-n:]) | |||
} | |||
} | |||
func (d *defaultWriter) RawWrite(writer util.BufWriter, source []byte) { | |||
n := 0 | |||
l := len(source) | |||
@@ -718,6 +747,13 @@ func (d *defaultWriter) Write(writer util.BufWriter, source []byte) { | |||
continue | |||
} | |||
} | |||
if c == '\x00' { | |||
d.RawWrite(writer, source[n:i]) | |||
d.RawWrite(writer, replacementCharacter) | |||
n = i + 1 | |||
escaped = false | |||
continue | |||
} | |||
if c == '&' { | |||
pos := i | |||
next := i + 1 | |||
@@ -729,7 +765,7 @@ func (d *defaultWriter) Write(writer util.BufWriter, source []byte) { | |||
if nnext < limit && nc == 'x' || nc == 'X' { | |||
start := nnext + 1 | |||
i, ok = util.ReadWhile(source, [2]int{start, limit}, util.IsHexDecimal) | |||
if ok && i < limit && source[i] == ';' { | |||
if ok && i < limit && source[i] == ';' && i-start < 7 { | |||
v, _ := strconv.ParseUint(util.BytesToReadOnlyString(source[start:i]), 16, 32) | |||
d.RawWrite(writer, source[n:pos]) | |||
n = i + 1 | |||
@@ -741,7 +777,7 @@ func (d *defaultWriter) Write(writer util.BufWriter, source []byte) { | |||
start := nnext | |||
i, ok = util.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) | |||
v, _ := strconv.ParseUint(util.BytesToReadOnlyString(source[start:i]), 10, 32) | |||
d.RawWrite(writer, source[n:pos]) | |||
n = i + 1 | |||
escapeRune(writer, rune(v)) | |||
@@ -783,6 +819,7 @@ var bPng = []byte("png;") | |||
var bGif = []byte("gif;") | |||
var bJpeg = []byte("jpeg;") | |||
var bWebp = []byte("webp;") | |||
var bSvg = []byte("svg;") | |||
var bJs = []byte("javascript:") | |||
var bVb = []byte("vbscript:") | |||
var bFile = []byte("file:") | |||
@@ -794,7 +831,8 @@ func IsDangerousURL(url []byte) bool { | |||
if bytes.HasPrefix(url, bDataImage) && len(url) >= 11 { | |||
v := url[11:] | |||
if bytes.HasPrefix(v, bPng) || bytes.HasPrefix(v, bGif) || | |||
bytes.HasPrefix(v, bJpeg) || bytes.HasPrefix(v, bWebp) { | |||
bytes.HasPrefix(v, bJpeg) || bytes.HasPrefix(v, bWebp) || | |||
bytes.HasPrefix(v, bSvg) { | |||
return false | |||
} | |||
return true | |||
@@ -70,6 +70,28 @@ type Reader interface { | |||
// Match performs regular expression searching to current line. | |||
FindSubMatch(reg *regexp.Regexp) [][]byte | |||
// FindClosure finds corresponding closure. | |||
FindClosure(opener, closer byte, options FindClosureOptions) (*Segments, bool) | |||
} | |||
// FindClosureOptions is options for Reader.FindClosure | |||
type FindClosureOptions struct { | |||
// CodeSpan is a flag for the FindClosure. If this is set to true, | |||
// FindClosure ignores closers in codespans. | |||
CodeSpan bool | |||
// Nesting is a flag for the FindClosure. If this is set to true, | |||
// FindClosure allows nesting. | |||
Nesting bool | |||
// Newline is a flag for the FindClosure. If this is set to true, | |||
// FindClosure searches for a closer over multiple lines. | |||
Newline bool | |||
// Advance is a flag for the FindClosure. If this is set to true, | |||
// FindClosure advances pointers when closer is found. | |||
Advance bool | |||
} | |||
type reader struct { | |||
@@ -92,6 +114,10 @@ func NewReader(source []byte) Reader { | |||
return r | |||
} | |||
func (r *reader) FindClosure(opener, closer byte, options FindClosureOptions) (*Segments, bool) { | |||
return findClosureReader(r, opener, closer, options) | |||
} | |||
func (r *reader) ResetPosition() { | |||
r.line = -1 | |||
r.head = 0 | |||
@@ -272,6 +298,10 @@ func NewBlockReader(source []byte, segments *Segments) BlockReader { | |||
return r | |||
} | |||
func (r *blockReader) FindClosure(opener, closer byte, options FindClosureOptions) (*Segments, bool) { | |||
return findClosureReader(r, opener, closer, options) | |||
} | |||
func (r *blockReader) ResetPosition() { | |||
r.line = -1 | |||
r.head = 0 | |||
@@ -541,3 +571,83 @@ func readRuneReader(r Reader) (rune, int, error) { | |||
r.Advance(size) | |||
return rn, size, nil | |||
} | |||
func findClosureReader(r Reader, opener, closer byte, opts FindClosureOptions) (*Segments, bool) { | |||
opened := 1 | |||
codeSpanOpener := 0 | |||
closed := false | |||
orgline, orgpos := r.Position() | |||
var ret *Segments | |||
for { | |||
bs, seg := r.PeekLine() | |||
if bs == nil { | |||
goto end | |||
} | |||
i := 0 | |||
for i < len(bs) { | |||
c := bs[i] | |||
if opts.CodeSpan && codeSpanOpener != 0 && c == '`' { | |||
codeSpanCloser := 0 | |||
for ; i < len(bs); i++ { | |||
if bs[i] == '`' { | |||
codeSpanCloser++ | |||
} else { | |||
i-- | |||
break | |||
} | |||
} | |||
if codeSpanCloser == codeSpanOpener { | |||
codeSpanOpener = 0 | |||
} | |||
} else if codeSpanOpener == 0 && c == '\\' && i < len(bs)-1 && util.IsPunct(bs[i+1]) { | |||
i += 2 | |||
continue | |||
} else if opts.CodeSpan && codeSpanOpener == 0 && c == '`' { | |||
for ; i < len(bs); i++ { | |||
if bs[i] == '`' { | |||
codeSpanOpener++ | |||
} else { | |||
i-- | |||
break | |||
} | |||
} | |||
} else if (opts.CodeSpan && codeSpanOpener == 0) || !opts.CodeSpan { | |||
if c == closer { | |||
opened-- | |||
if opened == 0 { | |||
if ret == nil { | |||
ret = NewSegments() | |||
} | |||
ret.Append(seg.WithStop(seg.Start + i)) | |||
r.Advance(i + 1) | |||
closed = true | |||
goto end | |||
} | |||
} else if c == opener { | |||
if !opts.Nesting { | |||
goto end | |||
} | |||
opened++ | |||
} | |||
} | |||
i++ | |||
} | |||
if !opts.Newline { | |||
goto end | |||
} | |||
r.AdvanceLine() | |||
if ret == nil { | |||
ret = NewSegments() | |||
} | |||
ret.Append(seg) | |||
} | |||
end: | |||
if !opts.Advance { | |||
r.SetPosition(orgline, orgpos) | |||
} | |||
if closed { | |||
return ret, true | |||
} | |||
return nil, false | |||
} |
@@ -8,6 +8,7 @@ import ( | |||
"regexp" | |||
"sort" | |||
"strconv" | |||
"unicode" | |||
"unicode/utf8" | |||
) | |||
@@ -27,6 +28,7 @@ func NewCopyOnWriteBuffer(buffer []byte) CopyOnWriteBuffer { | |||
} | |||
// Write writes given bytes to the buffer. | |||
// Write allocate new buffer and clears it at the first time. | |||
func (b *CopyOnWriteBuffer) Write(value []byte) { | |||
if !b.copied { | |||
b.buffer = make([]byte, 0, len(b.buffer)+20) | |||
@@ -35,7 +37,32 @@ func (b *CopyOnWriteBuffer) Write(value []byte) { | |||
b.buffer = append(b.buffer, value...) | |||
} | |||
// WriteString writes given string to the buffer. | |||
// WriteString allocate new buffer and clears it at the first time. | |||
func (b *CopyOnWriteBuffer) WriteString(value string) { | |||
b.Write(StringToReadOnlyBytes(value)) | |||
} | |||
// Append appends given bytes to the buffer. | |||
// Append copy buffer at the first time. | |||
func (b *CopyOnWriteBuffer) Append(value []byte) { | |||
if !b.copied { | |||
tmp := make([]byte, len(b.buffer), len(b.buffer)+20) | |||
copy(tmp, b.buffer) | |||
b.buffer = tmp | |||
b.copied = true | |||
} | |||
b.buffer = append(b.buffer, value...) | |||
} | |||
// AppendString appends given string to the buffer. | |||
// AppendString copy buffer at the first time. | |||
func (b *CopyOnWriteBuffer) AppendString(value string) { | |||
b.Append(StringToReadOnlyBytes(value)) | |||
} | |||
// WriteByte writes the given byte to the buffer. | |||
// WriteByte allocate new buffer and clears it at the first time. | |||
func (b *CopyOnWriteBuffer) WriteByte(c byte) { | |||
if !b.copied { | |||
b.buffer = make([]byte, 0, len(b.buffer)+20) | |||
@@ -44,6 +71,18 @@ func (b *CopyOnWriteBuffer) WriteByte(c byte) { | |||
b.buffer = append(b.buffer, c) | |||
} | |||
// AppendByte appends given bytes to the buffer. | |||
// AppendByte copy buffer at the first time. | |||
func (b *CopyOnWriteBuffer) AppendByte(c byte) { | |||
if !b.copied { | |||
tmp := make([]byte, len(b.buffer), len(b.buffer)+20) | |||
copy(tmp, b.buffer) | |||
b.buffer = tmp | |||
b.copied = true | |||
} | |||
b.buffer = append(b.buffer, c) | |||
} | |||
// Bytes returns bytes of this buffer. | |||
func (b *CopyOnWriteBuffer) Bytes() []byte { | |||
return b.buffer | |||
@@ -91,6 +130,9 @@ func VisualizeSpaces(bs []byte) []byte { | |||
bs = bytes.Replace(bs, []byte("\t"), []byte("[TAB]"), -1) | |||
bs = bytes.Replace(bs, []byte("\n"), []byte("[NEWLINE]\n"), -1) | |||
bs = bytes.Replace(bs, []byte("\r"), []byte("[CR]"), -1) | |||
bs = bytes.Replace(bs, []byte("\v"), []byte("[VTAB]"), -1) | |||
bs = bytes.Replace(bs, []byte("\x00"), []byte("[NUL]"), -1) | |||
bs = bytes.Replace(bs, []byte("\ufffd"), []byte("[U+FFFD]"), -1) | |||
return bs | |||
} | |||
@@ -110,30 +152,7 @@ func TabWidth(currentPos int) int { | |||
// 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) { | |||
if width == 0 { | |||
return 0, 0 | |||
} | |||
w := 0 | |||
l := len(bs) | |||
i := 0 | |||
hasTab := false | |||
for ; i < l; i++ { | |||
if bs[i] == '\t' { | |||
w += TabWidth(currentPos + w) | |||
hasTab = true | |||
} else if bs[i] == ' ' { | |||
w++ | |||
} else { | |||
break | |||
} | |||
} | |||
if w >= width { | |||
if !hasTab { | |||
return width, 0 | |||
} | |||
return i, w - width | |||
} | |||
return -1, -1 | |||
return IndentPositionPadding(bs, currentPos, 0, width) | |||
} | |||
// IndentPositionPadding searches an indent position with the given width for the given line. | |||
@@ -147,9 +166,9 @@ func IndentPositionPadding(bs []byte, currentPos, paddingv, width int) (pos, pad | |||
i := 0 | |||
l := len(bs) | |||
for ; i < l; i++ { | |||
if bs[i] == '\t' { | |||
if bs[i] == '\t' && w < width { | |||
w += TabWidth(currentPos + w) | |||
} else if bs[i] == ' ' { | |||
} else if bs[i] == ' ' && w < width { | |||
w++ | |||
} else { | |||
break | |||
@@ -162,52 +181,56 @@ func IndentPositionPadding(bs []byte, currentPos, paddingv, width int) (pos, pad | |||
} | |||
// DedentPosition dedents lines by the given width. | |||
// | |||
// Deprecated: This function has bugs. Use util.IndentPositionPadding and util.FirstNonSpacePosition. | |||
func DedentPosition(bs []byte, currentPos, width int) (pos, padding int) { | |||
if width == 0 { | |||
return 0, 0 | |||
} | |||
w := 0 | |||
l := len(bs) | |||
i := 0 | |||
for ; i < l; i++ { | |||
if bs[i] == '\t' { | |||
w += TabWidth(currentPos + w) | |||
} else if bs[i] == ' ' { | |||
w++ | |||
} else { | |||
break | |||
} | |||
} | |||
if w >= width { | |||
return i, w - width | |||
} | |||
return i, 0 | |||
if width == 0 { | |||
return 0, 0 | |||
} | |||
w := 0 | |||
l := len(bs) | |||
i := 0 | |||
for ; i < l; i++ { | |||
if bs[i] == '\t' { | |||
w += TabWidth(currentPos + w) | |||
} else if bs[i] == ' ' { | |||
w++ | |||
} else { | |||
break | |||
} | |||
} | |||
if w >= width { | |||
return i, w - width | |||
} | |||
return i, 0 | |||
} | |||
// DedentPositionPadding dedents lines by the given width. | |||
// This function is mostly same as DedentPosition except this function | |||
// takes account into additional paddings. | |||
// | |||
// Deprecated: This function has bugs. Use util.IndentPositionPadding and util.FirstNonSpacePosition. | |||
func DedentPositionPadding(bs []byte, currentPos, paddingv, width int) (pos, padding int) { | |||
if width == 0 { | |||
return 0, paddingv | |||
} | |||
w := 0 | |||
i := 0 | |||
l := len(bs) | |||
for ; i < l; i++ { | |||
if bs[i] == '\t' { | |||
w += TabWidth(currentPos + w) | |||
} else if bs[i] == ' ' { | |||
w++ | |||
} else { | |||
break | |||
} | |||
} | |||
if w >= width { | |||
return i - paddingv, w - width | |||
} | |||
return i - paddingv, 0 | |||
if width == 0 { | |||
return 0, paddingv | |||
} | |||
w := 0 | |||
i := 0 | |||
l := len(bs) | |||
for ; i < l; i++ { | |||
if bs[i] == '\t' { | |||
w += TabWidth(currentPos + w) | |||
} else if bs[i] == ' ' { | |||
w++ | |||
} else { | |||
break | |||
} | |||
} | |||
if w >= width { | |||
return i - paddingv, w - width | |||
} | |||
return i - paddingv, 0 | |||
} | |||
// IndentWidth calculate an indent width for the given line. | |||
@@ -249,6 +272,10 @@ func FirstNonSpacePosition(bs []byte) int { | |||
// If codeSpan is set true, it ignores characters in code spans. | |||
// If allowNesting is set true, closures correspond to nested opener will be | |||
// ignored. | |||
// | |||
// Deprecated: This function can not handle newlines. Many elements | |||
// can be existed over multiple lines(e.g. link labels). | |||
// Use text.Reader.FindClosure. | |||
func FindClosure(bs []byte, opener, closure byte, codeSpan, allowNesting bool) int { | |||
i := 0 | |||
opened := 1 | |||
@@ -668,7 +695,7 @@ func URLEscape(v []byte, resolveReference bool) []byte { | |||
n = i | |||
continue | |||
} | |||
if int(u8len) >= len(v) { | |||
if int(u8len) > len(v) { | |||
u8len = int8(len(v) - 1) | |||
} | |||
if u8len == 0 { | |||
@@ -754,7 +781,7 @@ func FindEmailIndex(b []byte) int { | |||
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 spaceTable = [256]int8{0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 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} | |||
@@ -777,11 +804,21 @@ func IsPunct(c byte) bool { | |||
return punctTable[c] == 1 | |||
} | |||
// IsPunctRune returns true if the given rune is a punctuation, otherwise false. | |||
func IsPunctRune(r rune) bool { | |||
return int32(r) <= 256 && IsPunct(byte(r)) || unicode.IsPunct(r) | |||
} | |||
// IsSpace returns true if the given character is a space, otherwise false. | |||
func IsSpace(c byte) bool { | |||
return spaceTable[c] == 1 | |||
} | |||
// IsSpaceRune returns true if the given rune is a space, otherwise false. | |||
func IsSpaceRune(r rune) bool { | |||
return int32(r) <= 256 && IsSpace(byte(r)) || unicode.IsSpace(r) | |||
} | |||
// IsNumeric returns true if the given character is a numeric, otherwise false. | |||
func IsNumeric(c byte) bool { | |||
return c >= '0' && c <= '9' | |||
@@ -13,8 +13,11 @@ func BytesToReadOnlyString(b []byte) string { | |||
} | |||
// StringToReadOnlyBytes returns bytes converted from given string. | |||
func StringToReadOnlyBytes(s string) []byte { | |||
func StringToReadOnlyBytes(s string) (bs []byte) { | |||
sh := (*reflect.StringHeader)(unsafe.Pointer(&s)) | |||
bh := reflect.SliceHeader{Data: sh.Data, Len: sh.Len, Cap: sh.Len} | |||
return *(*[]byte)(unsafe.Pointer(&bh)) | |||
bh := (*reflect.SliceHeader)(unsafe.Pointer(&bs)) | |||
bh.Data = sh.Data | |||
bh.Cap = sh.Len | |||
bh.Len = sh.Len | |||
return | |||
} |
@@ -853,7 +853,7 @@ github.com/xdg/stringprep | |||
# github.com/yohcop/openid-go v1.0.0 | |||
## explicit | |||
github.com/yohcop/openid-go | |||
# github.com/yuin/goldmark v1.1.30 | |||
# github.com/yuin/goldmark v1.4.13 | |||
## explicit | |||
github.com/yuin/goldmark | |||
github.com/yuin/goldmark/ast | |||