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/urfave/cli v1.22.1 | ||||
github.com/xanzy/go-gitlab v0.31.0 | github.com/xanzy/go-gitlab v0.31.0 | ||||
github.com/yohcop/openid-go v1.0.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 | github.com/yuin/goldmark-meta v0.0.0-20191126180153-f0638e958b60 | ||||
golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37 | golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37 | ||||
golang.org/x/mod v0.3.0 // indirect | 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.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= | ||||
github.com/yuin/goldmark v1.1.30 h1:j4d4Lw3zqZelDhBksEo3BnWg9xhXRQGJPPSL6OApZjI= | 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.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 h1:gZucqLjL1eDzVWrXj4uiWeMbAopJlBR2mKQAsTGdPwo= | ||||
github.com/yuin/goldmark-meta v0.0.0-20191126180153-f0638e958b60/go.mod h1:i9VhcIHN2PxXMbQrKqXNueok6QNONoPjNMoj9MygVL0= | github.com/yuin/goldmark-meta v0.0.0-20191126180153-f0638e958b60/go.mod h1:i9VhcIHN2PxXMbQrKqXNueok6QNONoPjNMoj9MygVL0= | ||||
github.com/ziutek/mymysql v1.5.4 h1:GB0qdRGsTwQSBVYuVShFBKaXSnSnYYC2d9knnE1LHFs= | github.com/ziutek/mymysql v1.5.4 h1:GB0qdRGsTwQSBVYuVShFBKaXSnSnYYC2d9knnE1LHFs= | ||||
@@ -12,5 +12,5 @@ fuzz: | |||||
rm -rf ./fuzz/crashers | rm -rf ./fuzz/crashers | ||||
rm -rf ./fuzz/suppressions | rm -rf ./fuzz/suppressions | ||||
rm -f ./fuzz/fuzz-fuzz.zip | rm -f ./fuzz/fuzz-fuzz.zip | ||||
cd ./fuzz && go-fuzz-build | |||||
cd ./fuzz && GO111MODULE=off go-fuzz-build | |||||
cd ./fuzz && go-fuzz | cd ./fuzz && go-fuzz |
@@ -1,14 +1,14 @@ | |||||
goldmark | 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://github.com/yuin/goldmark/actions?query=workflow:test) | ||||
[](https://coveralls.io/github/yuin/goldmark) | [](https://coveralls.io/github/yuin/goldmark) | ||||
[](https://goreportcard.com/report/github.com/yuin/goldmark) | [](https://goreportcard.com/report/github.com/yuin/goldmark) | ||||
> A Markdown parser written in Go. Easy to extend, standards-compliant, well-structured. | > 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 | Motivation | ||||
---------------------- | ---------------------- | ||||
@@ -173,6 +173,7 @@ Parser and Renderer options | |||||
- This extension enables Table, Strikethrough, Linkify and TaskList. | - 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-). | - 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 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` | - `extension.DefinitionList` | ||||
- [PHP Markdown Extra: Definition lists](https://michelf.ca/projects/php-markdown/extra/#def-list) | - [PHP Markdown Extra: Definition lists](https://michelf.ca/projects/php-markdown/extra/#def-list) | ||||
- `extension.Footnote` | - `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 | ### Typographer extension | ||||
The Typographer extension translates plain ASCII punctuation characters into typographic-punctuation HTML entities. | 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 | ```go | ||||
markdown := goldmark.New( | markdown := goldmark.New( | ||||
@@ -267,13 +280,96 @@ markdown := goldmark.New( | |||||
[]byte("https:"), | []byte("https:"), | ||||
}), | }), | ||||
extension.WithLinkifyURLRegexp( | 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 | Security | ||||
-------------------- | -------------------- | ||||
By default, goldmark does not render raw HTML or potentially-dangerous URLs. | 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 | goldmark, meanwhile, builds a clean, extensible AST structure, achieves full compliance with | ||||
CommonMark, and consumes less memory, all while being reasonably fast. | 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) | ### against cmark (CommonMark reference implementation written in C) | ||||
- MBP 2019 13″(i5, 16GB), Go1.17 | |||||
``` | ``` | ||||
----------- cmark ----------- | ----------- cmark ----------- | ||||
file: _data.md | file: _data.md | ||||
iteration: 50 | iteration: 50 | ||||
average: 0.0037760639 sec | |||||
go run ./goldmark_benchmark.go | |||||
average: 0.0044073057 sec | |||||
------- goldmark ------- | ------- goldmark ------- | ||||
file: _data.md | file: _data.md | ||||
iteration: 50 | iteration: 50 | ||||
average: 0.0040964230 sec | |||||
average: 0.0041611990 sec | |||||
``` | ``` | ||||
As you can see, goldmark's performance is on par with cmark's. | As you can see, goldmark's performance is on par with cmark's. | ||||
@@ -324,7 +421,17 @@ Extensions | |||||
extension for the goldmark Markdown parser. | extension for the goldmark Markdown parser. | ||||
- [goldmark-highlighting](https://github.com/yuin/goldmark-highlighting): A syntax-highlighting extension | - [goldmark-highlighting](https://github.com/yuin/goldmark-highlighting): A syntax-highlighting extension | ||||
for the goldmark markdown parser. | 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-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) | goldmark internal(for extension developers) | ||||
---------------------------------------------- | ---------------------------------------------- | ||||
@@ -45,11 +45,6 @@ type Attribute struct { | |||||
Value interface{} | Value interface{} | ||||
} | } | ||||
var attrNameIDS = []byte("#") | |||||
var attrNameID = []byte("id") | |||||
var attrNameClassS = []byte(".") | |||||
var attrNameClass = []byte("class") | |||||
// A Node interface defines basic AST node functionalities. | // A Node interface defines basic AST node functionalities. | ||||
type Node interface { | type Node interface { | ||||
// Type returns a type of this node. | // Type returns a type of this node. | ||||
@@ -116,6 +111,11 @@ type Node interface { | |||||
// tail of the children. | // tail of the children. | ||||
InsertAfter(self, v1, insertee Node) | 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. | // Dump dumps an AST tree structure to stdout. | ||||
// This function completely aimed for debugging. | // This function completely aimed for debugging. | ||||
// level is a indent level. Implementer should indent informations with | // level is a indent level. Implementer should indent informations with | ||||
@@ -169,7 +169,7 @@ type Node interface { | |||||
RemoveAttributes() | RemoveAttributes() | ||||
} | } | ||||
// A BaseNode struct implements the Node interface. | |||||
// A BaseNode struct implements the Node interface partialliy. | |||||
type BaseNode struct { | type BaseNode struct { | ||||
firstChild Node | firstChild Node | ||||
lastChild 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 . | // Text implements Node.Text . | ||||
func (n *BaseNode) Text(source []byte) []byte { | func (n *BaseNode) Text(source []byte) []byte { | ||||
var buf bytes.Buffer | var buf bytes.Buffer | ||||
@@ -7,7 +7,7 @@ import ( | |||||
textm "github.com/yuin/goldmark/text" | textm "github.com/yuin/goldmark/text" | ||||
) | ) | ||||
// A BaseBlock struct implements the Node interface. | |||||
// A BaseBlock struct implements the Node interface partialliy. | |||||
type BaseBlock struct { | type BaseBlock struct { | ||||
BaseNode | BaseNode | ||||
blankPreviousLines bool | blankPreviousLines bool | ||||
@@ -50,6 +50,8 @@ func (b *BaseBlock) SetLines(v *textm.Segments) { | |||||
// A Document struct is a root node of Markdown text. | // A Document struct is a root node of Markdown text. | ||||
type Document struct { | type Document struct { | ||||
BaseBlock | BaseBlock | ||||
meta map[string]interface{} | |||||
} | } | ||||
// KindDocument is a NodeKind of the Document node. | // KindDocument is a NodeKind of the Document node. | ||||
@@ -70,10 +72,42 @@ func (n *Document) Kind() NodeKind { | |||||
return KindDocument | 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. | // NewDocument returns a new Document node. | ||||
func NewDocument() *Document { | func NewDocument() *Document { | ||||
return &Document{ | return &Document{ | ||||
BaseBlock: BaseBlock{}, | BaseBlock: BaseBlock{}, | ||||
meta: nil, | |||||
} | } | ||||
} | } | ||||
@@ -311,7 +345,7 @@ type List struct { | |||||
Marker byte | Marker byte | ||||
// IsTight is a true if this list is a 'tight' list. | // 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 | IsTight bool | ||||
// Start is an initial number of this ordered list. | // 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. | // 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 | type HTMLBlockType int | ||||
const ( | const ( | ||||
@@ -8,7 +8,7 @@ import ( | |||||
"github.com/yuin/goldmark/util" | "github.com/yuin/goldmark/util" | ||||
) | ) | ||||
// A BaseInline struct implements the Node interface. | |||||
// A BaseInline struct implements the Node interface partialliy. | |||||
type BaseInline struct { | type BaseInline struct { | ||||
BaseNode | BaseNode | ||||
} | } | ||||
@@ -111,7 +111,7 @@ func (n *Text) SetRaw(v bool) { | |||||
} | } | ||||
// HardLineBreak returns true if this node ends with a hard line break. | // 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 { | func (n *Text) HardLineBreak() bool { | ||||
return n.flags&textHardLineBreak != 0 | return n.flags&textHardLineBreak != 0 | ||||
} | } | ||||
@@ -2,6 +2,7 @@ package ast | |||||
import ( | import ( | ||||
"fmt" | "fmt" | ||||
gast "github.com/yuin/goldmark/ast" | gast "github.com/yuin/goldmark/ast" | ||||
) | ) | ||||
@@ -9,13 +10,17 @@ import ( | |||||
// (PHP Markdown Extra) text. | // (PHP Markdown Extra) text. | ||||
type FootnoteLink struct { | type FootnoteLink struct { | ||||
gast.BaseInline | gast.BaseInline | ||||
Index int | |||||
Index int | |||||
RefCount int | |||||
RefIndex int | |||||
} | } | ||||
// Dump implements Node.Dump. | // Dump implements Node.Dump. | ||||
func (n *FootnoteLink) Dump(source []byte, level int) { | func (n *FootnoteLink) Dump(source []byte, level int) { | ||||
m := map[string]string{} | m := map[string]string{} | ||||
m["Index"] = fmt.Sprintf("%v", n.Index) | 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) | gast.DumpHelper(n, source, level, m, nil) | ||||
} | } | ||||
@@ -30,36 +35,44 @@ func (n *FootnoteLink) Kind() gast.NodeKind { | |||||
// NewFootnoteLink returns a new FootnoteLink node. | // NewFootnoteLink returns a new FootnoteLink node. | ||||
func NewFootnoteLink(index int) *FootnoteLink { | func NewFootnoteLink(index int) *FootnoteLink { | ||||
return &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. | // (PHP Markdown Extra) text. | ||||
type FootnoteBackLink struct { | |||||
type FootnoteBacklink struct { | |||||
gast.BaseInline | gast.BaseInline | ||||
Index int | |||||
Index int | |||||
RefCount int | |||||
RefIndex int | |||||
} | } | ||||
// Dump implements Node.Dump. | // 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 := map[string]string{} | ||||
m["Index"] = fmt.Sprintf("%v", n.Index) | 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) | 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. | // 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) | para.Parent().RemoveChild(para.Parent(), para) | ||||
} | } | ||||
cpos, padding := util.IndentPosition(line[pos+1:], pos+1, list.Offset-pos-1) | 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 | return ast.NewDefinitionDescription(), parser.HasChildren | ||||
} | } | ||||
@@ -2,6 +2,9 @@ package extension | |||||
import ( | import ( | ||||
"bytes" | "bytes" | ||||
"fmt" | |||||
"strconv" | |||||
"github.com/yuin/goldmark" | "github.com/yuin/goldmark" | ||||
gast "github.com/yuin/goldmark/ast" | gast "github.com/yuin/goldmark/ast" | ||||
"github.com/yuin/goldmark/extension/ast" | "github.com/yuin/goldmark/extension/ast" | ||||
@@ -10,10 +13,10 @@ import ( | |||||
"github.com/yuin/goldmark/renderer/html" | "github.com/yuin/goldmark/renderer/html" | ||||
"github.com/yuin/goldmark/text" | "github.com/yuin/goldmark/text" | ||||
"github.com/yuin/goldmark/util" | "github.com/yuin/goldmark/util" | ||||
"strconv" | |||||
) | ) | ||||
var footnoteListKey = parser.NewContextKey() | var footnoteListKey = parser.NewContextKey() | ||||
var footnoteLinkListKey = parser.NewContextKey() | |||||
type footnoteBlockParser struct { | type footnoteBlockParser struct { | ||||
} | } | ||||
@@ -164,7 +167,20 @@ func (s *footnoteParser) Parse(parent gast.Node, block text.Reader, pc parser.Co | |||||
return nil | 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 { | type footnoteASTTransformer struct { | ||||
@@ -180,23 +196,62 @@ func NewFootnoteASTTransformer() parser.ASTTransformer { | |||||
func (a *footnoteASTTransformer) Transform(node *gast.Document, reader text.Reader, pc parser.Context) { | func (a *footnoteASTTransformer) Transform(node *gast.Document, reader text.Reader, pc parser.Context) { | ||||
var list *ast.FootnoteList | 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(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; { | for footnote := list.FirstChild(); footnote != nil; { | ||||
var container gast.Node = footnote | var container gast.Node = footnote | ||||
next := footnote.NextSibling() | next := footnote.NextSibling() | ||||
if fc := container.LastChild(); fc != nil && gast.IsParagraph(fc) { | if fc := container.LastChild(); fc != nil && gast.IsParagraph(fc) { | ||||
container = fc | container = fc | ||||
} | } | ||||
index := footnote.(*ast.Footnote).Index | |||||
fn := footnote.(*ast.Footnote) | |||||
index := fn.Index | |||||
if index < 0 { | if index < 0 { | ||||
list.RemoveChild(list, footnote) | list.RemoveChild(list, footnote) | ||||
} else { | } 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 | footnote = next | ||||
} | } | ||||
@@ -214,19 +269,250 @@ func (a *footnoteASTTransformer) Transform(node *gast.Document, reader text.Read | |||||
node.AppendChild(node, list) | 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 | // FootnoteHTMLRenderer is a renderer.NodeRenderer implementation that | ||||
// renders FootnoteLink nodes. | // renders FootnoteLink nodes. | ||||
type FootnoteHTMLRenderer struct { | type FootnoteHTMLRenderer struct { | ||||
html.Config | |||||
FootnoteConfig | |||||
} | } | ||||
// NewFootnoteHTMLRenderer returns a new FootnoteHTMLRenderer. | // NewFootnoteHTMLRenderer returns a new FootnoteHTMLRenderer. | ||||
func NewFootnoteHTMLRenderer(opts ...html.Option) renderer.NodeRenderer { | |||||
func NewFootnoteHTMLRenderer(opts ...FootnoteOption) renderer.NodeRenderer { | |||||
r := &FootnoteHTMLRenderer{ | r := &FootnoteHTMLRenderer{ | ||||
Config: html.NewConfig(), | |||||
FootnoteConfig: NewFootnoteConfig(), | |||||
} | } | ||||
for _, opt := range opts { | for _, opt := range opts { | ||||
opt.SetHTMLOption(&r.Config) | |||||
opt.SetFootnoteOption(&r.FootnoteConfig) | |||||
} | } | ||||
return r | return r | ||||
} | } | ||||
@@ -234,7 +520,7 @@ func NewFootnoteHTMLRenderer(opts ...html.Option) renderer.NodeRenderer { | |||||
// RegisterFuncs implements renderer.NodeRenderer.RegisterFuncs. | // RegisterFuncs implements renderer.NodeRenderer.RegisterFuncs. | ||||
func (r *FootnoteHTMLRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) { | func (r *FootnoteHTMLRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) { | ||||
reg.Register(ast.KindFootnoteLink, r.renderFootnoteLink) | 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.KindFootnote, r.renderFootnote) | ||||
reg.Register(ast.KindFootnoteList, r.renderFootnoteList) | reg.Register(ast.KindFootnoteList, r.renderFootnoteList) | ||||
} | } | ||||
@@ -243,25 +529,53 @@ func (r *FootnoteHTMLRenderer) renderFootnoteLink(w util.BufWriter, source []byt | |||||
if entering { | if entering { | ||||
n := node.(*ast.FootnoteLink) | n := node.(*ast.FootnoteLink) | ||||
is := strconv.Itoa(n.Index) | 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(is) | ||||
_, _ = w.WriteString(`"><a href="#fn:`) | |||||
_, _ = w.WriteString(`"><a href="#`) | |||||
_, _ = w.Write(r.idPrefix(node)) | |||||
_, _ = w.WriteString(`fn:`) | |||||
_, _ = w.WriteString(is) | _, _ = 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(is) | ||||
_, _ = w.WriteString(`</a></sup>`) | _, _ = w.WriteString(`</a></sup>`) | ||||
} | } | ||||
return gast.WalkContinue, nil | 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 { | if entering { | ||||
n := node.(*ast.FootnoteBackLink) | |||||
n := node.(*ast.FootnoteBacklink) | |||||
is := strconv.Itoa(n.Index) | 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(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>`) | _, _ = w.WriteString(`</a>`) | ||||
} | } | ||||
return gast.WalkContinue, nil | return gast.WalkContinue, nil | ||||
@@ -271,9 +585,11 @@ func (r *FootnoteHTMLRenderer) renderFootnote(w util.BufWriter, source []byte, n | |||||
n := node.(*ast.Footnote) | n := node.(*ast.Footnote) | ||||
is := strconv.Itoa(n.Index) | is := strconv.Itoa(n.Index) | ||||
if entering { | if entering { | ||||
_, _ = w.WriteString(`<li id="fn:`) | |||||
_, _ = w.WriteString(`<li id="`) | |||||
_, _ = w.Write(r.idPrefix(node)) | |||||
_, _ = w.WriteString(`fn:`) | |||||
_, _ = w.WriteString(is) | _, _ = w.WriteString(is) | ||||
_, _ = w.WriteString(`" role="doc-endnote"`) | |||||
_, _ = w.WriteString(`"`) | |||||
if node.Attributes() != nil { | if node.Attributes() != nil { | ||||
html.RenderAttributes(w, node, html.ListItemAttributeFilter) | 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) { | 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 { | 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 { | if node.Attributes() != nil { | ||||
html.RenderAttributes(w, node, html.GlobalAttributeFilter) | html.RenderAttributes(w, node, html.GlobalAttributeFilter) | ||||
} | } | ||||
@@ -305,18 +615,59 @@ func (r *FootnoteHTMLRenderer) renderFootnoteList(w util.BufWriter, source []byt | |||||
_, _ = w.WriteString("<ol>\n") | _, _ = w.WriteString("<ol>\n") | ||||
} else { | } else { | ||||
_, _ = w.WriteString("</ol>\n") | _, _ = w.WriteString("</ol>\n") | ||||
_, _ = w.WriteString("</") | |||||
_, _ = w.WriteString(tag) | |||||
_, _ = w.WriteString(">\n") | |||||
_, _ = w.WriteString("</div>\n") | |||||
} | } | ||||
return gast.WalkContinue, nil | 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 { | type footnote struct { | ||||
options []FootnoteOption | |||||
} | } | ||||
// Footnote is an extension that allow you to use PHP Markdown Extra Footnotes. | // 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) { | func (e *footnote) Extend(m goldmark.Markdown) { | ||||
m.Parser().AddOptions( | m.Parser().AddOptions( | ||||
@@ -331,6 +682,6 @@ func (e *footnote) Extend(m goldmark.Markdown) { | |||||
), | ), | ||||
) | ) | ||||
m.Renderer().AddOptions(renderer.WithNodeRenderers( | 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" | "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 | // An LinkifyConfig struct is a data structure that holds configuration of the | ||||
// Linkify extension. | // Linkify extension. | ||||
@@ -24,10 +24,12 @@ type LinkifyConfig struct { | |||||
EmailRegexp *regexp.Regexp | 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. | // SetOption implements SetOptioner. | ||||
func (c *LinkifyConfig) SetOption(name parser.OptionName, value interface{}) { | func (c *LinkifyConfig) SetOption(name parser.OptionName, value interface{}) { | ||||
@@ -156,10 +158,12 @@ func (s *linkifyParser) Trigger() []byte { | |||||
return []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 { | func (s *linkifyParser) Parse(parent ast.Node, block text.Reader, pc parser.Context) ast.Node { | ||||
if pc.IsInLinkLabel() { | 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) | s := segment.WithStop(segment.Start + 1) | ||||
ast.MergeOrAppendTextSegment(parent, s) | 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) | 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 := ast.NewAutoLink(typ, n) | ||||
link.Protocol = protocol | link.Protocol = protocol | ||||
return link | return link | ||||
@@ -15,7 +15,121 @@ import ( | |||||
"github.com/yuin/goldmark/util" | "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 tableDelimLeft = regexp.MustCompile(`^\s*\:\-+\s*$`) | ||||
var tableDelimRight = regexp.MustCompile(`^\s*\-+\:\s*$`) | var tableDelimRight = regexp.MustCompile(`^\s*\-+\:\s*$`) | ||||
var tableDelimCenter = 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 { | if lines.Len() < 2 { | ||||
return | 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() | source := reader.Source() | ||||
line := segment.Value(source) | line := segment.Value(source) | ||||
pos := 0 | pos := 0 | ||||
@@ -79,18 +202,39 @@ func (b *tableParagraphTransformer) parseRow(segment text.Segment, alignments [] | |||||
} else { | } else { | ||||
alignment = alignments[i] | 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() | 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.TrimLeftSpace(source) | ||||
seg = seg.TrimRightSpace(source) | seg = seg.TrimRightSpace(source) | ||||
node.Lines().Append(seg) | node.Lines().Append(seg) | ||||
node.Alignment = alignment | |||||
row.AppendChild(row, node) | row.AppendChild(row, node) | ||||
pos += closure + 1 | |||||
pos = closure + 1 | |||||
} | } | ||||
for ; i < len(alignments); i++ { | for ; i < len(alignments); i++ { | ||||
row.AppendChild(row, ast.NewTableCell()) | 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 { | func (b *tableParagraphTransformer) parseDelimiter(segment text.Segment, reader text.Reader) []ast.Alignment { | ||||
line := segment.Value(reader.Source()) | line := segment.Value(reader.Source()) | ||||
if !tableDelimRegexp.Match(line) { | |||||
if !isTableDelim(line) { | |||||
return nil | return nil | ||||
} | } | ||||
cols := bytes.Split(line, []byte{'|'}) | cols := bytes.Split(line, []byte{'|'}) | ||||
@@ -128,19 +272,74 @@ func (b *tableParagraphTransformer) parseDelimiter(segment text.Segment, reader | |||||
return alignments | 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 | // TableHTMLRenderer is a renderer.NodeRenderer implementation that | ||||
// renders Table nodes. | // renders Table nodes. | ||||
type TableHTMLRenderer struct { | type TableHTMLRenderer struct { | ||||
html.Config | |||||
TableConfig | |||||
} | } | ||||
// NewTableHTMLRenderer returns a new TableHTMLRenderer. | // NewTableHTMLRenderer returns a new TableHTMLRenderer. | ||||
func NewTableHTMLRenderer(opts ...html.Option) renderer.NodeRenderer { | |||||
func NewTableHTMLRenderer(opts ...TableOption) renderer.NodeRenderer { | |||||
r := &TableHTMLRenderer{ | r := &TableHTMLRenderer{ | ||||
Config: html.NewConfig(), | |||||
TableConfig: NewTableConfig(), | |||||
} | } | ||||
for _, opt := range opts { | for _, opt := range opts { | ||||
opt.SetHTMLOption(&r.Config) | |||||
opt.SetTableOption(&r.TableConfig) | |||||
} | } | ||||
return r | return r | ||||
} | } | ||||
@@ -281,14 +480,33 @@ func (r *TableHTMLRenderer) renderTableCell(w util.BufWriter, source []byte, nod | |||||
tag = "th" | tag = "th" | ||||
} | } | ||||
if entering { | if entering { | ||||
align := "" | |||||
fmt.Fprintf(w, "<%s", tag) | |||||
if n.Alignment != ast.AlignNone { | 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 n.Attributes() != nil { | ||||
if tag == "td" { | if tag == "td" { | ||||
html.RenderAttributes(w, n, TableTdCellAttributeFilter) // <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> | html.RenderAttributes(w, n, TableThCellAttributeFilter) // <th> | ||||
} | } | ||||
} | } | ||||
fmt.Fprintf(w, "%s>", align) | |||||
_ = w.WriteByte('>') | |||||
} else { | } else { | ||||
fmt.Fprintf(w, "</%s>\n", tag) | fmt.Fprintf(w, "</%s>\n", tag) | ||||
} | } | ||||
@@ -304,16 +522,31 @@ func (r *TableHTMLRenderer) renderTableCell(w util.BufWriter, source []byte, nod | |||||
} | } | ||||
type table struct { | type table struct { | ||||
options []TableOption | |||||
} | } | ||||
// Table is an extension that allow you to use GFM tables . | // 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) { | 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( | 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" | "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 | // TypographicPunctuation is a key of the punctuations that can be replaced with | ||||
// typographic entities. | // typographic entities. | ||||
type TypographicPunctuation int | type TypographicPunctuation int | ||||
@@ -139,11 +160,10 @@ func NewTypographerParser(opts ...TypographerOption) parser.InlineParser { | |||||
} | } | ||||
func (s *typographerParser) Trigger() []byte { | func (s *typographerParser) Trigger() []byte { | ||||
return []byte{'\'', '"', '-', '.', '<', '>'} | |||||
return []byte{'\'', '"', '-', '.', ',', '<', '>', '*', '['} | |||||
} | } | ||||
func (s *typographerParser) Parse(parent gast.Node, block text.Reader, pc parser.Context) gast.Node { | func (s *typographerParser) Parse(parent gast.Node, block text.Reader, pc parser.Context) gast.Node { | ||||
before := block.PrecendingCharacter() | |||||
line, _ := block.PeekLine() | line, _ := block.PeekLine() | ||||
c := line[0] | c := line[0] | ||||
if len(line) > 2 { | if len(line) > 2 { | ||||
@@ -189,10 +209,12 @@ func (s *typographerParser) Parse(parent gast.Node, block text.Reader, pc parser | |||||
} | } | ||||
} | } | ||||
if c == '\'' || c == '"' { | if c == '\'' || c == '"' { | ||||
before := block.PrecendingCharacter() | |||||
d := parser.ScanDelimiter(line, before, 1, defaultTypographerDelimiterProcessor) | d := parser.ScanDelimiter(line, before, 1, defaultTypographerDelimiterProcessor) | ||||
if d == nil { | if d == nil { | ||||
return nil | return nil | ||||
} | } | ||||
counter := getUnclosedCounter(pc) | |||||
if c == '\'' { | if c == '\'' { | ||||
if s.Substitutions[Apostrophe] != nil { | if s.Substitutions[Apostrophe] != nil { | ||||
// Handle decade abbrevations such as '90s | // 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 { | if len(line) > 4 { | ||||
after = util.ToRune(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 := gast.NewString(s.Substitutions[Apostrophe]) | ||||
node.SetCode(true) | node.SetCode(true) | ||||
block.Advance(1) | block.Advance(1) | ||||
return node | 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 | // Convert normal apostrophes. This is probably more flexible than necessary but | ||||
// converts any apostrophe in between two alphanumerics. | // converts any apostrophe in between two alphanumerics. | ||||
if len(line) > 1 && (unicode.IsDigit(before) || unicode.IsLetter(before)) && (unicode.IsLetter(util.ToRune(line, 1))) { | 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 { | 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) | node.SetCode(true) | ||||
block.Advance(1) | block.Advance(1) | ||||
return node | 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 == '"' { | 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 := gast.NewString(s.Substitutions[LeftDoubleQuote]) | ||||
node.SetCode(true) | node.SetCode(true) | ||||
block.Advance(1) | block.Advance(1) | ||||
counter.Double++ | |||||
return node | 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) { | func (s *typographerParser) CloseBlock(parent gast.Node, pc parser.Context) { | ||||
// nothing to do | |||||
getUnclosedCounter(pc).Reset() | |||||
} | } | ||||
type typographer struct { | type typographer struct { | ||||
@@ -1,3 +1,3 @@ | |||||
module github.com/yuin/goldmark | 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) | reader.Advance(1) | ||||
line, _ := reader.PeekLine() | line, _ := reader.PeekLine() | ||||
i := 0 | 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 | name := attrNameClass | ||||
if c == '#' { | if c == '#' { | ||||
@@ -129,6 +133,11 @@ func parseAttribute(reader text.Reader) (Attribute, bool) { | |||||
if !ok { | if !ok { | ||||
return Attribute{}, false | return Attribute{}, false | ||||
} | } | ||||
if bytes.Equal(name, attrNameClass) { | |||||
if _, ok = value.([]byte); !ok { | |||||
return Attribute{}, false | |||||
} | |||||
} | |||||
return Attribute{Name: name, Value: value}, true | 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 { | if i == pos || level > 6 { | ||||
return nil, NoChildren | return nil, NoChildren | ||||
} | } | ||||
if i == len(line) { // alone '#' (without a new line character) | |||||
return ast.NewHeading(level), NoChildren | |||||
} | |||||
l := util.TrimLeftSpaceLength(line[i:]) | l := util.TrimLeftSpaceLength(line[i:]) | ||||
if l == 0 { | if l == 0 { | ||||
return nil, NoChildren | return nil, NoChildren | ||||
@@ -126,7 +129,8 @@ func (b *atxHeadingParser) Open(parent ast.Node, reader text.Reader, pc Context) | |||||
if closureClose > 0 { | if closureClose > 0 { | ||||
reader.Advance(closureClose) | reader.Advance(closureClose) | ||||
attrs, ok := ParseAttributes(reader) | attrs, ok := ParseAttributes(reader) | ||||
parsed = ok | |||||
rest, _ := reader.PeekLine() | |||||
parsed = ok && util.IsBlank(rest) | |||||
if parsed { | if parsed { | ||||
for _, attr := range attrs { | for _, attr := range attrs { | ||||
node.SetAttribute(attr.Name, attr.Value) | 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() | node := ast.NewCodeBlock() | ||||
reader.AdvanceAndSetPadding(pos, padding) | reader.AdvanceAndSetPadding(pos, padding) | ||||
_, segment = reader.PeekLine() | _, 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) | node.Lines().Append(segment) | ||||
reader.Advance(segment.Len() - 1) | reader.Advance(segment.Len() - 1) | ||||
return node, NoChildren | return node, NoChildren | ||||
@@ -49,6 +53,12 @@ func (b *codeBlockParser) Continue(node ast.Node, reader text.Reader, pc Context | |||||
} | } | ||||
reader.AdvanceAndSetPadding(pos, padding) | reader.AdvanceAndSetPadding(pos, padding) | ||||
_, segment = reader.PeekLine() | _, 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) | node.Lines().Append(segment) | ||||
reader.Advance(segment.Len() - 1) | reader.Advance(segment.Len() - 1) | ||||
return Continue | NoChildren | return Continue | NoChildren | ||||
@@ -77,3 +87,14 @@ func (b *codeBlockParser) CanInterruptParagraph() bool { | |||||
func (b *codeBlockParser) CanAcceptIndentedLine() bool { | func (b *codeBlockParser) CanAcceptIndentedLine() bool { | ||||
return true | 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 ( | import ( | ||||
"github.com/yuin/goldmark/ast" | "github.com/yuin/goldmark/ast" | ||||
"github.com/yuin/goldmark/text" | "github.com/yuin/goldmark/text" | ||||
"github.com/yuin/goldmark/util" | |||||
) | ) | ||||
type codeSpanParser struct { | 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() | block.AdvanceLine() | ||||
} | } | ||||
end: | end: | ||||
@@ -62,11 +59,11 @@ end: | |||||
// trim first halfspace and last halfspace | // trim first halfspace and last halfspace | ||||
segment := node.FirstChild().(*ast.Text).Segment | segment := node.FirstChild().(*ast.Text).Segment | ||||
shouldTrimmed := true | shouldTrimmed := true | ||||
if !(!segment.IsEmpty() && block.Source()[segment.Start] == ' ') { | |||||
if !(!segment.IsEmpty() && isSpaceOrNewline(block.Source()[segment.Start])) { | |||||
shouldTrimmed = false | shouldTrimmed = false | ||||
} | } | ||||
segment = node.LastChild().(*ast.Text).Segment | 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 | shouldTrimmed = false | ||||
} | } | ||||
if shouldTrimmed { | if shouldTrimmed { | ||||
@@ -81,3 +78,7 @@ end: | |||||
} | } | ||||
return node | return node | ||||
} | } | ||||
func isSpaceOrNewline(c byte) bool { | |||||
return c == ' ' || c == '\n' | |||||
} |
@@ -3,7 +3,6 @@ package parser | |||||
import ( | import ( | ||||
"fmt" | "fmt" | ||||
"strings" | "strings" | ||||
"unicode" | |||||
"github.com/yuin/goldmark/ast" | "github.com/yuin/goldmark/ast" | ||||
"github.com/yuin/goldmark/text" | "github.com/yuin/goldmark/text" | ||||
@@ -31,11 +30,11 @@ type Delimiter struct { | |||||
Segment text.Segment | Segment text.Segment | ||||
// CanOpen is set true if this delimiter can open a span for a new node. | // 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 | CanOpen bool | ||||
// CanClose is set true if this delimiter can close a span for a new node. | // 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 | CanClose bool | ||||
// Length is a remaining length of this delimiter. | // 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 | 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 && | isLeft := !afterIsWhitespace && | ||||
(!afterIsPunctuation || beforeIsWhitespace || beforeIsPunctuation) | (!afterIsPunctuation || beforeIsWhitespace || beforeIsPunctuation) | ||||
@@ -163,15 +162,11 @@ func ProcessDelimiters(bottom ast.Node, pc Context) { | |||||
var closer *Delimiter | var closer *Delimiter | ||||
if bottom != nil { | if bottom != nil { | ||||
if bottom != lastDelimiter { | if bottom != lastDelimiter { | ||||
for c := lastDelimiter.PreviousSibling(); c != nil; { | |||||
for c := lastDelimiter.PreviousSibling(); c != nil && c != bottom; { | |||||
if d, ok := c.(*Delimiter); ok { | if d, ok := c.(*Delimiter); ok { | ||||
closer = d | closer = d | ||||
} | } | ||||
prev := c.PreviousSibling() | |||||
if prev == bottom { | |||||
break | |||||
} | |||||
c = prev | |||||
c = c.PreviousSibling() | |||||
} | } | ||||
} | } | ||||
} else { | } else { | ||||
@@ -190,7 +185,7 @@ func ProcessDelimiters(bottom ast.Node, pc Context) { | |||||
found := false | found := false | ||||
maybeOpener := false | maybeOpener := false | ||||
var opener *Delimiter | 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) { | if opener.CanOpen && opener.Processor.CanOpenCloser(opener, closer) { | ||||
maybeOpener = true | maybeOpener = true | ||||
consume = opener.CalcComsumption(closer) | consume = opener.CalcComsumption(closer) | ||||
@@ -201,10 +196,11 @@ func ProcessDelimiters(bottom ast.Node, pc Context) { | |||||
} | } | ||||
} | } | ||||
if !found { | if !found { | ||||
next := closer.NextDelimiter | |||||
if !maybeOpener && !closer.CanOpen { | if !maybeOpener && !closer.CanOpen { | ||||
pc.RemoveDelimiter(closer) | pc.RemoveDelimiter(closer) | ||||
} | } | ||||
closer = closer.NextDelimiter | |||||
closer = next | |||||
continue | continue | ||||
} | } | ||||
opener.ConsumeCharacters(consume) | 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 { | func (b *fencedCodeBlockParser) Continue(node ast.Node, reader text.Reader, pc Context) State { | ||||
line, segment := reader.PeekLine() | line, segment := reader.PeekLine() | ||||
fdata := pc.Get(fencedCodeBlockInfoKey).(*fenceData) | fdata := pc.Get(fencedCodeBlockInfoKey).(*fenceData) | ||||
w, pos := util.IndentWidth(line, reader.LineOffset()) | w, pos := util.IndentWidth(line, reader.LineOffset()) | ||||
if w < 4 { | if w < 4 { | ||||
i := pos | i := pos | ||||
@@ -86,9 +87,19 @@ func (b *fencedCodeBlockParser) Continue(node ast.Node, reader text.Reader, pc C | |||||
return Close | 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) | 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) | node.Lines().Append(seg) | ||||
reader.AdvanceAndSetPadding(segment.Stop-segment.Start-pos-1, padding) | reader.AdvanceAndSetPadding(segment.Stop-segment.Start-pos-1, padding) | ||||
return Continue | NoChildren | return Continue | NoChildren | ||||
@@ -76,8 +76,8 @@ var allowedBlockTags = map[string]bool{ | |||||
"ul": true, | "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 htmlBlockType2OpenRegexp = regexp.MustCompile(`^[ ]{0,3}<!\-\-`) | ||||
var htmlBlockType2Close = []byte{'-', '-', '>'} | var htmlBlockType2Close = []byte{'-', '-', '>'} | ||||
@@ -85,15 +85,15 @@ var htmlBlockType2Close = []byte{'-', '-', '>'} | |||||
var htmlBlockType3OpenRegexp = regexp.MustCompile(`^[ ]{0,3}<\?`) | var htmlBlockType3OpenRegexp = regexp.MustCompile(`^[ ]{0,3}<\?`) | ||||
var htmlBlockType3Close = []byte{'?', '>'} | 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 htmlBlockType4Close = []byte{'>'} | ||||
var htmlBlockType5OpenRegexp = regexp.MustCompile(`^[ ]{0,3}<\!\[CDATA\[`) | var htmlBlockType5OpenRegexp = regexp.MustCompile(`^[ ]{0,3}<\!\[CDATA\[`) | ||||
var htmlBlockType5Close = []byte{']', ']', '>'} | 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 { | type htmlBlockParser struct { | ||||
} | } | ||||
@@ -201,7 +201,7 @@ func (b *htmlBlockParser) Continue(node ast.Node, reader text.Reader, pc Context | |||||
} | } | ||||
if bytes.Contains(line, closurePattern) { | if bytes.Contains(line, closurePattern) { | ||||
htmlBlock.ClosureLine = segment | htmlBlock.ClosureLine = segment | ||||
reader.Advance(segment.Len() - 1) | |||||
reader.Advance(segment.Len()) | |||||
return Close | return Close | ||||
} | } | ||||
@@ -2,7 +2,6 @@ package parser | |||||
import ( | import ( | ||||
"fmt" | "fmt" | ||||
"regexp" | |||||
"strings" | "strings" | ||||
"github.com/yuin/goldmark/ast" | "github.com/yuin/goldmark/ast" | ||||
@@ -49,6 +48,13 @@ func (s *linkLabelState) Kind() ast.NodeKind { | |||||
return kindLinkLabelState | 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) { | func pushLinkLabelState(pc Context, v *linkLabelState) { | ||||
tlist := pc.Get(linkLabelStateKey) | tlist := pc.Get(linkLabelStateKey) | ||||
var list *linkLabelState | var list *linkLabelState | ||||
@@ -113,8 +119,6 @@ func (s *linkParser) Trigger() []byte { | |||||
return []byte{'!', '[', ']'} | return []byte{'!', '[', ']'} | ||||
} | } | ||||
var linkDestinationRegexp = regexp.MustCompile(`\s*([^\s].+)`) | |||||
var linkTitleRegexp = regexp.MustCompile(`\s+(\)|["'\(].+)`) | |||||
var linkBottom = NewContextKey() | var linkBottom = NewContextKey() | ||||
func (s *linkParser) Parse(parent ast.Node, block text.Reader, pc Context) ast.Node { | 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) | block.Advance(1) | ||||
removeLinkLabelState(pc, last) | 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) | ast.MergeOrReplaceTextSegment(last.Parent(), last, last.Segment) | ||||
return nil | return nil | ||||
} | } | ||||
@@ -167,6 +178,13 @@ func (s *linkParser) Parse(parent ast.Node, block text.Reader, pc Context) ast.N | |||||
block.SetPosition(l, pos) | block.SetPosition(l, pos) | ||||
ssegment := text.NewSegment(last.Segment.Stop, segment.Start) | ssegment := text.NewSegment(last.Segment.Stop, segment.Start) | ||||
maybeReference := block.Value(ssegment) | 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)) | ref, ok := pc.Reference(util.ToLinkReference(maybeReference)) | ||||
if !ok { | if !ok { | ||||
ast.MergeOrReplaceTextSegment(last.Parent(), last, last.Segment) | 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 | 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 | 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 { | if _, ok := c.(*ast.Link); ok { | ||||
return true | return true | ||||
} | } | ||||
if s.containsLink(c.FirstChild()) { | |||||
return true | |||||
} | |||||
} | } | ||||
return false | 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) { | func (s *linkParser) parseReferenceLink(parent ast.Node, last *linkLabelState, block text.Reader, pc Context) (*ast.Link, bool) { | ||||
_, orgpos := block.Position() | _, orgpos := block.Position() | ||||
block.Advance(1) // skip '[' | 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 | 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 | 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)) | 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) { | func parseLinkDestination(block text.Reader) ([]byte, bool) { | ||||
block.SkipSpaces() | block.SkipSpaces() | ||||
line, _ := block.PeekLine() | line, _ := block.PeekLine() | ||||
buf := []byte{} | |||||
if block.Peek() == '<' { | if block.Peek() == '<' { | ||||
i := 1 | i := 1 | ||||
for i < len(line) { | for i < len(line) { | ||||
c := line[i] | c := line[i] | ||||
if c == '\\' && i < len(line)-1 && util.IsPunct(line[i+1]) { | if c == '\\' && i < len(line)-1 && util.IsPunct(line[i+1]) { | ||||
buf = append(buf, '\\', line[i+1]) | |||||
i += 2 | i += 2 | ||||
continue | continue | ||||
} else if c == '>' { | } else if c == '>' { | ||||
block.Advance(i + 1) | block.Advance(i + 1) | ||||
return line[1:i], true | return line[1:i], true | ||||
} | } | ||||
buf = append(buf, c) | |||||
i++ | i++ | ||||
} | } | ||||
return nil, false | return nil, false | ||||
@@ -316,7 +350,6 @@ func parseLinkDestination(block text.Reader) ([]byte, bool) { | |||||
for i < len(line) { | for i < len(line) { | ||||
c := line[i] | c := line[i] | ||||
if c == '\\' && i < len(line)-1 && util.IsPunct(line[i+1]) { | if c == '\\' && i < len(line)-1 && util.IsPunct(line[i+1]) { | ||||
buf = append(buf, '\\', line[i+1]) | |||||
i += 2 | i += 2 | ||||
continue | continue | ||||
} else if c == '(' { | } else if c == '(' { | ||||
@@ -329,7 +362,6 @@ func parseLinkDestination(block text.Reader) ([]byte, bool) { | |||||
} else if util.IsSpace(c) { | } else if util.IsSpace(c) { | ||||
break | break | ||||
} | } | ||||
buf = append(buf, c) | |||||
i++ | i++ | ||||
} | } | ||||
block.Advance(i) | block.Advance(i) | ||||
@@ -346,34 +378,24 @@ func parseLinkTitle(block text.Reader) ([]byte, bool) { | |||||
if opener == '(' { | if opener == '(' { | ||||
closer = ')' | 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) { | func (s *linkParser) CloseBlock(parent ast.Node, block text.Reader, pc Context) { | ||||
pc.Set(linkBottom, nil) | |||||
tlist := pc.Get(linkLabelStateKey) | tlist := pc.Get(linkLabelStateKey) | ||||
if tlist == nil { | if tlist == nil { | ||||
return | return | ||||
@@ -52,7 +52,7 @@ func (p *linkReferenceParagraphTransformer) Transform(node *ast.Paragraph, reade | |||||
func parseLinkReferenceDefinition(block text.Reader, pc Context) (int, int) { | func parseLinkReferenceDefinition(block text.Reader, pc Context) (int, int) { | ||||
block.SkipSpaces() | block.SkipSpaces() | ||||
line, segment := block.PeekLine() | |||||
line, _ := block.PeekLine() | |||||
if line == nil { | if line == nil { | ||||
return -1, -1 | return -1, -1 | ||||
} | } | ||||
@@ -67,39 +67,33 @@ func parseLinkReferenceDefinition(block text.Reader, pc Context) (int, int) { | |||||
if line[pos] != '[' { | if line[pos] != '[' { | ||||
return -1, -1 | return -1, -1 | ||||
} | } | ||||
open := segment.Start + pos + 1 | |||||
closes := -1 | |||||
block.Advance(pos + 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 | return -1, -1 | ||||
} | } | ||||
label := block.Value(text.NewSegment(open, closes)) | |||||
if util.IsBlank(label) { | |||||
if block.Peek() != ':' { | |||||
return -1, -1 | return -1, -1 | ||||
} | } | ||||
block.Advance(1) | |||||
block.SkipSpaces() | block.SkipSpaces() | ||||
destination, ok := parseLinkDestination(block) | destination, ok := parseLinkDestination(block) | ||||
if !ok { | if !ok { | ||||
return -1, -1 | return -1, -1 | ||||
} | } | ||||
line, segment = block.PeekLine() | |||||
line, _ = block.PeekLine() | |||||
isNewLine := line == nil || util.IsBlank(line) | isNewLine := line == nil || util.IsBlank(line) | ||||
endLine, _ := block.Position() | endLine, _ := block.Position() | ||||
@@ -117,45 +111,40 @@ func parseLinkReferenceDefinition(block text.Reader, pc Context) (int, int) { | |||||
return -1, -1 | return -1, -1 | ||||
} | } | ||||
block.Advance(1) | block.Advance(1) | ||||
open = -1 | |||||
closes = -1 | |||||
closer := opener | closer := opener | ||||
if opener == '(' { | if opener == '(' { | ||||
closer = ')' | closer = ')' | ||||
} | } | ||||
for { | |||||
line, segment = block.PeekLine() | |||||
if line == nil { | |||||
segments, found = block.FindClosure(opener, closer, linkFindClosureOptions) | |||||
if !found { | |||||
if !isNewLine { | |||||
return -1, -1 | 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() | 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 line != nil && !util.IsBlank(line) { | ||||
if !isNewLine { | if !isNewLine { | ||||
return -1, -1 | return -1, -1 | ||||
} | } | ||||
title := block.Value(text.NewSegment(open, closes)) | |||||
ref := NewReference(label, destination, title) | ref := NewReference(label, destination, title) | ||||
pc.AddReference(ref) | pc.AddReference(ref) | ||||
return startLine, endLine | return startLine, endLine | ||||
} | } | ||||
title := block.Value(text.NewSegment(open, closes)) | |||||
endLine, _ = block.Position() | endLine, _ = block.Position() | ||||
ref := NewReference(label, destination, title) | ref := NewReference(label, destination, title) | ||||
pc.AddReference(ref) | pc.AddReference(ref) | ||||
@@ -1,10 +1,11 @@ | |||||
package parser | package parser | ||||
import ( | import ( | ||||
"strconv" | |||||
"github.com/yuin/goldmark/ast" | "github.com/yuin/goldmark/ast" | ||||
"github.com/yuin/goldmark/text" | "github.com/yuin/goldmark/text" | ||||
"github.com/yuin/goldmark/util" | "github.com/yuin/goldmark/util" | ||||
"strconv" | |||||
) | ) | ||||
type listItemType int | type listItemType int | ||||
@@ -15,6 +16,10 @@ const ( | |||||
orderedList | orderedList | ||||
) | ) | ||||
var skipListParserKey = NewContextKey() | |||||
var emptyListItemWithBlankLines = NewContextKey() | |||||
var listItemFlagValue interface{} = true | |||||
// Same as | // Same as | ||||
// `^(([ ]*)([\-\*\+]))(\s+.*)?\n?$`.FindSubmatchIndex or | // `^(([ ]*)([\-\*\+]))(\s+.*)?\n?$`.FindSubmatchIndex or | ||||
// `^(([ ]*)(\d{1,9}[\.\)]))(\s+.*)?\n?$`.FindSubmatchIndex | // `^(([ ]*)(\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) { | func (b *listParser) Open(parent ast.Node, reader text.Reader, pc Context) (ast.Node, State) { | ||||
last := pc.LastOpenedBlock().Node | 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 | return nil, NoChildren | ||||
} | } | ||||
line, _ := reader.PeekLine() | line, _ := reader.PeekLine() | ||||
@@ -143,7 +148,7 @@ func (b *listParser) Open(parent ast.Node, reader text.Reader, pc Context) (ast. | |||||
return nil, NoChildren | return nil, NoChildren | ||||
} | } | ||||
//an empty list item cannot interrupt a paragraph: | //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 | return nil, NoChildren | ||||
} | } | ||||
} | } | ||||
@@ -153,6 +158,7 @@ func (b *listParser) Open(parent ast.Node, reader text.Reader, pc Context) (ast. | |||||
if start > -1 { | if start > -1 { | ||||
node.Start = start | node.Start = start | ||||
} | } | ||||
pc.Set(emptyListItemWithBlankLines, nil) | |||||
return node, HasChildren | return node, HasChildren | ||||
} | } | ||||
@@ -160,9 +166,8 @@ func (b *listParser) Continue(node ast.Node, reader text.Reader, pc Context) Sta | |||||
list := node.(*ast.List) | list := node.(*ast.List) | ||||
line, _ := reader.PeekLine() | line, _ := reader.PeekLine() | ||||
if util.IsBlank(line) { | 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 | return Continue | HasChildren | ||||
} | } | ||||
@@ -175,10 +180,23 @@ func (b *listParser) Continue(node ast.Node, reader text.Reader, pc Context) Sta | |||||
// - a | // - a | ||||
// - b <--- current line | // - b <--- current line | ||||
// it maybe a new child of the list. | // 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) | offset := lastOffset(node) | ||||
lastIsEmpty := node.LastChild().ChildCount() == 0 | |||||
indent, _ := util.IndentWidth(line, reader.LineOffset()) | indent, _ := util.IndentWidth(line, reader.LineOffset()) | ||||
if indent < offset { | |||||
if indent < offset || lastIsEmpty { | |||||
if indent < 4 { | if indent < 4 { | ||||
match, typ := matchesListItem(line, false) // may have a leading spaces more than 3 | match, typ := matchesListItem(line, false) // may have a leading spaces more than 3 | ||||
if typ != notList && match[1]-offset < 4 { | 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 Close | ||||
} | } | ||||
} | } | ||||
return Continue | HasChildren | 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 Close | ||||
} | } | ||||
return Continue | HasChildren | return Continue | HasChildren | ||||
@@ -230,8 +265,9 @@ func (b *listParser) Close(node ast.Node, reader text.Reader, pc Context) { | |||||
if list.IsTight { | if list.IsTight { | ||||
for child := node.FirstChild(); child != nil; child = child.NextSibling() { | 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) | paragraph, ok := gc.(*ast.Paragraph) | ||||
gc = gc.NextSibling() | |||||
if ok { | if ok { | ||||
textBlock := ast.NewTextBlock() | textBlock := ast.NewTextBlock() | ||||
textBlock.SetLines(paragraph.Lines()) | textBlock.SetLines(paragraph.Lines()) | ||||
@@ -17,9 +17,6 @@ func NewListItemParser() BlockParser { | |||||
return defaultListItemParser | return defaultListItemParser | ||||
} | } | ||||
var skipListParser = NewContextKey() | |||||
var skipListParserValue interface{} = true | |||||
func (b *listItemParser) Trigger() []byte { | func (b *listItemParser) Trigger() []byte { | ||||
return []byte{'-', '+', '*', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9'} | 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 { | if match[1]-offset > 3 { | ||||
return nil, NoChildren | return nil, NoChildren | ||||
} | } | ||||
pc.Set(emptyListItemWithBlankLines, nil) | |||||
itemOffset := calcListOffset(line, match) | itemOffset := calcListOffset(line, match) | ||||
node := ast.NewListItem(match[3] + itemOffset) | 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 | 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 { | func (b *listItemParser) Continue(node ast.Node, reader text.Reader, pc Context) State { | ||||
line, _ := reader.PeekLine() | line, _ := reader.PeekLine() | ||||
if util.IsBlank(line) { | if util.IsBlank(line) { | ||||
reader.Advance(len(line) - 1) | |||||
return Continue | HasChildren | return Continue | HasChildren | ||||
} | } | ||||
indent, _ := util.IndentWidth(line, reader.LineOffset()) | |||||
offset := lastOffset(node.Parent()) | 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) | _, typ := matchesListItem(line, true) | ||||
// new list item found | // new list item found | ||||
if typ != notList { | 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) | pos, padding := util.IndentPosition(line, reader.LineOffset(), offset) | ||||
reader.AdvanceAndSetPadding(pos, padding) | reader.AdvanceAndSetPadding(pos, padding) | ||||
@@ -138,6 +138,9 @@ type Context interface { | |||||
// Get returns a value associated with the given key. | // Get returns a value associated with the given key. | ||||
Get(ContextKey) interface{} | 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 sets the given value to the context. | ||||
Set(ContextKey, interface{}) | Set(ContextKey, interface{}) | ||||
@@ -252,6 +255,15 @@ func (p *parseContext) Get(key ContextKey) interface{} { | |||||
return p.store[key] | 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{}) { | func (p *parseContext) Set(key ContextKey, value interface{}) { | ||||
p.store[key] = value | p.store[key] = value | ||||
} | } | ||||
@@ -1103,6 +1115,12 @@ func (p *parser) walkBlock(block ast.Node, cb func(node ast.Node)) { | |||||
cb(block) | cb(block) | ||||
} | } | ||||
const ( | |||||
lineBreakHard uint8 = 1 << iota | |||||
lineBreakSoft | |||||
lineBreakVisible | |||||
) | |||||
func (p *parser) parseBlock(block text.BlockReader, parent ast.Node, pc Context) { | func (p *parser) parseBlock(block text.BlockReader, parent ast.Node, pc Context) { | ||||
if parent.IsRaw() { | if parent.IsRaw() { | ||||
return | return | ||||
@@ -1117,21 +1135,25 @@ func (p *parser) parseBlock(block text.BlockReader, parent ast.Node, pc Context) | |||||
break | break | ||||
} | } | ||||
lineLength := len(line) | 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 | 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 | 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 | 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 | 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() | l, startPosition := block.Position() | ||||
@@ -1195,11 +1217,14 @@ func (p *parser) parseBlock(block text.BlockReader, parent ast.Node, pc Context) | |||||
continue | continue | ||||
} | } | ||||
diff := startPosition.Between(currentPosition) | 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) | parent.AppendChild(parent, text) | ||||
block.AdvanceLine() | block.AdvanceLine() | ||||
} | } | ||||
@@ -2,10 +2,11 @@ package parser | |||||
import ( | import ( | ||||
"bytes" | "bytes" | ||||
"regexp" | |||||
"github.com/yuin/goldmark/ast" | "github.com/yuin/goldmark/ast" | ||||
"github.com/yuin/goldmark/text" | "github.com/yuin/goldmark/text" | ||||
"github.com/yuin/goldmark/util" | "github.com/yuin/goldmark/util" | ||||
"regexp" | |||||
) | ) | ||||
type rawHTMLParser struct { | 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]) { | if len(line) > 2 && line[1] == '/' && util.IsAlphaNumeric(line[2]) { | ||||
return s.parseMultiLineRegexp(closeTagRegexp, block, pc) | 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' { | 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 | return nil | ||||
} | } | ||||
var tagnamePattern = `([A-Za-z][A-Za-z0-9-]*)` | 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 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() | 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 | 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 { | func (s *rawHTMLParser) parseMultiLineRegexp(reg *regexp.Regexp, block text.Reader, pc Context) ast.Node { | ||||
sline, ssegment := block.Position() | sline, ssegment := block.Position() | ||||
@@ -102,7 +161,3 @@ func (s *rawHTMLParser) parseMultiLineRegexp(reg *regexp.Regexp, block text.Read | |||||
} | } | ||||
return nil | 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( | var GlobalAttributeFilter = util.NewBytesFilter( | ||||
[]byte("accesskey"), | []byte("accesskey"), | ||||
[]byte("autocapitalize"), | []byte("autocapitalize"), | ||||
[]byte("autofocus"), | |||||
[]byte("class"), | []byte("class"), | ||||
[]byte("contenteditable"), | []byte("contenteditable"), | ||||
[]byte("contextmenu"), | |||||
[]byte("dir"), | []byte("dir"), | ||||
[]byte("draggable"), | []byte("draggable"), | ||||
[]byte("dropzone"), | |||||
[]byte("enterkeyhint"), | |||||
[]byte("hidden"), | []byte("hidden"), | ||||
[]byte("id"), | []byte("id"), | ||||
[]byte("inert"), | |||||
[]byte("inputmode"), | |||||
[]byte("is"), | |||||
[]byte("itemid"), | |||||
[]byte("itemprop"), | []byte("itemprop"), | ||||
[]byte("itemref"), | |||||
[]byte("itemscope"), | |||||
[]byte("itemtype"), | |||||
[]byte("lang"), | []byte("lang"), | ||||
[]byte("part"), | |||||
[]byte("slot"), | []byte("slot"), | ||||
[]byte("spellcheck"), | []byte("spellcheck"), | ||||
[]byte("style"), | []byte("style"), | ||||
@@ -296,7 +304,7 @@ func (r *Renderer) renderHTMLBlock(w util.BufWriter, source []byte, node ast.Nod | |||||
l := n.Lines().Len() | l := n.Lines().Len() | ||||
for i := 0; i < l; i++ { | for i := 0; i < l; i++ { | ||||
line := n.Lines().At(i) | line := n.Lines().At(i) | ||||
_, _ = w.Write(line.Value(source)) | |||||
r.Writer.SecureWrite(w, line.Value(source)) | |||||
} | } | ||||
} else { | } else { | ||||
_, _ = w.WriteString("<!-- raw HTML omitted -->\n") | _, _ = 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 n.HasClosure() { | ||||
if r.Unsafe { | if r.Unsafe { | ||||
closure := n.ClosureLine | closure := n.ClosureLine | ||||
_, _ = w.Write(closure.Value(source)) | |||||
r.Writer.SecureWrite(w, closure.Value(source)) | |||||
} else { | } else { | ||||
_, _ = w.WriteString("<!-- raw HTML omitted -->\n") | _, _ = 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( | var ListAttributeFilter = GlobalAttributeFilter.Extend( | ||||
[]byte("start"), | []byte("start"), | ||||
[]byte("reversed"), | []byte("reversed"), | ||||
[]byte("type"), | |||||
) | ) | ||||
func (r *Renderer) renderList(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) { | 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) | value := segment.Value(source) | ||||
if bytes.HasSuffix(value, []byte("\n")) { | if bytes.HasSuffix(value, []byte("\n")) { | ||||
r.Writer.RawWrite(w, value[:len(value)-1]) | r.Writer.RawWrite(w, value[:len(value)-1]) | ||||
if c != n.LastChild() { | |||||
r.Writer.RawWrite(w, []byte(" ")) | |||||
} | |||||
r.Writer.RawWrite(w, []byte(" ")) | |||||
} else { | } else { | ||||
r.Writer.RawWrite(w, value) | 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.Write(util.EscapeHTML(util.URLEscape(n.Destination, true))) | ||||
} | } | ||||
_, _ = w.WriteString(`" alt="`) | _, _ = w.WriteString(`" alt="`) | ||||
_, _ = w.Write(n.Text(source)) | |||||
_, _ = w.Write(util.EscapeHTML(n.Text(source))) | |||||
_ = w.WriteByte('"') | _ = w.WriteByte('"') | ||||
if n.Title != nil { | if n.Title != nil { | ||||
_, _ = w.WriteString(` title="`) | _, _ = w.WriteString(` title="`) | ||||
@@ -669,8 +676,13 @@ type Writer interface { | |||||
// RawWrite writes the given source to writer without resolving references and | // RawWrite writes the given source to writer without resolving references and | ||||
// unescaping backslash escaped characters. | // unescaping backslash escaped characters. | ||||
RawWrite(writer util.BufWriter, source []byte) | 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 { | type defaultWriter struct { | ||||
} | } | ||||
@@ -685,6 +697,23 @@ func escapeRune(writer util.BufWriter, r rune) { | |||||
_, _ = writer.WriteRune(util.ToValidRune(r)) | _, _ = 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) { | func (d *defaultWriter) RawWrite(writer util.BufWriter, source []byte) { | ||||
n := 0 | n := 0 | ||||
l := len(source) | l := len(source) | ||||
@@ -718,6 +747,13 @@ func (d *defaultWriter) Write(writer util.BufWriter, source []byte) { | |||||
continue | continue | ||||
} | } | ||||
} | } | ||||
if c == '\x00' { | |||||
d.RawWrite(writer, source[n:i]) | |||||
d.RawWrite(writer, replacementCharacter) | |||||
n = i + 1 | |||||
escaped = false | |||||
continue | |||||
} | |||||
if c == '&' { | if c == '&' { | ||||
pos := i | pos := i | ||||
next := i + 1 | next := i + 1 | ||||
@@ -729,7 +765,7 @@ func (d *defaultWriter) Write(writer util.BufWriter, source []byte) { | |||||
if nnext < limit && nc == 'x' || nc == 'X' { | if nnext < limit && nc == 'x' || nc == 'X' { | ||||
start := nnext + 1 | start := nnext + 1 | ||||
i, ok = util.ReadWhile(source, [2]int{start, limit}, util.IsHexDecimal) | 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) | v, _ := strconv.ParseUint(util.BytesToReadOnlyString(source[start:i]), 16, 32) | ||||
d.RawWrite(writer, source[n:pos]) | d.RawWrite(writer, source[n:pos]) | ||||
n = i + 1 | n = i + 1 | ||||
@@ -741,7 +777,7 @@ func (d *defaultWriter) Write(writer util.BufWriter, source []byte) { | |||||
start := nnext | start := nnext | ||||
i, ok = util.ReadWhile(source, [2]int{start, limit}, util.IsNumeric) | i, ok = util.ReadWhile(source, [2]int{start, limit}, util.IsNumeric) | ||||
if ok && i < limit && i-start < 8 && source[i] == ';' { | 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]) | d.RawWrite(writer, source[n:pos]) | ||||
n = i + 1 | n = i + 1 | ||||
escapeRune(writer, rune(v)) | escapeRune(writer, rune(v)) | ||||
@@ -783,6 +819,7 @@ var bPng = []byte("png;") | |||||
var bGif = []byte("gif;") | var bGif = []byte("gif;") | ||||
var bJpeg = []byte("jpeg;") | var bJpeg = []byte("jpeg;") | ||||
var bWebp = []byte("webp;") | var bWebp = []byte("webp;") | ||||
var bSvg = []byte("svg;") | |||||
var bJs = []byte("javascript:") | var bJs = []byte("javascript:") | ||||
var bVb = []byte("vbscript:") | var bVb = []byte("vbscript:") | ||||
var bFile = []byte("file:") | var bFile = []byte("file:") | ||||
@@ -794,7 +831,8 @@ func IsDangerousURL(url []byte) bool { | |||||
if bytes.HasPrefix(url, bDataImage) && len(url) >= 11 { | if bytes.HasPrefix(url, bDataImage) && len(url) >= 11 { | ||||
v := url[11:] | v := url[11:] | ||||
if bytes.HasPrefix(v, bPng) || bytes.HasPrefix(v, bGif) || | 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 false | ||||
} | } | ||||
return true | return true | ||||
@@ -70,6 +70,28 @@ type Reader interface { | |||||
// Match performs regular expression searching to current line. | // Match performs regular expression searching to current line. | ||||
FindSubMatch(reg *regexp.Regexp) [][]byte | 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 { | type reader struct { | ||||
@@ -92,6 +114,10 @@ func NewReader(source []byte) Reader { | |||||
return r | return r | ||||
} | } | ||||
func (r *reader) FindClosure(opener, closer byte, options FindClosureOptions) (*Segments, bool) { | |||||
return findClosureReader(r, opener, closer, options) | |||||
} | |||||
func (r *reader) ResetPosition() { | func (r *reader) ResetPosition() { | ||||
r.line = -1 | r.line = -1 | ||||
r.head = 0 | r.head = 0 | ||||
@@ -272,6 +298,10 @@ func NewBlockReader(source []byte, segments *Segments) BlockReader { | |||||
return r | return r | ||||
} | } | ||||
func (r *blockReader) FindClosure(opener, closer byte, options FindClosureOptions) (*Segments, bool) { | |||||
return findClosureReader(r, opener, closer, options) | |||||
} | |||||
func (r *blockReader) ResetPosition() { | func (r *blockReader) ResetPosition() { | ||||
r.line = -1 | r.line = -1 | ||||
r.head = 0 | r.head = 0 | ||||
@@ -541,3 +571,83 @@ func readRuneReader(r Reader) (rune, int, error) { | |||||
r.Advance(size) | r.Advance(size) | ||||
return rn, size, nil | 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" | "regexp" | ||||
"sort" | "sort" | ||||
"strconv" | "strconv" | ||||
"unicode" | |||||
"unicode/utf8" | "unicode/utf8" | ||||
) | ) | ||||
@@ -27,6 +28,7 @@ func NewCopyOnWriteBuffer(buffer []byte) CopyOnWriteBuffer { | |||||
} | } | ||||
// Write writes given bytes to the buffer. | // Write writes given bytes to the buffer. | ||||
// Write allocate new buffer and clears it at the first time. | |||||
func (b *CopyOnWriteBuffer) Write(value []byte) { | func (b *CopyOnWriteBuffer) Write(value []byte) { | ||||
if !b.copied { | if !b.copied { | ||||
b.buffer = make([]byte, 0, len(b.buffer)+20) | 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...) | 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 writes the given byte to the buffer. | ||||
// WriteByte allocate new buffer and clears it at the first time. | |||||
func (b *CopyOnWriteBuffer) WriteByte(c byte) { | func (b *CopyOnWriteBuffer) WriteByte(c byte) { | ||||
if !b.copied { | if !b.copied { | ||||
b.buffer = make([]byte, 0, len(b.buffer)+20) | 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) | 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. | // Bytes returns bytes of this buffer. | ||||
func (b *CopyOnWriteBuffer) Bytes() []byte { | func (b *CopyOnWriteBuffer) Bytes() []byte { | ||||
return b.buffer | 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("\t"), []byte("[TAB]"), -1) | ||||
bs = bytes.Replace(bs, []byte("\n"), []byte("[NEWLINE]\n"), -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("\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 | return bs | ||||
} | } | ||||
@@ -110,30 +152,7 @@ func TabWidth(currentPos int) int { | |||||
// width=2 is in the tab character. In this case, IndentPosition returns | // width=2 is in the tab character. In this case, IndentPosition returns | ||||
// (pos=1, padding=2) | // (pos=1, padding=2) | ||||
func IndentPosition(bs []byte, currentPos, width int) (pos, padding int) { | 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. | // 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 | i := 0 | ||||
l := len(bs) | l := len(bs) | ||||
for ; i < l; i++ { | for ; i < l; i++ { | ||||
if bs[i] == '\t' { | |||||
if bs[i] == '\t' && w < width { | |||||
w += TabWidth(currentPos + w) | w += TabWidth(currentPos + w) | ||||
} else if bs[i] == ' ' { | |||||
} else if bs[i] == ' ' && w < width { | |||||
w++ | w++ | ||||
} else { | } else { | ||||
break | break | ||||
@@ -162,52 +181,56 @@ func IndentPositionPadding(bs []byte, currentPos, paddingv, width int) (pos, pad | |||||
} | } | ||||
// DedentPosition dedents lines by the given width. | // 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) { | 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. | // DedentPositionPadding dedents lines by the given width. | ||||
// This function is mostly same as DedentPosition except this function | // This function is mostly same as DedentPosition except this function | ||||
// takes account into additional paddings. | // 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) { | 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. | // 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 codeSpan is set true, it ignores characters in code spans. | ||||
// If allowNesting is set true, closures correspond to nested opener will be | // If allowNesting is set true, closures correspond to nested opener will be | ||||
// ignored. | // 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 { | func FindClosure(bs []byte, opener, closure byte, codeSpan, allowNesting bool) int { | ||||
i := 0 | i := 0 | ||||
opened := 1 | opened := 1 | ||||
@@ -668,7 +695,7 @@ func URLEscape(v []byte, resolveReference bool) []byte { | |||||
n = i | n = i | ||||
continue | continue | ||||
} | } | ||||
if int(u8len) >= len(v) { | |||||
if int(u8len) > len(v) { | |||||
u8len = int8(len(v) - 1) | u8len = int8(len(v) - 1) | ||||
} | } | ||||
if u8len == 0 { | if u8len == 0 { | ||||
@@ -754,7 +781,7 @@ func FindEmailIndex(b []byte) int { | |||||
var spaces = []byte(" \t\n\x0b\x0c\x0d") | 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} | 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 | 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. | // IsSpace returns true if the given character is a space, otherwise false. | ||||
func IsSpace(c byte) bool { | func IsSpace(c byte) bool { | ||||
return spaceTable[c] == 1 | 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. | // IsNumeric returns true if the given character is a numeric, otherwise false. | ||||
func IsNumeric(c byte) bool { | func IsNumeric(c byte) bool { | ||||
return c >= '0' && c <= '9' | return c >= '0' && c <= '9' | ||||
@@ -13,8 +13,11 @@ func BytesToReadOnlyString(b []byte) string { | |||||
} | } | ||||
// StringToReadOnlyBytes returns bytes converted from given 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)) | 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 | # github.com/yohcop/openid-go v1.0.0 | ||||
## explicit | ## explicit | ||||
github.com/yohcop/openid-go | github.com/yohcop/openid-go | ||||
# github.com/yuin/goldmark v1.1.30 | |||||
# github.com/yuin/goldmark v1.4.13 | |||||
## explicit | ## explicit | ||||
github.com/yuin/goldmark | github.com/yuin/goldmark | ||||
github.com/yuin/goldmark/ast | github.com/yuin/goldmark/ast | ||||