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