| @@ -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 | ||||