That's some text with a footnote.
+
+//= = = = = = = = = = = = = = = = = = = = = = = =//
+
+3
+//- - - - - - - - -//
+[^000]:0 [^]:
+//- - - - - - - - -//
+//= = = = = = = = = = = = = = = = = = = = = = = =//
+
+4
+//- - - - - - - - -//
+This[^3] is[^1] text with footnotes[^2].
+
+[^1]: Footnote one
+[^2]: Footnote two
+[^3]: Footnote three
+//- - - - - - - - -//
+www.commonmark.org/he <lp
+//= = = = = = = = = = = = = = = = = = = = = = = =//
+
+
+
+7
+//- - - - - - - - -//
+http://commonmark.org
+
+(Visit https://encrypted.google.com/search?q=Markup+(business))
+
+Anonymous FTP is available at ftp://foo.bar.baz.
+//- - - - - - - - -//
+http://commonmark.org
+(Visit https://encrypted.google.com/search?q=Markup+(business) )
+Anonymous FTP is available at ftp://foo.bar.baz .
+//= = = = = = = = = = = = = = = = = = = = = = = =//
+
+
+
+8
+//- - - - - - - - -//
+foo@bar.baz
+//- - - - - - - - -//
+foo@bar.baz
+//= = = = = = = = = = = = = = = = = = = = = = = =//
+
+
+
+9
+//- - - - - - - - -//
+hello@mail+xyz.example isn't valid, but hello+xyz@mail.example is.
+//- - - - - - - - -//
+hello@mail+xyz.example isn't valid, but hello+xyz@mail.example is.
+//= = = = = = = = = = = = = = = = = = = = = = = =//
+
+
+
+10
+//- - - - - - - - -//
+a.b-c_d@a.b
+
+a.b-c_d@a.b.
+
+a.b-c_d@a.b-
+
+a.b-c_d@a.b_
+//- - - - - - - - -//
+a.b-c_d@a.b
+a.b-c_d@a.b .
+a.b-c_d@a.b-
+a.b-c_d@a.b_
+//= = = = = = = = = = = = = = = = = = = = = = = =//
+
+
+
+11
+//- - - - - - - - -//
+https://github.com#sun,mon
+//- - - - - - - - -//
+https://github.com#sun,mon
+//= = = = = = = = = = = = = = = = = = = = = = = =//
+
+
+
+12
+//- - - - - - - - -//
+https://github.com/sunday's
+//- - - - - - - - -//
+https://github.com/sunday's
+//= = = = = = = = = = = = = = = = = = = = = = = =//
+
+13
+//- - - - - - - - -//
+https://github.com?q=stars:>1
+//- - - - - - - - -//
+https://github.com?q=stars:>1
+//= = = = = = = = = = = = = = = = = = = = = = = =//
+
+
+14
+//- - - - - - - - -//
+[https://google.com](https://google.com)
+//- - - - - - - - -//
+https://google.com
+//= = = = = = = = = = = = = = = = = = = = = = = =//
+
+
+15
+//- - - - - - - - -//
+This is a `git@github.com:vim/vim`
+//- - - - - - - - -//
+This is a git@github.com:vim/vim
+//= = = = = = = = = = = = = = = = = = = = = = = =//
+
+
+16
+//- - - - - - - - -//
+https://nic.college
+//- - - - - - - - -//
+https://nic.college
+//= = = = = = = = = = = = = = = = = = = = = = = =//
+
+
+17
+//- - - - - - - - -//
+http://server.intranet.acme.com:1313
+//- - - - - - - - -//
+http://server.intranet.acme.com:1313
+//= = = = = = = = = = = = = = = = = = = = = = = =//
+
+
+18
+//- - - - - - - - -//
+https://g.page/foo
+//- - - - - - - - -//
+https://g.page/foo
+//= = = = = = = = = = = = = = = = = = = = = = = =//
+
+
+19: Trailing punctuation (specifically, ?, !, ., ,, :, *, _, and ~) will not be considered part of the autolink
+//- - - - - - - - -//
+__http://test.com/~/a__
+__http://test.com/~/__
+__http://test.com/~__
+__http://test.com/a/~__
+//- - - - - - - - -//
+http://test.com/~/a
+http://test.com/~/
+http://test.com/ ~
+http://test.com/a/ ~
+//= = = = = = = = = = = = = = = = = = = = = = = =//
diff --git a/backend/goldmark/extension/_test/strikethrough.txt b/backend/goldmark/extension/_test/strikethrough.txt
new file mode 100644
index 0000000..5f37627
--- /dev/null
+++ b/backend/goldmark/extension/_test/strikethrough.txt
@@ -0,0 +1,39 @@
+1
+//- - - - - - - - -//
+~~Hi~~ Hello, world!
+//- - - - - - - - -//
+Hi Hello, world!
+//= = = = = = = = = = = = = = = = = = = = = = = =//
+
+2
+//- - - - - - - - -//
+This ~~has a
+
+new paragraph~~.
+//- - - - - - - - -//
+This ~~has a
+new paragraph~~.
+//= = = = = = = = = = = = = = = = = = = = = = = =//
+
+3
+//- - - - - - - - -//
+~Hi~ Hello, world!
+//- - - - - - - - -//
+Hi Hello, world!
+//= = = = = = = = = = = = = = = = = = = = = = = =//
+
+4: Three or more tildes do not create a strikethrough
+//- - - - - - - - -//
+This will ~~~not~~~ strike.
+//- - - - - - - - -//
+This will ~~~not~~~ strike.
+//= = = = = = = = = = = = = = = = = = = = = = = =//
+
+5: Leading three or more tildes do not create a strikethrough, create a code block
+//- - - - - - - - -//
+~~~Hi~~~ Hello, world!
+//- - - - - - - - -//
+
+//= = = = = = = = = = = = = = = = = = = = = = = =//
+
+
diff --git a/backend/goldmark/extension/_test/table.txt b/backend/goldmark/extension/_test/table.txt
new file mode 100644
index 0000000..eef5b67
--- /dev/null
+++ b/backend/goldmark/extension/_test/table.txt
@@ -0,0 +1,293 @@
+1
+//- - - - - - - - -//
+| foo | bar |
+| --- | --- |
+| baz | bim |
+//- - - - - - - - -//
+
+
+
+foo
+bar
+
+
+
+
+baz
+bim
+
+
+
+//= = = = = = = = = = = = = = = = = = = = = = = =//
+
+
+
+2
+//- - - - - - - - -//
+| abc | defghi |
+:-: | -----------:
+bar | baz
+//- - - - - - - - -//
+
+
+
+abc
+defghi
+
+
+
+
+bar
+baz
+
+
+
+//= = = = = = = = = = = = = = = = = = = = = = = =//
+
+
+
+3
+//- - - - - - - - -//
+| f\|oo |
+| ------ |
+| b `\|` az |
+| b **\|** im |
+//- - - - - - - - -//
+
+
+
+f|oo
+
+
+
+
+b | az
+
+
+b | im
+
+
+
+//= = = = = = = = = = = = = = = = = = = = = = = =//
+
+
+
+4
+//- - - - - - - - -//
+| abc | def |
+| --- | --- |
+| bar | baz |
+> bar
+//- - - - - - - - -//
+
+
+
+abc
+def
+
+
+
+
+bar
+baz
+
+
+
+
+bar
+
+//= = = = = = = = = = = = = = = = = = = = = = = =//
+
+
+
+5
+//- - - - - - - - -//
+| abc | def |
+| --- | --- |
+| bar | baz |
+bar
+
+bar
+//- - - - - - - - -//
+
+
+
+abc
+def
+
+
+
+
+bar
+baz
+
+
+bar
+
+
+
+
+bar
+//= = = = = = = = = = = = = = = = = = = = = = = =//
+
+
+
+6
+//- - - - - - - - -//
+| abc | def |
+| --- |
+| bar |
+//- - - - - - - - -//
+| abc | def |
+| --- |
+| bar |
+//= = = = = = = = = = = = = = = = = = = = = = = =//
+
+
+
+7
+//- - - - - - - - -//
+| abc | def |
+| --- | --- |
+| bar |
+| bar | baz | boo |
+//- - - - - - - - -//
+
+
+
+abc
+def
+
+
+
+
+bar
+
+
+
+bar
+baz
+
+
+
+//= = = = = = = = = = = = = = = = = = = = = = = =//
+
+
+
+8
+//- - - - - - - - -//
+| abc | def |
+| --- | --- |
+//- - - - - - - - -//
+
+//= = = = = = = = = = = = = = = = = = = = = = = =//
+
+
+
+9
+//- - - - - - - - -//
+Foo|Bar
+---|---
+`Yoyo`|Dyne
+//- - - - - - - - -//
+
+
+
+Foo
+Bar
+
+
+
+
+Yoyo
+Dyne
+
+
+
+//= = = = = = = = = = = = = = = = = = = = = = = =//
+
+
+10
+//- - - - - - - - -//
+foo|bar
+---|---
+`\` | second column
+//- - - - - - - - -//
+
+
+
+foo
+bar
+
+
+
+
+\
+second column
+
+
+
+//= = = = = = = = = = = = = = = = = = = = = = = =//
+
+
+11: Tables can interrupt paragraph
+//- - - - - - - - -//
+**xxx**
+| hello | hi |
+| :----: | :----:|
+//- - - - - - - - -//
+xxx
+
+//= = = = = = = = = = = = = = = = = = = = = = = =//
+
+12: A delimiter can not start with more than 3 spaces
+//- - - - - - - - -//
+Foo
+ ---
+//- - - - - - - - -//
+Foo
+---
+//= = = = = = = = = = = = = = = = = = = = = = = =//
+
+13: A delimiter can not start with more than 3 spaces(w/ tabs)
+ OPTIONS: {"enableEscape": true}
+//- - - - - - - - -//
+- aaa
+
+ Foo
+\t\t---
+//- - - - - - - - -//
+
+//= = = = = = = = = = = = = = = = = = = = = = = =//
+
+14: Delimiter-like line inside a list item
+//- - - - - - - - -//
+- [Marketing](marketing/_index.md)
+--
+//- - - - - - - - -//
+
+//= = = = = = = = = = = = = = = = = = = = = = = =//
+
diff --git a/backend/goldmark/extension/_test/tasklist.txt b/backend/goldmark/extension/_test/tasklist.txt
new file mode 100644
index 0000000..256eca4
--- /dev/null
+++ b/backend/goldmark/extension/_test/tasklist.txt
@@ -0,0 +1,51 @@
+1
+//- - - - - - - - -//
+- [ ] foo
+- [x] bar
+//- - - - - - - - -//
+
+//= = = = = = = = = = = = = = = = = = = = = = = =//
+
+
+
+2
+//- - - - - - - - -//
+- [x] foo
+ - [ ] bar
+ - [x] baz
+- [ ] bim
+//- - - - - - - - -//
+
+//= = = = = = = = = = = = = = = = = = = = = = = =//
+
+
+
+3
+//- - - - - - - - -//
+- test[x]=[x]
+//- - - - - - - - -//
+
+//= = = = = = = = = = = = = = = = = = = = = = = =//
+
+
+4
+//- - - - - - - - -//
++ [x] [x]
+//- - - - - - - - -//
+
+//= = = = = = = = = = = = = = = = = = = = = = = =//
diff --git a/backend/goldmark/extension/_test/typographer.txt b/backend/goldmark/extension/_test/typographer.txt
new file mode 100644
index 0000000..cf5fea6
--- /dev/null
+++ b/backend/goldmark/extension/_test/typographer.txt
@@ -0,0 +1,143 @@
+1
+//- - - - - - - - -//
+This should 'be' replaced
+//- - - - - - - - -//
+This should ‘be’ replaced
+//= = = = = = = = = = = = = = = = = = = = = = = =//
+
+2
+//- - - - - - - - -//
+This should "be" replaced
+//- - - - - - - - -//
+This should “be” replaced
+//= = = = = = = = = = = = = = = = = = = = = = = =//
+
+3
+//- - - - - - - - -//
+**--** *---* a...<< b>>
+//- - - - - - - - -//
+– — a…« b»
+//= = = = = = = = = = = = = = = = = = = = = = = =//
+
+4
+//- - - - - - - - -//
+Some say '90s, others say 90's, but I can't say which is best.
+//- - - - - - - - -//
+Some say ’90s, others say 90’s, but I can’t say which is best.
+//= = = = = = = = = = = = = = = = = = = = = = = =//
+
+5: contractions
+//- - - - - - - - -//
+Alice's, I'm ,Don't, You'd
+
+I've, I'll, You're
+
+[Cat][]'s Pajamas
+
+Yahoo!'s
+
+[Cat]: http://example.com
+//- - - - - - - - -//
+Alice’s, I’m ,Don’t, You’d
+I’ve, I’ll, You’re
+Cat ’s Pajamas
+Yahoo!’s
+//= = = = = = = = = = = = = = = = = = = = = = = =//
+
+6: "" after digits are an inch
+//- - - - - - - - -//
+My height is 5'6"".
+//- - - - - - - - -//
+My height is 5'6"".
+//= = = = = = = = = = = = = = = = = = = = = = = =//
+
+7: quote followed by ,.?! and spaces maybe a closer
+//- - - - - - - - -//
+reported "issue 1 (IE-only)", "issue 2", 'issue3 (FF-only)', 'issue4'
+//- - - - - - - - -//
+reported “issue 1 (IE-only)”, “issue 2”, ‘issue3 (FF-only)’, ‘issue4’
+//= = = = = = = = = = = = = = = = = = = = = = = =//
+
+8: handle inches in qoutes
+//- - - - - - - - -//
+"Monitor 21"" and "Monitor""
+//- - - - - - - - -//
+“Monitor 21"” and “Monitor”"
+//= = = = = = = = = = = = = = = = = = = = = = = =//
+
+9: Closing quotation marks within italics
+//- - - - - - - - -//
+*"At first, things were not clear."*
+//- - - - - - - - -//
+“At first, things were not clear.”
+//= = = = = = = = = = = = = = = = = = = = = = = =//
+
+10: Closing quotation marks within boldfacing
+//- - - - - - - - -//
+**"At first, things were not clear."**
+//- - - - - - - - -//
+“At first, things were not clear.”
+//= = = = = = = = = = = = = = = = = = = = = = = =//
+
+11: Closing quotation marks within boldfacing and italics
+//- - - - - - - - -//
+***"At first, things were not clear."***
+//- - - - - - - - -//
+“At first, things were not clear.”
+//= = = = = = = = = = = = = = = = = = = = = = = =//
+
+12: Closing quotation marks within boldfacing and italics
+//- - - - - - - - -//
+***"At first, things were not clear."***
+//- - - - - - - - -//
+“At first, things were not clear.”
+//= = = = = = = = = = = = = = = = = = = = = = = =//
+
+13: Plural possessives
+//- - - - - - - - -//
+John's dog is named Sam. The Smiths' dog is named Rover.
+//- - - - - - - - -//
+John’s dog is named Sam. The Smiths’ dog is named Rover.
+//= = = = = = = = = = = = = = = = = = = = = = = =//
+
+14: Links within quotation marks and parenthetical phrases
+//- - - - - - - - -//
+This is not difficult (see "[Introduction to Hugo Templating](https://gohugo.io/templates/introduction/)").
+//- - - - - - - - -//
+This is not difficult (see “Introduction to Hugo Templating ”).
+//= = = = = = = = = = = = = = = = = = = = = = = =//
+
+15: Quotation marks within links
+//- - - - - - - - -//
+Apple's early Cairo font gave us ["moof" and the "dogcow."](https://www.macworld.com/article/2926184/we-miss-you-clarus-the-dogcow.html)
+//- - - - - - - - -//
+Apple’s early Cairo font gave us “moof” and the “dogcow.”
+//= = = = = = = = = = = = = = = = = = = = = = = =//
+
+16: Single closing quotation marks with slang/informalities
+//- - - - - - - - -//
+"I'm not doin' that," Bill said with emphasis.
+//- - - - - - - - -//
+“I’m not doin’ that,” Bill said with emphasis.
+//= = = = = = = = = = = = = = = = = = = = = = = =//
+
+17: Closing single quotation marks in quotations-within-quotations
+//- - - - - - - - -//
+Janet said, "When everything is 'breaking news,' nothing is 'breaking news.'"
+//- - - - - - - - -//
+Janet said, “When everything is ‘breaking news,’ nothing is ‘breaking news.’”
+//= = = = = = = = = = = = = = = = = = = = = = = =//
+
+18: Opening single quotation marks for abbreviations
+//- - - - - - - - -//
+We're talking about the internet --- 'net for short. Let's rock 'n roll!
+//- - - - - - - - -//
+We’re talking about the internet — ’net for short. Let’s rock ’n roll!
+//= = = = = = = = = = = = = = = = = = = = = = = =//
+
+19: Quotes in alt text
+//- - - - - - - - -//
+
+//- - - - - - - - -//
+
+//= = = = = = = = = = = = = = = = = = = = = = = =//
diff --git a/backend/goldmark/extension/ast/definition_list.go b/backend/goldmark/extension/ast/definition_list.go
new file mode 100644
index 0000000..0ff7412
--- /dev/null
+++ b/backend/goldmark/extension/ast/definition_list.go
@@ -0,0 +1,99 @@
+package ast
+
+import (
+ gast "github.com/yuin/goldmark/ast"
+)
+
+// A DefinitionList struct represents a definition list of Markdown
+// (PHPMarkdownExtra) text.
+type DefinitionList struct {
+ gast.BaseBlock
+ Offset int
+ TemporaryParagraph *gast.Paragraph
+}
+
+// Dump implements Node.Dump.
+func (n *DefinitionList) Dump(source []byte, level int) {
+ gast.DumpHelper(n, source, level, nil, nil)
+}
+
+// Pos implements Node.Pos.
+func (n *DefinitionList) Pos() int {
+ if n.FirstChild() != nil {
+ return n.FirstChild().Pos()
+ }
+ return -1
+}
+
+// KindDefinitionList is a NodeKind of the DefinitionList node.
+var KindDefinitionList = gast.NewNodeKind("DefinitionList")
+
+// Kind implements Node.Kind.
+func (n *DefinitionList) Kind() gast.NodeKind {
+ return KindDefinitionList
+}
+
+// NewDefinitionList returns a new DefinitionList node.
+func NewDefinitionList(offset int, para *gast.Paragraph) *DefinitionList {
+ return &DefinitionList{
+ Offset: offset,
+ TemporaryParagraph: para,
+ }
+}
+
+// A DefinitionTerm struct represents a definition list term of Markdown
+// (PHPMarkdownExtra) text.
+type DefinitionTerm struct {
+ gast.BaseBlock
+}
+
+// Dump implements Node.Dump.
+func (n *DefinitionTerm) Dump(source []byte, level int) {
+ gast.DumpHelper(n, source, level, nil, nil)
+}
+
+// Pos implements Node.Pos.
+func (n *DefinitionTerm) Pos() int {
+ if n.Lines().Len() == 0 {
+ return -1
+ }
+ return n.Lines().At(0).Start
+}
+
+// KindDefinitionTerm is a NodeKind of the DefinitionTerm node.
+var KindDefinitionTerm = gast.NewNodeKind("DefinitionTerm")
+
+// Kind implements Node.Kind.
+func (n *DefinitionTerm) Kind() gast.NodeKind {
+ return KindDefinitionTerm
+}
+
+// NewDefinitionTerm returns a new DefinitionTerm node.
+func NewDefinitionTerm() *DefinitionTerm {
+ return &DefinitionTerm{}
+}
+
+// A DefinitionDescription struct represents a definition list description of Markdown
+// (PHPMarkdownExtra) text.
+type DefinitionDescription struct {
+ gast.BaseBlock
+ IsTight bool
+}
+
+// Dump implements Node.Dump.
+func (n *DefinitionDescription) Dump(source []byte, level int) {
+ gast.DumpHelper(n, source, level, nil, nil)
+}
+
+// KindDefinitionDescription is a NodeKind of the DefinitionDescription node.
+var KindDefinitionDescription = gast.NewNodeKind("DefinitionDescription")
+
+// Kind implements Node.Kind.
+func (n *DefinitionDescription) Kind() gast.NodeKind {
+ return KindDefinitionDescription
+}
+
+// NewDefinitionDescription returns a new DefinitionDescription node.
+func NewDefinitionDescription() *DefinitionDescription {
+ return &DefinitionDescription{}
+}
diff --git a/backend/goldmark/extension/ast/footnote.go b/backend/goldmark/extension/ast/footnote.go
new file mode 100644
index 0000000..b24eafe
--- /dev/null
+++ b/backend/goldmark/extension/ast/footnote.go
@@ -0,0 +1,138 @@
+package ast
+
+import (
+ "fmt"
+
+ gast "github.com/yuin/goldmark/ast"
+)
+
+// A FootnoteLink struct represents a link to a footnote of Markdown
+// (PHP Markdown Extra) text.
+type FootnoteLink struct {
+ gast.BaseInline
+ 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)
+}
+
+// KindFootnoteLink is a NodeKind of the FootnoteLink node.
+var KindFootnoteLink = gast.NewNodeKind("FootnoteLink")
+
+// Kind implements Node.Kind.
+func (n *FootnoteLink) Kind() gast.NodeKind {
+ return KindFootnoteLink
+}
+
+// NewFootnoteLink returns a new FootnoteLink node.
+func NewFootnoteLink(index int) *FootnoteLink {
+ return &FootnoteLink{
+ Index: index,
+ RefCount: 0,
+ RefIndex: 0,
+ }
+}
+
+// A FootnoteBacklink struct represents a link to a footnote of Markdown
+// (PHP Markdown Extra) text.
+type FootnoteBacklink struct {
+ gast.BaseInline
+ Index int
+ RefCount int
+ RefIndex int
+}
+
+// Dump implements Node.Dump.
+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")
+
+// Kind implements Node.Kind.
+func (n *FootnoteBacklink) Kind() gast.NodeKind {
+ return KindFootnoteBacklink
+}
+
+// NewFootnoteBacklink returns a new FootnoteBacklink node.
+func NewFootnoteBacklink(index int) *FootnoteBacklink {
+ return &FootnoteBacklink{
+ Index: index,
+ RefCount: 0,
+ RefIndex: 0,
+ }
+}
+
+// A Footnote struct represents a footnote of Markdown
+// (PHP Markdown Extra) text.
+type Footnote struct {
+ gast.BaseBlock
+ Ref []byte
+ Index int
+}
+
+// Dump implements Node.Dump.
+func (n *Footnote) Dump(source []byte, level int) {
+ m := map[string]string{}
+ m["Index"] = fmt.Sprintf("%v", n.Index)
+ m["Ref"] = string(n.Ref)
+ gast.DumpHelper(n, source, level, m, nil)
+}
+
+// KindFootnote is a NodeKind of the Footnote node.
+var KindFootnote = gast.NewNodeKind("Footnote")
+
+// Kind implements Node.Kind.
+func (n *Footnote) Kind() gast.NodeKind {
+ return KindFootnote
+}
+
+// NewFootnote returns a new Footnote node.
+func NewFootnote(ref []byte) *Footnote {
+ return &Footnote{
+ Ref: ref,
+ Index: -1,
+ }
+}
+
+// A FootnoteList struct represents footnotes of Markdown
+// (PHP Markdown Extra) text.
+type FootnoteList struct {
+ gast.BaseBlock
+ Count int
+}
+
+// Dump implements Node.Dump.
+func (n *FootnoteList) Dump(source []byte, level int) {
+ m := map[string]string{}
+ m["Count"] = fmt.Sprintf("%v", n.Count)
+ gast.DumpHelper(n, source, level, m, nil)
+}
+
+// KindFootnoteList is a NodeKind of the FootnoteList node.
+var KindFootnoteList = gast.NewNodeKind("FootnoteList")
+
+// Kind implements Node.Kind.
+func (n *FootnoteList) Kind() gast.NodeKind {
+ return KindFootnoteList
+}
+
+// NewFootnoteList returns a new FootnoteList node.
+func NewFootnoteList() *FootnoteList {
+ return &FootnoteList{
+ Count: 0,
+ }
+}
diff --git a/backend/goldmark/extension/ast/strikethrough.go b/backend/goldmark/extension/ast/strikethrough.go
new file mode 100644
index 0000000..a9216b7
--- /dev/null
+++ b/backend/goldmark/extension/ast/strikethrough.go
@@ -0,0 +1,29 @@
+// Package ast defines AST nodes that represents extension's elements
+package ast
+
+import (
+ gast "github.com/yuin/goldmark/ast"
+)
+
+// A Strikethrough struct represents a strikethrough of GFM text.
+type Strikethrough struct {
+ gast.BaseInline
+}
+
+// Dump implements Node.Dump.
+func (n *Strikethrough) Dump(source []byte, level int) {
+ gast.DumpHelper(n, source, level, nil, nil)
+}
+
+// KindStrikethrough is a NodeKind of the Strikethrough node.
+var KindStrikethrough = gast.NewNodeKind("Strikethrough")
+
+// Kind implements Node.Kind.
+func (n *Strikethrough) Kind() gast.NodeKind {
+ return KindStrikethrough
+}
+
+// NewStrikethrough returns a new Strikethrough node.
+func NewStrikethrough() *Strikethrough {
+ return &Strikethrough{}
+}
diff --git a/backend/goldmark/extension/ast/table.go b/backend/goldmark/extension/ast/table.go
new file mode 100644
index 0000000..ba87048
--- /dev/null
+++ b/backend/goldmark/extension/ast/table.go
@@ -0,0 +1,159 @@
+package ast
+
+import (
+ "fmt"
+ "strings"
+
+ gast "github.com/yuin/goldmark/ast"
+)
+
+// Alignment is a text alignment of table cells.
+type Alignment int
+
+const (
+ // AlignLeft indicates text should be left justified.
+ AlignLeft Alignment = iota + 1
+
+ // AlignRight indicates text should be right justified.
+ AlignRight
+
+ // AlignCenter indicates text should be centered.
+ AlignCenter
+
+ // AlignNone indicates text should be aligned by default manner.
+ AlignNone
+)
+
+func (a Alignment) String() string {
+ switch a {
+ case AlignLeft:
+ return "left"
+ case AlignRight:
+ return "right"
+ case AlignCenter:
+ return "center"
+ case AlignNone:
+ return "none"
+ }
+ return ""
+}
+
+// A Table struct represents a table of Markdown(GFM) text.
+type Table struct {
+ gast.BaseBlock
+
+ // Alignments returns alignments of the columns.
+ Alignments []Alignment
+}
+
+// Dump implements Node.Dump.
+func (n *Table) Dump(source []byte, level int) {
+ gast.DumpHelper(n, source, level, nil, func(level int) {
+ indent := strings.Repeat(" ", level)
+ fmt.Printf("%sAlignments {\n", indent)
+ for i, alignment := range n.Alignments {
+ indent2 := strings.Repeat(" ", level+1)
+ fmt.Printf("%s%s", indent2, alignment.String())
+ if i != len(n.Alignments)-1 {
+ fmt.Println("")
+ }
+ }
+ fmt.Printf("\n%s}\n", indent)
+ })
+}
+
+// KindTable is a NodeKind of the Table node.
+var KindTable = gast.NewNodeKind("Table")
+
+// Kind implements Node.Kind.
+func (n *Table) Kind() gast.NodeKind {
+ return KindTable
+}
+
+// NewTable returns a new Table node.
+func NewTable() *Table {
+ return &Table{
+ Alignments: []Alignment{},
+ }
+}
+
+// A TableRow struct represents a table row of Markdown(GFM) text.
+type TableRow struct {
+ gast.BaseBlock
+ Alignments []Alignment
+}
+
+// Dump implements Node.Dump.
+func (n *TableRow) Dump(source []byte, level int) {
+ gast.DumpHelper(n, source, level, nil, nil)
+}
+
+// KindTableRow is a NodeKind of the TableRow node.
+var KindTableRow = gast.NewNodeKind("TableRow")
+
+// Kind implements Node.Kind.
+func (n *TableRow) Kind() gast.NodeKind {
+ return KindTableRow
+}
+
+// NewTableRow returns a new TableRow node.
+func NewTableRow(alignments []Alignment) *TableRow {
+ return &TableRow{Alignments: alignments}
+}
+
+// A TableHeader struct represents a table header of Markdown(GFM) text.
+type TableHeader struct {
+ gast.BaseBlock
+ Alignments []Alignment
+}
+
+// KindTableHeader is a NodeKind of the TableHeader node.
+var KindTableHeader = gast.NewNodeKind("TableHeader")
+
+// Kind implements Node.Kind.
+func (n *TableHeader) Kind() gast.NodeKind {
+ return KindTableHeader
+}
+
+// Dump implements Node.Dump.
+func (n *TableHeader) Dump(source []byte, level int) {
+ gast.DumpHelper(n, source, level, nil, nil)
+}
+
+// NewTableHeader returns a new TableHeader node.
+func NewTableHeader(row *TableRow) *TableHeader {
+ n := &TableHeader{}
+ n.SetPos(row.Pos())
+ for c := row.FirstChild(); c != nil; {
+ next := c.NextSibling()
+ n.AppendChild(n, c)
+ c = next
+ }
+ return n
+}
+
+// A TableCell struct represents a table cell of a Markdown(GFM) text.
+type TableCell struct {
+ gast.BaseBlock
+ Alignment Alignment
+}
+
+// Dump implements Node.Dump.
+func (n *TableCell) Dump(source []byte, level int) {
+ gast.DumpHelper(n, source, level, nil, nil)
+}
+
+// KindTableCell is a NodeKind of the TableCell node.
+var KindTableCell = gast.NewNodeKind("TableCell")
+
+// Kind implements Node.Kind.
+func (n *TableCell) Kind() gast.NodeKind {
+ return KindTableCell
+}
+
+// NewTableCell returns a new TableCell node.
+func NewTableCell() *TableCell {
+ return &TableCell{
+ Alignment: AlignNone,
+ }
+}
diff --git a/backend/goldmark/extension/ast/tasklist.go b/backend/goldmark/extension/ast/tasklist.go
new file mode 100644
index 0000000..16abf95
--- /dev/null
+++ b/backend/goldmark/extension/ast/tasklist.go
@@ -0,0 +1,36 @@
+package ast
+
+import (
+ "fmt"
+
+ gast "github.com/yuin/goldmark/ast"
+)
+
+// A TaskCheckBox struct represents a checkbox of a task list.
+type TaskCheckBox struct {
+ gast.BaseInline
+ IsChecked bool
+}
+
+// Dump implements Node.Dump.
+func (n *TaskCheckBox) Dump(source []byte, level int) {
+ m := map[string]string{
+ "Checked": fmt.Sprintf("%v", n.IsChecked),
+ }
+ gast.DumpHelper(n, source, level, m, nil)
+}
+
+// KindTaskCheckBox is a NodeKind of the TaskCheckBox node.
+var KindTaskCheckBox = gast.NewNodeKind("TaskCheckBox")
+
+// Kind implements Node.Kind.
+func (n *TaskCheckBox) Kind() gast.NodeKind {
+ return KindTaskCheckBox
+}
+
+// NewTaskCheckBox returns a new TaskCheckBox node.
+func NewTaskCheckBox(checked bool) *TaskCheckBox {
+ return &TaskCheckBox{
+ IsChecked: checked,
+ }
+}
diff --git a/backend/goldmark/extension/ast_test.go b/backend/goldmark/extension/ast_test.go
new file mode 100644
index 0000000..8fb4039
--- /dev/null
+++ b/backend/goldmark/extension/ast_test.go
@@ -0,0 +1,123 @@
+package extension
+
+import (
+ "bytes"
+ "testing"
+
+ "github.com/yuin/goldmark"
+ "github.com/yuin/goldmark/renderer/html"
+ "github.com/yuin/goldmark/testutil"
+ "github.com/yuin/goldmark/text"
+)
+
+func TestASTBlockNodeText(t *testing.T) {
+ var cases = []struct {
+ Name string
+ Source string
+ T1 string
+ T2 string
+ C bool
+ }{
+ {
+ Name: "DefinitionList",
+ Source: `c1
+: c2
+ c3
+
+a
+
+c4
+: c5
+ c6`,
+ T1: `c1c2
+c3`,
+ T2: `c4c5
+c6`,
+ },
+ {
+ Name: "Table",
+ Source: `| h1 | h2 |
+| -- | -- |
+| c1 | c2 |
+
+a
+
+
+| h3 | h4 |
+| -- | -- |
+| c3 | c4 |`,
+
+ T1: `h1h2c1c2`,
+ T2: `h3h4c3c4`,
+ },
+ }
+
+ for _, cs := range cases {
+ t.Run(cs.Name, func(t *testing.T) {
+ s := []byte(cs.Source)
+ md := goldmark.New(
+ goldmark.WithRendererOptions(
+ html.WithUnsafe(),
+ ),
+ goldmark.WithExtensions(
+ DefinitionList,
+ Table,
+ ),
+ )
+ n := md.Parser().Parse(text.NewReader(s))
+ c1 := n.FirstChild()
+ c2 := c1.NextSibling().NextSibling()
+ if cs.C {
+ c1 = c1.FirstChild()
+ c2 = c2.FirstChild()
+ }
+ if !bytes.Equal(c1.Text(s), []byte(cs.T1)) { // nolint: staticcheck
+
+ t.Errorf("%s unmatch:\n%s", cs.Name, testutil.DiffPretty(c1.Text(s), []byte(cs.T1))) // nolint: staticcheck
+
+ }
+ if !bytes.Equal(c2.Text(s), []byte(cs.T2)) { // nolint: staticcheck
+
+ t.Errorf("%s(EOF) unmatch: %s", cs.Name, testutil.DiffPretty(c2.Text(s), []byte(cs.T2))) // nolint: staticcheck
+
+ }
+ })
+ }
+
+}
+
+func TestASTInlineNodeText(t *testing.T) {
+ var cases = []struct {
+ Name string
+ Source string
+ T1 string
+ }{
+ {
+ Name: "Strikethrough",
+ Source: `~c1 *c2*~`,
+ T1: `c1 c2`,
+ },
+ }
+
+ for _, cs := range cases {
+ t.Run(cs.Name, func(t *testing.T) {
+ s := []byte(cs.Source)
+ md := goldmark.New(
+ goldmark.WithRendererOptions(
+ html.WithUnsafe(),
+ ),
+ goldmark.WithExtensions(
+ Strikethrough,
+ ),
+ )
+ n := md.Parser().Parse(text.NewReader(s))
+ c1 := n.FirstChild().FirstChild()
+ if !bytes.Equal(c1.Text(s), []byte(cs.T1)) { // nolint: staticcheck
+
+ t.Errorf("%s unmatch:\n%s", cs.Name, testutil.DiffPretty(c1.Text(s), []byte(cs.T1))) // nolint: staticcheck
+
+ }
+ })
+ }
+
+}
diff --git a/backend/goldmark/extension/cjk.go b/backend/goldmark/extension/cjk.go
new file mode 100644
index 0000000..a3238c2
--- /dev/null
+++ b/backend/goldmark/extension/cjk.go
@@ -0,0 +1,72 @@
+package extension
+
+import (
+ "github.com/yuin/goldmark"
+ "github.com/yuin/goldmark/parser"
+ "github.com/yuin/goldmark/renderer/html"
+)
+
+// A CJKOption sets options for CJK support mostly for HTML based renderers.
+type CJKOption func(*cjk)
+
+// A EastAsianLineBreaks is a style of east asian line breaks.
+type EastAsianLineBreaks int
+
+const (
+ //EastAsianLineBreaksNone renders line breaks as it is.
+ EastAsianLineBreaksNone EastAsianLineBreaks = iota
+ // EastAsianLineBreaksSimple is a style where soft line breaks are ignored
+ // if both sides of the break are east asian wide characters.
+ EastAsianLineBreaksSimple
+ // EastAsianLineBreaksCSS3Draft is a style where soft line breaks are ignored
+ // even if only one side of the break is an east asian wide character.
+ EastAsianLineBreaksCSS3Draft
+)
+
+// WithEastAsianLineBreaks is a functional option that indicates whether softline breaks
+// between east asian wide characters should be ignored.
+// style defauts to [EastAsianLineBreaksSimple] .
+func WithEastAsianLineBreaks(style ...EastAsianLineBreaks) CJKOption {
+ return func(c *cjk) {
+ if len(style) == 0 {
+ c.EastAsianLineBreaks = EastAsianLineBreaksSimple
+ return
+ }
+ c.EastAsianLineBreaks = style[0]
+ }
+}
+
+// WithEscapedSpace is a functional option that indicates that a '\' escaped half-space(0x20) should not be rendered.
+func WithEscapedSpace() CJKOption {
+ return func(c *cjk) {
+ c.EscapedSpace = true
+ }
+}
+
+type cjk struct {
+ EastAsianLineBreaks EastAsianLineBreaks
+ EscapedSpace bool
+}
+
+// CJK is a goldmark extension that provides functionalities for CJK languages.
+var CJK = NewCJK(WithEastAsianLineBreaks(), WithEscapedSpace())
+
+// NewCJK returns a new extension with given options.
+func NewCJK(opts ...CJKOption) goldmark.Extender {
+ e := &cjk{
+ EastAsianLineBreaks: EastAsianLineBreaksNone,
+ }
+ for _, opt := range opts {
+ opt(e)
+ }
+ return e
+}
+
+func (e *cjk) Extend(m goldmark.Markdown) {
+ m.Renderer().AddOptions(html.WithEastAsianLineBreaks(
+ html.EastAsianLineBreaks(e.EastAsianLineBreaks)))
+ if e.EscapedSpace {
+ m.Renderer().AddOptions(html.WithWriter(html.NewWriter(html.WithEscapedSpace())))
+ m.Parser().AddOptions(parser.WithEscapedSpace())
+ }
+}
diff --git a/backend/goldmark/extension/cjk_test.go b/backend/goldmark/extension/cjk_test.go
new file mode 100644
index 0000000..0eaa26c
--- /dev/null
+++ b/backend/goldmark/extension/cjk_test.go
@@ -0,0 +1,269 @@
+package extension
+
+import (
+ "testing"
+
+ "github.com/yuin/goldmark"
+ "github.com/yuin/goldmark/renderer/html"
+ "github.com/yuin/goldmark/testutil"
+)
+
+func TestEscapedSpace(t *testing.T) {
+ markdown := goldmark.New(goldmark.WithRendererOptions(
+ html.WithXHTML(),
+ html.WithUnsafe(),
+ ))
+ no := 1
+ testutil.DoTestCase(
+ markdown,
+ testutil.MarkdownTestCase{
+ No: no,
+ Description: "Without spaces around an emphasis started with east asian punctuations, it is not interpreted as an emphasis(as defined in CommonMark spec)",
+ Markdown: "太郎は**「こんにちわ」**と言った\nんです",
+ Expected: "太郎は**「こんにちわ」**と言った\nんです
",
+ },
+ t,
+ )
+
+ no = 2
+ testutil.DoTestCase(
+ markdown,
+ testutil.MarkdownTestCase{
+ No: no,
+ Description: "With spaces around an emphasis started with east asian punctuations, it is interpreted as an emphasis(but remains unnecessary spaces)",
+ Markdown: "太郎は **「こんにちわ」** と言った\nんです",
+ Expected: "太郎は 「こんにちわ」 と言った\nんです
",
+ },
+ t,
+ )
+
+ // Enables EscapedSpace
+ markdown = goldmark.New(goldmark.WithRendererOptions(
+ html.WithXHTML(),
+ html.WithUnsafe(),
+ ),
+ goldmark.WithExtensions(NewCJK(WithEscapedSpace())),
+ )
+
+ no = 3
+ testutil.DoTestCase(
+ markdown,
+ testutil.MarkdownTestCase{
+ No: no,
+ Description: "With spaces around an emphasis started with east asian punctuations,it is interpreted as an emphasis",
+ Markdown: "太郎は\\ **「こんにちわ」**\\ と言った\nんです",
+ Expected: "太郎は「こんにちわ」 と言った\nんです
",
+ },
+ t,
+ )
+
+ // ' ' triggers Linkify extension inline parser.
+ // Escaped spaces should not trigger the inline parser.
+
+ markdown = goldmark.New(goldmark.WithRendererOptions(
+ html.WithXHTML(),
+ html.WithUnsafe(),
+ ),
+ goldmark.WithExtensions(
+ NewCJK(WithEscapedSpace()),
+ Linkify,
+ ),
+ )
+
+ no = 4
+ testutil.DoTestCase(
+ markdown,
+ testutil.MarkdownTestCase{
+ No: no,
+ Description: "Escaped space and linkfy extension",
+ Markdown: "太郎は\\ **「こんにちわ」**\\ と言った\nんです",
+ Expected: "太郎は「こんにちわ」 と言った\nんです
",
+ },
+ t,
+ )
+}
+
+func TestEastAsianLineBreaks(t *testing.T) {
+ markdown := goldmark.New(goldmark.WithRendererOptions(
+ html.WithXHTML(),
+ html.WithUnsafe(),
+ ))
+ no := 1
+ testutil.DoTestCase(
+ markdown,
+ testutil.MarkdownTestCase{
+ No: no,
+ Description: "Soft line breaks are rendered as a newline, so some asian users will see it as an unnecessary space",
+ Markdown: "太郎は\\ **「こんにちわ」**\\ と言った\nんです",
+ Expected: "太郎は\\ 「こんにちわ」 \\ と言った\nんです
",
+ },
+ t,
+ )
+
+ // Enables EastAsianLineBreaks
+
+ markdown = goldmark.New(goldmark.WithRendererOptions(
+ html.WithXHTML(),
+ html.WithUnsafe(),
+ ),
+ goldmark.WithExtensions(NewCJK(WithEastAsianLineBreaks())),
+ )
+
+ no = 2
+ testutil.DoTestCase(
+ markdown,
+ testutil.MarkdownTestCase{
+ No: no,
+ Description: "Soft line breaks between east asian wide characters are ignored",
+ Markdown: "太郎は\\ **「こんにちわ」**\\ と言った\nんです",
+ Expected: "太郎は\\ 「こんにちわ」 \\ と言ったんです
",
+ },
+ t,
+ )
+
+ no = 3
+ testutil.DoTestCase(
+ markdown,
+ testutil.MarkdownTestCase{
+ No: no,
+ Description: "Soft line breaks between western characters are rendered as a newline",
+ Markdown: "太郎は\\ **「こんにちわ」**\\ と言ったa\nbんです",
+ Expected: "太郎は\\ 「こんにちわ」 \\ と言ったa\nbんです
",
+ },
+ t,
+ )
+
+ no = 4
+ testutil.DoTestCase(
+ markdown,
+ testutil.MarkdownTestCase{
+ No: no,
+ Description: "Soft line breaks between a western character and an east asian wide character are rendered as a newline",
+ Markdown: "太郎は\\ **「こんにちわ」**\\ と言ったa\nんです",
+ Expected: "太郎は\\ 「こんにちわ」 \\ と言ったa\nんです
",
+ },
+ t,
+ )
+
+ no = 5
+ testutil.DoTestCase(
+ markdown,
+ testutil.MarkdownTestCase{
+ No: no,
+ Description: "Soft line breaks between an east asian wide character and a western character are rendered as a newline",
+ Markdown: "太郎は\\ **「こんにちわ」**\\ と言った\nbんです",
+ Expected: "太郎は\\ 「こんにちわ」 \\ と言った\nbんです
",
+ },
+ t,
+ )
+
+ // WithHardWraps take precedence over WithEastAsianLineBreaks
+ markdown = goldmark.New(goldmark.WithRendererOptions(
+ html.WithHardWraps(),
+ html.WithXHTML(),
+ html.WithUnsafe(),
+ ),
+ goldmark.WithExtensions(NewCJK(WithEastAsianLineBreaks())),
+ )
+ no = 6
+ testutil.DoTestCase(
+ markdown,
+ testutil.MarkdownTestCase{
+ No: no,
+ Description: "WithHardWraps take precedence over WithEastAsianLineBreaks",
+ Markdown: "太郎は\\ **「こんにちわ」**\\ と言った\nんです",
+ Expected: "太郎は\\ 「こんにちわ」 \\ と言った \nんです
",
+ },
+ t,
+ )
+
+ // Tests with EastAsianLineBreaksStyleSimple
+ markdown = goldmark.New(goldmark.WithRendererOptions(
+ html.WithXHTML(),
+ html.WithUnsafe(),
+ ),
+ goldmark.WithExtensions(
+ NewCJK(WithEastAsianLineBreaks()),
+ Linkify,
+ ),
+ )
+ no = 7
+ testutil.DoTestCase(
+ markdown,
+ testutil.MarkdownTestCase{
+ No: no,
+ Description: "WithEastAsianLineBreaks and linkfy extension",
+ Markdown: "太郎は\\ **「こんにちわ」**\\ と言った\r\nんです",
+ Expected: "太郎は\\ 「こんにちわ」 \\ と言ったんです
",
+ },
+ t,
+ )
+ no = 8
+ testutil.DoTestCase(
+ markdown,
+ testutil.MarkdownTestCase{
+ No: no,
+ Description: "Soft line breaks between east asian wide characters or punctuations are ignored",
+ Markdown: "太郎は\\ **「こんにちわ」**\\ と、\r\n言った\r\nんです",
+ Expected: "太郎は\\ 「こんにちわ」 \\ と、言ったんです
",
+ },
+ t,
+ )
+ no = 9
+ testutil.DoTestCase(
+ markdown,
+ testutil.MarkdownTestCase{
+ No: no,
+ Description: "Soft line breaks between an east asian wide character and a western character are ignored",
+ Markdown: "私はプログラマーです。\n東京の会社に勤めています。\nGoでWebアプリケーションを開発しています。",
+ Expected: "私はプログラマーです。東京の会社に勤めています。\nGoでWebアプリケーションを開発しています。
",
+ },
+ t,
+ )
+
+ // Tests with EastAsianLineBreaksCSS3Draft
+ markdown = goldmark.New(goldmark.WithRendererOptions(
+ html.WithXHTML(),
+ html.WithUnsafe(),
+ ),
+ goldmark.WithExtensions(
+ NewCJK(WithEastAsianLineBreaks(EastAsianLineBreaksCSS3Draft)),
+ ),
+ )
+ no = 10
+ testutil.DoTestCase(
+ markdown,
+ testutil.MarkdownTestCase{
+ No: no,
+ Description: "Soft line breaks between a western character and an east asian wide character are ignored",
+ Markdown: "太郎は\\ **「こんにちわ」**\\ と言ったa\nんです",
+ Expected: "太郎は\\ 「こんにちわ」 \\ と言ったaんです
",
+ },
+ t,
+ )
+
+ no = 11
+ testutil.DoTestCase(
+ markdown,
+ testutil.MarkdownTestCase{
+ No: no,
+ Description: "Soft line breaks between an east asian wide character and a western character are ignored",
+ Markdown: "太郎は\\ **「こんにちわ」**\\ と言った\nbんです",
+ Expected: "太郎は\\ 「こんにちわ」 \\ と言ったbんです
",
+ },
+ t,
+ )
+
+ no = 12
+ testutil.DoTestCase(
+ markdown,
+ testutil.MarkdownTestCase{
+ No: no,
+ Description: "Soft line breaks between an east asian wide character and a western character are ignored",
+ Markdown: "私はプログラマーです。\n東京の会社に勤めています。\nGoでWebアプリケーションを開発しています。",
+ Expected: "私はプログラマーです。東京の会社に勤めています。GoでWebアプリケーションを開発しています。
",
+ },
+ t,
+ )
+
+}
diff --git a/backend/goldmark/extension/definition_list.go b/backend/goldmark/extension/definition_list.go
new file mode 100644
index 0000000..b7a86c0
--- /dev/null
+++ b/backend/goldmark/extension/definition_list.go
@@ -0,0 +1,274 @@
+package extension
+
+import (
+ "github.com/yuin/goldmark"
+ gast "github.com/yuin/goldmark/ast"
+ "github.com/yuin/goldmark/extension/ast"
+ "github.com/yuin/goldmark/parser"
+ "github.com/yuin/goldmark/renderer"
+ "github.com/yuin/goldmark/renderer/html"
+ "github.com/yuin/goldmark/text"
+ "github.com/yuin/goldmark/util"
+)
+
+type definitionListParser struct {
+}
+
+var defaultDefinitionListParser = &definitionListParser{}
+
+// NewDefinitionListParser return a new parser.BlockParser that
+// can parse PHP Markdown Extra Definition lists.
+func NewDefinitionListParser() parser.BlockParser {
+ return defaultDefinitionListParser
+}
+
+func (b *definitionListParser) Trigger() []byte {
+ return []byte{':'}
+}
+
+func (b *definitionListParser) Open(parent gast.Node, reader text.Reader, pc parser.Context) (gast.Node, parser.State) {
+ if _, ok := parent.(*ast.DefinitionList); ok {
+ return nil, parser.NoChildren
+ }
+ line, _ := reader.PeekLine()
+ pos := pc.BlockOffset()
+ indent := pc.BlockIndent()
+ if pos < 0 || line[pos] != ':' || indent != 0 {
+ return nil, parser.NoChildren
+ }
+
+ last := parent.LastChild()
+ // need 1 or more spaces after ':'
+ w, _ := util.IndentWidth(line[pos+1:], pos+1)
+ if w < 1 {
+ return nil, parser.NoChildren
+ }
+ if w >= 8 { // starts with indented code
+ w = 5
+ }
+ w += pos + 1 /* 1 = ':' */
+
+ para, lastIsParagraph := last.(*gast.Paragraph)
+ var list *ast.DefinitionList
+ status := parser.HasChildren
+ var ok bool
+ if lastIsParagraph {
+ list, ok = last.PreviousSibling().(*ast.DefinitionList)
+ if ok { // is not first item
+ list.Offset = w
+ list.TemporaryParagraph = para
+ } else { // is first item
+ list = ast.NewDefinitionList(w, para)
+ status |= parser.RequireParagraph
+ }
+ } else if list, ok = last.(*ast.DefinitionList); ok { // multiple description
+ list.Offset = w
+ list.TemporaryParagraph = nil
+ } else {
+ return nil, parser.NoChildren
+ }
+
+ return list, status
+}
+
+func (b *definitionListParser) Continue(node gast.Node, reader text.Reader, pc parser.Context) parser.State {
+ line, _ := reader.PeekLine()
+ if util.IsBlank(line) {
+ return parser.Continue | parser.HasChildren
+ }
+ list, _ := node.(*ast.DefinitionList)
+ w, _ := util.IndentWidth(line, reader.LineOffset())
+ if w < list.Offset {
+ return parser.Close
+ }
+ pos, padding := util.IndentPosition(line, reader.LineOffset(), list.Offset)
+ reader.AdvanceAndSetPadding(pos, padding)
+ return parser.Continue | parser.HasChildren
+}
+
+func (b *definitionListParser) Close(node gast.Node, reader text.Reader, pc parser.Context) {
+ // nothing to do
+}
+
+func (b *definitionListParser) CanInterruptParagraph() bool {
+ return true
+}
+
+func (b *definitionListParser) CanAcceptIndentedLine() bool {
+ return false
+}
+
+type definitionDescriptionParser struct {
+}
+
+var defaultDefinitionDescriptionParser = &definitionDescriptionParser{}
+
+// NewDefinitionDescriptionParser return a new parser.BlockParser that
+// can parse definition description starts with ':'.
+func NewDefinitionDescriptionParser() parser.BlockParser {
+ return defaultDefinitionDescriptionParser
+}
+
+func (b *definitionDescriptionParser) Trigger() []byte {
+ return []byte{':'}
+}
+
+func (b *definitionDescriptionParser) Open(
+ parent gast.Node, reader text.Reader, pc parser.Context) (gast.Node, parser.State) {
+ line, _ := reader.PeekLine()
+ pos := pc.BlockOffset()
+ indent := pc.BlockIndent()
+ if pos < 0 || line[pos] != ':' || indent != 0 {
+ return nil, parser.NoChildren
+ }
+ list, _ := parent.(*ast.DefinitionList)
+ if list == nil {
+ return nil, parser.NoChildren
+ }
+ para := list.TemporaryParagraph
+ list.TemporaryParagraph = nil
+ if para != nil {
+ lines := para.Lines()
+ l := lines.Len()
+ for i := range l {
+ term := ast.NewDefinitionTerm()
+ segment := lines.At(i)
+ term.Lines().Append(segment.TrimRightSpace(reader.Source()))
+ list.AppendChild(list, term)
+ }
+ para.Parent().RemoveChild(para.Parent(), para)
+ }
+ cpos, padding := util.IndentPosition(line[pos+1:], pos+1, list.Offset-pos-1)
+ reader.AdvanceAndSetPadding(cpos+1, padding)
+
+ return ast.NewDefinitionDescription(), parser.HasChildren
+}
+
+func (b *definitionDescriptionParser) Continue(node gast.Node, reader text.Reader, pc parser.Context) parser.State {
+ // definitionListParser detects end of the description.
+ // so this method will never be called.
+ return parser.Continue | parser.HasChildren
+}
+
+func (b *definitionDescriptionParser) Close(node gast.Node, reader text.Reader, pc parser.Context) {
+ desc := node.(*ast.DefinitionDescription)
+ desc.IsTight = !desc.HasBlankPreviousLines()
+ if desc.IsTight {
+ for gc := desc.FirstChild(); gc != nil; gc = gc.NextSibling() {
+ paragraph, ok := gc.(*gast.Paragraph)
+ if ok {
+ textBlock := gast.NewTextBlock()
+ textBlock.SetLines(paragraph.Lines())
+ desc.ReplaceChild(desc, paragraph, textBlock)
+ }
+ }
+ }
+}
+
+func (b *definitionDescriptionParser) CanInterruptParagraph() bool {
+ return true
+}
+
+func (b *definitionDescriptionParser) CanAcceptIndentedLine() bool {
+ return false
+}
+
+// DefinitionListHTMLRenderer is a renderer.NodeRenderer implementation that
+// renders DefinitionList nodes.
+type DefinitionListHTMLRenderer struct {
+ html.Config
+}
+
+// NewDefinitionListHTMLRenderer returns a new DefinitionListHTMLRenderer.
+func NewDefinitionListHTMLRenderer(opts ...html.Option) renderer.NodeRenderer {
+ r := &DefinitionListHTMLRenderer{
+ Config: html.NewConfig(),
+ }
+ for _, opt := range opts {
+ opt.SetHTMLOption(&r.Config)
+ }
+ return r
+}
+
+// RegisterFuncs implements renderer.NodeRenderer.RegisterFuncs.
+func (r *DefinitionListHTMLRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) {
+ reg.Register(ast.KindDefinitionList, r.renderDefinitionList)
+ reg.Register(ast.KindDefinitionTerm, r.renderDefinitionTerm)
+ reg.Register(ast.KindDefinitionDescription, r.renderDefinitionDescription)
+}
+
+// DefinitionListAttributeFilter defines attribute names which dl elements can have.
+var DefinitionListAttributeFilter = html.GlobalAttributeFilter
+
+func (r *DefinitionListHTMLRenderer) renderDefinitionList(
+ w util.BufWriter, source []byte, n gast.Node, entering bool) (gast.WalkStatus, error) {
+ if entering {
+ if n.Attributes() != nil {
+ _, _ = w.WriteString("\n")
+ } else {
+ _, _ = w.WriteString("\n")
+ }
+ } else {
+ _, _ = w.WriteString(" \n")
+ }
+ return gast.WalkContinue, nil
+}
+
+// DefinitionTermAttributeFilter defines attribute names which dd elements can have.
+var DefinitionTermAttributeFilter = html.GlobalAttributeFilter
+
+func (r *DefinitionListHTMLRenderer) renderDefinitionTerm(
+ w util.BufWriter, source []byte, n gast.Node, entering bool) (gast.WalkStatus, error) {
+ if entering {
+ if n.Attributes() != nil {
+ _, _ = w.WriteString("')
+ } else {
+ _, _ = w.WriteString("")
+ }
+ } else {
+ _, _ = w.WriteString(" \n")
+ }
+ return gast.WalkContinue, nil
+}
+
+// DefinitionDescriptionAttributeFilter defines attribute names which dd elements can have.
+var DefinitionDescriptionAttributeFilter = html.GlobalAttributeFilter
+
+func (r *DefinitionListHTMLRenderer) renderDefinitionDescription(
+ w util.BufWriter, source []byte, node gast.Node, entering bool) (gast.WalkStatus, error) {
+ if entering {
+ n := node.(*ast.DefinitionDescription)
+ _, _ = w.WriteString(" ")
+ } else {
+ _, _ = w.WriteString(">\n")
+ }
+ } else {
+ _, _ = w.WriteString(" \n")
+ }
+ return gast.WalkContinue, nil
+}
+
+type definitionList struct {
+}
+
+// DefinitionList is an extension that allow you to use PHP Markdown Extra Definition lists.
+var DefinitionList = &definitionList{}
+
+func (e *definitionList) Extend(m goldmark.Markdown) {
+ m.Parser().AddOptions(parser.WithBlockParsers(
+ util.Prioritized(NewDefinitionListParser(), 101),
+ util.Prioritized(NewDefinitionDescriptionParser(), 102),
+ ))
+ m.Renderer().AddOptions(renderer.WithNodeRenderers(
+ util.Prioritized(NewDefinitionListHTMLRenderer(), 500),
+ ))
+}
diff --git a/backend/goldmark/extension/definition_list_test.go b/backend/goldmark/extension/definition_list_test.go
new file mode 100644
index 0000000..d9dfa6c
--- /dev/null
+++ b/backend/goldmark/extension/definition_list_test.go
@@ -0,0 +1,21 @@
+package extension
+
+import (
+ "testing"
+
+ "github.com/yuin/goldmark"
+ "github.com/yuin/goldmark/renderer/html"
+ "github.com/yuin/goldmark/testutil"
+)
+
+func TestDefinitionList(t *testing.T) {
+ markdown := goldmark.New(
+ goldmark.WithRendererOptions(
+ html.WithUnsafe(),
+ ),
+ goldmark.WithExtensions(
+ DefinitionList,
+ ),
+ )
+ testutil.DoTestCaseFile(markdown, "_test/definition_list.txt", t, testutil.ParseCliCaseArg()...)
+}
diff --git a/backend/goldmark/extension/footnote.go b/backend/goldmark/extension/footnote.go
new file mode 100644
index 0000000..30eb85c
--- /dev/null
+++ b/backend/goldmark/extension/footnote.go
@@ -0,0 +1,691 @@
+package extension
+
+import (
+ "bytes"
+ "fmt"
+ "strconv"
+
+ "github.com/yuin/goldmark"
+ gast "github.com/yuin/goldmark/ast"
+ "github.com/yuin/goldmark/extension/ast"
+ "github.com/yuin/goldmark/parser"
+ "github.com/yuin/goldmark/renderer"
+ "github.com/yuin/goldmark/renderer/html"
+ "github.com/yuin/goldmark/text"
+ "github.com/yuin/goldmark/util"
+)
+
+var footnoteListKey = parser.NewContextKey()
+var footnoteLinkListKey = parser.NewContextKey()
+
+type footnoteBlockParser struct {
+}
+
+var defaultFootnoteBlockParser = &footnoteBlockParser{}
+
+// NewFootnoteBlockParser returns a new parser.BlockParser that can parse
+// footnotes of the Markdown(PHP Markdown Extra) text.
+func NewFootnoteBlockParser() parser.BlockParser {
+ return defaultFootnoteBlockParser
+}
+
+func (b *footnoteBlockParser) Trigger() []byte {
+ return []byte{'['}
+}
+
+func (b *footnoteBlockParser) Open(parent gast.Node, reader text.Reader, pc parser.Context) (gast.Node, parser.State) {
+ line, segment := reader.PeekLine()
+ pos := pc.BlockOffset()
+ if pos < 0 || line[pos] != '[' {
+ return nil, parser.NoChildren
+ }
+ pos++
+ if pos > len(line)-1 || line[pos] != '^' {
+ return nil, parser.NoChildren
+ }
+ open := pos + 1
+ var closes int
+ closure := util.FindClosure(line[pos+1:], '[', ']', false, false) //nolint:staticcheck
+ closes = pos + 1 + closure
+ next := closes + 1
+ if closure > -1 {
+ if next >= len(line) || line[next] != ':' {
+ return nil, parser.NoChildren
+ }
+ } else {
+ return nil, parser.NoChildren
+ }
+ padding := segment.Padding
+ label := reader.Value(text.NewSegment(segment.Start+open-padding, segment.Start+closes-padding))
+ if util.IsBlank(label) {
+ return nil, parser.NoChildren
+ }
+ item := ast.NewFootnote(label)
+
+ pos = next + 1 - padding
+ if pos >= len(line) {
+ reader.Advance(pos)
+ return item, parser.NoChildren
+ }
+ reader.AdvanceAndSetPadding(pos, padding)
+ return item, parser.HasChildren
+}
+
+func (b *footnoteBlockParser) Continue(node gast.Node, reader text.Reader, pc parser.Context) parser.State {
+ line, _ := reader.PeekLine()
+ if util.IsBlank(line) {
+ return parser.Continue | parser.HasChildren
+ }
+ childpos, padding := util.IndentPosition(line, reader.LineOffset(), 4)
+ if childpos < 0 {
+ return parser.Close
+ }
+ reader.AdvanceAndSetPadding(childpos, padding)
+ return parser.Continue | parser.HasChildren
+}
+
+func (b *footnoteBlockParser) Close(node gast.Node, reader text.Reader, pc parser.Context) {
+ var list *ast.FootnoteList
+ if tlist := pc.Get(footnoteListKey); tlist != nil {
+ list = tlist.(*ast.FootnoteList)
+ } else {
+ list = ast.NewFootnoteList()
+ pc.Set(footnoteListKey, list)
+ node.Parent().InsertBefore(node.Parent(), node, list)
+ }
+ node.Parent().RemoveChild(node.Parent(), node)
+ list.AppendChild(list, node)
+}
+
+func (b *footnoteBlockParser) CanInterruptParagraph() bool {
+ return true
+}
+
+func (b *footnoteBlockParser) CanAcceptIndentedLine() bool {
+ return false
+}
+
+type footnoteParser struct {
+}
+
+var defaultFootnoteParser = &footnoteParser{}
+
+// NewFootnoteParser returns a new parser.InlineParser that can parse
+// footnote links of the Markdown(PHP Markdown Extra) text.
+func NewFootnoteParser() parser.InlineParser {
+ return defaultFootnoteParser
+}
+
+func (s *footnoteParser) Trigger() []byte {
+ // footnote syntax probably conflict with the image syntax.
+ // So we need trigger this parser with '!'.
+ return []byte{'!', '['}
+}
+
+func (s *footnoteParser) Parse(parent gast.Node, block text.Reader, pc parser.Context) gast.Node {
+ line, segment := block.PeekLine()
+ pos := 1
+ if len(line) > 0 && line[0] == '!' {
+ pos++
+ }
+ if pos >= len(line) || line[pos] != '^' {
+ return nil
+ }
+ pos++
+ if pos >= len(line) {
+ return nil
+ }
+ open := pos
+ closure := util.FindClosure(line[pos:], '[', ']', false, false) //nolint:staticcheck
+ if closure < 0 {
+ return nil
+ }
+ closes := pos + closure
+ value := block.Value(text.NewSegment(segment.Start+open, segment.Start+closes))
+ block.Advance(closes + 1)
+
+ var list *ast.FootnoteList
+ if tlist := pc.Get(footnoteListKey); tlist != nil {
+ list = tlist.(*ast.FootnoteList)
+ }
+ if list == nil {
+ return nil
+ }
+ index := 0
+ for def := list.FirstChild(); def != nil; def = def.NextSibling() {
+ d := def.(*ast.Footnote)
+ if bytes.Equal(d.Ref, value) {
+ if d.Index < 0 {
+ list.Count++
+ d.Index = list.Count
+ }
+ index = d.Index
+ break
+ }
+ }
+ if index == 0 {
+ return nil
+ }
+
+ 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 {
+}
+
+var defaultFootnoteASTTransformer = &footnoteASTTransformer{}
+
+// NewFootnoteASTTransformer returns a new parser.ASTTransformer that
+// insert a footnote list to the last of the document.
+func NewFootnoteASTTransformer() parser.ASTTransformer {
+ return defaultFootnoteASTTransformer
+}
+
+func (a *footnoteASTTransformer) Transform(node *gast.Document, reader text.Reader, pc parser.Context) {
+ var list *ast.FootnoteList
+ 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
+ }
+ fn := footnote.(*ast.Footnote)
+ index := fn.Index
+ if index < 0 {
+ list.RemoveChild(list, footnote)
+ } else {
+ 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
+ }
+ list.SortChildren(func(n1, n2 gast.Node) int {
+ if n1.(*ast.Footnote).Index < n2.(*ast.Footnote).Index {
+ return -1
+ }
+ return 1
+ })
+ if list.Count <= 0 {
+ list.Parent().RemoveChild(list.Parent(), list)
+ return
+ }
+
+ node.AppendChild(node, list)
+}
+
+// FootnoteConfig holds configuration values for the footnote extension.
+//
+// Link* and Backlink* configurations have some variables:
+// 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).
+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 any) {
+ 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[T []byte | string](a T) FootnoteOption {
+ return &withFootnoteIDPrefix{[]byte(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[T []byte | string](a T) FootnoteOption {
+ return &withFootnoteLinkTitle{[]byte(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[T []byte | string](a T) FootnoteOption {
+ return &withFootnoteBacklinkTitle{[]byte(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[T []byte | string](a T) FootnoteOption {
+ return &withFootnoteLinkClass{[]byte(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[T []byte | string](a T) FootnoteOption {
+ return &withFootnoteBacklinkClass{[]byte(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[T []byte | string](a T) FootnoteOption {
+ return &withFootnoteBacklinkHTML{[]byte(a)}
+}
+
+// FootnoteHTMLRenderer is a renderer.NodeRenderer implementation that
+// renders FootnoteLink nodes.
+type FootnoteHTMLRenderer struct {
+ FootnoteConfig
+}
+
+// NewFootnoteHTMLRenderer returns a new FootnoteHTMLRenderer.
+func NewFootnoteHTMLRenderer(opts ...FootnoteOption) renderer.NodeRenderer {
+ r := &FootnoteHTMLRenderer{
+ FootnoteConfig: NewFootnoteConfig(),
+ }
+ for _, opt := range opts {
+ opt.SetFootnoteOption(&r.FootnoteConfig)
+ }
+ return r
+}
+
+// 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.KindFootnote, r.renderFootnote)
+ reg.Register(ast.KindFootnoteList, r.renderFootnoteList)
+}
+
+func (r *FootnoteHTMLRenderer) renderFootnoteLink(
+ w util.BufWriter, source []byte, node gast.Node, entering bool) (gast.WalkStatus, error) {
+ if entering {
+ n := node.(*ast.FootnoteLink)
+ is := strconv.Itoa(n.Index)
+ _, _ = w.WriteString(` `)
+ }
+ return gast.WalkContinue, nil
+}
+
+func (r *FootnoteHTMLRenderer) renderFootnoteBacklink(
+ w util.BufWriter, source []byte, node gast.Node, entering bool) (gast.WalkStatus, error) {
+ if entering {
+ n := node.(*ast.FootnoteBacklink)
+ is := strconv.Itoa(n.Index)
+ _, _ = w.WriteString(` `)
+ }
+ return gast.WalkContinue, nil
+}
+
+func (r *FootnoteHTMLRenderer) renderFootnote(
+ w util.BufWriter, source []byte, node gast.Node, entering bool) (gast.WalkStatus, error) {
+ n := node.(*ast.Footnote)
+ is := strconv.Itoa(n.Index)
+ if entering {
+ _, _ = w.WriteString(` \n")
+ } else {
+ _, _ = w.WriteString(" \n")
+ }
+ return gast.WalkContinue, nil
+}
+
+func (r *FootnoteHTMLRenderer) renderFootnoteList(
+ w util.BufWriter, source []byte, node gast.Node, entering bool) (gast.WalkStatus, error) {
+ if entering {
+ _, _ = w.WriteString(`\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{
+ 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(
+ parser.WithBlockParsers(
+ util.Prioritized(NewFootnoteBlockParser(), 999),
+ ),
+ parser.WithInlineParsers(
+ util.Prioritized(NewFootnoteParser(), 101),
+ ),
+ parser.WithASTTransformers(
+ util.Prioritized(NewFootnoteASTTransformer(), 999),
+ ),
+ )
+ m.Renderer().AddOptions(renderer.WithNodeRenderers(
+ util.Prioritized(NewFootnoteHTMLRenderer(e.options...), 500),
+ ))
+}
diff --git a/backend/goldmark/extension/footnote_test.go b/backend/goldmark/extension/footnote_test.go
new file mode 100644
index 0000000..af22443
--- /dev/null
+++ b/backend/goldmark/extension/footnote_test.go
@@ -0,0 +1,141 @@
+package extension
+
+import (
+ "testing"
+
+ "github.com/yuin/goldmark"
+ gast "github.com/yuin/goldmark/ast"
+ "github.com/yuin/goldmark/parser"
+ "github.com/yuin/goldmark/renderer/html"
+ "github.com/yuin/goldmark/testutil"
+ "github.com/yuin/goldmark/text"
+ "github.com/yuin/goldmark/util"
+)
+
+func TestFootnote(t *testing.T) {
+ markdown := goldmark.New(
+ goldmark.WithRendererOptions(
+ html.WithUnsafe(),
+ ),
+ goldmark.WithExtensions(
+ Footnote,
+ ),
+ )
+ testutil.DoTestCaseFile(markdown, "_test/footnote.txt", t, testutil.ParseCliCaseArg()...)
+}
+
+type footnoteID struct {
+}
+
+func (a *footnoteID) Transform(node *gast.Document, reader text.Reader, pc parser.Context) {
+ node.Meta()["footnote-prefix"] = "article12-"
+}
+
+func TestFootnoteOptions(t *testing.T) {
+ markdown := goldmark.New(
+ goldmark.WithRendererOptions(
+ html.WithUnsafe(),
+ ),
+ goldmark.WithExtensions(
+ NewFootnote(
+ WithFootnoteIDPrefix("article12-"),
+ WithFootnoteLinkClass("link-class"),
+ WithFootnoteBacklinkClass("backlink-class"),
+ WithFootnoteLinkTitle("link-title-%%-^^"),
+ WithFootnoteBacklinkTitle("backlink-title"),
+ WithFootnoteBacklinkHTML("^"),
+ ),
+ ),
+ )
+
+ testutil.DoTestCase(
+ markdown,
+ testutil.MarkdownTestCase{
+ No: 1,
+ Description: "Footnote with options",
+ Markdown: `That's some text with a footnote.[^1]
+
+Same footnote.[^1]
+
+Another one.[^2]
+
+[^1]: And that's the footnote.
+[^2]: Another footnote.
+`,
+ Expected: `That's some text with a footnote.1
+Same footnote.1
+Another one.2
+`,
+ },
+ t,
+ )
+
+ markdown = goldmark.New(
+ goldmark.WithParserOptions(
+ parser.WithASTTransformers(
+ util.Prioritized(&footnoteID{}, 100),
+ ),
+ ),
+ goldmark.WithRendererOptions(
+ html.WithUnsafe(),
+ ),
+ 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
+ }),
+ WithFootnoteLinkClass([]byte("link-class")),
+ WithFootnoteBacklinkClass([]byte("backlink-class")),
+ WithFootnoteLinkTitle([]byte("link-title-%%-^^")),
+ WithFootnoteBacklinkTitle([]byte("backlink-title")),
+ WithFootnoteBacklinkHTML([]byte("^")),
+ ),
+ ),
+ )
+
+ testutil.DoTestCase(
+ markdown,
+ testutil.MarkdownTestCase{
+ No: 2,
+ Description: "Footnote with an id prefix function",
+ Markdown: `That's some text with a footnote.[^1]
+
+Same footnote.[^1]
+
+Another one.[^2]
+
+[^1]: And that's the footnote.
+[^2]: Another footnote.
+`,
+ Expected: `That's some text with a footnote.1
+Same footnote.1
+Another one.2
+`,
+ },
+ t,
+ )
+}
diff --git a/backend/goldmark/extension/gfm.go b/backend/goldmark/extension/gfm.go
new file mode 100644
index 0000000..a570fbd
--- /dev/null
+++ b/backend/goldmark/extension/gfm.go
@@ -0,0 +1,18 @@
+package extension
+
+import (
+ "github.com/yuin/goldmark"
+)
+
+type gfm struct {
+}
+
+// GFM is an extension that provides Github Flavored markdown functionalities.
+var GFM = &gfm{}
+
+func (e *gfm) Extend(m goldmark.Markdown) {
+ Linkify.Extend(m)
+ Table.Extend(m)
+ Strikethrough.Extend(m)
+ TaskList.Extend(m)
+}
diff --git a/backend/goldmark/extension/linkify.go b/backend/goldmark/extension/linkify.go
new file mode 100644
index 0000000..f76e31d
--- /dev/null
+++ b/backend/goldmark/extension/linkify.go
@@ -0,0 +1,323 @@
+package extension
+
+import (
+ "bytes"
+ "regexp"
+
+ "github.com/yuin/goldmark"
+ "github.com/yuin/goldmark/ast"
+ "github.com/yuin/goldmark/parser"
+ "github.com/yuin/goldmark/text"
+ "github.com/yuin/goldmark/util"
+)
+
+var wwwURLRegxp = regexp.MustCompile(`^www\.[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-z]+(?:[/#?][-a-zA-Z0-9@:%_\+.~#!?&/=\(\);,'">\^{}\[\]` + "`" + `]*)?`) //nolint:golint,lll
+
+var urlRegexp = regexp.MustCompile(`^(?:http|https|ftp)://[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-z]+(?::\d+)?(?:[/#?][-a-zA-Z0-9@:%_+.~#$!?&/=\(\);,'">\^{}\[\]` + "`" + `]*)?`) //nolint:golint,lll
+
+// An LinkifyConfig struct is a data structure that holds configuration of the
+// Linkify extension.
+type LinkifyConfig struct {
+ AllowedProtocols [][]byte
+ URLRegexp *regexp.Regexp
+ WWWRegexp *regexp.Regexp
+ EmailRegexp *regexp.Regexp
+}
+
+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 any) {
+ switch name {
+ case optLinkifyAllowedProtocols:
+ c.AllowedProtocols = value.([][]byte)
+ case optLinkifyURLRegexp:
+ c.URLRegexp = value.(*regexp.Regexp)
+ case optLinkifyWWWRegexp:
+ c.WWWRegexp = value.(*regexp.Regexp)
+ case optLinkifyEmailRegexp:
+ c.EmailRegexp = value.(*regexp.Regexp)
+ }
+}
+
+// A LinkifyOption interface sets options for the LinkifyOption.
+type LinkifyOption interface {
+ parser.Option
+ SetLinkifyOption(*LinkifyConfig)
+}
+
+type withLinkifyAllowedProtocols struct {
+ value [][]byte
+}
+
+func (o *withLinkifyAllowedProtocols) SetParserOption(c *parser.Config) {
+ c.Options[optLinkifyAllowedProtocols] = o.value
+}
+
+func (o *withLinkifyAllowedProtocols) SetLinkifyOption(p *LinkifyConfig) {
+ p.AllowedProtocols = o.value
+}
+
+// WithLinkifyAllowedProtocols is a functional option that specify allowed
+// protocols in autolinks. Each protocol must end with ':' like
+// 'http:' .
+func WithLinkifyAllowedProtocols[T []byte | string](value []T) LinkifyOption {
+ opt := &withLinkifyAllowedProtocols{}
+ for _, v := range value {
+ opt.value = append(opt.value, []byte(v))
+ }
+ return opt
+}
+
+type withLinkifyURLRegexp struct {
+ value *regexp.Regexp
+}
+
+func (o *withLinkifyURLRegexp) SetParserOption(c *parser.Config) {
+ c.Options[optLinkifyURLRegexp] = o.value
+}
+
+func (o *withLinkifyURLRegexp) SetLinkifyOption(p *LinkifyConfig) {
+ p.URLRegexp = o.value
+}
+
+// WithLinkifyURLRegexp is a functional option that specify
+// a pattern of the URL including a protocol.
+func WithLinkifyURLRegexp(value *regexp.Regexp) LinkifyOption {
+ return &withLinkifyURLRegexp{
+ value: value,
+ }
+}
+
+type withLinkifyWWWRegexp struct {
+ value *regexp.Regexp
+}
+
+func (o *withLinkifyWWWRegexp) SetParserOption(c *parser.Config) {
+ c.Options[optLinkifyWWWRegexp] = o.value
+}
+
+func (o *withLinkifyWWWRegexp) SetLinkifyOption(p *LinkifyConfig) {
+ p.WWWRegexp = o.value
+}
+
+// WithLinkifyWWWRegexp is a functional option that specify
+// a pattern of the URL without a protocol.
+// This pattern must start with 'www.' .
+func WithLinkifyWWWRegexp(value *regexp.Regexp) LinkifyOption {
+ return &withLinkifyWWWRegexp{
+ value: value,
+ }
+}
+
+type withLinkifyEmailRegexp struct {
+ value *regexp.Regexp
+}
+
+func (o *withLinkifyEmailRegexp) SetParserOption(c *parser.Config) {
+ c.Options[optLinkifyEmailRegexp] = o.value
+}
+
+func (o *withLinkifyEmailRegexp) SetLinkifyOption(p *LinkifyConfig) {
+ p.EmailRegexp = o.value
+}
+
+// WithLinkifyEmailRegexp is a functional otpion that specify
+// a pattern of the email address.
+func WithLinkifyEmailRegexp(value *regexp.Regexp) LinkifyOption {
+ return &withLinkifyEmailRegexp{
+ value: value,
+ }
+}
+
+type linkifyParser struct {
+ LinkifyConfig
+}
+
+// NewLinkifyParser return a new InlineParser can parse
+// text that seems like a URL.
+func NewLinkifyParser(opts ...LinkifyOption) parser.InlineParser {
+ p := &linkifyParser{
+ LinkifyConfig: LinkifyConfig{
+ AllowedProtocols: nil,
+ URLRegexp: urlRegexp,
+ WWWRegexp: wwwURLRegxp,
+ },
+ }
+ for _, o := range opts {
+ o.SetLinkifyOption(&p.LinkifyConfig)
+ }
+ return p
+}
+
+func (s *linkifyParser) Trigger() []byte {
+ // ' ' indicates any white spaces and a line head
+ return []byte{' ', '*', '_', '~', '('}
+}
+
+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() {
+ return nil
+ }
+ line, segment := block.PeekLine()
+ consumes := 0
+ start := segment.Start
+ c := line[0]
+ // advance if current position is not a line head.
+ if c == ' ' || c == '*' || c == '_' || c == '~' || c == '(' {
+ consumes++
+ start++
+ line = line[1:]
+ }
+
+ var m []int
+ var protocol []byte
+ var typ ast.AutoLinkType = ast.AutoLinkURL
+ if s.LinkifyConfig.AllowedProtocols == nil {
+ if bytes.HasPrefix(line, protoHTTP) || bytes.HasPrefix(line, protoHTTPS) || bytes.HasPrefix(line, protoFTP) {
+ m = s.LinkifyConfig.URLRegexp.FindSubmatchIndex(line)
+ }
+ } else {
+ for _, prefix := range s.LinkifyConfig.AllowedProtocols {
+ if bytes.HasPrefix(line, prefix) {
+ m = s.LinkifyConfig.URLRegexp.FindSubmatchIndex(line)
+ break
+ }
+ }
+ }
+ if m == nil && bytes.HasPrefix(line, domainWWW) {
+ m = s.LinkifyConfig.WWWRegexp.FindSubmatchIndex(line)
+ protocol = []byte("http")
+ }
+ if m != nil && m[0] != 0 {
+ m = nil
+ }
+ if m != nil && m[0] == 0 {
+ lastChar := line[m[1]-1]
+ if lastChar == '.' {
+ m[1]--
+ } else if lastChar == ')' {
+ closing := 0
+ for i := m[1] - 1; i >= m[0]; i-- {
+ switch line[i] {
+ case ')':
+ closing++
+ case '(':
+ closing--
+ }
+ }
+ if closing > 0 {
+ m[1] -= closing
+ }
+ } else if lastChar == ';' {
+ i := m[1] - 2
+ for ; i >= m[0]; i-- {
+ if util.IsAlphaNumeric(line[i]) {
+ continue
+ }
+ break
+ }
+ if i != m[1]-2 {
+ if line[i] == '&' {
+ m[1] -= m[1] - i
+ }
+ }
+ }
+ }
+ if m == nil {
+ if len(line) > 0 && util.IsPunct(line[0]) {
+ return nil
+ }
+ typ = ast.AutoLinkEmail
+ stop := -1
+ if s.LinkifyConfig.EmailRegexp == nil {
+ stop = util.FindEmailIndex(line)
+ } else {
+ m := s.LinkifyConfig.EmailRegexp.FindSubmatchIndex(line)
+ if m != nil && m[0] == 0 {
+ stop = m[1]
+ }
+ }
+ if stop < 0 {
+ return nil
+ }
+ at := bytes.IndexByte(line, '@')
+ m = []int{0, stop, at, stop - 1}
+ if bytes.IndexByte(line[m[2]:m[3]], '.') < 0 {
+ return nil
+ }
+ lastChar := line[m[1]-1]
+ if lastChar == '.' {
+ m[1]--
+ }
+ if m[1] < len(line) {
+ nextChar := line[m[1]]
+ if nextChar == '-' || nextChar == '_' {
+ return nil
+ }
+ }
+ }
+ if m == nil {
+ return nil
+ }
+ if consumes != 0 {
+ s := segment.WithStop(segment.Start + 1)
+ ast.MergeOrAppendTextSegment(parent, s)
+ }
+ 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+i))
+ link := ast.NewAutoLink(typ, n)
+ link.Protocol = protocol
+ return link
+}
+
+func (s *linkifyParser) CloseBlock(parent ast.Node, pc parser.Context) {
+ // nothing to do
+}
+
+type linkify struct {
+ options []LinkifyOption
+}
+
+// Linkify is an extension that allow you to parse text that seems like a URL.
+var Linkify = &linkify{}
+
+// NewLinkify creates a new [goldmark.Extender] that
+// allow you to parse text that seems like a URL.
+func NewLinkify(opts ...LinkifyOption) goldmark.Extender {
+ return &linkify{
+ options: opts,
+ }
+}
+
+func (e *linkify) Extend(m goldmark.Markdown) {
+ m.Parser().AddOptions(
+ parser.WithInlineParsers(
+ util.Prioritized(NewLinkifyParser(e.options...), 999),
+ ),
+ )
+}
diff --git a/backend/goldmark/extension/linkify_test.go b/backend/goldmark/extension/linkify_test.go
new file mode 100644
index 0000000..4d70ea4
--- /dev/null
+++ b/backend/goldmark/extension/linkify_test.go
@@ -0,0 +1,100 @@
+package extension
+
+import (
+ "regexp"
+ "testing"
+
+ "github.com/yuin/goldmark"
+ "github.com/yuin/goldmark/renderer/html"
+ "github.com/yuin/goldmark/testutil"
+)
+
+func TestLinkify(t *testing.T) {
+ markdown := goldmark.New(
+ goldmark.WithRendererOptions(
+ html.WithUnsafe(),
+ ),
+ goldmark.WithExtensions(
+ Linkify,
+ ),
+ )
+ testutil.DoTestCaseFile(markdown, "_test/linkify.txt", t, testutil.ParseCliCaseArg()...)
+}
+
+func TestLinkifyWithAllowedProtocols(t *testing.T) {
+ markdown := goldmark.New(
+ goldmark.WithRendererOptions(
+ html.WithXHTML(),
+ html.WithUnsafe(),
+ ),
+ goldmark.WithExtensions(
+ NewLinkify(
+ WithLinkifyAllowedProtocols([]string{
+ "ssh:",
+ }),
+ WithLinkifyURLRegexp(
+ regexp.MustCompile(`\w+://[^\s]+`),
+ ),
+ ),
+ ),
+ )
+ testutil.DoTestCase(
+ markdown,
+ testutil.MarkdownTestCase{
+ No: 1,
+ Markdown: `hoge ssh://user@hoge.com. http://example.com/`,
+ Expected: `hoge ssh://user@hoge.com . http://example.com/
`,
+ },
+ t,
+ )
+}
+
+func TestLinkifyWithWWWRegexp(t *testing.T) {
+ markdown := goldmark.New(
+ goldmark.WithRendererOptions(
+ html.WithXHTML(),
+ html.WithUnsafe(),
+ ),
+ goldmark.WithExtensions(
+ NewLinkify(
+ WithLinkifyWWWRegexp(
+ regexp.MustCompile(`www\.example\.com`),
+ ),
+ ),
+ ),
+ )
+ testutil.DoTestCase(
+ markdown,
+ testutil.MarkdownTestCase{
+ No: 1,
+ Markdown: `www.google.com www.example.com`,
+ Expected: `www.google.com www.example.com
`,
+ },
+ t,
+ )
+}
+
+func TestLinkifyWithEmailRegexp(t *testing.T) {
+ markdown := goldmark.New(
+ goldmark.WithRendererOptions(
+ html.WithXHTML(),
+ html.WithUnsafe(),
+ ),
+ goldmark.WithExtensions(
+ NewLinkify(
+ WithLinkifyEmailRegexp(
+ regexp.MustCompile(`user@example\.com`),
+ ),
+ ),
+ ),
+ )
+ testutil.DoTestCase(
+ markdown,
+ testutil.MarkdownTestCase{
+ No: 1,
+ Markdown: `hoge@example.com user@example.com`,
+ Expected: `hoge@example.com user@example.com
`,
+ },
+ t,
+ )
+}
diff --git a/backend/goldmark/extension/package.go b/backend/goldmark/extension/package.go
new file mode 100644
index 0000000..2ec1d1e
--- /dev/null
+++ b/backend/goldmark/extension/package.go
@@ -0,0 +1,2 @@
+// Package extension is a collection of builtin extensions.
+package extension
diff --git a/backend/goldmark/extension/strikethrough.go b/backend/goldmark/extension/strikethrough.go
new file mode 100644
index 0000000..9fc0bec
--- /dev/null
+++ b/backend/goldmark/extension/strikethrough.go
@@ -0,0 +1,118 @@
+package extension
+
+import (
+ "github.com/yuin/goldmark"
+ gast "github.com/yuin/goldmark/ast"
+ "github.com/yuin/goldmark/extension/ast"
+ "github.com/yuin/goldmark/parser"
+ "github.com/yuin/goldmark/renderer"
+ "github.com/yuin/goldmark/renderer/html"
+ "github.com/yuin/goldmark/text"
+ "github.com/yuin/goldmark/util"
+)
+
+type strikethroughDelimiterProcessor struct {
+}
+
+func (p *strikethroughDelimiterProcessor) IsDelimiter(b byte) bool {
+ return b == '~'
+}
+
+func (p *strikethroughDelimiterProcessor) CanOpenCloser(opener, closer *parser.Delimiter) bool {
+ return opener.Char == closer.Char
+}
+
+func (p *strikethroughDelimiterProcessor) OnMatch(consumes int) gast.Node {
+ return ast.NewStrikethrough()
+}
+
+var defaultStrikethroughDelimiterProcessor = &strikethroughDelimiterProcessor{}
+
+type strikethroughParser struct {
+}
+
+var defaultStrikethroughParser = &strikethroughParser{}
+
+// NewStrikethroughParser return a new InlineParser that parses
+// strikethrough expressions.
+func NewStrikethroughParser() parser.InlineParser {
+ return defaultStrikethroughParser
+}
+
+func (s *strikethroughParser) Trigger() []byte {
+ return []byte{'~'}
+}
+
+func (s *strikethroughParser) Parse(parent gast.Node, block text.Reader, pc parser.Context) gast.Node {
+ before := block.PrecendingCharacter()
+ line, segment := block.PeekLine()
+ node := parser.ScanDelimiter(line, before, 1, defaultStrikethroughDelimiterProcessor)
+ if node == nil || node.OriginalLength > 2 || before == '~' {
+ return nil
+ }
+
+ node.Segment = segment.WithStop(segment.Start + node.OriginalLength)
+ block.Advance(node.OriginalLength)
+ pc.PushDelimiter(node)
+ return node
+}
+
+func (s *strikethroughParser) CloseBlock(parent gast.Node, pc parser.Context) {
+ // nothing to do
+}
+
+// StrikethroughHTMLRenderer is a renderer.NodeRenderer implementation that
+// renders Strikethrough nodes.
+type StrikethroughHTMLRenderer struct {
+ html.Config
+}
+
+// NewStrikethroughHTMLRenderer returns a new StrikethroughHTMLRenderer.
+func NewStrikethroughHTMLRenderer(opts ...html.Option) renderer.NodeRenderer {
+ r := &StrikethroughHTMLRenderer{
+ Config: html.NewConfig(),
+ }
+ for _, opt := range opts {
+ opt.SetHTMLOption(&r.Config)
+ }
+ return r
+}
+
+// RegisterFuncs implements renderer.NodeRenderer.RegisterFuncs.
+func (r *StrikethroughHTMLRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) {
+ reg.Register(ast.KindStrikethrough, r.renderStrikethrough)
+}
+
+// StrikethroughAttributeFilter defines attribute names which dd elements can have.
+var StrikethroughAttributeFilter = html.GlobalAttributeFilter
+
+func (r *StrikethroughHTMLRenderer) renderStrikethrough(
+ w util.BufWriter, source []byte, n gast.Node, entering bool) (gast.WalkStatus, error) {
+ if entering {
+ if n.Attributes() != nil {
+ _, _ = w.WriteString("')
+ } else {
+ _, _ = w.WriteString("")
+ }
+ } else {
+ _, _ = w.WriteString("")
+ }
+ return gast.WalkContinue, nil
+}
+
+type strikethrough struct {
+}
+
+// Strikethrough is an extension that allow you to use strikethrough expression like '~~text~~' .
+var Strikethrough = &strikethrough{}
+
+func (e *strikethrough) Extend(m goldmark.Markdown) {
+ m.Parser().AddOptions(parser.WithInlineParsers(
+ util.Prioritized(NewStrikethroughParser(), 500),
+ ))
+ m.Renderer().AddOptions(renderer.WithNodeRenderers(
+ util.Prioritized(NewStrikethroughHTMLRenderer(), 500),
+ ))
+}
diff --git a/backend/goldmark/extension/strikethrough_test.go b/backend/goldmark/extension/strikethrough_test.go
new file mode 100644
index 0000000..3274c0e
--- /dev/null
+++ b/backend/goldmark/extension/strikethrough_test.go
@@ -0,0 +1,21 @@
+package extension
+
+import (
+ "testing"
+
+ "github.com/yuin/goldmark"
+ "github.com/yuin/goldmark/renderer/html"
+ "github.com/yuin/goldmark/testutil"
+)
+
+func TestStrikethrough(t *testing.T) {
+ markdown := goldmark.New(
+ goldmark.WithRendererOptions(
+ html.WithUnsafe(),
+ ),
+ goldmark.WithExtensions(
+ Strikethrough,
+ ),
+ )
+ testutil.DoTestCaseFile(markdown, "_test/strikethrough.txt", t, testutil.ParseCliCaseArg()...)
+}
diff --git a/backend/goldmark/extension/table.go b/backend/goldmark/extension/table.go
new file mode 100644
index 0000000..1d74182
--- /dev/null
+++ b/backend/goldmark/extension/table.go
@@ -0,0 +1,569 @@
+package extension
+
+import (
+ "bytes"
+ "fmt"
+ "regexp"
+
+ "github.com/yuin/goldmark"
+ gast "github.com/yuin/goldmark/ast"
+ "github.com/yuin/goldmark/extension/ast"
+ "github.com/yuin/goldmark/parser"
+ "github.com/yuin/goldmark/renderer"
+ "github.com/yuin/goldmark/renderer/html"
+ "github.com/yuin/goldmark/text"
+ "github.com/yuin/goldmark/util"
+)
+
+var escapedPipeCellListKey = parser.NewContextKey()
+
+type escapedPipeCell struct {
+ Cell *ast.TableCell
+ Pos []int
+ Transformed bool
+}
+
+// TableCellAlignMethod 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 any) {
+ 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 {
+ if w, _ := util.IndentWidth(bs, 0); w > 3 {
+ return false
+ }
+ allSep := true
+ for _, b := range bs {
+ if b != '-' {
+ allSep = false
+ }
+ if !(util.IsSpace(b) || b == '-' || b == '|' || b == ':') {
+ return false
+ }
+ }
+ return !allSep
+}
+
+var tableDelimLeft = regexp.MustCompile(`^\s*\:\-+\s*$`)
+var tableDelimRight = regexp.MustCompile(`^\s*\-+\:\s*$`)
+var tableDelimCenter = regexp.MustCompile(`^\s*\:\-+\:\s*$`)
+var tableDelimNone = regexp.MustCompile(`^\s*\-+\s*$`)
+
+type tableParagraphTransformer struct {
+}
+
+var defaultTableParagraphTransformer = &tableParagraphTransformer{}
+
+// NewTableParagraphTransformer returns a new ParagraphTransformer
+// that can transform paragraphs into tables.
+func NewTableParagraphTransformer() parser.ParagraphTransformer {
+ return defaultTableParagraphTransformer
+}
+
+func (b *tableParagraphTransformer) Transform(node *gast.Paragraph, reader text.Reader, pc parser.Context) {
+ ppos := node.Pos()
+ lines := node.Lines()
+ if lines.Len() < 2 {
+ return
+ }
+ 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.SetPos(ppos)
+ 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)
+ }
+ }
+}
+
+func (b *tableParagraphTransformer) parseRow(segment text.Segment,
+ alignments []ast.Alignment, isHeader bool, reader text.Reader, pc parser.Context) *ast.TableRow {
+ npos := segment
+ source := reader.Source()
+ segment = segment.TrimLeftSpace(source)
+ segment = segment.TrimRightSpace(source)
+ line := segment.Value(source)
+ pos := 0
+ limit := len(line)
+ row := ast.NewTableRow(alignments)
+ row.SetPos(npos.Start)
+ if len(line) > 0 && line[pos] == '|' {
+ pos++
+ }
+ if len(line) > 0 && line[limit-1] == '|' {
+ limit--
+ }
+ i := 0
+ for ; pos < limit; i++ {
+ alignment := ast.AlignNone
+ if i >= len(alignments) {
+ if !isHeader {
+ return row
+ }
+ } else {
+ alignment = alignments[i]
+ }
+
+ var escapedCell *escapedPipeCell
+ node := ast.NewTableCell()
+ node.SetPos(npos.Start + pos - npos.Padding)
+ 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() any {
+ 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)
+ row.AppendChild(row, node)
+ pos = closure + 1
+ }
+ for ; i < len(alignments); i++ {
+ row.AppendChild(row, ast.NewTableCell())
+ }
+ return row
+}
+
+func (b *tableParagraphTransformer) parseDelimiter(segment text.Segment, reader text.Reader) []ast.Alignment {
+
+ line := segment.Value(reader.Source())
+ if !isTableDelim(line) {
+ return nil
+ }
+ cols := bytes.Split(line, []byte{'|'})
+ if util.IsBlank(cols[0]) {
+ cols = cols[1:]
+ }
+ if len(cols) > 0 && util.IsBlank(cols[len(cols)-1]) {
+ cols = cols[:len(cols)-1]
+ }
+
+ var alignments []ast.Alignment
+ for _, col := range cols {
+ if tableDelimLeft.Match(col) {
+ alignments = append(alignments, ast.AlignLeft)
+ } else if tableDelimRight.Match(col) {
+ alignments = append(alignments, ast.AlignRight)
+ } else if tableDelimCenter.Match(col) {
+ alignments = append(alignments, ast.AlignCenter)
+ } else if tableDelimNone.Match(col) {
+ alignments = append(alignments, ast.AlignNone)
+ } else {
+ return nil
+ }
+ }
+ 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 {
+ TableConfig
+}
+
+// NewTableHTMLRenderer returns a new TableHTMLRenderer.
+func NewTableHTMLRenderer(opts ...TableOption) renderer.NodeRenderer {
+ r := &TableHTMLRenderer{
+ TableConfig: NewTableConfig(),
+ }
+ for _, opt := range opts {
+ opt.SetTableOption(&r.TableConfig)
+ }
+ return r
+}
+
+// RegisterFuncs implements renderer.NodeRenderer.RegisterFuncs.
+func (r *TableHTMLRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) {
+ reg.Register(ast.KindTable, r.renderTable)
+ reg.Register(ast.KindTableHeader, r.renderTableHeader)
+ reg.Register(ast.KindTableRow, r.renderTableRow)
+ reg.Register(ast.KindTableCell, r.renderTableCell)
+}
+
+// TableAttributeFilter defines attribute names which table elements can have.
+//
+// - align: Deprecated
+// - bgcolor: Deprecated
+// - border: Deprecated
+// - cellpadding: Deprecated
+// - cellspacing: Deprecated
+// - frame: Deprecated
+// - rules: Deprecated
+// - summary: Deprecated
+// - width: Deprecated.
+var TableAttributeFilter = html.GlobalAttributeFilter.ExtendString(`align,bgcolor,border,cellpadding,cellspacing,frame,rules,summary,width`) // nolint: lll
+
+func (r *TableHTMLRenderer) renderTable(
+ w util.BufWriter, source []byte, n gast.Node, entering bool) (gast.WalkStatus, error) {
+ if entering {
+ _, _ = w.WriteString("\n")
+ } else {
+ _, _ = w.WriteString("
\n")
+ }
+ return gast.WalkContinue, nil
+}
+
+// TableHeaderAttributeFilter defines attribute names which elements can have.
+//
+// - align: Deprecated since HTML4, Obsolete since HTML5
+// - bgcolor: Not Standardized
+// - char: Deprecated since HTML4, Obsolete since HTML5
+// - charoff: Deprecated since HTML4, Obsolete since HTML5
+// - valign: Deprecated since HTML4, Obsolete since HTML5.
+var TableHeaderAttributeFilter = html.GlobalAttributeFilter.ExtendString(`align,bgcolor,char,charoff,valign`)
+
+func (r *TableHTMLRenderer) renderTableHeader(
+ w util.BufWriter, source []byte, n gast.Node, entering bool) (gast.WalkStatus, error) {
+ if entering {
+ _, _ = w.WriteString("\n")
+ _, _ = w.WriteString("\n") // Header has no separate handle
+ } else {
+ _, _ = w.WriteString(" \n")
+ _, _ = w.WriteString(" \n")
+ if n.NextSibling() != nil {
+ _, _ = w.WriteString(" \n")
+ }
+ }
+ return gast.WalkContinue, nil
+}
+
+// TableRowAttributeFilter defines attribute names which elements can have.
+//
+// - align: Obsolete since HTML5
+// - bgcolor: Obsolete since HTML5
+// - char: Obsolete since HTML5
+// - charoff: Obsolete since HTML5
+// - valign: Obsolete since HTML5.
+var TableRowAttributeFilter = html.GlobalAttributeFilter.ExtendString(`align,bgcolor,char,charoff,valign`)
+
+func (r *TableHTMLRenderer) renderTableRow(
+ w util.BufWriter, source []byte, n gast.Node, entering bool) (gast.WalkStatus, error) {
+ if entering {
+ _, _ = w.WriteString(" \n")
+ } else {
+ _, _ = w.WriteString(" \n")
+ if n.Parent().LastChild() == n {
+ _, _ = w.WriteString(" \n")
+ }
+ }
+ return gast.WalkContinue, nil
+}
+
+// TableThCellAttributeFilter defines attribute names which table cells can have.
+//
+// - abbr: [OK] Contains a short abbreviated description of the cell's content [NOT OK in ]
+// - align: Obsolete since HTML5
+// - axis: Obsolete since HTML5
+// - bgcolor: Not Standardized
+// - char: Obsolete since HTML5
+// - charoff: Obsolete since HTML5
+// - colspan: [OK] Number of columns that the cell is to span
+// - headers: [OK] This attribute contains a list of space-separated strings,
+// each corresponding to the id attribute of the elements that apply to this element
+// - height: Deprecated since HTML4. Obsolete since HTML5
+// - rowspan: [OK] Number of rows that the cell is to span
+// - scope: [OK] This enumerated attribute defines the cells that the header
+// (defined in the ) element relates to [NOT OK in ]
+// - valign: Obsolete since HTML5
+// - width: Deprecated since HTML4. Obsolete since HTML5.
+var TableThCellAttributeFilter = html.GlobalAttributeFilter.ExtendString(`abbr,align,axis,bgcolor,char,charoff,colspan,headers,height,rowspan,scope,valign,width`) // nolint:lll
+
+// TableTdCellAttributeFilter defines attribute names which table cells can have.
+//
+// - abbr: Obsolete since HTML5. [OK in ]
+// - align: Obsolete since HTML5
+// - axis: Obsolete since HTML5
+// - bgcolor: Not Standardized
+// - char: Obsolete since HTML5
+// - charoff: Obsolete since HTML5
+// - colspan: [OK] Number of columns that the cell is to span
+// - headers: [OK] This attribute contains a list of space-separated strings, each corresponding
+// to the id attribute of the elements that apply to this element
+// - height: Deprecated since HTML4. Obsolete since HTML5
+// - rowspan: [OK] Number of rows that the cell is to span
+// - scope: Obsolete since HTML5. [OK in ]
+// - valign: Obsolete since HTML5
+// - width: Deprecated since HTML4. Obsolete since HTML5.
+var TableTdCellAttributeFilter = html.GlobalAttributeFilter.ExtendString(`abbr,align,axis,bgcolor,char,charoff,colspan,headers,height,rowspan,scope,valign,width`) // nolint: lll
+
+func (r *TableHTMLRenderer) renderTableCell(
+ w util.BufWriter, source []byte, node gast.Node, entering bool) (gast.WalkStatus, error) {
+ n := node.(*ast.TableCell)
+ tag := "td"
+ if n.Parent().Kind() == ast.KindTableHeader {
+ tag = "th"
+ }
+ if entering {
+ _, _ = fmt.Fprintf(w, "<%s", tag)
+ if n.Alignment != ast.AlignNone {
+ 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 {
+ switch v := v.(type) {
+ case []byte:
+ cob = util.NewCopyOnWriteBuffer(v)
+ case string:
+ cob = util.NewCopyOnWriteBuffer([]byte(v))
+ }
+ cob.AppendByte(';')
+ }
+ style := fmt.Sprintf("text-align:%s", n.Alignment.String())
+ cob.AppendString(style)
+ n.SetAttributeString("style", cob.Bytes())
+ }
+ }
+ if n.Attributes() != nil {
+ if tag == "td" {
+ html.RenderAttributes(w, n, TableTdCellAttributeFilter) //
+ } else {
+ html.RenderAttributes(w, n, TableThCellAttributeFilter) //
+ }
+ }
+ _ = w.WriteByte('>')
+ } else {
+ _, _ = fmt.Fprintf(w, "%s>\n", tag)
+ }
+ return gast.WalkContinue, nil
+}
+
+type table struct {
+ options []TableOption
+}
+
+// Table is an extension that allow you to use GFM tables .
+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),
+ ),
+ parser.WithASTTransformers(
+ util.Prioritized(defaultTableASTTransformer, 0),
+ ),
+ )
+ m.Renderer().AddOptions(renderer.WithNodeRenderers(
+ util.Prioritized(NewTableHTMLRenderer(e.options...), 500),
+ ))
+}
diff --git a/backend/goldmark/extension/table_test.go b/backend/goldmark/extension/table_test.go
new file mode 100644
index 0000000..21a4663
--- /dev/null
+++ b/backend/goldmark/extension/table_test.go
@@ -0,0 +1,394 @@
+package extension
+
+import (
+ "testing"
+
+ "github.com/yuin/goldmark"
+ "github.com/yuin/goldmark/ast"
+ east "github.com/yuin/goldmark/extension/ast"
+ "github.com/yuin/goldmark/parser"
+ "github.com/yuin/goldmark/renderer/html"
+ "github.com/yuin/goldmark/testutil"
+ "github.com/yuin/goldmark/text"
+ "github.com/yuin/goldmark/util"
+)
+
+func TestTable(t *testing.T) {
+ markdown := goldmark.New(
+ goldmark.WithRendererOptions(
+ html.WithUnsafe(),
+ html.WithXHTML(),
+ ),
+ goldmark.WithExtensions(
+ Table,
+ ),
+ )
+ testutil.DoTestCaseFile(markdown, "_test/table.txt", t, testutil.ParseCliCaseArg()...)
+}
+
+func TestTableWithAlignDefault(t *testing.T) {
+ markdown := goldmark.New(
+ goldmark.WithRendererOptions(
+ html.WithXHTML(),
+ html.WithUnsafe(),
+ ),
+ goldmark.WithExtensions(
+ NewTable(
+ WithTableCellAlignMethod(TableCellAlignDefault),
+ ),
+ ),
+ )
+ testutil.DoTestCase(
+ markdown,
+ testutil.MarkdownTestCase{
+ No: 1,
+ Description: "Cell with TableCellAlignDefault and XHTML should be rendered as an align attribute",
+ Markdown: `
+| abc | defghi |
+:-: | -----------:
+bar | baz
+`,
+ Expected: `
+
+
+abc
+defghi
+
+
+
+
+bar
+baz
+
+
+
`,
+ },
+ t,
+ )
+
+ markdown = goldmark.New(
+ goldmark.WithRendererOptions(
+ html.WithUnsafe(),
+ ),
+ goldmark.WithExtensions(
+ NewTable(
+ WithTableCellAlignMethod(TableCellAlignDefault),
+ ),
+ ),
+ )
+ testutil.DoTestCase(
+ markdown,
+ testutil.MarkdownTestCase{
+ No: 2,
+ Description: "Cell with TableCellAlignDefault and HTML5 should be rendered as a style attribute",
+ Markdown: `
+| abc | defghi |
+:-: | -----------:
+bar | baz
+`,
+ Expected: `
+
+
+abc
+defghi
+
+
+
+
+bar
+baz
+
+
+
`,
+ },
+ t,
+ )
+}
+
+func TestTableWithAlignAttribute(t *testing.T) {
+ markdown := goldmark.New(
+ goldmark.WithRendererOptions(
+ html.WithXHTML(),
+ html.WithUnsafe(),
+ ),
+ goldmark.WithExtensions(
+ NewTable(
+ WithTableCellAlignMethod(TableCellAlignAttribute),
+ ),
+ ),
+ )
+ testutil.DoTestCase(
+ markdown,
+ testutil.MarkdownTestCase{
+ No: 1,
+ Description: "Cell with TableCellAlignAttribute and XHTML should be rendered as an align attribute",
+ Markdown: `
+| abc | defghi |
+:-: | -----------:
+bar | baz
+`,
+ Expected: `
+
+
+abc
+defghi
+
+
+
+
+bar
+baz
+
+
+
`,
+ },
+ t,
+ )
+
+ markdown = goldmark.New(
+ goldmark.WithRendererOptions(
+ html.WithUnsafe(),
+ ),
+ goldmark.WithExtensions(
+ NewTable(
+ WithTableCellAlignMethod(TableCellAlignAttribute),
+ ),
+ ),
+ )
+ testutil.DoTestCase(
+ markdown,
+ testutil.MarkdownTestCase{
+ No: 2,
+ Description: "Cell with TableCellAlignAttribute and HTML5 should be rendered as an align attribute",
+ Markdown: `
+| abc | defghi |
+:-: | -----------:
+bar | baz
+`,
+ Expected: `
+
+
+abc
+defghi
+
+
+
+
+bar
+baz
+
+
+
`,
+ },
+ t,
+ )
+}
+
+type tableStyleTransformer struct {
+}
+
+func (a *tableStyleTransformer) Transform(node *ast.Document, reader text.Reader, pc parser.Context) {
+ cell := node.FirstChild().FirstChild().FirstChild().(*east.TableCell)
+ cell.SetAttributeString("style", []byte("font-size:1em"))
+}
+
+func TestTableWithAlignStyle(t *testing.T) {
+ markdown := goldmark.New(
+ goldmark.WithRendererOptions(
+ html.WithXHTML(),
+ html.WithUnsafe(),
+ ),
+ goldmark.WithExtensions(
+ NewTable(
+ WithTableCellAlignMethod(TableCellAlignStyle),
+ ),
+ ),
+ )
+ testutil.DoTestCase(
+ markdown,
+ testutil.MarkdownTestCase{
+ No: 1,
+ Description: "Cell with TableCellAlignStyle and XHTML should be rendered as a style attribute",
+ Markdown: `
+| abc | defghi |
+:-: | -----------:
+bar | baz
+`,
+ Expected: `
+
+
+abc
+defghi
+
+
+
+
+bar
+baz
+
+
+
`,
+ },
+ t,
+ )
+
+ markdown = goldmark.New(
+ goldmark.WithRendererOptions(
+ html.WithUnsafe(),
+ ),
+ goldmark.WithExtensions(
+ NewTable(
+ WithTableCellAlignMethod(TableCellAlignStyle),
+ ),
+ ),
+ )
+ testutil.DoTestCase(
+ markdown,
+ testutil.MarkdownTestCase{
+ No: 2,
+ Description: "Cell with TableCellAlignStyle and HTML5 should be rendered as a style attribute",
+ Markdown: `
+| abc | defghi |
+:-: | -----------:
+bar | baz
+`,
+ Expected: `
+
+
+abc
+defghi
+
+
+
+
+bar
+baz
+
+
+
`,
+ },
+ t,
+ )
+
+ markdown = goldmark.New(
+ goldmark.WithParserOptions(
+ parser.WithASTTransformers(
+ util.Prioritized(&tableStyleTransformer{}, 0),
+ ),
+ ),
+ goldmark.WithRendererOptions(
+ html.WithUnsafe(),
+ ),
+ goldmark.WithExtensions(
+ NewTable(
+ WithTableCellAlignMethod(TableCellAlignStyle),
+ ),
+ ),
+ )
+
+ testutil.DoTestCase(
+ markdown,
+ testutil.MarkdownTestCase{
+ No: 3,
+ Description: "Styled cell should not be broken the style by the alignments",
+ Markdown: `
+| abc | defghi |
+:-: | -----------:
+bar | baz
+`,
+ Expected: `
+
+
+abc
+defghi
+
+
+
+
+bar
+baz
+
+
+
`,
+ },
+ t,
+ )
+}
+
+func TestTableWithAlignNone(t *testing.T) {
+ markdown := goldmark.New(
+ goldmark.WithRendererOptions(
+ html.WithXHTML(),
+ html.WithUnsafe(),
+ ),
+ goldmark.WithExtensions(
+ NewTable(
+ WithTableCellAlignMethod(TableCellAlignNone),
+ ),
+ ),
+ )
+ testutil.DoTestCase(
+ markdown,
+ testutil.MarkdownTestCase{
+ No: 1,
+ Description: "Cell with TableCellAlignStyle and XHTML should not be rendered",
+ Markdown: `
+| abc | defghi |
+:-: | -----------:
+bar | baz
+`,
+ Expected: `
+
+
+abc
+defghi
+
+
+
+
+bar
+baz
+
+
+
`,
+ },
+ t,
+ )
+}
+
+func TestTableFuzzedPanics(t *testing.T) {
+ markdown := goldmark.New(
+ goldmark.WithRendererOptions(
+ html.WithXHTML(),
+ html.WithUnsafe(),
+ ),
+ goldmark.WithExtensions(
+ NewTable(),
+ ),
+ )
+ testutil.DoTestCase(
+ markdown,
+ testutil.MarkdownTestCase{
+ No: 1,
+ Description: "This should not panic",
+ Markdown: "* 0\n-|\n\t0",
+ Expected: ``,
+ },
+ t,
+ )
+}
diff --git a/backend/goldmark/extension/tasklist.go b/backend/goldmark/extension/tasklist.go
new file mode 100644
index 0000000..4467ebf
--- /dev/null
+++ b/backend/goldmark/extension/tasklist.go
@@ -0,0 +1,120 @@
+package extension
+
+import (
+ "regexp"
+
+ "github.com/yuin/goldmark"
+ gast "github.com/yuin/goldmark/ast"
+ "github.com/yuin/goldmark/extension/ast"
+ "github.com/yuin/goldmark/parser"
+ "github.com/yuin/goldmark/renderer"
+ "github.com/yuin/goldmark/renderer/html"
+ "github.com/yuin/goldmark/text"
+ "github.com/yuin/goldmark/util"
+)
+
+var taskListRegexp = regexp.MustCompile(`^\[([\sxX])\]\s*`)
+
+type taskCheckBoxParser struct {
+}
+
+var defaultTaskCheckBoxParser = &taskCheckBoxParser{}
+
+// NewTaskCheckBoxParser returns a new InlineParser that can parse
+// checkboxes in list items.
+// This parser must take precedence over the parser.LinkParser.
+func NewTaskCheckBoxParser() parser.InlineParser {
+ return defaultTaskCheckBoxParser
+}
+
+func (s *taskCheckBoxParser) Trigger() []byte {
+ return []byte{'['}
+}
+
+func (s *taskCheckBoxParser) Parse(parent gast.Node, block text.Reader, pc parser.Context) gast.Node {
+ // Given AST structure must be like
+ // - List
+ // - ListItem : parent.Parent
+ // - TextBlock : parent
+ // (current line)
+ if parent.Parent() == nil || parent.Parent().FirstChild() != parent {
+ return nil
+ }
+
+ if parent.HasChildren() {
+ return nil
+ }
+ if _, ok := parent.Parent().(*gast.ListItem); !ok {
+ return nil
+ }
+ line, _ := block.PeekLine()
+ m := taskListRegexp.FindSubmatchIndex(line)
+ if m == nil {
+ return nil
+ }
+ value := line[m[2]:m[3]][0]
+ block.Advance(m[1])
+ checked := value == 'x' || value == 'X'
+ return ast.NewTaskCheckBox(checked)
+}
+
+func (s *taskCheckBoxParser) CloseBlock(parent gast.Node, pc parser.Context) {
+ // nothing to do
+}
+
+// TaskCheckBoxHTMLRenderer is a renderer.NodeRenderer implementation that
+// renders checkboxes in list items.
+type TaskCheckBoxHTMLRenderer struct {
+ html.Config
+}
+
+// NewTaskCheckBoxHTMLRenderer returns a new TaskCheckBoxHTMLRenderer.
+func NewTaskCheckBoxHTMLRenderer(opts ...html.Option) renderer.NodeRenderer {
+ r := &TaskCheckBoxHTMLRenderer{
+ Config: html.NewConfig(),
+ }
+ for _, opt := range opts {
+ opt.SetHTMLOption(&r.Config)
+ }
+ return r
+}
+
+// RegisterFuncs implements renderer.NodeRenderer.RegisterFuncs.
+func (r *TaskCheckBoxHTMLRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) {
+ reg.Register(ast.KindTaskCheckBox, r.renderTaskCheckBox)
+}
+
+func (r *TaskCheckBoxHTMLRenderer) renderTaskCheckBox(
+ w util.BufWriter, source []byte, node gast.Node, entering bool) (gast.WalkStatus, error) {
+ if !entering {
+ return gast.WalkContinue, nil
+ }
+ n := node.(*ast.TaskCheckBox)
+
+ if n.IsChecked {
+ _, _ = w.WriteString(` ")
+ } else {
+ _, _ = w.WriteString("> ")
+ }
+ return gast.WalkContinue, nil
+}
+
+type taskList struct {
+}
+
+// TaskList is an extension that allow you to use GFM task lists.
+var TaskList = &taskList{}
+
+func (e *taskList) Extend(m goldmark.Markdown) {
+ m.Parser().AddOptions(parser.WithInlineParsers(
+ util.Prioritized(NewTaskCheckBoxParser(), 0),
+ ))
+ m.Renderer().AddOptions(renderer.WithNodeRenderers(
+ util.Prioritized(NewTaskCheckBoxHTMLRenderer(), 500),
+ ))
+}
diff --git a/backend/goldmark/extension/tasklist_test.go b/backend/goldmark/extension/tasklist_test.go
new file mode 100644
index 0000000..e376227
--- /dev/null
+++ b/backend/goldmark/extension/tasklist_test.go
@@ -0,0 +1,21 @@
+package extension
+
+import (
+ "testing"
+
+ "github.com/yuin/goldmark"
+ "github.com/yuin/goldmark/renderer/html"
+ "github.com/yuin/goldmark/testutil"
+)
+
+func TestTaskList(t *testing.T) {
+ markdown := goldmark.New(
+ goldmark.WithRendererOptions(
+ html.WithUnsafe(),
+ ),
+ goldmark.WithExtensions(
+ TaskList,
+ ),
+ )
+ testutil.DoTestCaseFile(markdown, "_test/tasklist.txt", t, testutil.ParseCliCaseArg()...)
+}
diff --git a/backend/goldmark/extension/typographer.go b/backend/goldmark/extension/typographer.go
new file mode 100644
index 0000000..3a3f106
--- /dev/null
+++ b/backend/goldmark/extension/typographer.go
@@ -0,0 +1,348 @@
+package extension
+
+import (
+ "unicode"
+
+ "github.com/yuin/goldmark"
+ gast "github.com/yuin/goldmark/ast"
+ "github.com/yuin/goldmark/parser"
+ "github.com/yuin/goldmark/text"
+ "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
+
+const (
+ // LeftSingleQuote is ' .
+ LeftSingleQuote TypographicPunctuation = iota + 1
+ // RightSingleQuote is ' .
+ RightSingleQuote
+ // LeftDoubleQuote is " .
+ LeftDoubleQuote
+ // RightDoubleQuote is " .
+ RightDoubleQuote
+ // EnDash is -- .
+ EnDash
+ // EmDash is --- .
+ EmDash
+ // Ellipsis is ... .
+ Ellipsis
+ // LeftAngleQuote is << .
+ LeftAngleQuote
+ // RightAngleQuote is >> .
+ RightAngleQuote
+ // Apostrophe is ' .
+ Apostrophe
+
+ typographicPunctuationMax
+)
+
+// An TypographerConfig struct is a data structure that holds configuration of the
+// Typographer extension.
+type TypographerConfig struct {
+ Substitutions [][]byte
+}
+
+func newDefaultSubstitutions() [][]byte {
+ replacements := make([][]byte, typographicPunctuationMax)
+ replacements[LeftSingleQuote] = []byte("‘")
+ replacements[RightSingleQuote] = []byte("’")
+ replacements[LeftDoubleQuote] = []byte("“")
+ replacements[RightDoubleQuote] = []byte("”")
+ replacements[EnDash] = []byte("–")
+ replacements[EmDash] = []byte("—")
+ replacements[Ellipsis] = []byte("…")
+ replacements[LeftAngleQuote] = []byte("«")
+ replacements[RightAngleQuote] = []byte("»")
+ replacements[Apostrophe] = []byte("’")
+
+ return replacements
+}
+
+// SetOption implements SetOptioner.
+func (b *TypographerConfig) SetOption(name parser.OptionName, value any) {
+ switch name {
+ case optTypographicSubstitutions:
+ b.Substitutions = value.([][]byte)
+ }
+}
+
+// A TypographerOption interface sets options for the TypographerParser.
+type TypographerOption interface {
+ parser.Option
+ SetTypographerOption(*TypographerConfig)
+}
+
+const optTypographicSubstitutions parser.OptionName = "TypographicSubstitutions"
+
+// TypographicSubstitutions is a list of the substitutions for the Typographer extension.
+type TypographicSubstitutions map[TypographicPunctuation][]byte
+
+type withTypographicSubstitutions struct {
+ value [][]byte
+}
+
+func (o *withTypographicSubstitutions) SetParserOption(c *parser.Config) {
+ c.Options[optTypographicSubstitutions] = o.value
+}
+
+func (o *withTypographicSubstitutions) SetTypographerOption(p *TypographerConfig) {
+ p.Substitutions = o.value
+}
+
+// WithTypographicSubstitutions is a functional otpion that specify replacement text
+// for punctuations.
+func WithTypographicSubstitutions[T []byte | string](values map[TypographicPunctuation]T) TypographerOption {
+ replacements := newDefaultSubstitutions()
+ for k, v := range values {
+ replacements[k] = []byte(v)
+ }
+
+ return &withTypographicSubstitutions{replacements}
+}
+
+type typographerDelimiterProcessor struct {
+}
+
+func (p *typographerDelimiterProcessor) IsDelimiter(b byte) bool {
+ return b == '\'' || b == '"'
+}
+
+func (p *typographerDelimiterProcessor) CanOpenCloser(opener, closer *parser.Delimiter) bool {
+ return opener.Char == closer.Char
+}
+
+func (p *typographerDelimiterProcessor) OnMatch(consumes int) gast.Node {
+ return nil
+}
+
+var defaultTypographerDelimiterProcessor = &typographerDelimiterProcessor{}
+
+type typographerParser struct {
+ TypographerConfig
+}
+
+// NewTypographerParser return a new InlineParser that parses
+// typographer expressions.
+func NewTypographerParser(opts ...TypographerOption) parser.InlineParser {
+ p := &typographerParser{
+ TypographerConfig: TypographerConfig{
+ Substitutions: newDefaultSubstitutions(),
+ },
+ }
+ for _, o := range opts {
+ o.SetTypographerOption(&p.TypographerConfig)
+ }
+ return p
+}
+
+func (s *typographerParser) Trigger() []byte {
+ return []byte{'\'', '"', '-', '.', ',', '<', '>', '*', '['}
+}
+
+func (s *typographerParser) Parse(parent gast.Node, block text.Reader, pc parser.Context) gast.Node {
+ line, _ := block.PeekLine()
+ c := line[0]
+ if len(line) > 2 {
+ if c == '-' {
+ if s.Substitutions[EmDash] != nil && line[1] == '-' && line[2] == '-' { // ---
+ node := gast.NewString(s.Substitutions[EmDash])
+ node.SetCode(true)
+ block.Advance(3)
+ return node
+ }
+ } else if c == '.' {
+ if s.Substitutions[Ellipsis] != nil && line[1] == '.' && line[2] == '.' { // ...
+ node := gast.NewString(s.Substitutions[Ellipsis])
+ node.SetCode(true)
+ block.Advance(3)
+ return node
+ }
+ return nil
+ }
+ }
+ if len(line) > 1 {
+ if c == '<' {
+ if s.Substitutions[LeftAngleQuote] != nil && line[1] == '<' { // <<
+ node := gast.NewString(s.Substitutions[LeftAngleQuote])
+ node.SetCode(true)
+ block.Advance(2)
+ return node
+ }
+ return nil
+ } else if c == '>' {
+ if s.Substitutions[RightAngleQuote] != nil && line[1] == '>' { // >>
+ node := gast.NewString(s.Substitutions[RightAngleQuote])
+ node.SetCode(true)
+ block.Advance(2)
+ return node
+ }
+ return nil
+ } else if s.Substitutions[EnDash] != nil && c == '-' && line[1] == '-' { // --
+ node := gast.NewString(s.Substitutions[EnDash])
+ node.SetCode(true)
+ block.Advance(2)
+ return node
+ }
+ }
+ 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
+ if d.CanOpen && !d.CanClose && len(line) > 3 &&
+ util.IsNumeric(line[1]) && util.IsNumeric(line[2]) && line[3] == 's' {
+ after := rune(' ')
+ if len(line) > 4 {
+ after = util.ToRune(line, 4)
+ }
+ 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))) {
+ node := gast.NewString(s.Substitutions[Apostrophe])
+ node.SetCode(true)
+ block.Advance(1)
+ return node
+ }
+ }
+ if s.Substitutions[LeftSingleQuote] != nil && d.CanOpen && !d.CanClose {
+ 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 {
+ // plural possesive and abbreviations: Smiths', doin'
+ if len(line) > 1 && unicode.IsSpace(util.ToRune(line, 0)) || unicode.IsPunct(util.ToRune(line, 0)) &&
+ (len(line) > 2 && !unicode.IsDigit(util.ToRune(line, 1))) {
+ node := gast.NewString(s.Substitutions[RightSingleQuote])
+ node.SetCode(true)
+ block.Advance(1)
+ return node
+ }
+ }
+ if s.Substitutions[RightSingleQuote] != nil && counter.Single > 0 {
+ isClose := d.CanClose && !d.CanOpen
+ maybeClose := d.CanClose && d.CanOpen && len(line) > 1 && unicode.IsPunct(util.ToRune(line, 1)) &&
+ (len(line) == 2 || (len(line) > 2 && util.IsPunct(line[2]) || util.IsSpace(line[2])))
+ if isClose || maybeClose {
+ node := gast.NewString(s.Substitutions[RightSingleQuote])
+ node.SetCode(true)
+ block.Advance(1)
+ counter.Single--
+ return node
+ }
+ }
+ }
+ if c == '"' {
+ if s.Substitutions[LeftDoubleQuote] != nil && d.CanOpen && !d.CanClose {
+ node := gast.NewString(s.Substitutions[LeftDoubleQuote])
+ node.SetCode(true)
+ block.Advance(1)
+ counter.Double++
+ 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
+ }
+ }
+ }
+ }
+ return nil
+}
+
+func (s *typographerParser) CloseBlock(parent gast.Node, pc parser.Context) {
+ getUnclosedCounter(pc).Reset()
+}
+
+type typographer struct {
+ options []TypographerOption
+}
+
+// Typographer is an extension that replaces punctuations with typographic entities.
+var Typographer = &typographer{}
+
+// NewTypographer returns a new Extender that replaces punctuations with typographic entities.
+func NewTypographer(opts ...TypographerOption) goldmark.Extender {
+ return &typographer{
+ options: opts,
+ }
+}
+
+func (e *typographer) Extend(m goldmark.Markdown) {
+ m.Parser().AddOptions(parser.WithInlineParsers(
+ util.Prioritized(NewTypographerParser(e.options...), 9999),
+ ))
+}
diff --git a/backend/goldmark/extension/typographer_test.go b/backend/goldmark/extension/typographer_test.go
new file mode 100644
index 0000000..f8eded1
--- /dev/null
+++ b/backend/goldmark/extension/typographer_test.go
@@ -0,0 +1,21 @@
+package extension
+
+import (
+ "testing"
+
+ "github.com/yuin/goldmark"
+ "github.com/yuin/goldmark/renderer/html"
+ "github.com/yuin/goldmark/testutil"
+)
+
+func TestTypographer(t *testing.T) {
+ markdown := goldmark.New(
+ goldmark.WithRendererOptions(
+ html.WithUnsafe(),
+ ),
+ goldmark.WithExtensions(
+ Typographer,
+ ),
+ )
+ testutil.DoTestCaseFile(markdown, "_test/typographer.txt", t, testutil.ParseCliCaseArg()...)
+}
diff --git a/backend/goldmark/extra_test.go b/backend/goldmark/extra_test.go
new file mode 100644
index 0000000..b6fba43
--- /dev/null
+++ b/backend/goldmark/extra_test.go
@@ -0,0 +1,281 @@
+package goldmark_test
+
+import (
+ "bytes"
+ "os"
+ "strconv"
+ "strings"
+ "testing"
+ "time"
+
+ . "github.com/yuin/goldmark"
+ "github.com/yuin/goldmark/ast"
+ "github.com/yuin/goldmark/parser"
+ "github.com/yuin/goldmark/renderer/html"
+ "github.com/yuin/goldmark/testutil"
+ "github.com/yuin/goldmark/text"
+)
+
+var testTimeoutMultiplier = 1.0
+
+func init() {
+ m, err := strconv.ParseFloat(os.Getenv("GOLDMARK_TEST_TIMEOUT_MULTIPLIER"), 64)
+ if err == nil {
+ testTimeoutMultiplier = m
+ }
+}
+
+func TestExtras(t *testing.T) {
+ markdown := New(WithRendererOptions(
+ html.WithXHTML(),
+ html.WithUnsafe(),
+ ))
+ testutil.DoTestCaseFile(markdown, "_test/extra.txt", t, testutil.ParseCliCaseArg()...)
+}
+
+func TestEndsWithNonSpaceCharacters(t *testing.T) {
+ markdown := New(WithRendererOptions(
+ html.WithXHTML(),
+ html.WithUnsafe(),
+ ))
+ source := []byte("```\na\n```")
+ var b bytes.Buffer
+ err := markdown.Convert(source, &b)
+ if err != nil {
+ t.Error(err.Error())
+ }
+ if b.String() != "a\n \n" {
+ t.Errorf("%s \n---------\n %s", source, b.String())
+ }
+}
+
+func TestWindowsNewLine(t *testing.T) {
+ markdown := New(WithRendererOptions(
+ html.WithXHTML(),
+ ))
+ source := []byte("a \r\nb\n")
+ var b bytes.Buffer
+ err := markdown.Convert(source, &b)
+ if err != nil {
+ t.Error(err.Error())
+ }
+ if b.String() != "a \nb
\n" {
+ t.Errorf("%s\n---------\n%s", source, b.String())
+ }
+
+ source = []byte("a\\\r\nb\r\n")
+ var b2 bytes.Buffer
+ err = markdown.Convert(source, &b2)
+ if err != nil {
+ t.Error(err.Error())
+ }
+ if b2.String() != "a \nb
\n" {
+ t.Errorf("\n%s\n---------\n%s", source, b2.String())
+ }
+}
+
+type myIDs struct {
+}
+
+func (s *myIDs) Generate(value []byte, kind ast.NodeKind) []byte {
+ return []byte("my-id")
+}
+
+func (s *myIDs) Put(value []byte) {
+}
+
+func TestAutogeneratedIDs(t *testing.T) {
+ ctx := parser.NewContext(parser.WithIDs(&myIDs{}))
+ markdown := New(WithParserOptions(parser.WithAutoHeadingID()))
+ source := []byte("# Title1\n## Title2")
+ var b bytes.Buffer
+ err := markdown.Convert(source, &b, parser.WithContext(ctx))
+ if err != nil {
+ t.Error(err.Error())
+ }
+ if b.String() != `Title1
+Title2
+` {
+ t.Errorf("%s\n---------\n%s", source, b.String())
+ }
+}
+
+func nowMillis() int64 {
+ // TODO: replace UnixNano to UnixMillis(drops Go1.16 support)
+ return time.Now().UnixNano() / 1000000
+}
+
+func TestDeepNestedLabelPerformance(t *testing.T) {
+ if testing.Short() {
+ t.Skip("skipping performance test in short mode")
+ }
+ markdown := New(WithRendererOptions(
+ html.WithXHTML(),
+ html.WithUnsafe(),
+ ))
+
+ started := nowMillis()
+ n := 50000
+ source := []byte(strings.Repeat("[", n) + strings.Repeat("]", n))
+ var b bytes.Buffer
+ _ = markdown.Convert(source, &b)
+ finished := nowMillis()
+ if (finished - started) > int64(5000*testTimeoutMultiplier) {
+ t.Error("Parsing deep nested labels took too long")
+ }
+}
+
+func TestManyProcessingInstructionPerformance(t *testing.T) {
+ if testing.Short() {
+ t.Skip("skipping performance test in short mode")
+ }
+ markdown := New(WithRendererOptions(
+ html.WithXHTML(),
+ html.WithUnsafe(),
+ ))
+
+ started := nowMillis()
+ n := 50000
+ source := []byte("a " + strings.Repeat("", n))
+ var b bytes.Buffer
+ _ = markdown.Convert(source, &b)
+ finished := nowMillis()
+ if (finished - started) > int64(5000*testTimeoutMultiplier) {
+ t.Error("Parsing processing instructions took too long")
+ }
+}
+
+func TestManyCDATAPerformance(t *testing.T) {
+ if testing.Short() {
+ t.Skip("skipping performance test in short mode")
+ }
+ markdown := New(WithRendererOptions(
+ html.WithXHTML(),
+ html.WithUnsafe(),
+ ))
+
+ started := nowMillis()
+ n := 50000
+ source := []byte(strings.Repeat("a int64(5000*testTimeoutMultiplier) {
+ t.Error("Parsing processing instructions took too long")
+ }
+}
+
+func TestManyDeclPerformance(t *testing.T) {
+ if testing.Short() {
+ t.Skip("skipping performance test in short mode")
+ }
+ markdown := New(WithRendererOptions(
+ html.WithXHTML(),
+ html.WithUnsafe(),
+ ))
+
+ started := nowMillis()
+ n := 50000
+ source := []byte(strings.Repeat("a int64(5000*testTimeoutMultiplier) {
+ t.Error("Parsing processing instructions took too long")
+ }
+}
+
+func TestManyCommentPerformance(t *testing.T) {
+ if testing.Short() {
+ t.Skip("skipping performance test in short mode")
+ }
+ markdown := New(WithRendererOptions(
+ html.WithXHTML(),
+ html.WithUnsafe(),
+ ))
+
+ started := nowMillis()
+ n := 50000
+ source := []byte(strings.Repeat("a ")
+var emptyComment2 = []byte("")
+var openComment = []byte("")
+
+func (s *rawHTMLParser) parseComment(block text.Reader, _ Context) ast.Node {
+ savedLine, savedSegment := block.Position()
+ node := ast.NewRawHTML()
+ line, segment := block.PeekLine()
+ if bytes.HasPrefix(line, emptyComment1) {
+ node.Segments.Append(segment.WithStop(segment.Start + len(emptyComment1)))
+ block.Advance(len(emptyComment1))
+ return node
+ }
+ if bytes.HasPrefix(line, emptyComment2) {
+ node.Segments.Append(segment.WithStop(segment.Start + len(emptyComment2)))
+ block.Advance(len(emptyComment2))
+ return node
+ }
+ offset := len(openComment)
+ line = line[offset:]
+ for {
+ index := bytes.Index(line, closeComment)
+ if index > -1 {
+ node.Segments.Append(segment.WithStop(segment.Start + offset + index + len(closeComment)))
+ block.Advance(offset + index + len(closeComment))
+ return node
+ }
+ offset = 0
+ node.Segments.Append(segment)
+ block.AdvanceLine()
+ line, segment = block.PeekLine()
+ if line == nil {
+ break
+ }
+ }
+ block.SetPosition(savedLine, savedSegment)
+ return nil
+}
+
+func (s *rawHTMLParser) parseUntil(block text.Reader, closer []byte, _ 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, _ Context) ast.Node {
+ sline, ssegment := block.Position()
+ if block.Match(reg) {
+ node := ast.NewRawHTML()
+ eline, esegment := block.Position()
+ block.SetPosition(sline, ssegment)
+ for {
+ line, segment := block.PeekLine()
+ if line == nil {
+ break
+ }
+ l, _ := block.Position()
+ start := segment.Start
+ if l == sline {
+ start = ssegment.Start
+ }
+ end := segment.Stop
+ if l == eline {
+ end = esegment.Start
+ }
+
+ node.Segments.Append(text.NewSegment(start, end))
+ if l == eline {
+ block.Advance(end - start)
+ break
+ }
+ block.AdvanceLine()
+ }
+ return node
+ }
+ return nil
+}
diff --git a/backend/goldmark/parser/setext_headings.go b/backend/goldmark/parser/setext_headings.go
new file mode 100644
index 0000000..3558baa
--- /dev/null
+++ b/backend/goldmark/parser/setext_headings.go
@@ -0,0 +1,127 @@
+package parser
+
+import (
+ "github.com/yuin/goldmark/ast"
+ "github.com/yuin/goldmark/text"
+ "github.com/yuin/goldmark/util"
+)
+
+var temporaryParagraphKey = NewContextKey()
+
+type setextHeadingParser struct {
+ HeadingConfig
+}
+
+func matchesSetextHeadingBar(line []byte) (byte, bool) {
+ start := 0
+ end := len(line)
+ space := util.TrimLeftLength(line, []byte{' '})
+ if space > 3 {
+ return 0, false
+ }
+ start += space
+ level1 := util.TrimLeftLength(line[start:end], []byte{'='})
+ c := byte('=')
+ var level2 int
+ if level1 == 0 {
+ level2 = util.TrimLeftLength(line[start:end], []byte{'-'})
+ c = '-'
+ }
+ if util.IsSpace(line[end-1]) {
+ end -= util.TrimRightSpaceLength(line[start:end])
+ }
+ if !((level1 > 0 && start+level1 == end) || (level2 > 0 && start+level2 == end)) {
+ return 0, false
+ }
+ return c, true
+}
+
+// NewSetextHeadingParser return a new BlockParser that can parse Setext headings.
+func NewSetextHeadingParser(opts ...HeadingOption) BlockParser {
+ p := &setextHeadingParser{}
+ for _, o := range opts {
+ o.SetHeadingOption(&p.HeadingConfig)
+ }
+ return p
+}
+
+func (b *setextHeadingParser) Trigger() []byte {
+ return []byte{'-', '='}
+}
+
+func (b *setextHeadingParser) Open(parent ast.Node, reader text.Reader, pc Context) (ast.Node, State) {
+ last := pc.LastOpenedBlock().Node
+ if last == nil {
+ return nil, NoChildren
+ }
+ paragraph, ok := last.(*ast.Paragraph)
+ if !ok || paragraph.Parent() != parent {
+ return nil, NoChildren
+ }
+ line, segment := reader.PeekLine()
+ c, ok := matchesSetextHeadingBar(line)
+ if !ok {
+ return nil, NoChildren
+ }
+ level := 1
+ if c == '-' {
+ level = 2
+ }
+ node := ast.NewHeading(level)
+ node.Lines().Append(segment)
+ pc.Set(temporaryParagraphKey, last)
+ return node, NoChildren | RequireParagraph
+}
+
+func (b *setextHeadingParser) Continue(node ast.Node, reader text.Reader, pc Context) State {
+ return Close
+}
+
+func (b *setextHeadingParser) Close(node ast.Node, reader text.Reader, pc Context) {
+ heading := node.(*ast.Heading)
+ segment := node.Lines().At(0)
+ heading.Lines().Clear()
+ tmp := pc.Get(temporaryParagraphKey).(*ast.Paragraph)
+ pc.Set(temporaryParagraphKey, nil)
+ if tmp.Lines().Len() == 0 {
+ next := heading.NextSibling()
+ segment = segment.TrimLeftSpace(reader.Source())
+ if next == nil || !ast.IsParagraph(next) {
+ para := ast.NewParagraph()
+ para.Lines().Append(segment)
+ heading.Parent().InsertAfter(heading.Parent(), heading, para)
+ } else {
+ next.Lines().Unshift(segment)
+ }
+ heading.Parent().RemoveChild(heading.Parent(), heading)
+ } else {
+ heading.SetPos(tmp.Lines().At(0).Start)
+ heading.SetLines(tmp.Lines())
+ heading.SetBlankPreviousLines(tmp.HasBlankPreviousLines())
+ tp := tmp.Parent()
+ if tp != nil {
+ tp.RemoveChild(tp, tmp)
+ }
+ }
+
+ if b.Attribute {
+ parseLastLineAttributes(node, reader, pc)
+ }
+
+ if b.AutoHeadingID {
+ id, ok := node.AttributeString("id")
+ if !ok {
+ generateAutoHeadingID(heading, reader, pc)
+ } else {
+ pc.IDs().Put(id.([]byte))
+ }
+ }
+}
+
+func (b *setextHeadingParser) CanInterruptParagraph() bool {
+ return true
+}
+
+func (b *setextHeadingParser) CanAcceptIndentedLine() bool {
+ return false
+}
diff --git a/backend/goldmark/parser/thematic_break.go b/backend/goldmark/parser/thematic_break.go
new file mode 100644
index 0000000..ea015c8
--- /dev/null
+++ b/backend/goldmark/parser/thematic_break.go
@@ -0,0 +1,75 @@
+package parser
+
+import (
+ "github.com/yuin/goldmark/ast"
+ "github.com/yuin/goldmark/text"
+ "github.com/yuin/goldmark/util"
+)
+
+type thematicBreakPraser struct {
+}
+
+var defaultThematicBreakPraser = &thematicBreakPraser{}
+
+// NewThematicBreakParser returns a new BlockParser that
+// parses thematic breaks.
+func NewThematicBreakParser() BlockParser {
+ return defaultThematicBreakPraser
+}
+
+func isThematicBreak(line []byte, offset int) bool {
+ w, pos := util.IndentWidth(line, offset)
+ if w > 3 {
+ return false
+ }
+ mark := byte(0)
+ count := 0
+ for i := pos; i < len(line); i++ {
+ c := line[i]
+ if util.IsSpace(c) {
+ continue
+ }
+ if mark == 0 {
+ mark = c
+ count = 1
+ if mark == '*' || mark == '-' || mark == '_' {
+ continue
+ }
+ return false
+ }
+ if c != mark {
+ return false
+ }
+ count++
+ }
+ return count > 2
+}
+
+func (b *thematicBreakPraser) Trigger() []byte {
+ return []byte{'-', '*', '_'}
+}
+
+func (b *thematicBreakPraser) Open(parent ast.Node, reader text.Reader, pc Context) (ast.Node, State) {
+ line, _ := reader.PeekLine()
+ if isThematicBreak(line, reader.LineOffset()) {
+ reader.AdvanceToEOL()
+ return ast.NewThematicBreak(), NoChildren
+ }
+ return nil, NoChildren
+}
+
+func (b *thematicBreakPraser) Continue(node ast.Node, reader text.Reader, pc Context) State {
+ return Close
+}
+
+func (b *thematicBreakPraser) Close(node ast.Node, reader text.Reader, pc Context) {
+ // nothing to do
+}
+
+func (b *thematicBreakPraser) CanInterruptParagraph() bool {
+ return true
+}
+
+func (b *thematicBreakPraser) CanAcceptIndentedLine() bool {
+ return false
+}
diff --git a/backend/goldmark/renderer/html/html.go b/backend/goldmark/renderer/html/html.go
new file mode 100644
index 0000000..c0b72ce
--- /dev/null
+++ b/backend/goldmark/renderer/html/html.go
@@ -0,0 +1,962 @@
+// Package html implements renderer that outputs HTMLs.
+package html
+
+import (
+ "bytes"
+ "fmt"
+ "strconv"
+ "unicode"
+ "unicode/utf8"
+
+ "github.com/yuin/goldmark/ast"
+ "github.com/yuin/goldmark/renderer"
+ "github.com/yuin/goldmark/util"
+)
+
+// A Config struct has configurations for the HTML based renderers.
+type Config struct {
+ Writer Writer
+ HardWraps bool
+ EastAsianLineBreaks EastAsianLineBreaks
+ XHTML bool
+ Unsafe bool
+}
+
+// NewConfig returns a new Config with defaults.
+func NewConfig() Config {
+ return Config{
+ Writer: DefaultWriter,
+ HardWraps: false,
+ EastAsianLineBreaks: EastAsianLineBreaksNone,
+ XHTML: false,
+ Unsafe: false,
+ }
+}
+
+// SetOption implements renderer.NodeRenderer.SetOption.
+func (c *Config) SetOption(name renderer.OptionName, value any) {
+ switch name {
+ case optHardWraps:
+ c.HardWraps = value.(bool)
+ case optEastAsianLineBreaks:
+ c.EastAsianLineBreaks = value.(EastAsianLineBreaks)
+ case optXHTML:
+ c.XHTML = value.(bool)
+ case optUnsafe:
+ c.Unsafe = value.(bool)
+ case optTextWriter:
+ c.Writer = value.(Writer)
+ }
+}
+
+// An Option interface sets options for HTML based renderers.
+type Option interface {
+ SetHTMLOption(*Config)
+}
+
+// TextWriter is an option name used in WithWriter.
+const optTextWriter renderer.OptionName = "Writer"
+
+type withWriter struct {
+ value Writer
+}
+
+func (o *withWriter) SetConfig(c *renderer.Config) {
+ c.Options[optTextWriter] = o.value
+}
+
+func (o *withWriter) SetHTMLOption(c *Config) {
+ c.Writer = o.value
+}
+
+// WithWriter is a functional option that allow you to set the given writer to
+// the renderer.
+func WithWriter(writer Writer) interface {
+ renderer.Option
+ Option
+} {
+ return &withWriter{writer}
+}
+
+// HardWraps is an option name used in WithHardWraps.
+const optHardWraps renderer.OptionName = "HardWraps"
+
+type withHardWraps struct {
+}
+
+func (o *withHardWraps) SetConfig(c *renderer.Config) {
+ c.Options[optHardWraps] = true
+}
+
+func (o *withHardWraps) SetHTMLOption(c *Config) {
+ c.HardWraps = true
+}
+
+// WithHardWraps is a functional option that indicates whether softline breaks
+// should be rendered as ' '.
+func WithHardWraps() interface {
+ renderer.Option
+ Option
+} {
+ return &withHardWraps{}
+}
+
+// EastAsianLineBreaks is an option name used in WithEastAsianLineBreaks.
+const optEastAsianLineBreaks renderer.OptionName = "EastAsianLineBreaks"
+
+// A EastAsianLineBreaks is a style of east asian line breaks.
+type EastAsianLineBreaks int
+
+const (
+ //EastAsianLineBreaksNone renders line breaks as it is.
+ EastAsianLineBreaksNone EastAsianLineBreaks = iota
+ // EastAsianLineBreaksSimple follows east_asian_line_breaks in Pandoc.
+ EastAsianLineBreaksSimple
+ // EastAsianLineBreaksCSS3Draft follows CSS text level3 "Segment Break Transformation Rules" with some enhancements.
+ EastAsianLineBreaksCSS3Draft
+)
+
+func (b EastAsianLineBreaks) softLineBreak(thisLastRune rune, siblingFirstRune rune) bool {
+ switch b {
+ case EastAsianLineBreaksNone:
+ return false
+ case EastAsianLineBreaksSimple:
+ return !(util.IsEastAsianWideRune(thisLastRune) && util.IsEastAsianWideRune(siblingFirstRune))
+ case EastAsianLineBreaksCSS3Draft:
+ return eastAsianLineBreaksCSS3DraftSoftLineBreak(thisLastRune, siblingFirstRune)
+ }
+ return false
+}
+
+func eastAsianLineBreaksCSS3DraftSoftLineBreak(thisLastRune rune, siblingFirstRune rune) bool {
+ // Implements CSS text level3 Segment Break Transformation Rules with some enhancements.
+ // References:
+ // - https://www.w3.org/TR/2020/WD-css-text-3-20200429/#line-break-transform
+ // - https://github.com/w3c/csswg-drafts/issues/5086
+
+ // Rule1:
+ // If the character immediately before or immediately after the segment break is
+ // the zero-width space character (U+200B), then the break is removed, leaving behind the zero-width space.
+ if thisLastRune == '\u200B' || siblingFirstRune == '\u200B' {
+ return false
+ }
+
+ // Rule2:
+ // Otherwise, if the East Asian Width property of both the character before and after the segment break is
+ // F, W, or H (not A), and neither side is Hangul, then the segment break is removed.
+ thisLastRuneEastAsianWidth := util.EastAsianWidth(thisLastRune)
+ siblingFirstRuneEastAsianWidth := util.EastAsianWidth(siblingFirstRune)
+ if (thisLastRuneEastAsianWidth == "F" ||
+ thisLastRuneEastAsianWidth == "W" ||
+ thisLastRuneEastAsianWidth == "H") &&
+ (siblingFirstRuneEastAsianWidth == "F" ||
+ siblingFirstRuneEastAsianWidth == "W" ||
+ siblingFirstRuneEastAsianWidth == "H") {
+ return unicode.Is(unicode.Hangul, thisLastRune) || unicode.Is(unicode.Hangul, siblingFirstRune)
+ }
+
+ // Rule3:
+ // Otherwise, if either the character before or after the segment break belongs to
+ // the space-discarding character set and it is a Unicode Punctuation (P*) or U+3000,
+ // then the segment break is removed.
+ if util.IsSpaceDiscardingUnicodeRune(thisLastRune) ||
+ unicode.IsPunct(thisLastRune) ||
+ thisLastRune == '\u3000' ||
+ util.IsSpaceDiscardingUnicodeRune(siblingFirstRune) ||
+ unicode.IsPunct(siblingFirstRune) ||
+ siblingFirstRune == '\u3000' {
+ return false
+ }
+
+ // Rule4:
+ // Otherwise, the segment break is converted to a space (U+0020).
+ return true
+}
+
+type withEastAsianLineBreaks struct {
+ eastAsianLineBreaksStyle EastAsianLineBreaks
+}
+
+func (o *withEastAsianLineBreaks) SetConfig(c *renderer.Config) {
+ c.Options[optEastAsianLineBreaks] = o.eastAsianLineBreaksStyle
+}
+
+func (o *withEastAsianLineBreaks) SetHTMLOption(c *Config) {
+ c.EastAsianLineBreaks = o.eastAsianLineBreaksStyle
+}
+
+// WithEastAsianLineBreaks is a functional option that indicates whether softline breaks
+// between east asian wide characters should be ignored.
+func WithEastAsianLineBreaks(e EastAsianLineBreaks) interface {
+ renderer.Option
+ Option
+} {
+ return &withEastAsianLineBreaks{e}
+}
+
+// XHTML is an option name used in WithXHTML.
+const optXHTML renderer.OptionName = "XHTML"
+
+type withXHTML struct {
+}
+
+func (o *withXHTML) SetConfig(c *renderer.Config) {
+ c.Options[optXHTML] = true
+}
+
+func (o *withXHTML) SetHTMLOption(c *Config) {
+ c.XHTML = true
+}
+
+// WithXHTML is a functional option indicates that nodes should be rendered in
+// xhtml instead of HTML5.
+func WithXHTML() interface {
+ Option
+ renderer.Option
+} {
+ return &withXHTML{}
+}
+
+// Unsafe is an option name used in WithUnsafe.
+const optUnsafe renderer.OptionName = "Unsafe"
+
+type withUnsafe struct {
+}
+
+func (o *withUnsafe) SetConfig(c *renderer.Config) {
+ c.Options[optUnsafe] = true
+}
+
+func (o *withUnsafe) SetHTMLOption(c *Config) {
+ c.Unsafe = true
+}
+
+// WithUnsafe is a functional option that renders dangerous contents
+// (raw htmls and potentially dangerous links) as it is.
+func WithUnsafe() interface {
+ renderer.Option
+ Option
+} {
+ return &withUnsafe{}
+}
+
+// A Renderer struct is an implementation of renderer.NodeRenderer that renders
+// nodes as (X)HTML.
+type Renderer struct {
+ Config
+}
+
+// NewRenderer returns a new Renderer with given options.
+func NewRenderer(opts ...Option) renderer.NodeRenderer {
+ r := &Renderer{
+ Config: NewConfig(),
+ }
+
+ for _, opt := range opts {
+ opt.SetHTMLOption(&r.Config)
+ }
+ return r
+}
+
+// RegisterFuncs implements NodeRenderer.RegisterFuncs .
+func (r *Renderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) {
+ // blocks
+
+ reg.Register(ast.KindDocument, r.renderDocument)
+ reg.Register(ast.KindHeading, r.renderHeading)
+ reg.Register(ast.KindBlockquote, r.renderBlockquote)
+ reg.Register(ast.KindCodeBlock, r.renderCodeBlock)
+ reg.Register(ast.KindFencedCodeBlock, r.renderFencedCodeBlock)
+ reg.Register(ast.KindHTMLBlock, r.renderHTMLBlock)
+ reg.Register(ast.KindList, r.renderList)
+ reg.Register(ast.KindListItem, r.renderListItem)
+ reg.Register(ast.KindParagraph, r.renderParagraph)
+ reg.Register(ast.KindTextBlock, r.renderTextBlock)
+ reg.Register(ast.KindThematicBreak, r.renderThematicBreak)
+ reg.Register(ast.KindLinkReferenceDefinition, func(
+ _ util.BufWriter, _ []byte, _ ast.Node, _ bool) (ast.WalkStatus, error) {
+ return ast.WalkSkipChildren, nil
+ })
+
+ // inlines
+
+ reg.Register(ast.KindAutoLink, r.renderAutoLink)
+ reg.Register(ast.KindCodeSpan, r.renderCodeSpan)
+ reg.Register(ast.KindEmphasis, r.renderEmphasis)
+ reg.Register(ast.KindImage, r.renderImage)
+ reg.Register(ast.KindLink, r.renderLink)
+ reg.Register(ast.KindRawHTML, r.renderRawHTML)
+ reg.Register(ast.KindText, r.renderText)
+ reg.Register(ast.KindString, r.renderString)
+}
+
+func (r *Renderer) writeLines(w util.BufWriter, source []byte, n ast.Node) {
+ l := n.Lines().Len()
+ for i := range l {
+ line := n.Lines().At(i)
+ r.Writer.RawWrite(w, line.Value(source))
+ }
+}
+
+// GlobalAttributeFilter defines attribute names which any elements can have.
+var GlobalAttributeFilter = util.NewBytesFilterString(`accesskey,autocapitalize,autofocus,class,contenteditable,dir,draggable,enterkeyhint,hidden,id,inert,inputmode,is,itemid,itemprop,itemref,itemscope,itemtype,lang,part,role,slot,spellcheck,style,tabindex,title,translate`) // nolint:lll
+
+func (r *Renderer) renderDocument(
+ w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
+ // nothing to do
+ return ast.WalkContinue, nil
+}
+
+// HeadingAttributeFilter defines attribute names which heading elements can have.
+var HeadingAttributeFilter = GlobalAttributeFilter
+
+func (r *Renderer) renderHeading(
+ w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
+ n := node.(*ast.Heading)
+ if entering {
+ _, _ = w.WriteString("')
+ } else {
+ _, _ = w.WriteString(" \n")
+ }
+ return ast.WalkContinue, nil
+}
+
+// BlockquoteAttributeFilter defines attribute names which blockquote elements can have.
+var BlockquoteAttributeFilter = GlobalAttributeFilter.ExtendString(`cite`)
+
+func (r *Renderer) renderBlockquote(
+ w util.BufWriter, source []byte, n ast.Node, entering bool) (ast.WalkStatus, error) {
+ if entering {
+ if n.Attributes() != nil {
+ _, _ = w.WriteString("')
+ } else {
+ _, _ = w.WriteString("\n")
+ }
+ } else {
+ _, _ = w.WriteString(" \n")
+ }
+ return ast.WalkContinue, nil
+}
+
+func (r *Renderer) renderCodeBlock(w util.BufWriter, source []byte, n ast.Node, entering bool) (ast.WalkStatus, error) {
+ if entering {
+ _, _ = w.WriteString("")
+ r.writeLines(w, source, n)
+ } else {
+ _, _ = w.WriteString(" \n")
+ }
+ return ast.WalkContinue, nil
+}
+
+func (r *Renderer) renderFencedCodeBlock(
+ w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
+ n := node.(*ast.FencedCodeBlock)
+ if entering {
+ _, _ = w.WriteString("')
+ r.writeLines(w, source, n)
+ } else {
+ _, _ = w.WriteString(" \n")
+ }
+ return ast.WalkContinue, nil
+}
+
+func (r *Renderer) renderHTMLBlock(
+ w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
+ n := node.(*ast.HTMLBlock)
+ if entering {
+ if r.Unsafe {
+ l := n.Lines().Len()
+ for i := range l {
+ line := n.Lines().At(i)
+ r.Writer.SecureWrite(w, line.Value(source))
+ }
+ } else {
+ _, _ = w.WriteString("\n")
+ }
+ } else {
+ if n.HasClosure() {
+ if r.Unsafe {
+ closure := n.ClosureLine
+ r.Writer.SecureWrite(w, closure.Value(source))
+ } else {
+ _, _ = w.WriteString("\n")
+ }
+ }
+ }
+ return ast.WalkContinue, nil
+}
+
+// ListAttributeFilter defines attribute names which list elements can have.
+var ListAttributeFilter = GlobalAttributeFilter.ExtendString(`start,reversed,type`)
+
+func (r *Renderer) renderList(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
+ n := node.(*ast.List)
+ tag := "ul"
+ if n.IsOrdered() {
+ tag = "ol"
+ }
+ if entering {
+ _ = w.WriteByte('<')
+ _, _ = w.WriteString(tag)
+ if n.IsOrdered() && n.Start != 1 {
+ _, _ = fmt.Fprintf(w, " start=\"%d\"", n.Start)
+ }
+ if n.Attributes() != nil {
+ RenderAttributes(w, n, ListAttributeFilter)
+ }
+ _, _ = w.WriteString(">\n")
+ } else {
+ _, _ = w.WriteString("")
+ _, _ = w.WriteString(tag)
+ _, _ = w.WriteString(">\n")
+ }
+ return ast.WalkContinue, nil
+}
+
+// ListItemAttributeFilter defines attribute names which list item elements can have.
+var ListItemAttributeFilter = GlobalAttributeFilter.ExtendString(`value`)
+
+func (r *Renderer) renderListItem(w util.BufWriter, source []byte, n ast.Node, entering bool) (ast.WalkStatus, error) {
+ if entering {
+ if n.Attributes() != nil {
+ _, _ = w.WriteString("')
+ } else {
+ _, _ = w.WriteString(" ")
+ }
+ fc := n.FirstChild()
+ if fc != nil {
+ if _, ok := fc.(*ast.TextBlock); !ok {
+ _ = w.WriteByte('\n')
+ }
+ }
+ } else {
+ _, _ = w.WriteString(" \n")
+ }
+ return ast.WalkContinue, nil
+}
+
+// ParagraphAttributeFilter defines attribute names which paragraph elements can have.
+var ParagraphAttributeFilter = GlobalAttributeFilter
+
+func (r *Renderer) renderParagraph(w util.BufWriter, source []byte, n ast.Node, entering bool) (ast.WalkStatus, error) {
+ if entering {
+ if n.Attributes() != nil {
+ _, _ = w.WriteString("')
+ } else {
+ _, _ = w.WriteString("
")
+ }
+ } else {
+ _, _ = w.WriteString("
\n")
+ }
+ return ast.WalkContinue, nil
+}
+
+func (r *Renderer) renderTextBlock(w util.BufWriter, source []byte, n ast.Node, entering bool) (ast.WalkStatus, error) {
+ if !entering {
+ if n.NextSibling() != nil && n.FirstChild() != nil {
+ _ = w.WriteByte('\n')
+ }
+ }
+ return ast.WalkContinue, nil
+}
+
+// ThematicAttributeFilter defines attribute names which hr elements can have.
+var ThematicAttributeFilter = GlobalAttributeFilter.ExtendString(`align,color,noshade,size,width`)
+
+func (r *Renderer) renderThematicBreak(
+ w util.BufWriter, source []byte, n ast.Node, entering bool) (ast.WalkStatus, error) {
+ if !entering {
+ return ast.WalkContinue, nil
+ }
+ _, _ = w.WriteString(" \n")
+ } else {
+ _, _ = w.WriteString(">\n")
+ }
+ return ast.WalkContinue, nil
+}
+
+// LinkAttributeFilter defines attribute names which link elements can have.
+var LinkAttributeFilter = GlobalAttributeFilter.ExtendString(`download,href,lang,media,ping,referrerpolicy,rel,shape,target`) // nolint:lll
+
+func (r *Renderer) renderAutoLink(
+ w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
+ n := node.(*ast.AutoLink)
+ if !entering {
+ return ast.WalkContinue, nil
+ }
+ _, _ = w.WriteString(`')
+ } else {
+ _, _ = w.WriteString(`">`)
+ }
+ _, _ = w.Write(util.EscapeHTML(label))
+ _, _ = w.WriteString(` `)
+ return ast.WalkContinue, nil
+}
+
+// CodeAttributeFilter defines attribute names which code elements can have.
+var CodeAttributeFilter = GlobalAttributeFilter
+
+func (r *Renderer) renderCodeSpan(w util.BufWriter, source []byte, n ast.Node, entering bool) (ast.WalkStatus, error) {
+ if entering {
+ if n.Attributes() != nil {
+ _, _ = w.WriteString("')
+ } else {
+ _, _ = w.WriteString("")
+ }
+ for c := n.FirstChild(); c != nil; c = c.NextSibling() {
+ segment := c.(*ast.Text).Segment
+ value := segment.Value(source)
+ if bytes.HasSuffix(value, []byte("\n")) {
+ r.Writer.RawWrite(w, value[:len(value)-1])
+ r.Writer.RawWrite(w, []byte(" "))
+ } else {
+ r.Writer.RawWrite(w, value)
+ }
+ }
+ return ast.WalkSkipChildren, nil
+ }
+ _, _ = w.WriteString("")
+ return ast.WalkContinue, nil
+}
+
+// EmphasisAttributeFilter defines attribute names which emphasis elements can have.
+var EmphasisAttributeFilter = GlobalAttributeFilter
+
+func (r *Renderer) renderEmphasis(
+ w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
+ n := node.(*ast.Emphasis)
+ tag := "em"
+ if n.Level == 2 {
+ tag = "strong"
+ }
+ if entering {
+ _ = w.WriteByte('<')
+ _, _ = w.WriteString(tag)
+ if n.Attributes() != nil {
+ RenderAttributes(w, n, EmphasisAttributeFilter)
+ }
+ _ = w.WriteByte('>')
+ } else {
+ _, _ = w.WriteString("")
+ _, _ = w.WriteString(tag)
+ _ = w.WriteByte('>')
+ }
+ return ast.WalkContinue, nil
+}
+
+func (r *Renderer) renderLink(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
+ n := node.(*ast.Link)
+ if entering {
+ _, _ = w.WriteString("')
+ } else {
+ _, _ = w.WriteString(" ")
+ }
+ return ast.WalkContinue, nil
+}
+
+// ImageAttributeFilter defines attribute names which image elements can have.
+var ImageAttributeFilter = GlobalAttributeFilter.ExtendString(`align,border,crossorigin,decoding,height,importance,intrinsicsize,ismap,loading,referrerpolicy,sizes,srcset,usemap,width`) // nolint: lll
+
+func (r *Renderer) renderImage(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
+ if !entering {
+ return ast.WalkContinue, nil
+ }
+ n := node.(*ast.Image)
+ _, _ = w.WriteString(" ")
+ } else {
+ _, _ = w.WriteString(">")
+ }
+ return ast.WalkSkipChildren, nil
+}
+
+func (r *Renderer) renderRawHTML(
+ w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
+ if !entering {
+ return ast.WalkSkipChildren, nil
+ }
+ if r.Unsafe {
+ n := node.(*ast.RawHTML)
+ l := n.Segments.Len()
+ for i := range l {
+ segment := n.Segments.At(i)
+ _, _ = w.Write(segment.Value(source))
+ }
+ return ast.WalkSkipChildren, nil
+ }
+ _, _ = w.WriteString("")
+ return ast.WalkSkipChildren, nil
+}
+
+func (r *Renderer) renderText(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
+ if !entering {
+ return ast.WalkContinue, nil
+ }
+ n := node.(*ast.Text)
+ segment := n.Segment
+ if n.IsRaw() {
+ r.Writer.RawWrite(w, segment.Value(source))
+ } else {
+ value := segment.Value(source)
+ r.Writer.Write(w, value)
+ if n.HardLineBreak() || (n.SoftLineBreak() && r.HardWraps) {
+ if r.XHTML {
+ _, _ = w.WriteString(" \n")
+ } else {
+ _, _ = w.WriteString(" \n")
+ }
+ } else if n.SoftLineBreak() {
+ if r.EastAsianLineBreaks != EastAsianLineBreaksNone && len(value) != 0 {
+ sibling := node.NextSibling()
+ if sibling != nil && sibling.Kind() == ast.KindText {
+ if siblingText := sibling.(*ast.Text).Value(source); len(siblingText) != 0 {
+ thisLastRune := util.ToRune(value, len(value)-1)
+ siblingFirstRune, _ := utf8.DecodeRune(siblingText)
+ if r.EastAsianLineBreaks.softLineBreak(thisLastRune, siblingFirstRune) {
+ _ = w.WriteByte('\n')
+ }
+ }
+ }
+ } else {
+ _ = w.WriteByte('\n')
+ }
+ }
+ }
+ return ast.WalkContinue, nil
+}
+
+func (r *Renderer) renderString(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
+ if !entering {
+ return ast.WalkContinue, nil
+ }
+ n := node.(*ast.String)
+ if n.IsCode() {
+ _, _ = w.Write(n.Value)
+ } else {
+ if n.IsRaw() {
+ r.Writer.RawWrite(w, n.Value)
+ } else {
+ r.Writer.Write(w, n.Value)
+ }
+ }
+ return ast.WalkContinue, nil
+}
+
+func (r *Renderer) renderTexts(w util.BufWriter, source []byte, n ast.Node) {
+ for c := n.FirstChild(); c != nil; c = c.NextSibling() {
+ if s, ok := c.(*ast.String); ok {
+ _, _ = r.renderString(w, source, s, true)
+ } else if t, ok := c.(*ast.Text); ok {
+ _, _ = r.renderText(w, source, t, true)
+ } else {
+ r.renderTexts(w, source, c)
+ }
+ }
+}
+
+var dataPrefix = []byte("data-")
+
+// RenderAttributes renders given node's attributes.
+// You can specify attribute names to render by the filter.
+// If filter is nil, RenderAttributes renders all attributes.
+func RenderAttributes(w util.BufWriter, node ast.Node, filter util.BytesFilter) {
+ for _, attr := range node.Attributes() {
+ if filter != nil && !filter.Contains(attr.Name) {
+ if !bytes.HasPrefix(attr.Name, dataPrefix) {
+ continue
+ }
+ }
+ _, _ = w.WriteString(" ")
+ _, _ = w.Write(attr.Name)
+ _, _ = w.WriteString(`="`)
+ // TODO: convert numeric values to strings
+ var value []byte
+ switch typed := attr.Value.(type) {
+ case []byte:
+ value = typed
+ case string:
+ value = util.StringToReadOnlyBytes(typed)
+ }
+ _, _ = w.Write(util.EscapeHTML(value))
+ _ = w.WriteByte('"')
+ }
+}
+
+// A Writer interface writes textual contents to a writer.
+type Writer interface {
+ // Write writes the given source to writer with resolving references and unescaping
+ // backslash escaped characters.
+ Write(writer util.BufWriter, source []byte)
+
+ // 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")
+
+// A WriterConfig struct has configurations for the HTML based writers.
+type WriterConfig struct {
+ // EscapedSpace is an option that indicates that a '\' escaped half-space(0x20) should not be rendered.
+ EscapedSpace bool
+}
+
+// A WriterOption interface sets options for HTML based writers.
+type WriterOption func(*WriterConfig)
+
+// WithEscapedSpace is a WriterOption indicates that a '\' escaped half-space(0x20) should not be rendered.
+func WithEscapedSpace() WriterOption {
+ return func(c *WriterConfig) {
+ c.EscapedSpace = true
+ }
+}
+
+type defaultWriter struct {
+ WriterConfig
+}
+
+// NewWriter returns a new Writer.
+func NewWriter(opts ...WriterOption) Writer {
+ w := &defaultWriter{}
+ for _, opt := range opts {
+ opt(&w.WriterConfig)
+ }
+ return w
+}
+
+func escapeRune(writer util.BufWriter, r rune) {
+ if r < 256 {
+ v := util.EscapeHTMLByte(byte(r))
+ if v != nil {
+ _, _ = writer.Write(v)
+ return
+ }
+ }
+ _, _ = writer.WriteRune(util.ToValidRune(r))
+}
+
+func (d *defaultWriter) SecureWrite(writer util.BufWriter, source []byte) {
+ n := 0
+ l := len(source)
+ for i := range l {
+ 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)
+ for i := range l {
+ v := util.EscapeHTMLByte(source[i])
+ if v != nil {
+ _, _ = writer.Write(source[i-n : i])
+ n = 0
+ _, _ = writer.Write(v)
+ continue
+ }
+ n++
+ }
+ if n != 0 {
+ _, _ = writer.Write(source[l-n:])
+ }
+}
+
+func (d *defaultWriter) Write(writer util.BufWriter, source []byte) {
+ escaped := false
+ var ok bool
+ limit := len(source)
+ n := 0
+ for i := 0; i < limit; i++ {
+ c := source[i]
+ if escaped {
+ if util.IsPunct(c) {
+ d.RawWrite(writer, source[n:i-1])
+ n = i
+ escaped = false
+ continue
+ }
+ if d.EscapedSpace && c == ' ' {
+ d.RawWrite(writer, source[n:i-1])
+ n = i + 1
+ escaped = false
+ 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
+ if next < limit && source[next] == '#' {
+ nnext := next + 1
+ if nnext < limit {
+ nc := source[nnext]
+ // code point like #x22;
+ 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] == ';' && i-start < 7 {
+ v, _ := strconv.ParseUint(util.BytesToReadOnlyString(source[start:i]), 16, 32)
+ d.RawWrite(writer, source[n:pos])
+ n = i + 1
+ escapeRune(writer, rune(v))
+ continue
+ }
+ // code point like #1234;
+ } else if nc >= '0' && nc <= '9' {
+ start := nnext
+ i, ok = 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]), 10, 32)
+ d.RawWrite(writer, source[n:pos])
+ n = i + 1
+ escapeRune(writer, rune(v))
+ continue
+ }
+ }
+ }
+ } else {
+ start := next
+ i, ok = util.ReadWhile(source, [2]int{start, limit}, util.IsAlphaNumeric)
+ // entity reference
+ if ok && i < limit && source[i] == ';' {
+ name := util.BytesToReadOnlyString(source[start:i])
+ entity, ok := util.LookUpHTML5EntityByName(name)
+ if ok {
+ d.RawWrite(writer, source[n:pos])
+ n = i + 1
+ d.RawWrite(writer, entity.Characters)
+ continue
+ }
+ }
+ }
+ i = next - 1
+ }
+ if c == '\\' {
+ escaped = true
+ continue
+ }
+ escaped = false
+ }
+ d.RawWrite(writer, source[n:])
+}
+
+// DefaultWriter is a default instance of the Writer.
+var DefaultWriter = NewWriter()
+
+var bDataImage = []byte("data:image/")
+var bPng = []byte("png;")
+var bGif = []byte("gif;")
+var bJpeg = []byte("jpeg;")
+var bWebp = []byte("webp;")
+var bSvg = []byte("svg+xml;")
+var bJs = []byte("javascript:")
+var bVb = []byte("vbscript:")
+var bFile = []byte("file:")
+var bData = []byte("data:")
+
+func hasPrefix(s, prefix []byte) bool {
+ return len(s) >= len(prefix) && bytes.Equal(bytes.ToLower(s[0:len(prefix)]), bytes.ToLower(prefix))
+}
+
+// IsDangerousURL returns true if the given url seems a potentially dangerous url,
+// otherwise false.
+func IsDangerousURL(url []byte) bool {
+ if hasPrefix(url, bDataImage) && len(url) >= 11 {
+ v := url[11:]
+ if hasPrefix(v, bPng) || hasPrefix(v, bGif) ||
+ hasPrefix(v, bJpeg) || hasPrefix(v, bWebp) ||
+ hasPrefix(v, bSvg) {
+ return false
+ }
+ return true
+ }
+ return hasPrefix(url, bJs) || hasPrefix(url, bVb) ||
+ hasPrefix(url, bFile) || hasPrefix(url, bData)
+}
diff --git a/backend/goldmark/renderer/renderer.go b/backend/goldmark/renderer/renderer.go
new file mode 100644
index 0000000..8f1c912
--- /dev/null
+++ b/backend/goldmark/renderer/renderer.go
@@ -0,0 +1,174 @@
+// Package renderer renders the given AST to certain formats.
+package renderer
+
+import (
+ "bufio"
+ "io"
+ "sync"
+
+ "github.com/yuin/goldmark/ast"
+ "github.com/yuin/goldmark/util"
+)
+
+// A Config struct is a data structure that holds configuration of the Renderer.
+type Config struct {
+ Options map[OptionName]any
+ NodeRenderers util.PrioritizedSlice
+}
+
+// NewConfig returns a new Config.
+func NewConfig() *Config {
+ return &Config{
+ Options: map[OptionName]any{},
+ NodeRenderers: util.PrioritizedSlice{},
+ }
+}
+
+// An OptionName is a name of the option.
+type OptionName string
+
+// An Option interface is a functional option type for the Renderer.
+type Option interface {
+ SetConfig(*Config)
+}
+
+type withNodeRenderers struct {
+ value []util.PrioritizedValue
+}
+
+func (o *withNodeRenderers) SetConfig(c *Config) {
+ c.NodeRenderers = append(c.NodeRenderers, o.value...)
+}
+
+// WithNodeRenderers is a functional option that allow you to add
+// NodeRenderers to the renderer.
+func WithNodeRenderers(ps ...util.PrioritizedValue) Option {
+ return &withNodeRenderers{ps}
+}
+
+type withOption struct {
+ name OptionName
+ value any
+}
+
+func (o *withOption) SetConfig(c *Config) {
+ c.Options[o.name] = o.value
+}
+
+// WithOption is a functional option that allow you to set
+// an arbitrary option to the parser.
+func WithOption(name OptionName, value any) Option {
+ return &withOption{name, value}
+}
+
+// A SetOptioner interface sets given option to the object.
+type SetOptioner interface {
+ // SetOption sets given option to the object.
+ // Unacceptable options may be passed.
+ // Thus implementations must ignore unacceptable options.
+ SetOption(name OptionName, value any)
+}
+
+// NodeRendererFunc is a function that renders a given node.
+type NodeRendererFunc func(writer util.BufWriter, source []byte, n ast.Node, entering bool) (ast.WalkStatus, error)
+
+// A NodeRenderer interface offers NodeRendererFuncs.
+type NodeRenderer interface {
+ // RendererFuncs registers NodeRendererFuncs to given NodeRendererFuncRegisterer.
+ RegisterFuncs(NodeRendererFuncRegisterer)
+}
+
+// A NodeRendererFuncRegisterer registers given NodeRendererFunc to this object.
+type NodeRendererFuncRegisterer interface {
+ // Register registers given NodeRendererFunc to this object.
+ Register(ast.NodeKind, NodeRendererFunc)
+}
+
+// A Renderer interface renders given AST node to given
+// writer with given Renderer.
+type Renderer interface {
+ Render(w io.Writer, source []byte, n ast.Node) error
+
+ // AddOptions adds given option to this renderer.
+ AddOptions(...Option)
+}
+
+type renderer struct {
+ config *Config
+ options map[OptionName]any
+ nodeRendererFuncsTmp map[ast.NodeKind]NodeRendererFunc
+ maxKind int
+ nodeRendererFuncs []NodeRendererFunc
+ initSync sync.Once
+}
+
+// NewRenderer returns a new Renderer with given options.
+func NewRenderer(options ...Option) Renderer {
+ config := NewConfig()
+ for _, opt := range options {
+ opt.SetConfig(config)
+ }
+
+ r := &renderer{
+ options: map[OptionName]any{},
+ config: config,
+ nodeRendererFuncsTmp: map[ast.NodeKind]NodeRendererFunc{},
+ }
+
+ return r
+}
+
+func (r *renderer) AddOptions(opts ...Option) {
+ for _, opt := range opts {
+ opt.SetConfig(r.config)
+ }
+}
+
+func (r *renderer) Register(kind ast.NodeKind, v NodeRendererFunc) {
+ r.nodeRendererFuncsTmp[kind] = v
+ if int(kind) > r.maxKind {
+ r.maxKind = int(kind)
+ }
+}
+
+// Render renders the given AST node to the given writer with the given Renderer.
+func (r *renderer) Render(w io.Writer, source []byte, n ast.Node) error {
+ r.initSync.Do(func() {
+ r.options = r.config.Options
+ r.config.NodeRenderers.Sort()
+ l := len(r.config.NodeRenderers)
+ for i := l - 1; i >= 0; i-- {
+ v := r.config.NodeRenderers[i]
+ nr, _ := v.Value.(NodeRenderer)
+ if se, ok := v.Value.(SetOptioner); ok {
+ for oname, ovalue := range r.options {
+ se.SetOption(oname, ovalue)
+ }
+ }
+ nr.RegisterFuncs(r)
+ }
+ r.nodeRendererFuncs = make([]NodeRendererFunc, r.maxKind+1)
+ for kind, nr := range r.nodeRendererFuncsTmp {
+ r.nodeRendererFuncs[kind] = nr
+ }
+ r.config = nil
+ r.nodeRendererFuncsTmp = nil
+ })
+ writer, ok := w.(util.BufWriter)
+ if !ok {
+ writer = bufio.NewWriter(w)
+ }
+ err := ast.Walk(n, func(n ast.Node, entering bool) (ast.WalkStatus, error) {
+ s := ast.WalkStatus(ast.WalkContinue)
+ var err error
+ f := r.nodeRendererFuncs[n.Kind()]
+ if f != nil {
+ s, err = f(writer, source, n, entering)
+ }
+ return s, err
+ })
+ if err != nil {
+ return err
+ }
+ return writer.Flush()
+}
diff --git a/backend/goldmark/testutil/testutil.go b/backend/goldmark/testutil/testutil.go
new file mode 100644
index 0000000..2cab2e5
--- /dev/null
+++ b/backend/goldmark/testutil/testutil.go
@@ -0,0 +1,405 @@
+// Package testutil provides utilities for unit tests.
+package testutil
+
+import (
+ "bufio"
+ "bytes"
+ "encoding/hex"
+ "encoding/json"
+ "fmt"
+ "os"
+ "regexp"
+ "runtime/debug"
+ "slices"
+ "strconv"
+ "strings"
+
+ "github.com/yuin/goldmark"
+ "github.com/yuin/goldmark/parser"
+ "github.com/yuin/goldmark/util"
+)
+
+// TestingT is a subset of the functionality provided by testing.T.
+type TestingT interface {
+ Logf(string, ...any)
+ Skipf(string, ...any)
+ Errorf(string, ...any)
+ FailNow()
+}
+
+// MarkdownTestCase represents a test case.
+type MarkdownTestCase struct {
+ No int
+ Description string
+ Options MarkdownTestCaseOptions
+ Markdown string
+ Expected string
+}
+
+func source(t *MarkdownTestCase) string {
+ ret := t.Markdown
+ if t.Options.Trim {
+ ret = strings.TrimSpace(ret)
+ }
+ if t.Options.EnableEscape {
+ return string(applyEscapeSequence([]byte(ret)))
+ }
+ return ret
+}
+
+func expected(t *MarkdownTestCase) string {
+ ret := t.Expected
+ if t.Options.Trim {
+ ret = strings.TrimSpace(ret)
+ }
+ if t.Options.EnableEscape {
+ return string(applyEscapeSequence([]byte(ret)))
+ }
+ return ret
+}
+
+// MarkdownTestCaseOptions represents options for each test case.
+type MarkdownTestCaseOptions struct {
+ EnableEscape bool
+ Trim bool
+}
+
+const attributeSeparator = "//- - - - - - - - -//"
+const caseSeparator = "//= = = = = = = = = = = = = = = = = = = = = = = =//"
+
+var optionsRegexp = regexp.MustCompile(`(?i)\s*options:(.*)`)
+
+// ParseCliCaseArg parses -case command line args.
+func ParseCliCaseArg() []int {
+ ret := []int{}
+ for _, a := range os.Args {
+ if strings.HasPrefix(a, "case=") {
+ parts := strings.Split(a, "=")
+ for _, cas := range strings.Split(parts[1], ",") {
+ value, err := strconv.Atoi(strings.TrimSpace(cas))
+ if err == nil {
+ ret = append(ret, value)
+ }
+ }
+ }
+ }
+ return ret
+}
+
+// DoTestCaseFile runs test cases in a given file.
+func DoTestCaseFile(m goldmark.Markdown, filename string, t TestingT, no ...int) {
+ fp, err := os.Open(filename)
+ if err != nil {
+ panic(err)
+ }
+ defer func() {
+ _ = fp.Close()
+ }()
+
+ scanner := bufio.NewScanner(fp)
+ c := MarkdownTestCase{
+ No: -1,
+ Description: "",
+ Options: MarkdownTestCaseOptions{},
+ Markdown: "",
+ Expected: "",
+ }
+ cases := []MarkdownTestCase{}
+ line := 0
+ for scanner.Scan() {
+ line++
+ if util.IsBlank([]byte(scanner.Text())) {
+ continue
+ }
+ header := scanner.Text()
+ c.Description = ""
+ if strings.Contains(header, ":") {
+ parts := strings.Split(header, ":")
+ c.No, err = strconv.Atoi(strings.TrimSpace(parts[0]))
+ c.Description = strings.Join(parts[1:], ":")
+ } else {
+ c.No, err = strconv.Atoi(scanner.Text())
+ }
+ if err != nil {
+ panic(fmt.Sprintf("%s: invalid case No at line %d", filename, line))
+ }
+ if !scanner.Scan() {
+ panic(fmt.Sprintf("%s: invalid case at line %d", filename, line))
+ }
+ line++
+ matches := optionsRegexp.FindAllStringSubmatch(scanner.Text(), -1)
+ if len(matches) != 0 {
+ err = json.Unmarshal([]byte(matches[0][1]), &c.Options)
+ if err != nil {
+ panic(fmt.Sprintf("%s: invalid options at line %d", filename, line))
+ }
+ scanner.Scan()
+ line++
+ }
+ if scanner.Text() != attributeSeparator {
+ panic(fmt.Sprintf("%s: invalid separator '%s' at line %d", filename, scanner.Text(), line))
+ }
+ buf := []string{}
+ for scanner.Scan() {
+ line++
+ text := scanner.Text()
+ if text == attributeSeparator {
+ break
+ }
+ buf = append(buf, text)
+ }
+ c.Markdown = strings.Join(buf, "\n")
+ buf = []string{}
+ for scanner.Scan() {
+ line++
+ text := scanner.Text()
+ if text == caseSeparator {
+ break
+ }
+ buf = append(buf, text)
+ }
+ c.Expected = strings.Join(buf, "\n")
+ if len(c.Expected) != 0 {
+ c.Expected = c.Expected + "\n"
+ }
+ shouldAdd := len(no) == 0
+ if !shouldAdd {
+ shouldAdd = slices.Contains(no, c.No)
+ }
+ if shouldAdd {
+ cases = append(cases, c)
+ }
+ }
+ DoTestCases(m, cases, t)
+}
+
+// DoTestCases runs a set of test cases.
+func DoTestCases(m goldmark.Markdown, cases []MarkdownTestCase, t TestingT, opts ...parser.ParseOption) {
+ for _, testCase := range cases {
+ DoTestCase(m, testCase, t, opts...)
+ }
+}
+
+// DoTestCase runs a test case.
+func DoTestCase(m goldmark.Markdown, testCase MarkdownTestCase, t TestingT, opts ...parser.ParseOption) {
+ var ok bool
+ var out bytes.Buffer
+ defer func() {
+ description := ""
+ if len(testCase.Description) != 0 {
+ description = ": " + testCase.Description
+ }
+ if err := recover(); err != nil {
+ format := `============= case %d%s ================
+Markdown:
+-----------
+%s
+
+Expected:
+----------
+%s
+
+Actual
+---------
+%v
+%s
+`
+ t.Errorf(format, testCase.No, description, source(&testCase), expected(&testCase), err, debug.Stack())
+ } else if !ok {
+ format := `============= case %d%s ================
+Markdown:
+-----------
+%s
+
+Expected:
+----------
+%s
+
+Actual
+---------
+%s
+
+Diff
+---------
+%s
+`
+ t.Errorf(format, testCase.No, description, source(&testCase), expected(&testCase), out.Bytes(),
+ DiffPretty([]byte(expected(&testCase)), out.Bytes()))
+ }
+ }()
+
+ if err := m.Convert([]byte(source(&testCase)), &out, opts...); err != nil {
+ panic(err)
+ }
+ ok = bytes.Equal(bytes.TrimSpace(out.Bytes()), bytes.TrimSpace([]byte(expected(&testCase))))
+}
+
+type diffType int
+
+const (
+ diffRemoved diffType = iota
+ diffAdded
+ diffNone
+)
+
+type diff struct {
+ Type diffType
+ Lines [][]byte
+}
+
+func simpleDiff(v1, v2 []byte) []diff {
+ return simpleDiffAux(
+ bytes.Split(v1, []byte("\n")),
+ bytes.Split(v2, []byte("\n")))
+}
+
+func simpleDiffAux(v1lines, v2lines [][]byte) []diff {
+ v1index := map[string][]int{}
+ for i, line := range v1lines {
+ key := util.BytesToReadOnlyString(line)
+ if _, ok := v1index[key]; !ok {
+ v1index[key] = []int{}
+ }
+ v1index[key] = append(v1index[key], i)
+ }
+ overlap := map[int]int{}
+ v1start := 0
+ v2start := 0
+ length := 0
+ for v2pos, line := range v2lines {
+ newOverlap := map[int]int{}
+ key := util.BytesToReadOnlyString(line)
+ if _, ok := v1index[key]; !ok {
+ v1index[key] = []int{}
+ }
+ for _, v1pos := range v1index[key] {
+ value := 0
+ if v1pos != 0 {
+ if v, ok := overlap[v1pos-1]; ok {
+ value = v
+ }
+ }
+ newOverlap[v1pos] = value + 1
+ if newOverlap[v1pos] > length {
+ length = newOverlap[v1pos]
+ v1start = v1pos - length + 1
+ v2start = v2pos - length + 1
+ }
+ }
+ overlap = newOverlap
+ }
+ if length == 0 {
+ diffs := []diff{}
+ if len(v1lines) != 0 {
+ diffs = append(diffs, diff{diffRemoved, v1lines})
+ }
+ if len(v2lines) != 0 {
+ diffs = append(diffs, diff{diffAdded, v2lines})
+ }
+ return diffs
+ }
+ diffs := simpleDiffAux(v1lines[:v1start], v2lines[:v2start])
+ diffs = append(diffs, diff{diffNone, v2lines[v2start : v2start+length]})
+ diffs = append(diffs, simpleDiffAux(v1lines[v1start+length:],
+ v2lines[v2start+length:])...)
+ return diffs
+}
+
+// DiffPretty returns pretty formatted diff between given bytes.
+func DiffPretty(v1, v2 []byte) []byte {
+ var b bytes.Buffer
+ diffs := simpleDiff(v1, v2)
+ for _, diff := range diffs {
+ c := " "
+ switch diff.Type {
+ case diffAdded:
+ c = "+"
+ case diffRemoved:
+ c = "-"
+ case diffNone:
+ c = " "
+ }
+ for _, line := range diff.Lines {
+ if c != " " {
+ fmt.Fprintf(&b, "%s | %s\n", c, util.VisualizeSpaces(line))
+ } else {
+ fmt.Fprintf(&b, "%s | %s\n", c, line)
+ }
+ }
+ }
+ return b.Bytes()
+}
+
+func applyEscapeSequence(b []byte) []byte {
+ result := make([]byte, 0, len(b))
+ for i := 0; i < len(b); i++ {
+ if b[i] == '\\' && i != len(b)-1 {
+ switch b[i+1] {
+ case 'a':
+ result = append(result, '\a')
+ i++
+ continue
+ case 'b':
+ result = append(result, '\b')
+ i++
+ continue
+ case 'f':
+ result = append(result, '\f')
+ i++
+ continue
+ case 'n':
+ result = append(result, '\n')
+ i++
+ continue
+ case 'r':
+ result = append(result, '\r')
+ i++
+ continue
+ case 't':
+ result = append(result, '\t')
+ i++
+ continue
+ case 'v':
+ result = append(result, '\v')
+ i++
+ continue
+ case '\\':
+ result = append(result, '\\')
+ i++
+ continue
+ case 'x':
+ if len(b) >= i+3 && util.IsHexDecimal(b[i+2]) && util.IsHexDecimal(b[i+3]) {
+ v, _ := hex.DecodeString(string(b[i+2 : i+4]))
+ result = append(result, v[0])
+ i += 3
+ continue
+ }
+ case 'u', 'U':
+ if len(b) > i+2 {
+ num := []byte{}
+ for j := i + 2; j < len(b); j++ {
+ if util.IsHexDecimal(b[j]) {
+ num = append(num, b[j])
+ continue
+ }
+ break
+ }
+ if len(num) >= 4 && len(num) < 8 {
+ v, _ := strconv.ParseInt(string(num[:4]), 16, 32)
+ result = append(result, []byte(string(rune(v)))...)
+ i += 5
+ continue
+ }
+ if len(num) >= 8 {
+ v, _ := strconv.ParseInt(string(num[:8]), 16, 32)
+ result = append(result, []byte(string(rune(v)))...)
+ i += 9
+ continue
+ }
+ }
+ }
+ }
+ result = append(result, b[i])
+ }
+ return result
+}
diff --git a/backend/goldmark/testutil/testutil_test.go b/backend/goldmark/testutil/testutil_test.go
new file mode 100644
index 0000000..2000a00
--- /dev/null
+++ b/backend/goldmark/testutil/testutil_test.go
@@ -0,0 +1,7 @@
+package testutil
+
+import "testing"
+
+// This will fail to compile if the TestingT interface is changed in a way
+// that doesn't conform to testing.T.
+var _ TestingT = (*testing.T)(nil)
diff --git a/backend/goldmark/text/package.go b/backend/goldmark/text/package.go
new file mode 100644
index 0000000..d241ac6
--- /dev/null
+++ b/backend/goldmark/text/package.go
@@ -0,0 +1,2 @@
+// Package text provides functionalities to manipulate texts.
+package text
diff --git a/backend/goldmark/text/reader.go b/backend/goldmark/text/reader.go
new file mode 100644
index 0000000..21083dc
--- /dev/null
+++ b/backend/goldmark/text/reader.go
@@ -0,0 +1,701 @@
+package text
+
+import (
+ "bytes"
+ "io"
+ "regexp"
+ "unicode/utf8"
+
+ "github.com/yuin/goldmark/util"
+)
+
+const invalidValue = -1
+
+// EOF indicates the end of file.
+const EOF = byte(0xff)
+
+// A Reader interface provides abstracted method for reading text.
+type Reader interface {
+ io.RuneReader
+
+ // Source returns a source of the reader.
+ Source() []byte
+
+ // ResetPosition resets positions.
+ ResetPosition()
+
+ // Peek returns a byte at current position without advancing the internal pointer.
+ Peek() byte
+
+ // PeekLine returns the current line without advancing the internal pointer.
+ PeekLine() ([]byte, Segment)
+
+ // PrecendingCharacter returns a character just before current internal pointer.
+ PrecendingCharacter() rune
+
+ // Value returns a value of the given segment.
+ Value(Segment) []byte
+
+ // LineOffset returns a distance from the line head to current position.
+ LineOffset() int
+
+ // Position returns current line number and position.
+ Position() (int, Segment)
+
+ // SetPosition sets current line number and position.
+ SetPosition(int, Segment)
+
+ // SetPadding sets padding to the reader.
+ SetPadding(int)
+
+ // Advance advances the internal pointer.
+ Advance(int)
+
+ // AdvanceAndSetPadding advances the internal pointer and add padding to the
+ // reader.
+ AdvanceAndSetPadding(int, int)
+
+ // AdvanceToEOL advances the internal pointer to the end of line.
+ // If the line ends with a newline, it will be included in the segment.
+ // If the line ends with EOF, it will not be included in the segment.
+ AdvanceToEOL()
+
+ // AdvanceLine advances the internal pointer to the next line head.
+ AdvanceLine()
+
+ // SkipSpaces skips space characters and returns a non-blank line.
+ // If it reaches EOF, returns false.
+ SkipSpaces() (Segment, int, bool)
+
+ // SkipSpaces skips blank lines and returns a non-blank line.
+ // If it reaches EOF, returns false.
+ SkipBlankLines() (Segment, int, bool)
+
+ // Match performs regular expression matching to current line.
+ Match(reg *regexp.Regexp) bool
+
+ // Match performs regular expression searching to current line.
+ FindSubMatch(reg *regexp.Regexp) [][]byte
+
+ // 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 {
+ source []byte
+ sourceLength int
+ line int
+ peekedLine []byte
+ pos Segment
+ head int
+ lineOffset int
+}
+
+// NewReader return a new Reader that can read UTF-8 bytes .
+func NewReader(source []byte) Reader {
+ r := &reader{
+ source: source,
+ sourceLength: len(source),
+ }
+ r.ResetPosition()
+ 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
+ r.lineOffset = -1
+ r.AdvanceLine()
+}
+
+func (r *reader) Source() []byte {
+ return r.source
+}
+
+func (r *reader) Value(seg Segment) []byte {
+ return seg.Value(r.source)
+}
+
+func (r *reader) Peek() byte {
+ if r.pos.Start >= 0 && r.pos.Start < r.sourceLength {
+ if r.pos.Padding != 0 {
+ return space[0]
+ }
+ return r.source[r.pos.Start]
+ }
+ return EOF
+}
+
+func (r *reader) PeekLine() ([]byte, Segment) {
+ if r.pos.Start >= 0 && r.pos.Start < r.sourceLength {
+ if r.peekedLine == nil {
+ r.peekedLine = r.pos.Value(r.Source())
+ }
+ return r.peekedLine, r.pos
+ }
+ return nil, r.pos
+}
+
+// io.RuneReader interface.
+func (r *reader) ReadRune() (rune, int, error) {
+ return readRuneReader(r)
+}
+
+func (r *reader) LineOffset() int {
+ if r.lineOffset < 0 {
+ v := 0
+ for i := r.head; i < r.pos.Start; i++ {
+ if r.source[i] == '\t' {
+ v += util.TabWidth(v)
+ } else {
+ v++
+ }
+ }
+ r.lineOffset = v - r.pos.Padding
+ }
+ return r.lineOffset
+}
+
+func (r *reader) PrecendingCharacter() rune {
+ if r.pos.Start <= 0 {
+ if r.pos.Padding != 0 {
+ return rune(' ')
+ }
+ return rune('\n')
+ }
+ i := r.pos.Start - 1
+ for ; i >= 0; i-- {
+ if utf8.RuneStart(r.source[i]) {
+ break
+ }
+ }
+ rn, _ := utf8.DecodeRune(r.source[i:])
+ return rn
+}
+
+func (r *reader) Advance(n int) {
+ r.lineOffset = -1
+ if n < len(r.peekedLine) && r.pos.Padding == 0 {
+ r.pos.Start += n
+ r.peekedLine = nil
+ return
+ }
+ r.peekedLine = nil
+ l := r.sourceLength
+ for ; n > 0 && r.pos.Start < l; n-- {
+ if r.pos.Padding != 0 {
+ r.pos.Padding--
+ continue
+ }
+ if r.source[r.pos.Start] == '\n' {
+ r.AdvanceLine()
+ continue
+ }
+ r.pos.Start++
+ }
+}
+
+func (r *reader) AdvanceAndSetPadding(n, padding int) {
+ r.Advance(n)
+ if padding > r.pos.Padding {
+ r.SetPadding(padding)
+ }
+}
+
+func (r *reader) AdvanceToEOL() {
+ if r.pos.Start >= r.sourceLength {
+ return
+ }
+
+ r.lineOffset = -1
+ i := -1
+ if r.peekedLine != nil {
+ r.pos.Start += len(r.peekedLine) - r.pos.Padding - 1
+ if r.source[r.pos.Start] == '\n' {
+ i = 0
+ }
+ }
+ if i == -1 {
+ i = bytes.IndexByte(r.source[r.pos.Start:], '\n')
+ }
+ r.peekedLine = nil
+ if i != -1 {
+ r.pos.Start += i
+ } else {
+ r.pos.Start = r.sourceLength
+ }
+ r.pos.Padding = 0
+}
+
+func (r *reader) AdvanceLine() {
+ r.lineOffset = -1
+ r.peekedLine = nil
+ r.pos.Start = r.pos.Stop
+ r.head = r.pos.Start
+ if r.pos.Start < 0 || r.pos.Start >= r.sourceLength {
+ return
+ }
+ r.pos.Stop = r.sourceLength
+ i := 0
+ if r.source[r.pos.Start] != '\n' {
+ i = bytes.IndexByte(r.source[r.pos.Start:], '\n')
+ }
+ if i != -1 {
+ r.pos.Stop = r.pos.Start + i + 1
+ }
+ r.line++
+ r.pos.Padding = 0
+}
+
+func (r *reader) Position() (int, Segment) {
+ return r.line, r.pos
+}
+
+func (r *reader) SetPosition(line int, pos Segment) {
+ r.lineOffset = -1
+ r.line = line
+ r.pos = pos
+}
+
+func (r *reader) SetPadding(v int) {
+ r.pos.Padding = v
+}
+
+func (r *reader) SkipSpaces() (Segment, int, bool) {
+ return skipSpacesReader(r)
+}
+
+func (r *reader) SkipBlankLines() (Segment, int, bool) {
+ return skipBlankLinesReader(r)
+}
+
+func (r *reader) Match(reg *regexp.Regexp) bool {
+ return matchReader(r, reg)
+}
+
+func (r *reader) FindSubMatch(reg *regexp.Regexp) [][]byte {
+ return findSubMatchReader(r, reg)
+}
+
+// A BlockReader interface is a reader that is optimized for Blocks.
+type BlockReader interface {
+ Reader
+ // Reset resets current state and sets new segments to the reader.
+ Reset(segment *Segments)
+}
+
+type blockReader struct {
+ source []byte
+ segments *Segments
+ segmentsLength int
+ line int
+ pos Segment
+ head int
+ last int
+ lineOffset int
+}
+
+// NewBlockReader returns a new BlockReader.
+func NewBlockReader(source []byte, segments *Segments) BlockReader {
+ r := &blockReader{
+ source: source,
+ }
+ if segments != nil {
+ r.Reset(segments)
+ }
+ return r
+}
+
+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
+ r.last = 0
+ r.lineOffset = -1
+ r.pos.Start = -1
+ r.pos.Stop = -1
+ r.pos.Padding = 0
+ if r.segmentsLength > 0 {
+ last := r.segments.At(r.segmentsLength - 1)
+ r.last = last.Stop
+ }
+ r.AdvanceLine()
+}
+
+func (r *blockReader) Reset(segments *Segments) {
+ r.segments = segments
+ r.segmentsLength = segments.Len()
+ r.ResetPosition()
+}
+
+func (r *blockReader) Source() []byte {
+ return r.source
+}
+
+func (r *blockReader) Value(seg Segment) []byte {
+ line := r.segmentsLength - 1
+ ret := make([]byte, 0, seg.Stop-seg.Start+1)
+ for ; line >= 0; line-- {
+ if seg.Start >= r.segments.At(line).Start {
+ break
+ }
+ }
+ i := seg.Start
+ for ; line < r.segmentsLength; line++ {
+ s := r.segments.At(line)
+ if i < 0 {
+ i = s.Start
+ }
+ ret = s.ConcatPadding(ret)
+ for ; i < seg.Stop && i < s.Stop; i++ {
+ ret = append(ret, r.source[i])
+ }
+ i = -1
+ if s.Stop > seg.Stop {
+ break
+ }
+ }
+ return ret
+}
+
+// io.RuneReader interface.
+func (r *blockReader) ReadRune() (rune, int, error) {
+ return readRuneReader(r)
+}
+
+func (r *blockReader) PrecendingCharacter() rune {
+ if r.pos.Padding != 0 {
+ return rune(' ')
+ }
+ if r.segments.Len() < 1 {
+ return rune('\n')
+ }
+ firstSegment := r.segments.At(0)
+ if r.line == 0 && r.pos.Start <= firstSegment.Start {
+ return rune('\n')
+ }
+ l := len(r.source)
+ i := r.pos.Start - 1
+ for ; i < l && i >= 0; i-- {
+ if utf8.RuneStart(r.source[i]) {
+ break
+ }
+ }
+ if i < 0 || i >= l {
+ return rune('\n')
+ }
+ rn, _ := utf8.DecodeRune(r.source[i:])
+ return rn
+}
+
+func (r *blockReader) LineOffset() int {
+ if r.lineOffset < 0 {
+ v := 0
+ for i := r.head; i < r.pos.Start; i++ {
+ if r.source[i] == '\t' {
+ v += util.TabWidth(v)
+ } else {
+ v++
+ }
+ }
+ r.lineOffset = v - r.pos.Padding
+ }
+ return r.lineOffset
+}
+
+func (r *blockReader) Peek() byte {
+ if r.line < r.segmentsLength && r.pos.Start >= 0 && r.pos.Start < r.last {
+ if r.pos.Padding != 0 {
+ return space[0]
+ }
+ return r.source[r.pos.Start]
+ }
+ return EOF
+}
+
+func (r *blockReader) PeekLine() ([]byte, Segment) {
+ if r.line < r.segmentsLength && r.pos.Start >= 0 && r.pos.Start < r.last {
+ return r.pos.Value(r.source), r.pos
+ }
+ return nil, r.pos
+}
+
+func (r *blockReader) Advance(n int) {
+ r.lineOffset = -1
+
+ if n < r.pos.Stop-r.pos.Start && r.pos.Padding == 0 {
+ r.pos.Start += n
+ return
+ }
+
+ for ; n > 0; n-- {
+ if r.pos.Padding != 0 {
+ r.pos.Padding--
+ continue
+ }
+ if r.pos.Start >= r.pos.Stop-1 && r.pos.Stop < r.last {
+ r.AdvanceLine()
+ continue
+ }
+ r.pos.Start++
+ }
+}
+
+func (r *blockReader) AdvanceAndSetPadding(n, padding int) {
+ r.Advance(n)
+ if padding > r.pos.Padding {
+ r.SetPadding(padding)
+ }
+}
+
+func (r *blockReader) AdvanceToEOL() {
+ r.lineOffset = -1
+ r.pos.Padding = 0
+ c := r.source[r.pos.Stop-1]
+ if c == '\n' {
+ r.pos.Start = r.pos.Stop - 1
+ } else {
+ r.pos.Start = r.pos.Stop
+ }
+}
+
+func (r *blockReader) AdvanceLine() {
+ r.SetPosition(r.line+1, NewSegment(invalidValue, invalidValue))
+ r.head = r.pos.Start
+}
+
+func (r *blockReader) Position() (int, Segment) {
+ return r.line, r.pos
+}
+
+func (r *blockReader) SetPosition(line int, pos Segment) {
+ r.lineOffset = -1
+ r.line = line
+ if pos.Start == invalidValue {
+ if r.line < r.segmentsLength {
+ s := r.segments.At(line)
+ r.head = s.Start
+ r.pos = s
+ }
+ } else {
+ r.pos = pos
+ if r.line < r.segmentsLength {
+ s := r.segments.At(line)
+ r.head = s.Start
+ }
+ }
+}
+
+func (r *blockReader) SetPadding(v int) {
+ r.lineOffset = -1
+ r.pos.Padding = v
+}
+
+func (r *blockReader) SkipSpaces() (Segment, int, bool) {
+ return skipSpacesReader(r)
+}
+
+func (r *blockReader) SkipBlankLines() (Segment, int, bool) {
+ return skipBlankLinesReader(r)
+}
+
+func (r *blockReader) Match(reg *regexp.Regexp) bool {
+ return matchReader(r, reg)
+}
+
+func (r *blockReader) FindSubMatch(reg *regexp.Regexp) [][]byte {
+ return findSubMatchReader(r, reg)
+}
+
+func skipBlankLinesReader(r Reader) (Segment, int, bool) {
+ lines := 0
+ for {
+ line, seg := r.PeekLine()
+ if line == nil {
+ return seg, lines, false
+ }
+ if util.IsBlank(line) {
+ lines++
+ r.AdvanceLine()
+ } else {
+ return seg, lines, true
+ }
+ }
+}
+
+func skipSpacesReader(r Reader) (Segment, int, bool) {
+ chars := 0
+ for {
+ line, segment := r.PeekLine()
+ if line == nil {
+ return segment, chars, false
+ }
+ for i, c := range line {
+ if util.IsSpace(c) {
+ chars++
+ r.Advance(1)
+ continue
+ }
+ return segment.WithStart(segment.Start + i + 1), chars, true
+ }
+ }
+}
+
+func matchReader(r Reader, reg *regexp.Regexp) bool {
+ oldline, oldseg := r.Position()
+ match := reg.FindReaderSubmatchIndex(r)
+ r.SetPosition(oldline, oldseg)
+ if match == nil {
+ return false
+ }
+ r.Advance(match[1] - match[0])
+ return true
+}
+
+func findSubMatchReader(r Reader, reg *regexp.Regexp) [][]byte {
+ oldLine, oldSeg := r.Position()
+ match := reg.FindReaderSubmatchIndex(r)
+ r.SetPosition(oldLine, oldSeg)
+ if match == nil {
+ return nil
+ }
+ var bb bytes.Buffer
+ bb.Grow(match[1] - match[0])
+ for i := 0; i < match[1]; {
+ r, size, _ := readRuneReader(r)
+ i += size
+ bb.WriteRune(r)
+ }
+ bs := bb.Bytes()
+ var result [][]byte
+ for i := 0; i < len(match); i += 2 {
+ if match[i] < 0 {
+ result = append(result, []byte{})
+ continue
+ }
+ result = append(result, bs[match[i]:match[i+1]])
+ }
+
+ r.SetPosition(oldLine, oldSeg)
+ r.Advance(match[1] - match[0])
+ return result
+}
+
+func readRuneReader(r Reader) (rune, int, error) {
+ line, _ := r.PeekLine()
+ if line == nil {
+ return 0, 0, io.EOF
+ }
+ rn, size := utf8.DecodeRune(line)
+ if rn == utf8.RuneError {
+ return 0, 0, io.EOF
+ }
+ r.Advance(size)
+ return rn, size, nil
+}
+
+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
+}
diff --git a/backend/goldmark/text/reader_test.go b/backend/goldmark/text/reader_test.go
new file mode 100644
index 0000000..6957b40
--- /dev/null
+++ b/backend/goldmark/text/reader_test.go
@@ -0,0 +1,16 @@
+package text
+
+import (
+ "regexp"
+ "testing"
+)
+
+func TestFindSubMatchReader(t *testing.T) {
+ s := "微笑"
+ r := NewReader([]byte(":" + s + ":"))
+ reg := regexp.MustCompile(`:(\p{L}+):`)
+ match := r.FindSubMatch(reg)
+ if len(match) != 2 || string(match[1]) != s {
+ t.Fatal("no match cjk")
+ }
+}
diff --git a/backend/goldmark/text/segment.go b/backend/goldmark/text/segment.go
new file mode 100644
index 0000000..30655dd
--- /dev/null
+++ b/backend/goldmark/text/segment.go
@@ -0,0 +1,233 @@
+package text
+
+import (
+ "bytes"
+
+ "github.com/yuin/goldmark/util"
+)
+
+var space = []byte(" ")
+
+// A Segment struct holds information about source positions.
+type Segment struct {
+ // Start is a start position of the segment.
+ Start int
+
+ // Stop is a stop position of the segment.
+ // This value should be excluded.
+ Stop int
+
+ // Padding is a padding length of the segment.
+ Padding int
+
+ // ForceNewline is true if the segment should be ended with a newline.
+ // Some elements(i.e. CodeBlock, FencedCodeBlock) does not trim trailing
+ // newlines. Spec defines that EOF is treated as a newline, so we need to
+ // add a newline to the end of the segment if it is not empty.
+ //
+ // i.e.:
+ //
+ // ```go
+ // const test = "test"
+ //
+ // This code does not close the code block and ends with EOF. In this case,
+ // we need to add a newline to the end of the last line like `const test = "test"\n`.
+ ForceNewline bool
+}
+
+// NewSegment return a new Segment.
+func NewSegment(start, stop int) Segment {
+ return Segment{
+ Start: start,
+ Stop: stop,
+ Padding: 0,
+ }
+}
+
+// NewSegmentPadding returns a new Segment with the given padding.
+func NewSegmentPadding(start, stop, n int) Segment {
+ return Segment{
+ Start: start,
+ Stop: stop,
+ Padding: n,
+ }
+}
+
+// Value returns a value of the segment.
+func (t *Segment) Value(buffer []byte) []byte {
+ var result []byte
+ if t.Padding == 0 {
+ result = buffer[t.Start:t.Stop]
+ } else {
+ result = make([]byte, 0, t.Padding+t.Stop-t.Start+1)
+ result = append(result, bytes.Repeat(space, t.Padding)...)
+ result = append(result, buffer[t.Start:t.Stop]...)
+ }
+ if t.ForceNewline && len(result) > 0 && result[len(result)-1] != '\n' {
+ result = append(result, '\n')
+ }
+ return result
+}
+
+// Len returns a length of the segment.
+func (t *Segment) Len() int {
+ return t.Stop - t.Start + t.Padding
+}
+
+// Between returns a segment between this segment and the given segment.
+func (t *Segment) Between(other Segment) Segment {
+ if t.Stop != other.Stop {
+ panic("invalid state")
+ }
+ return NewSegmentPadding(
+ t.Start,
+ other.Start,
+ t.Padding-other.Padding,
+ )
+}
+
+// IsEmpty returns true if this segment is empty, otherwise false.
+func (t *Segment) IsEmpty() bool {
+ return t.Start >= t.Stop && t.Padding == 0
+}
+
+// TrimRightSpace returns a new segment by slicing off all trailing
+// space characters.
+func (t *Segment) TrimRightSpace(buffer []byte) Segment {
+ v := buffer[t.Start:t.Stop]
+ l := util.TrimRightSpaceLength(v)
+ if l == len(v) {
+ return NewSegment(t.Start, t.Start)
+ }
+ return NewSegmentPadding(t.Start, t.Stop-l, t.Padding)
+}
+
+// TrimLeftSpace returns a new segment by slicing off all leading
+// space characters including padding.
+func (t *Segment) TrimLeftSpace(buffer []byte) Segment {
+ v := buffer[t.Start:t.Stop]
+ l := util.TrimLeftSpaceLength(v)
+ return NewSegment(t.Start+l, t.Stop)
+}
+
+// TrimLeftSpaceWidth returns a new segment by slicing off leading space
+// characters until the given width.
+func (t *Segment) TrimLeftSpaceWidth(width int, buffer []byte) Segment {
+ padding := t.Padding
+ for ; width > 0; width-- {
+ if padding == 0 {
+ break
+ }
+ padding--
+ }
+ if width == 0 {
+ return NewSegmentPadding(t.Start, t.Stop, padding)
+ }
+ text := buffer[t.Start:t.Stop]
+ start := t.Start
+ for _, c := range text {
+ if start >= t.Stop-1 || width <= 0 {
+ break
+ }
+ if c == ' ' {
+ width--
+ } else if c == '\t' {
+ width -= 4
+ } else {
+ break
+ }
+ start++
+ }
+ if width < 0 {
+ padding = width * -1
+ }
+ return NewSegmentPadding(start, t.Stop, padding)
+}
+
+// WithStart returns a new Segment with same value except Start.
+func (t *Segment) WithStart(v int) Segment {
+ return NewSegmentPadding(v, t.Stop, t.Padding)
+}
+
+// WithStop returns a new Segment with same value except Stop.
+func (t *Segment) WithStop(v int) Segment {
+ return NewSegmentPadding(t.Start, v, t.Padding)
+}
+
+// ConcatPadding concats the padding to the given slice.
+func (t *Segment) ConcatPadding(v []byte) []byte {
+ if t.Padding > 0 {
+ return append(v, bytes.Repeat(space, t.Padding)...)
+ }
+ return v
+}
+
+// Segments is a collection of the Segment.
+type Segments struct {
+ values []Segment
+}
+
+// NewSegments return a new Segments.
+func NewSegments() *Segments {
+ return &Segments{
+ values: nil,
+ }
+}
+
+// Append appends the given segment after the tail of the collection.
+func (s *Segments) Append(t Segment) {
+ s.values = append(s.values, t)
+}
+
+// AppendAll appends all elements of given segments after the tail of the collection.
+func (s *Segments) AppendAll(t []Segment) {
+ s.values = append(s.values, t...)
+}
+
+// Len returns the length of the collection.
+func (s *Segments) Len() int {
+ if s.values == nil {
+ return 0
+ }
+ return len(s.values)
+}
+
+// At returns a segment at the given index.
+func (s *Segments) At(i int) Segment {
+ return s.values[i]
+}
+
+// Set sets the given Segment.
+func (s *Segments) Set(i int, v Segment) {
+ s.values[i] = v
+}
+
+// SetSliced replace the collection with a subsliced value.
+func (s *Segments) SetSliced(lo, hi int) {
+ s.values = s.values[lo:hi]
+}
+
+// Sliced returns a subslice of the collection.
+func (s *Segments) Sliced(lo, hi int) []Segment {
+ return s.values[lo:hi]
+}
+
+// Clear delete all element of the collection.
+func (s *Segments) Clear() {
+ s.values = nil
+}
+
+// Unshift insert the given Segment to head of the collection.
+func (s *Segments) Unshift(v Segment) {
+ s.values = append(s.values[0:1], s.values[0:]...)
+ s.values[0] = v
+}
+
+// Value returns a string value of the collection.
+func (s *Segments) Value(buffer []byte) []byte {
+ var result []byte
+ for _, v := range s.values {
+ result = append(result, v.Value(buffer)...)
+ }
+ return result
+}
diff --git a/backend/goldmark/util/html5entities.gen.go b/backend/goldmark/util/html5entities.gen.go
new file mode 100644
index 0000000..631a59a
--- /dev/null
+++ b/backend/goldmark/util/html5entities.gen.go
@@ -0,0 +1,9 @@
+// Code generated by _tools; DO NOT EDIT.
+package util
+const _html5entitiesLength = 2124
+const _html5entitiesName string = "AEligAMPAacuteAcircAcyAfrAgraveAlphaAmacrAndAogonAopfApplyFunctionAringAscrAssignAtildeAumlBackslashBarvBarwedBcyBecauseBernoullisBetaBfrBopfBreveBscrBumpeqCHcyCOPYCacuteCapCapitalDifferentialDCayleysCcaronCcedilCcircCconintCdotCedillaCenterDotCfrChiCircleDotCircleMinusCirclePlusCircleTimesClockwiseContourIntegralCloseCurlyDoubleQuoteCloseCurlyQuoteColonColoneCongruentConintContourIntegralCopfCoproductCounterClockwiseContourIntegralCrossCscrCupCupCapDDDDotrahdDJcyDScyDZcyDaggerDarrDashvDcaronDcyDelDeltaDfrDiacriticalAcuteDiacriticalDotDiacriticalDoubleAcuteDiacriticalGraveDiacriticalTildeDiamondDifferentialDDopfDotDotDotDotEqualDoubleContourIntegralDoubleDotDoubleDownArrowDoubleLeftArrowDoubleLeftRightArrowDoubleLeftTeeDoubleLongLeftArrowDoubleLongLeftRightArrowDoubleLongRightArrowDoubleRightArrowDoubleRightTeeDoubleUpArrowDoubleUpDownArrowDoubleVerticalBarDownArrowDownArrowBarDownArrowUpArrowDownBreveDownLeftRightVectorDownLeftTeeVectorDownLeftVectorDownLeftVectorBarDownRightTeeVectorDownRightVectorDownRightVectorBarDownTeeDownTeeArrowDownarrowDscrDstrokENGETHEacuteEcaronEcircEcyEdotEfrEgraveElementEmacrEmptySmallSquareEmptyVerySmallSquareEogonEopfEpsilonEqualEqualTildeEquilibriumEscrEsimEtaEumlExistsExponentialEFcyFfrFilledSmallSquareFilledVerySmallSquareFopfForAllFouriertrfFscrGJcyGTGammaGammadGbreveGcedilGcircGcyGdotGfrGgGopfGreaterEqualGreaterEqualLessGreaterFullEqualGreaterGreaterGreaterLessGreaterSlantEqualGreaterTildeGscrGtHARDcyHacekHatHcircHfrHilbertSpaceHopfHorizontalLineHscrHstrokHumpDownHumpHumpEqualIEcyIJligIOcyIacuteIcircIcyIdotIfrIgraveImImacrImaginaryIImpliesIntIntegralIntersectionInvisibleCommaInvisibleTimesIogonIopfIotaIscrItildeIukcyIumlJcircJcyJfrJopfJscrJsercyJukcyKHcyKJcyKappaKcedilKcyKfrKopfKscrLJcyLTLacuteLambdaLangLaplacetrfLarrLcaronLcedilLcyLeftAngleBracketLeftArrowLeftArrowBarLeftArrowRightArrowLeftCeilingLeftDoubleBracketLeftDownTeeVectorLeftDownVectorLeftDownVectorBarLeftFloorLeftRightArrowLeftRightVectorLeftTeeLeftTeeArrowLeftTeeVectorLeftTriangleLeftTriangleBarLeftTriangleEqualLeftUpDownVectorLeftUpTeeVectorLeftUpVectorLeftUpVectorBarLeftVectorLeftVectorBarLeftarrowLeftrightarrowLessEqualGreaterLessFullEqualLessGreaterLessLessLessSlantEqualLessTildeLfrLlLleftarrowLmidotLongLeftArrowLongLeftRightArrowLongRightArrowLongleftarrowLongleftrightarrowLongrightarrowLopfLowerLeftArrowLowerRightArrowLscrLshLstrokLtMapMcyMediumSpaceMellintrfMfrMinusPlusMopfMscrMuNJcyNacuteNcaronNcedilNcyNegativeMediumSpaceNegativeThickSpaceNegativeThinSpaceNegativeVeryThinSpaceNestedGreaterGreaterNestedLessLessNewLineNfrNoBreakNonBreakingSpaceNopfNotNotCongruentNotCupCapNotDoubleVerticalBarNotElementNotEqualNotEqualTildeNotExistsNotGreaterNotGreaterEqualNotGreaterFullEqualNotGreaterGreaterNotGreaterLessNotGreaterSlantEqualNotGreaterTildeNotHumpDownHumpNotHumpEqualNotLeftTriangleNotLeftTriangleBarNotLeftTriangleEqualNotLessNotLessEqualNotLessGreaterNotLessLessNotLessSlantEqualNotLessTildeNotNestedGreaterGreaterNotNestedLessLessNotPrecedesNotPrecedesEqualNotPrecedesSlantEqualNotReverseElementNotRightTriangleNotRightTriangleBarNotRightTriangleEqualNotSquareSubsetNotSquareSubsetEqualNotSquareSupersetNotSquareSupersetEqualNotSubsetNotSubsetEqualNotSucceedsNotSucceedsEqualNotSucceedsSlantEqualNotSucceedsTildeNotSupersetNotSupersetEqualNotTildeNotTildeEqualNotTildeFullEqualNotTildeTildeNotVerticalBarNscrNtildeNuOEligOacuteOcircOcyOdblacOfrOgraveOmacrOmegaOmicronOopfOpenCurlyDoubleQuoteOpenCurlyQuoteOrOscrOslashOtildeOtimesOumlOverBarOverBraceOverBracketOverParenthesisPartialDPcyPfrPhiPiPlusMinusPoincareplanePopfPrPrecedesPrecedesEqualPrecedesSlantEqualPrecedesTildePrimeProductProportionProportionalPscrPsiQUOTQfrQopfQscrRBarrREGRacuteRangRarrRarrtlRcaronRcedilRcyReReverseElementReverseEquilibriumReverseUpEquilibriumRfrRhoRightAngleBracketRightArrowRightArrowBarRightArrowLeftArrowRightCeilingRightDoubleBracketRightDownTeeVectorRightDownVectorRightDownVectorBarRightFloorRightTeeRightTeeArrowRightTeeVectorRightTriangleRightTriangleBarRightTriangleEqualRightUpDownVectorRightUpTeeVectorRightUpVectorRightUpVectorBarRightVectorRightVectorBarRightarrowRopfRoundImpliesRrightarrowRscrRshRuleDelayedSHCHcySHcySOFTcySacuteScScaronScedilScircScySfrShortDownArrowShortLeftArrowShortRightArrowShortUpArrowSigmaSmallCircleSopfSqrtSquareSquareIntersectionSquareSubsetSquareSubsetEqualSquareSupersetSquareSupersetEqualSquareUnionSscrStarSubSubsetSubsetEqualSucceedsSucceedsEqualSucceedsSlantEqualSucceedsTildeSuchThatSumSupSupersetSupersetEqualSupsetTHORNTRADETSHcyTScyTabTauTcaronTcedilTcyTfrThereforeThetaThickSpaceThinSpaceTildeTildeEqualTildeFullEqualTildeTildeTopfTripleDotTscrTstrokUacuteUarrUarrocirUbrcyUbreveUcircUcyUdblacUfrUgraveUmacrUnderBarUnderBraceUnderBracketUnderParenthesisUnionUnionPlusUogonUopfUpArrowUpArrowBarUpArrowDownArrowUpDownArrowUpEquilibriumUpTeeUpTeeArrowUparrowUpdownarrowUpperLeftArrowUpperRightArrowUpsiUpsilonUringUscrUtildeUumlVDashVbarVcyVdashVdashlVeeVerbarVertVerticalBarVerticalLineVerticalSeparatorVerticalTildeVeryThinSpaceVfrVopfVscrVvdashWcircWedgeWfrWopfWscrXfrXiXopfXscrYAcyYIcyYUcyYacuteYcircYcyYfrYopfYscrYumlZHcyZacuteZcaronZcyZdotZeroWidthSpaceZetaZfrZopfZscraacuteabreveacacEacdacircacuteacyaeligafafragravealefsymalephalphaamacramalgampandandandanddandslopeandvangangeangleangmsdangmsdaaangmsdabangmsdacangmsdadangmsdaeangmsdafangmsdagangmsdahangrtangrtvbangrtvbdangsphangstangzarraogonaopfapapEapacirapeapidaposapproxapproxeqaringascrastasympasympeqatildeaumlawconintawintbNotbackcongbackepsilonbackprimebacksimbacksimeqbarveebarwedbarwedgebbrkbbrktbrkbcongbcybdquobecausbecausebemptyvbepsibernoubetabethbetweenbfrbigcapbigcircbigcupbigodotbigoplusbigotimesbigsqcupbigstarbigtriangledownbigtriangleupbiguplusbigveebigwedgebkarowblacklozengeblacksquareblacktriangleblacktriangledownblacktriangleleftblacktrianglerightblankblk12blk14blk34blockbnebnequivbnotbopfbotbottombowtieboxDLboxDRboxDlboxDrboxHboxHDboxHUboxHdboxHuboxULboxURboxUlboxUrboxVboxVHboxVLboxVRboxVhboxVlboxVrboxboxboxdLboxdRboxdlboxdrboxhboxhDboxhUboxhdboxhuboxminusboxplusboxtimesboxuLboxuRboxulboxurboxvboxvHboxvLboxvRboxvhboxvlboxvrbprimebrevebrvbarbscrbsemibsimbsimebsolbsolbbsolhsubbullbulletbumpbumpEbumpebumpeqcacutecapcapandcapbrcupcapcapcapcupcapdotcapscaretcaronccapsccaronccedilccircccupsccupssmcdotcedilcemptyvcentcenterdotcfrchcycheckcheckmarkchicircirEcirccirceqcirclearrowleftcirclearrowrightcircledRcircledScircledastcircledcirccircleddashcirecirfnintcirmidcirscirclubsclubsuitcoloncolonecoloneqcommacommatcompcompfncomplementcomplexescongcongdotconintcopfcoprodcopycopysrcrarrcrosscscrcsubcsubecsupcsupectdotcudarrlcudarrrcueprcuesccularrcularrpcupcupbrcapcupcapcupcupcupdotcuporcupscurarrcurarrmcurlyeqpreccurlyeqsucccurlyveecurlywedgecurrencurvearrowleftcurvearrowrightcuveecuwedcwconintcwintcylctydArrdHardaggerdalethdarrdashdashvdbkarowdblacdcarondcyddddaggerddarrddotseqdegdeltademptyvdfishtdfrdharldharrdiamdiamonddiamondsuitdiamsdiedigammadisindivdividedivideontimesdivonxdjcydlcorndlcropdollardopfdotdoteqdoteqdotdotminusdotplusdotsquaredoublebarwedgedownarrowdowndownarrowsdownharpoonleftdownharpoonrightdrbkarowdrcorndrcropdscrdscydsoldstrokdtdotdtridtrifduarrduhardwangledzcydzigrarreDDoteDoteacuteeasterecaronecirecircecolonecyedoteeefDotefregegraveegsegsdotelelintersellelselsdotemacremptyemptysetemptyvemspemsp13emsp14engenspeogoneopfepareparsleplusepsiepsilonepsiveqcirceqcoloneqsimeqslantgtreqslantlessequalsequestequivequivDDeqvparslerDoterarrescresdotesimetaetheumleuroexclexistexpectationexponentialefallingdotseqfcyfemaleffiligffligfflligffrfiligfjligflatflligfltnsfnoffopfforallforkforkvfpartintfrac12frac13frac14frac15frac16frac18frac23frac25frac34frac35frac38frac45frac56frac58frac78fraslfrownfscrgEgElgacutegammagammadgapgbrevegcircgcygdotgegelgeqgeqqgeqslantgesgesccgesdotgesdotogesdotolgeslgeslesgfrggggggimelgjcyglglEglagljgnEgnapgnapproxgnegneqgneqqgnsimgopfgravegscrgsimgsimegsimlgtgtccgtcirgtdotgtlPargtquestgtrapproxgtrarrgtrdotgtreqlessgtreqqlessgtrlessgtrsimgvertneqqgvnEhArrhairsphalfhamilthardcyharrharrcirharrwhbarhcircheartsheartsuithellipherconhfrhksearowhkswarowhoarrhomththookleftarrowhookrightarrowhopfhorbarhscrhslashhstrokhybullhypheniacuteicicircicyiecyiexcliffifrigraveiiiiiintiiintiinfiniiotaijligimacrimageimaglineimagpartimathimofimpedinincareinfininfintieinodotintintcalintegersintercalintlarhkintprodiocyiogoniopfiotaiprodiquestiscrisinisinEisindotisinsisinsvisinvititildeiukcyiumljcircjcyjfrjmathjopfjscrjsercyjukcykappakappavkcedilkcykfrkgreenkhcykjcykopfkscrlAarrlArrlAtaillBarrlElEglHarlacutelaemptyvlagranlambdalanglangdlanglelaplaquolarrlarrblarrbfslarrfslarrhklarrlplarrpllarrsimlarrtllatlataillatelateslbarrlbbrklbracelbracklbrkelbrksldlbrkslulcaronlcedillceillcublcyldcaldquoldquorldrdharldrusharldshleleftarrowleftarrowtailleftharpoondownleftharpoonupleftleftarrowsleftrightarrowleftrightarrowsleftrightharpoonsleftrightsquigarrowleftthreetimeslegleqleqqleqslantleslescclesdotlesdotolesdotorlesglesgeslessapproxlessdotlesseqgtrlesseqqgtrlessgtrlesssimlfishtlfloorlfrlglgElhardlharulharullhblkljcyllllarrllcornerllhardlltrilmidotlmoustlmoustachelnElnaplnapproxlnelneqlneqqlnsimloangloarrlobrklongleftarrowlongleftrightarrowlongmapstolongrightarrowlooparrowleftlooparrowrightloparlopflopluslotimeslowastlowbarlozlozengelozflparlparltlrarrlrcornerlrharlrhardlrmlrtrilsaquolscrlshlsimlsimelsimglsqblsquolsquorlstrokltltccltcirltdotlthreeltimesltlarrltquestltrParltriltrieltriflurdsharluruharlvertneqqlvnEmDDotmacrmalemaltmaltesemapmapstomapstodownmapstoleftmapstoupmarkermcommamcymdashmeasuredanglemfrmhomicromidmidastmidcirmiddotminusminusbminusdminusdumlcpmldrmnplusmodelsmopfmpmscrmstposmumultimapmumapnGgnGtnGtvnLeftarrownLeftrightarrownLlnLtnLtvnRightarrownVDashnVdashnablanacutenangnapnapEnapidnaposnapproxnaturnaturalnaturalsnbspnbumpnbumpencapncaronncedilncongncongdotncupncyndashneneArrnearhknearrnearrownedotnequivnesearnesimnexistnexistsnfrngEngengeqngeqqngeqslantngesngsimngtngtrnhArrnharrnhparninisnisdnivnjcynlArrnlEnlarrnldrnlenleftarrownleftrightarrownleqnleqqnleqslantnlesnlessnlsimnltnltrinltrienmidnopfnotnotinnotinEnotindotnotinvanotinvbnotinvcnotninotnivanotnivbnotnivcnparnparallelnparslnpartnpolintnprnprcuenprenprecnpreceqnrArrnrarrnrarrcnrarrwnrightarrownrtrinrtrienscnsccuenscenscrnshortmidnshortparallelnsimnsimensimeqnsmidnsparnsqsubensqsupensubnsubEnsubensubsetnsubseteqnsubseteqqnsuccnsucceqnsupnsupEnsupensupsetnsupseteqnsupseteqqntglntildentlgntriangleleftntrianglelefteqntrianglerightntrianglerighteqnunumnumeronumspnvDashnvHarrnvapnvdashnvgenvgtnvinfinnvlArrnvlenvltnvltrienvrArrnvrtrienvsimnwArrnwarhknwarrnwarrownwnearoSoacuteoastocirocircocyodashodblacodivodotodsoldoeligofcirofrogonograveogtohbarohmointolarrolcirolcrossolineoltomacromegaomicronomidominusoopfoparoperpoplusororarrordorderorderofordfordmorigoforororslopeorvoscroslashosolotildeotimesotimesasoumlovbarparparaparallelparsimparslpartpcypercntperiodpermilperppertenkpfrphiphivphmmatphonepipitchforkpivplanckplanckhplankvplusplusacirplusbpluscirplusdoplusdupluseplusmnplussimplustwopmpointintpopfpoundprprEprapprcuepreprecprecapproxpreccurlyeqpreceqprecnapproxprecneqqprecnsimprecsimprimeprimesprnEprnapprnsimprodprofalarproflineprofsurfpropproptoprsimprurelpscrpsipuncspqfrqintqopfqprimeqscrquaternionsquatintquestquesteqquotrAarrrArrrAtailrBarrrHarraceracuteradicraemptyvrangrangdrangerangleraquorarrrarraprarrbrarrbfsrarrcrarrfsrarrhkrarrlprarrplrarrsimrarrtlrarrwratailratiorationalsrbarrrbbrkrbracerbrackrbrkerbrksldrbrkslurcaronrcedilrceilrcubrcyrdcardldharrdquordquorrdshrealrealinerealpartrealsrectregrfishtrfloorrfrrhardrharurharulrhorhovrightarrowrightarrowtailrightharpoondownrightharpoonuprightleftarrowsrightleftharpoonsrightrightarrowsrightsquigarrowrightthreetimesringrisingdotseqrlarrrlharrlmrmoustrmoustachernmidroangroarrrobrkroparropfroplusrotimesrparrpargtrppolintrrarrrsaquorscrrshrsqbrsquorsquorrthreertimesrtrirtriertrifrtriltriruluharrxsacutesbquoscscEscapscaronsccuescescedilscircscnEscnapscnsimscpolintscsimscysdotsdotbsdoteseArrsearhksearrsearrowsectsemiseswarsetminussetmnsextsfrsfrownsharpshchcyshcyshortmidshortparallelshysigmasigmafsigmavsimsimdotsimesimeqsimgsimgEsimlsimlEsimnesimplussimrarrslarrsmallsetminussmashpsmeparslsmidsmilesmtsmtesmtessoftcysolsolbsolbarsopfspadesspadesuitsparsqcapsqcapssqcupsqcupssqsubsqsubesqsubsetsqsubseteqsqsupsqsupesqsupsetsqsupseteqsqusquaresquarfsqufsrarrsscrssetmnssmilesstarfstarstarfstraightepsilonstraightphistrnssubsubEsubdotsubesubedotsubmultsubnEsubnesubplussubrarrsubsetsubseteqsubseteqqsubsetneqsubsetneqqsubsimsubsubsubsupsuccsuccapproxsucccurlyeqsucceqsuccnapproxsuccneqqsuccnsimsuccsimsumsungsupsup1sup2sup3supEsupdotsupdsubsupesupedotsuphsolsuphsubsuplarrsupmultsupnEsupnesupplussupsetsupseteqsupseteqqsupsetneqsupsetneqqsupsimsupsubsupsupswArrswarhkswarrswarrowswnwarszligtargettautbrktcarontcediltcytdottelrectfrthere4thereforethetathetasymthetavthickapproxthicksimthinspthkapthksimthorntildetimestimesbtimesbartimesdtinttoeatoptopbottopcirtopftopforktosatprimetradetriangletriangledowntrianglelefttrianglelefteqtriangleqtrianglerighttrianglerighteqtridottrietriminustriplustrisbtritimetrpeziumtscrtscytshcytstroktwixttwoheadleftarrowtwoheadrightarrowuArruHaruacuteuarrubrcyubreveucircucyudarrudblacudharufishtufrugraveuharluharruhblkulcornulcornerulcropultriumacrumluogonuopfuparrowupdownarrowupharpoonleftupharpoonrightuplusupsiupsihupsilonupuparrowsurcornurcornerurcropuringurtriuscrutdotutildeutriutrifuuarruumluwanglevArrvBarvBarvvDashvangrtvarepsilonvarkappavarnothingvarphivarpivarproptovarrvarrhovarsigmavarsubsetneqvarsubsetneqqvarsupsetneqvarsupsetneqqvarthetavartriangleleftvartrianglerightvcyvdashveeveebarveeeqvellipverbarvertvfrvltrivnsubvnsupvopfvpropvrtrivscrvsubnEvsubnevsupnEvsupnevzigzagwcircwedbarwedgewedgeqweierpwfrwopfwpwrwreathwscrxcapxcircxcupxdtrixfrxhArrxharrxixlArrxlarrxmapxnisxodotxopfxoplusxotimexrArrxrarrxscrxsqcupxuplusxutrixveexwedgeyacuteyacyycircycyyenyfryicyyopfyscryucyyumlzacutezcaronzcyzdotzeetrfzetazfrzhcyzigrarrzopfzscrzwjzwnj"
+const _html5entitiesNameIndex = "\x05\x03\x06\x05\x03\x03\x06\x05\x05\x03\x05\x04\x0d\x05\x04\x06\x06\x04\x09\x04\x06\x03\x07\x0a\x04\x03\x04\x05\x04\x06\x04\x04\x06\x03\x14\x07\x06\x06\x05\x07\x04\x07\x09\x03\x03\x09\x0b\x0a\x0b\x18\x15\x0f\x05\x06\x09\x06\x0f\x04\x09\x1f\x05\x04\x03\x06\x02\x08\x04\x04\x04\x06\x04\x05\x06\x03\x03\x05\x03\x10\x0e\x16\x10\x10\x07\x0d\x04\x03\x06\x08\x15\x09\x0f\x0f\x14\x0d\x13\x18\x14\x10\x0e\x0d\x11\x11\x09\x0c\x10\x09\x13\x11\x0e\x11\x12\x0f\x12\x07\x0c\x09\x04\x06\x03\x03\x06\x06\x05\x03\x04\x03\x06\x07\x05\x10\x14\x05\x04\x07\x05\x0a\x0b\x04\x04\x03\x04\x06\x0c\x03\x03\x11\x15\x04\x06\x0a\x04\x04\x02\x05\x06\x06\x06\x05\x03\x04\x03\x02\x04\x0c\x10\x10\x0e\x0b\x11\x0c\x04\x02\x06\x05\x03\x05\x03\x0c\x04\x0e\x04\x06\x0c\x09\x04\x05\x04\x06\x05\x03\x04\x03\x06\x02\x05\x0a\x07\x03\x08\x0c\x0e\x0e\x05\x04\x04\x04\x06\x05\x04\x05\x03\x03\x04\x04\x06\x05\x04\x04\x05\x06\x03\x03\x04\x04\x04\x02\x06\x06\x04\x0a\x04\x06\x06\x03\x10\x09\x0c\x13\x0b\x11\x11\x0e\x11\x09\x0e\x0f\x07\x0c\x0d\x0c\x0f\x11\x10\x0f\x0c\x0f\x0a\x0d\x09\x0e\x10\x0d\x0b\x08\x0e\x09\x03\x02\x0a\x06\x0d\x12\x0e\x0d\x12\x0e\x04\x0e\x0f\x04\x03\x06\x02\x03\x03\x0b\x09\x03\x09\x04\x04\x02\x04\x06\x06\x06\x03\x13\x12\x11\x15\x14\x0e\x07\x03\x07\x10\x04\x03\x0c\x09\x14\x0a\x08\x0d\x09\x0a\x0f\x13\x11\x0e\x14\x0f\x0f\x0c\x0f\x12\x14\x07\x0c\x0e\x0b\x11\x0c\x17\x11\x0b\x10\x15\x11\x10\x13\x15\x0f\x14\x11\x16\x09\x0e\x0b\x10\x15\x10\x0b\x10\x08\x0d\x11\x0d\x0e\x04\x06\x02\x05\x06\x05\x03\x06\x03\x06\x05\x05\x07\x04\x14\x0e\x02\x04\x06\x06\x06\x04\x07\x09\x0b\x0f\x08\x03\x03\x03\x02\x09\x0d\x04\x02\x08\x0d\x12\x0d\x05\x07\x0a\x0c\x04\x03\x04\x03\x04\x04\x05\x03\x06\x04\x04\x06\x06\x06\x03\x02\x0e\x12\x14\x03\x03\x11\x0a\x0d\x13\x0c\x12\x12\x0f\x12\x0a\x08\x0d\x0e\x0d\x10\x12\x11\x10\x0d\x10\x0b\x0e\x0a\x04\x0c\x0b\x04\x03\x0b\x06\x04\x06\x06\x02\x06\x06\x05\x03\x03\x0e\x0e\x0f\x0c\x05\x0b\x04\x04\x06\x12\x0c\x11\x0e\x13\x0b\x04\x04\x03\x06\x0b\x08\x0d\x12\x0d\x08\x03\x03\x08\x0d\x06\x05\x05\x05\x04\x03\x03\x06\x06\x03\x03\x09\x05\x0a\x09\x05\x0a\x0e\x0a\x04\x09\x04\x06\x06\x04\x08\x05\x06\x05\x03\x06\x03\x06\x05\x08\x0a\x0c\x10\x05\x09\x05\x04\x07\x0a\x10\x0b\x0d\x05\x0a\x07\x0b\x0e\x0f\x04\x07\x05\x04\x06\x04\x05\x04\x03\x05\x06\x03\x06\x04\x0b\x0c\x11\x0d\x0d\x03\x04\x04\x06\x05\x05\x03\x04\x04\x03\x02\x04\x04\x04\x04\x04\x06\x05\x03\x03\x04\x04\x04\x04\x06\x06\x03\x04\x0e\x04\x03\x04\x04\x06\x06\x02\x03\x03\x05\x05\x03\x05\x02\x03\x06\x07\x05\x05\x05\x05\x03\x03\x06\x04\x08\x04\x03\x04\x05\x06\x08\x08\x08\x08\x08\x08\x08\x08\x05\x07\x08\x06\x05\x07\x05\x04\x02\x03\x06\x03\x04\x04\x06\x08\x05\x04\x03\x05\x07\x06\x04\x08\x05\x04\x08\x0b\x09\x07\x09\x06\x06\x08\x04\x08\x05\x03\x05\x06\x07\x07\x05\x06\x04\x04\x07\x03\x06\x07\x06\x07\x08\x09\x08\x07\x0f\x0d\x08\x06\x08\x06\x0c\x0b\x0d\x11\x11\x12\x05\x05\x05\x05\x05\x03\x07\x04\x04\x03\x06\x06\x05\x05\x05\x05\x04\x05\x05\x05\x05\x05\x05\x05\x05\x04\x05\x05\x05\x05\x05\x05\x06\x05\x05\x05\x05\x04\x05\x05\x05\x05\x08\x07\x08\x05\x05\x05\x05\x04\x05\x05\x05\x05\x05\x05\x06\x05\x06\x04\x05\x04\x05\x04\x05\x08\x04\x06\x04\x05\x05\x06\x06\x03\x06\x08\x06\x06\x06\x04\x05\x05\x05\x06\x06\x05\x05\x07\x04\x05\x07\x04\x09\x03\x04\x05\x09\x03\x03\x04\x04\x06\x0f\x10\x08\x08\x0a\x0b\x0b\x04\x08\x06\x07\x05\x08\x05\x06\x07\x05\x06\x04\x06\x0a\x09\x04\x07\x06\x04\x06\x04\x06\x05\x05\x04\x04\x05\x04\x05\x05\x07\x07\x05\x05\x06\x07\x03\x08\x06\x06\x06\x05\x04\x06\x07\x0b\x0b\x08\x0a\x06\x0e\x0f\x05\x05\x08\x05\x06\x04\x04\x06\x06\x04\x04\x05\x07\x05\x06\x03\x02\x07\x05\x07\x03\x05\x07\x06\x03\x05\x05\x04\x07\x0b\x05\x03\x07\x05\x03\x06\x0d\x06\x04\x06\x06\x06\x04\x03\x05\x08\x08\x07\x09\x0e\x09\x0e\x0f\x10\x08\x06\x06\x04\x04\x04\x06\x05\x04\x05\x05\x05\x07\x04\x08\x05\x04\x06\x06\x06\x04\x05\x06\x03\x04\x02\x05\x03\x02\x06\x03\x06\x02\x08\x03\x03\x06\x05\x05\x08\x06\x04\x06\x06\x03\x04\x05\x04\x04\x06\x05\x04\x07\x05\x06\x07\x05\x0a\x0b\x06\x06\x05\x07\x08\x05\x05\x04\x05\x04\x03\x03\x04\x04\x04\x05\x0b\x0c\x0d\x03\x06\x06\x05\x06\x03\x05\x05\x04\x05\x05\x04\x04\x06\x04\x05\x08\x06\x06\x06\x06\x06\x06\x06\x06\x06\x06\x06\x06\x06\x06\x06\x05\x05\x04\x02\x03\x06\x05\x06\x03\x06\x05\x03\x04\x02\x03\x03\x04\x08\x03\x05\x06\x07\x08\x04\x06\x03\x02\x03\x05\x04\x02\x03\x03\x03\x03\x04\x08\x03\x04\x05\x05\x04\x05\x04\x04\x05\x05\x02\x04\x05\x05\x06\x07\x09\x06\x06\x09\x0a\x07\x06\x09\x04\x04\x06\x04\x06\x06\x04\x07\x05\x04\x05\x06\x09\x06\x06\x03\x08\x08\x05\x06\x0d\x0e\x04\x06\x04\x06\x06\x06\x06\x06\x02\x05\x03\x04\x05\x03\x03\x06\x02\x06\x05\x06\x05\x05\x05\x05\x08\x08\x05\x04\x05\x02\x06\x05\x08\x06\x03\x06\x08\x08\x08\x07\x04\x05\x04\x04\x05\x06\x04\x04\x05\x07\x05\x06\x05\x02\x06\x05\x04\x05\x03\x03\x05\x04\x04\x06\x05\x05\x06\x06\x03\x03\x06\x04\x04\x04\x04\x05\x04\x06\x05\x02\x03\x04\x06\x08\x06\x06\x04\x05\x06\x03\x05\x04\x05\x07\x06\x06\x06\x06\x07\x06\x03\x06\x04\x05\x05\x05\x06\x06\x05\x07\x07\x06\x06\x05\x04\x03\x04\x05\x06\x07\x08\x04\x02\x09\x0d\x0f\x0d\x0e\x0e\x0f\x11\x13\x0e\x03\x03\x04\x08\x03\x05\x06\x07\x08\x04\x06\x0a\x07\x09\x0a\x07\x07\x06\x06\x03\x02\x03\x05\x05\x06\x05\x04\x02\x05\x08\x06\x05\x06\x06\x0a\x03\x04\x08\x03\x04\x05\x05\x05\x05\x05\x0d\x12\x0a\x0e\x0d\x0e\x05\x04\x06\x07\x06\x06\x03\x07\x04\x04\x06\x05\x08\x05\x06\x03\x05\x06\x04\x03\x04\x05\x05\x04\x05\x06\x06\x02\x04\x05\x05\x06\x06\x06\x07\x06\x04\x05\x05\x08\x07\x09\x04\x05\x04\x04\x04\x07\x03\x06\x0a\x0a\x08\x06\x06\x03\x05\x0d\x03\x03\x05\x03\x06\x06\x06\x05\x06\x06\x07\x04\x04\x06\x06\x04\x02\x04\x06\x02\x08\x05\x03\x03\x04\x0a\x0f\x03\x03\x04\x0b\x06\x06\x05\x06\x04\x03\x04\x05\x05\x07\x05\x07\x08\x04\x05\x06\x04\x06\x06\x05\x08\x04\x03\x05\x02\x05\x06\x05\x07\x05\x06\x06\x05\x06\x07\x03\x03\x03\x04\x05\x09\x04\x05\x03\x04\x05\x05\x05\x02\x03\x04\x03\x04\x05\x03\x05\x04\x03\x0a\x0f\x04\x05\x09\x04\x05\x05\x03\x05\x06\x04\x04\x03\x05\x06\x08\x07\x07\x07\x05\x07\x07\x07\x04\x09\x06\x05\x07\x03\x06\x04\x05\x07\x05\x05\x06\x06\x0b\x05\x06\x03\x06\x04\x04\x09\x0e\x04\x05\x06\x05\x05\x07\x07\x04\x05\x05\x07\x09\x0a\x05\x07\x04\x05\x05\x07\x09\x0a\x04\x06\x04\x0d\x0f\x0e\x10\x02\x03\x06\x05\x06\x06\x04\x06\x04\x04\x07\x06\x04\x04\x07\x06\x07\x05\x05\x06\x05\x07\x06\x02\x06\x04\x04\x05\x03\x05\x06\x04\x04\x06\x05\x05\x03\x04\x06\x03\x05\x03\x04\x05\x05\x07\x05\x03\x05\x05\x07\x04\x06\x04\x04\x05\x05\x02\x05\x03\x05\x07\x04\x04\x06\x04\x07\x03\x04\x06\x04\x06\x06\x08\x04\x05\x03\x04\x08\x06\x05\x04\x03\x06\x06\x06\x04\x07\x03\x03\x04\x06\x05\x02\x09\x03\x06\x07\x06\x04\x08\x05\x07\x06\x06\x05\x06\x07\x07\x02\x08\x04\x05\x02\x03\x04\x05\x03\x04\x0a\x0b\x06\x0b\x08\x08\x07\x05\x06\x04\x05\x06\x04\x08\x08\x08\x04\x06\x05\x06\x04\x03\x06\x03\x04\x04\x06\x04\x0b\x07\x05\x07\x04\x05\x04\x06\x05\x04\x04\x06\x05\x08\x04\x05\x05\x06\x05\x04\x06\x05\x07\x05\x06\x06\x06\x06\x07\x06\x05\x06\x05\x09\x05\x05\x06\x06\x05\x07\x07\x06\x06\x05\x04\x03\x04\x07\x05\x06\x04\x04\x07\x08\x05\x04\x03\x06\x06\x03\x05\x05\x06\x03\x04\x0a\x0e\x10\x0e\x0f\x11\x10\x0f\x0f\x04\x0c\x05\x05\x03\x06\x0a\x05\x05\x05\x05\x05\x04\x06\x07\x04\x06\x08\x05\x06\x04\x03\x04\x05\x06\x06\x06\x04\x05\x05\x08\x07\x02\x06\x05\x02\x03\x04\x06\x05\x03\x06\x05\x04\x05\x06\x08\x05\x03\x04\x05\x05\x05\x06\x05\x07\x04\x04\x06\x08\x05\x04\x03\x06\x05\x06\x04\x08\x0d\x03\x05\x06\x06\x03\x06\x04\x05\x04\x05\x04\x05\x05\x07\x07\x05\x0d\x06\x08\x04\x05\x03\x04\x05\x06\x03\x04\x06\x04\x06\x09\x04\x05\x06\x05\x06\x05\x06\x08\x0a\x05\x06\x08\x0a\x03\x06\x06\x04\x05\x04\x06\x06\x06\x04\x05\x0f\x0b\x05\x03\x04\x06\x04\x07\x07\x05\x05\x07\x07\x06\x08\x09\x09\x0a\x06\x06\x06\x04\x0a\x0b\x06\x0b\x08\x08\x07\x03\x04\x03\x04\x04\x04\x04\x06\x07\x04\x07\x07\x07\x07\x07\x05\x05\x07\x06\x08\x09\x09\x0a\x06\x06\x06\x05\x06\x05\x07\x06\x05\x06\x03\x04\x06\x06\x03\x04\x06\x03\x06\x09\x05\x08\x06\x0b\x08\x06\x05\x06\x05\x05\x05\x06\x08\x06\x04\x04\x03\x06\x06\x04\x07\x04\x06\x05\x08\x0c\x0c\x0e\x09\x0d\x0f\x06\x04\x08\x07\x05\x07\x08\x04\x04\x05\x06\x05\x10\x11\x04\x04\x06\x04\x05\x06\x05\x03\x05\x06\x05\x06\x03\x06\x05\x05\x05\x06\x08\x06\x05\x05\x03\x05\x04\x07\x0b\x0d\x0e\x05\x04\x05\x07\x0a\x06\x08\x06\x05\x05\x04\x05\x06\x04\x05\x05\x04\x07\x04\x04\x05\x05\x06\x0a\x08\x0a\x06\x05\x09\x04\x06\x08\x0c\x0d\x0c\x0d\x08\x0f\x10\x03\x05\x03\x06\x05\x06\x06\x04\x03\x05\x05\x05\x04\x05\x05\x04\x06\x06\x06\x06\x07\x05\x06\x05\x06\x06\x03\x04\x02\x02\x06\x04\x04\x05\x04\x05\x03\x05\x05\x02\x05\x05\x04\x04\x05\x04\x06\x06\x05\x05\x04\x06\x06\x05\x04\x06\x06\x04\x05\x03\x03\x03\x04\x04\x04\x04\x04\x06\x06\x03\x04\x06\x04\x03\x04\x07\x04\x04\x03\x04"
+var _html5entitiesCodePoints = [...]int{198, 38, 193, 194, 1040, 120068, 192, 913, 256, 10835, 260, 120120, 8289, 197, 119964, 8788, 195, 196, 8726, 10983, 8966, 1041, 8757, 8492, 914, 120069, 120121, 728, 8492, 8782, 1063, 169, 262, 8914, 8517, 8493, 268, 199, 264, 8752, 266, 184, 183, 8493, 935, 8857, 8854, 8853, 8855, 8754, 8221, 8217, 8759, 10868, 8801, 8751, 8750, 8450, 8720, 8755, 10799, 119966, 8915, 8781, 8517, 10513, 1026, 1029, 1039, 8225, 8609, 10980, 270, 1044, 8711, 916, 120071, 180, 729, 733, 96, 732, 8900, 8518, 120123, 168, 8412, 8784, 8751, 168, 8659, 8656, 8660, 10980, 10232, 10234, 10233, 8658, 8872, 8657, 8661, 8741, 8595, 10515, 8693, 785, 10576, 10590, 8637, 10582, 10591, 8641, 10583, 8868, 8615, 8659, 119967, 272, 330, 208, 201, 282, 202, 1069, 278, 120072, 200, 8712, 274, 9723, 9643, 280, 120124, 917, 10869, 8770, 8652, 8496, 10867, 919, 203, 8707, 8519, 1060, 120073, 9724, 9642, 120125, 8704, 8497, 8497, 1027, 62, 915, 988, 286, 290, 284, 1043, 288, 120074, 8921, 120126, 8805, 8923, 8807, 10914, 8823, 10878, 8819, 119970, 8811, 1066, 711, 94, 292, 8460, 8459, 8461, 9472, 8459, 294, 8782, 8783, 1045, 306, 1025, 205, 206, 1048, 304, 8465, 204, 8465, 298, 8520, 8658, 8748, 8747, 8898, 8291, 8290, 302, 120128, 921, 8464, 296, 1030, 207, 308, 1049, 120077, 120129, 119973, 1032, 1028, 1061, 1036, 922, 310, 1050, 120078, 120130, 119974, 1033, 60, 313, 923, 10218, 8466, 8606, 317, 315, 1051, 10216, 8592, 8676, 8646, 8968, 10214, 10593, 8643, 10585, 8970, 8596, 10574, 8867, 8612, 10586, 8882, 10703, 8884, 10577, 10592, 8639, 10584, 8636, 10578, 8656, 8660, 8922, 8806, 8822, 10913, 10877, 8818, 120079, 8920, 8666, 319, 10229, 10231, 10230, 10232, 10234, 10233, 120131, 8601, 8600, 8466, 8624, 321, 8810, 10501, 1052, 8287, 8499, 120080, 8723, 120132, 8499, 924, 1034, 323, 327, 325, 1053, 8203, 8203, 8203, 8203, 8811, 8810, 10, 120081, 8288, 160, 8469, 10988, 8802, 8813, 8742, 8713, 8800, 877, 24, 8708, 8815, 8817, 880, 24, 881, 24, 8825, 1087, 24, 8821, 878, 24, 878, 24, 8938, 1070, 24, 8940, 8814, 8816, 8824, 881, 24, 1087, 24, 8820, 1091, 24, 1091, 24, 8832, 1092, 24, 8928, 8716, 8939, 1070, 24, 8941, 884, 24, 8930, 884, 24, 8931, 883, 402, 8840, 8833, 1092, 24, 8929, 883, 24, 883, 402, 8841, 8769, 8772, 8775, 8777, 8740, 119977, 209, 925, 338, 211, 212, 1054, 336, 120082, 210, 332, 937, 927, 120134, 8220, 8216, 10836, 119978, 216, 213, 10807, 214, 8254, 9182, 9140, 9180, 8706, 1055, 120083, 934, 928, 177, 8460, 8473, 10939, 8826, 10927, 8828, 8830, 8243, 8719, 8759, 8733, 119979, 936, 34, 120084, 8474, 119980, 10512, 174, 340, 10219, 8608, 10518, 344, 342, 1056, 8476, 8715, 8651, 10607, 8476, 929, 10217, 8594, 8677, 8644, 8969, 10215, 10589, 8642, 10581, 8971, 8866, 8614, 10587, 8883, 10704, 8885, 10575, 10588, 8638, 10580, 8640, 10579, 8658, 8477, 10608, 8667, 8475, 8625, 10740, 1065, 1064, 1068, 346, 10940, 352, 350, 348, 1057, 120086, 8595, 8592, 8594, 8593, 931, 8728, 120138, 8730, 9633, 8851, 8847, 8849, 8848, 8850, 8852, 119982, 8902, 8912, 8912, 8838, 8827, 10928, 8829, 8831, 8715, 8721, 8913, 8835, 8839, 8913, 222, 8482, 1035, 1062, 9, 932, 356, 354, 1058, 120087, 8756, 920, 828, 202, 8201, 8764, 8771, 8773, 8776, 120139, 8411, 119983, 358, 218, 8607, 10569, 1038, 364, 219, 1059, 368, 120088, 217, 362, 95, 9183, 9141, 9181, 8899, 8846, 370, 120140, 8593, 10514, 8645, 8597, 10606, 8869, 8613, 8657, 8661, 8598, 8599, 978, 933, 366, 119984, 360, 220, 8875, 10987, 1042, 8873, 10982, 8897, 8214, 8214, 8739, 124, 10072, 8768, 8202, 120089, 120141, 119985, 8874, 372, 8896, 120090, 120142, 119986, 120091, 926, 120143, 119987, 1071, 1031, 1070, 221, 374, 1067, 120092, 120144, 119988, 376, 1046, 377, 381, 1047, 379, 8203, 918, 8488, 8484, 119989, 225, 259, 8766, 876, 19, 8767, 226, 180, 1072, 230, 8289, 120094, 224, 8501, 8501, 945, 257, 10815, 38, 8743, 10837, 10844, 10840, 10842, 8736, 10660, 8736, 8737, 10664, 10665, 10666, 10667, 10668, 10669, 10670, 10671, 8735, 8894, 10653, 8738, 197, 9084, 261, 120146, 8776, 10864, 10863, 8778, 8779, 39, 8776, 8778, 229, 119990, 42, 8776, 8781, 227, 228, 8755, 10769, 10989, 8780, 1014, 8245, 8765, 8909, 8893, 8965, 8965, 9141, 9142, 8780, 1073, 8222, 8757, 8757, 10672, 1014, 8492, 946, 8502, 8812, 120095, 8898, 9711, 8899, 10752, 10753, 10754, 10758, 9733, 9661, 9651, 10756, 8897, 8896, 10509, 10731, 9642, 9652, 9662, 9666, 9656, 9251, 9618, 9617, 9619, 9608, 6, 421, 880, 421, 8976, 120147, 8869, 8869, 8904, 9559, 9556, 9558, 9555, 9552, 9574, 9577, 9572, 9575, 9565, 9562, 9564, 9561, 9553, 9580, 9571, 9568, 9579, 9570, 9567, 10697, 9557, 9554, 9488, 9484, 9472, 9573, 9576, 9516, 9524, 8863, 8862, 8864, 9563, 9560, 9496, 9492, 9474, 9578, 9569, 9566, 9532, 9508, 9500, 8245, 728, 166, 119991, 8271, 8765, 8909, 92, 10693, 10184, 8226, 8226, 8782, 10926, 8783, 8783, 263, 8745, 10820, 10825, 10827, 10823, 10816, 874, 5024, 8257, 711, 10829, 269, 231, 265, 10828, 10832, 267, 184, 10674, 162, 183, 120096, 1095, 10003, 10003, 967, 9675, 10691, 710, 8791, 8634, 8635, 174, 9416, 8859, 8858, 8861, 8791, 10768, 10991, 10690, 9827, 9827, 58, 8788, 8788, 44, 64, 8705, 8728, 8705, 8450, 8773, 10861, 8750, 120148, 8720, 169, 8471, 8629, 10007, 119992, 10959, 10961, 10960, 10962, 8943, 10552, 10549, 8926, 8927, 8630, 10557, 8746, 10824, 10822, 10826, 8845, 10821, 874, 5024, 8631, 10556, 8926, 8927, 8910, 8911, 164, 8630, 8631, 8910, 8911, 8754, 8753, 9005, 8659, 10597, 8224, 8504, 8595, 8208, 8867, 10511, 733, 271, 1076, 8518, 8225, 8650, 10871, 176, 948, 10673, 10623, 120097, 8643, 8642, 8900, 8900, 9830, 9830, 168, 989, 8946, 247, 247, 8903, 8903, 1106, 8990, 8973, 36, 120149, 729, 8784, 8785, 8760, 8724, 8865, 8966, 8595, 8650, 8643, 8642, 10512, 8991, 8972, 119993, 1109, 10742, 273, 8945, 9663, 9662, 8693, 10607, 10662, 1119, 10239, 10871, 8785, 233, 10862, 283, 8790, 234, 8789, 1101, 279, 8519, 8786, 120098, 10906, 232, 10902, 10904, 10905, 9191, 8467, 10901, 10903, 275, 8709, 8709, 8709, 8195, 8196, 8197, 331, 8194, 281, 120150, 8917, 10723, 10865, 949, 949, 1013, 8790, 8789, 8770, 10902, 10901, 61, 8799, 8801, 10872, 10725, 8787, 10609, 8495, 8784, 8770, 951, 240, 235, 8364, 33, 8707, 8496, 8519, 8786, 1092, 9792, 64259, 64256, 64260, 120099, 64257, 10, 06, 9837, 64258, 9649, 402, 120151, 8704, 8916, 10969, 10765, 189, 8531, 188, 8533, 8537, 8539, 8532, 8534, 190, 8535, 8540, 8536, 8538, 8541, 8542, 8260, 8994, 119995, 8807, 10892, 501, 947, 989, 10886, 287, 285, 1075, 289, 8805, 8923, 8805, 8807, 10878, 10878, 10921, 10880, 10882, 10884, 892, 5024, 10900, 120100, 8811, 8921, 8503, 1107, 8823, 10898, 10917, 10916, 8809, 10890, 10890, 10888, 10888, 8809, 8935, 120152, 96, 8458, 8819, 10894, 10896, 62, 10919, 10874, 8919, 10645, 10876, 10886, 10616, 8919, 8923, 10892, 8823, 8819, 880, 5024, 880, 5024, 8660, 8202, 189, 8459, 1098, 8596, 10568, 8621, 8463, 293, 9829, 9829, 8230, 8889, 120101, 10533, 10534, 8703, 8763, 8617, 8618, 120153, 8213, 119997, 8463, 295, 8259, 8208, 237, 8291, 238, 1080, 1077, 161, 8660, 120102, 236, 8520, 10764, 8749, 10716, 8489, 307, 299, 8465, 8464, 8465, 305, 8887, 437, 8712, 8453, 8734, 10717, 305, 8747, 8890, 8484, 8890, 10775, 10812, 1105, 303, 120154, 953, 10812, 191, 119998, 8712, 8953, 8949, 8948, 8947, 8712, 8290, 297, 1110, 239, 309, 1081, 120103, 567, 120155, 119999, 1112, 1108, 954, 1008, 311, 1082, 120104, 312, 1093, 1116, 120156, 120000, 8666, 8656, 10523, 10510, 8806, 10891, 10594, 314, 10676, 8466, 955, 10216, 10641, 10216, 10885, 171, 8592, 8676, 10527, 10525, 8617, 8619, 10553, 10611, 8610, 10923, 10521, 10925, 1092, 5024, 10508, 10098, 123, 91, 10635, 10639, 10637, 318, 316, 8968, 123, 1083, 10550, 8220, 8222, 10599, 10571, 8626, 8804, 8592, 8610, 8637, 8636, 8647, 8596, 8646, 8651, 8621, 8907, 8922, 8804, 8806, 10877, 10877, 10920, 10879, 10881, 10883, 892, 5024, 10899, 10885, 8918, 8922, 10891, 8822, 8818, 10620, 8970, 120105, 8822, 10897, 8637, 8636, 10602, 9604, 1113, 8810, 8647, 8990, 10603, 9722, 320, 9136, 9136, 8808, 10889, 10889, 10887, 10887, 8808, 8934, 10220, 8701, 10214, 10229, 10231, 10236, 10230, 8619, 8620, 10629, 120157, 10797, 10804, 8727, 95, 9674, 9674, 10731, 40, 10643, 8646, 8991, 8651, 10605, 8206, 8895, 8249, 120001, 8624, 8818, 10893, 10895, 91, 8216, 8218, 322, 60, 10918, 10873, 8918, 8907, 8905, 10614, 10875, 10646, 9667, 8884, 9666, 10570, 10598, 880, 5024, 880, 5024, 8762, 175, 9794, 10016, 10016, 8614, 8614, 8615, 8612, 8613, 9646, 10793, 1084, 8212, 8737, 120106, 8487, 181, 8739, 42, 10992, 183, 8722, 8863, 8760, 10794, 10971, 8230, 8723, 8871, 120158, 8723, 120002, 8766, 956, 8888, 8888, 892, 24, 881, 402, 881, 24, 8653, 8654, 892, 24, 881, 402, 881, 24, 8655, 8879, 8878, 8711, 324, 873, 402, 8777, 1086, 24, 877, 24, 329, 8777, 9838, 9838, 8469, 160, 878, 24, 878, 24, 10819, 328, 326, 8775, 1086, 24, 10818, 1085, 8211, 8800, 8663, 10532, 8599, 8599, 878, 24, 8802, 10536, 877, 24, 8708, 8708, 120107, 880, 24, 8817, 8817, 880, 24, 1087, 24, 1087, 24, 8821, 8815, 8815, 8654, 8622, 10994, 8715, 8956, 8954, 8715, 1114, 8653, 880, 24, 8602, 8229, 8816, 8602, 8622, 8816, 880, 24, 1087, 24, 1087, 24, 8814, 8820, 8814, 8938, 8940, 8740, 120159, 172, 8713, 895, 24, 894, 24, 8713, 8951, 8950, 8716, 8716, 8958, 8957, 8742, 8742, 1100, 421, 870, 24, 10772, 8832, 8928, 1092, 24, 8832, 1092, 24, 8655, 8603, 1054, 24, 860, 24, 8603, 8939, 8941, 8833, 8929, 1092, 24, 120003, 8740, 8742, 8769, 8772, 8772, 8740, 8742, 8930, 8931, 8836, 1094, 24, 8840, 883, 402, 8840, 1094, 24, 8833, 1092, 24, 8837, 1095, 24, 8841, 883, 402, 8841, 1095, 24, 8825, 241, 8824, 8938, 8940, 8939, 8941, 957, 35, 8470, 8199, 8877, 10500, 878, 402, 8876, 880, 402, 6, 402, 10718, 10498, 880, 402, 6, 402, 888, 402, 10499, 888, 402, 876, 402, 8662, 10531, 8598, 8598, 10535, 9416, 243, 8859, 8858, 244, 1086, 8861, 337, 10808, 8857, 10684, 339, 10687, 120108, 731, 242, 10689, 10677, 937, 8750, 8634, 10686, 10683, 8254, 10688, 333, 969, 959, 10678, 8854, 120160, 10679, 10681, 8853, 8744, 8635, 10845, 8500, 8500, 170, 186, 8886, 10838, 10839, 10843, 8500, 248, 8856, 245, 8855, 10806, 246, 9021, 8741, 182, 8741, 10995, 11005, 8706, 1087, 37, 46, 8240, 8869, 8241, 120109, 966, 981, 8499, 9742, 960, 8916, 982, 8463, 8462, 8463, 43, 10787, 8862, 10786, 8724, 10789, 10866, 177, 10790, 10791, 177, 10773, 120161, 163, 8826, 10931, 10935, 8828, 10927, 8826, 10935, 8828, 10927, 10937, 10933, 8936, 8830, 8242, 8473, 10933, 10937, 8936, 8719, 9006, 8978, 8979, 8733, 8733, 8830, 8880, 120005, 968, 8200, 120110, 10764, 120162, 8279, 120006, 8461, 10774, 63, 8799, 34, 8667, 8658, 10524, 10511, 10596, 876, 17, 341, 8730, 10675, 10217, 10642, 10661, 10217, 187, 8594, 10613, 8677, 10528, 10547, 10526, 8618, 8620, 10565, 10612, 8611, 8605, 10522, 8758, 8474, 10509, 10099, 125, 93, 10636, 10638, 10640, 345, 343, 8969, 125, 1088, 10551, 10601, 8221, 8221, 8627, 8476, 8475, 8476, 8477, 9645, 174, 10621, 8971, 120111, 8641, 8640, 10604, 961, 1009, 8594, 8611, 8641, 8640, 8644, 8652, 8649, 8605, 8908, 730, 8787, 8644, 8652, 8207, 9137, 9137, 10990, 10221, 8702, 10215, 10630, 120163, 10798, 10805, 41, 10644, 10770, 8649, 8250, 120007, 8625, 93, 8217, 8217, 8908, 8906, 9657, 8885, 9656, 10702, 10600, 8478, 347, 8218, 8827, 10932, 10936, 353, 8829, 10928, 351, 349, 10934, 10938, 8937, 10771, 8831, 1089, 8901, 8865, 10854, 8664, 10533, 8600, 8600, 167, 59, 10537, 8726, 8726, 10038, 120112, 8994, 9839, 1097, 1096, 8739, 8741, 173, 963, 962, 962, 8764, 10858, 8771, 8771, 10910, 10912, 10909, 10911, 8774, 10788, 10610, 8592, 8726, 10803, 10724, 8739, 8995, 10922, 10924, 1092, 5024, 1100, 47, 10692, 9023, 120164, 9824, 9824, 8741, 8851, 885, 5024, 8852, 885, 5024, 8847, 8849, 8847, 8849, 8848, 8850, 8848, 8850, 9633, 9633, 9642, 9642, 8594, 120008, 8726, 8995, 8902, 9734, 9733, 1013, 981, 175, 8834, 10949, 10941, 8838, 10947, 10945, 10955, 8842, 10943, 10617, 8834, 8838, 10949, 8842, 10955, 10951, 10965, 10963, 8827, 10936, 8829, 10928, 10938, 10934, 8937, 8831, 8721, 9834, 8835, 185, 178, 179, 10950, 10942, 10968, 8839, 10948, 10185, 10967, 10619, 10946, 10956, 8843, 10944, 8835, 8839, 10950, 8843, 10956, 10952, 10964, 10966, 8665, 10534, 8601, 8601, 10538, 223, 8982, 964, 9140, 357, 355, 1090, 8411, 8981, 120113, 8756, 8756, 952, 977, 977, 8776, 8764, 8201, 8776, 8764, 254, 732, 215, 8864, 10801, 10800, 8749, 10536, 8868, 9014, 10993, 120165, 10970, 10537, 8244, 8482, 9653, 9663, 9667, 8884, 8796, 9657, 8885, 9708, 8796, 10810, 10809, 10701, 10811, 9186, 120009, 1094, 1115, 359, 8812, 8606, 8608, 8657, 10595, 250, 8593, 1118, 365, 251, 1091, 8645, 369, 10606, 10622, 120114, 249, 8639, 8638, 9600, 8988, 8988, 8975, 9720, 363, 168, 371, 120166, 8593, 8597, 8639, 8638, 8846, 965, 978, 965, 8648, 8989, 8989, 8974, 367, 9721, 120010, 8944, 361, 9653, 9652, 8648, 252, 10663, 8661, 10984, 10985, 8872, 10652, 1013, 1008, 8709, 981, 982, 8733, 8597, 1009, 962, 884, 5024, 1095, 5024, 884, 5024, 1095, 5024, 977, 8882, 8883, 1074, 8866, 8744, 8891, 8794, 8942, 124, 124, 120115, 8882, 883, 402, 883, 402, 120167, 8733, 8883, 120011, 1095, 5024, 884, 5024, 1095, 5024, 884, 5024, 10650, 373, 10847, 8743, 8793, 8472, 120116, 120168, 8472, 8768, 8768, 120012, 8898, 9711, 8899, 9661, 120117, 10234, 10231, 958, 10232, 10229, 10236, 8955, 10752, 120169, 10753, 10754, 10233, 10230, 120013, 10758, 10756, 9651, 8897, 8896, 253, 1103, 375, 1099, 165, 120118, 1111, 120170, 120014, 1102, 255, 378, 382, 1079, 380, 8488, 950, 120119, 1078, 8669, 120171, 120015, 8205, 8204}
+var _html5entitiesCodePointsIndex = "\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x02\x01\x01\x01\x02\x02\x01\x02\x01\x02\x02\x01\x02\x01\x01\x01\x01\x02\x02\x01\x02\x02\x01\x02\x01\x01\x01\x02\x01\x02\x01\x02\x01\x02\x01\x01\x02\x01\x02\x02\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x02\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x02\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x02\x02\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x02\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x02\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x02\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x02\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x02\x02\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x02\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x02\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x02\x02\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x02\x02\x02\x01\x01\x02\x02\x02\x01\x01\x01\x01\x01\x02\x01\x02\x02\x01\x01\x01\x01\x01\x01\x02\x02\x01\x01\x01\x01\x02\x01\x01\x01\x01\x01\x01\x01\x01\x02\x01\x01\x02\x01\x01\x01\x02\x01\x01\x02\x02\x02\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x02\x01\x01\x01\x01\x01\x01\x02\x02\x02\x01\x01\x01\x01\x01\x01\x01\x01\x01\x02\x02\x01\x01\x01\x01\x01\x01\x01\x01\x01\x02\x02\x01\x01\x01\x02\x01\x02\x01\x01\x02\x02\x01\x01\x01\x01\x01\x02\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x02\x01\x02\x01\x02\x01\x02\x01\x02\x01\x02\x01\x02\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x02\x01\x02\x02\x01\x01\x02\x02\x02\x01\x02\x02\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x02\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x02\x01\x01\x01\x01\x01\x01\x01\x01\x01\x02\x01\x02\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x02\x02\x02\x02\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x02\x02\x01\x01\x01\x01\x02\x02\x02\x02\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01"
+var _html5entitiesCharacters = [...]byte{0xc3, 0x86, 0x26, 0xc3, 0x81, 0xc3, 0x82, 0xd0, 0x90, 0xf0, 0x9d, 0x94, 0x84, 0xc3, 0x80, 0xce, 0x91, 0xc4, 0x80, 0xe2, 0xa9, 0x93, 0xc4, 0x84, 0xf0, 0x9d, 0x94, 0xb8, 0xe2, 0x81, 0xa1, 0xc3, 0x85, 0xf0, 0x9d, 0x92, 0x9c, 0xe2, 0x89, 0x94, 0xc3, 0x83, 0xc3, 0x84, 0xe2, 0x88, 0x96, 0xe2, 0xab, 0xa7, 0xe2, 0x8c, 0x86, 0xd0, 0x91, 0xe2, 0x88, 0xb5, 0xe2, 0x84, 0xac, 0xce, 0x92, 0xf0, 0x9d, 0x94, 0x85, 0xf0, 0x9d, 0x94, 0xb9, 0xcb, 0x98, 0xe2, 0x84, 0xac, 0xe2, 0x89, 0x8e, 0xd0, 0xa7, 0xc2, 0xa9, 0xc4, 0x86, 0xe2, 0x8b, 0x92, 0xe2, 0x85, 0x85, 0xe2, 0x84, 0xad, 0xc4, 0x8c, 0xc3, 0x87, 0xc4, 0x88, 0xe2, 0x88, 0xb0, 0xc4, 0x8a, 0xc2, 0xb8, 0xc2, 0xb7, 0xe2, 0x84, 0xad, 0xce, 0xa7, 0xe2, 0x8a, 0x99, 0xe2, 0x8a, 0x96, 0xe2, 0x8a, 0x95, 0xe2, 0x8a, 0x97, 0xe2, 0x88, 0xb2, 0xe2, 0x80, 0x9d, 0xe2, 0x80, 0x99, 0xe2, 0x88, 0xb7, 0xe2, 0xa9, 0xb4, 0xe2, 0x89, 0xa1, 0xe2, 0x88, 0xaf, 0xe2, 0x88, 0xae, 0xe2, 0x84, 0x82, 0xe2, 0x88, 0x90, 0xe2, 0x88, 0xb3, 0xe2, 0xa8, 0xaf, 0xf0, 0x9d, 0x92, 0x9e, 0xe2, 0x8b, 0x93, 0xe2, 0x89, 0x8d, 0xe2, 0x85, 0x85, 0xe2, 0xa4, 0x91, 0xd0, 0x82, 0xd0, 0x85, 0xd0, 0x8f, 0xe2, 0x80, 0xa1, 0xe2, 0x86, 0xa1, 0xe2, 0xab, 0xa4, 0xc4, 0x8e, 0xd0, 0x94, 0xe2, 0x88, 0x87, 0xce, 0x94, 0xf0, 0x9d, 0x94, 0x87, 0xc2, 0xb4, 0xcb, 0x99, 0xcb, 0x9d, 0x60, 0xcb, 0x9c, 0xe2, 0x8b, 0x84, 0xe2, 0x85, 0x86, 0xf0, 0x9d, 0x94, 0xbb, 0xc2, 0xa8, 0xe2, 0x83, 0x9c, 0xe2, 0x89, 0x90, 0xe2, 0x88, 0xaf, 0xc2, 0xa8, 0xe2, 0x87, 0x93, 0xe2, 0x87, 0x90, 0xe2, 0x87, 0x94, 0xe2, 0xab, 0xa4, 0xe2, 0x9f, 0xb8, 0xe2, 0x9f, 0xba, 0xe2, 0x9f, 0xb9, 0xe2, 0x87, 0x92, 0xe2, 0x8a, 0xa8, 0xe2, 0x87, 0x91, 0xe2, 0x87, 0x95, 0xe2, 0x88, 0xa5, 0xe2, 0x86, 0x93, 0xe2, 0xa4, 0x93, 0xe2, 0x87, 0xb5, 0xcc, 0x91, 0xe2, 0xa5, 0x90, 0xe2, 0xa5, 0x9e, 0xe2, 0x86, 0xbd, 0xe2, 0xa5, 0x96, 0xe2, 0xa5, 0x9f, 0xe2, 0x87, 0x81, 0xe2, 0xa5, 0x97, 0xe2, 0x8a, 0xa4, 0xe2, 0x86, 0xa7, 0xe2, 0x87, 0x93, 0xf0, 0x9d, 0x92, 0x9f, 0xc4, 0x90, 0xc5, 0x8a, 0xc3, 0x90, 0xc3, 0x89, 0xc4, 0x9a, 0xc3, 0x8a, 0xd0, 0xad, 0xc4, 0x96, 0xf0, 0x9d, 0x94, 0x88, 0xc3, 0x88, 0xe2, 0x88, 0x88, 0xc4, 0x92, 0xe2, 0x97, 0xbb, 0xe2, 0x96, 0xab, 0xc4, 0x98, 0xf0, 0x9d, 0x94, 0xbc, 0xce, 0x95, 0xe2, 0xa9, 0xb5, 0xe2, 0x89, 0x82, 0xe2, 0x87, 0x8c, 0xe2, 0x84, 0xb0, 0xe2, 0xa9, 0xb3, 0xce, 0x97, 0xc3, 0x8b, 0xe2, 0x88, 0x83, 0xe2, 0x85, 0x87, 0xd0, 0xa4, 0xf0, 0x9d, 0x94, 0x89, 0xe2, 0x97, 0xbc, 0xe2, 0x96, 0xaa, 0xf0, 0x9d, 0x94, 0xbd, 0xe2, 0x88, 0x80, 0xe2, 0x84, 0xb1, 0xe2, 0x84, 0xb1, 0xd0, 0x83, 0x3e, 0xce, 0x93, 0xcf, 0x9c, 0xc4, 0x9e, 0xc4, 0xa2, 0xc4, 0x9c, 0xd0, 0x93, 0xc4, 0xa0, 0xf0, 0x9d, 0x94, 0x8a, 0xe2, 0x8b, 0x99, 0xf0, 0x9d, 0x94, 0xbe, 0xe2, 0x89, 0xa5, 0xe2, 0x8b, 0x9b, 0xe2, 0x89, 0xa7, 0xe2, 0xaa, 0xa2, 0xe2, 0x89, 0xb7, 0xe2, 0xa9, 0xbe, 0xe2, 0x89, 0xb3, 0xf0, 0x9d, 0x92, 0xa2, 0xe2, 0x89, 0xab, 0xd0, 0xaa, 0xcb, 0x87, 0x5e, 0xc4, 0xa4, 0xe2, 0x84, 0x8c, 0xe2, 0x84, 0x8b, 0xe2, 0x84, 0x8d, 0xe2, 0x94, 0x80, 0xe2, 0x84, 0x8b, 0xc4, 0xa6, 0xe2, 0x89, 0x8e, 0xe2, 0x89, 0x8f, 0xd0, 0x95, 0xc4, 0xb2, 0xd0, 0x81, 0xc3, 0x8d, 0xc3, 0x8e, 0xd0, 0x98, 0xc4, 0xb0, 0xe2, 0x84, 0x91, 0xc3, 0x8c, 0xe2, 0x84, 0x91, 0xc4, 0xaa, 0xe2, 0x85, 0x88, 0xe2, 0x87, 0x92, 0xe2, 0x88, 0xac, 0xe2, 0x88, 0xab, 0xe2, 0x8b, 0x82, 0xe2, 0x81, 0xa3, 0xe2, 0x81, 0xa2, 0xc4, 0xae, 0xf0, 0x9d, 0x95, 0x80, 0xce, 0x99, 0xe2, 0x84, 0x90, 0xc4, 0xa8, 0xd0, 0x86, 0xc3, 0x8f, 0xc4, 0xb4, 0xd0, 0x99, 0xf0, 0x9d, 0x94, 0x8d, 0xf0, 0x9d, 0x95, 0x81, 0xf0, 0x9d, 0x92, 0xa5, 0xd0, 0x88, 0xd0, 0x84, 0xd0, 0xa5, 0xd0, 0x8c, 0xce, 0x9a, 0xc4, 0xb6, 0xd0, 0x9a, 0xf0, 0x9d, 0x94, 0x8e, 0xf0, 0x9d, 0x95, 0x82, 0xf0, 0x9d, 0x92, 0xa6, 0xd0, 0x89, 0x3c, 0xc4, 0xb9, 0xce, 0x9b, 0xe2, 0x9f, 0xaa, 0xe2, 0x84, 0x92, 0xe2, 0x86, 0x9e, 0xc4, 0xbd, 0xc4, 0xbb, 0xd0, 0x9b, 0xe2, 0x9f, 0xa8, 0xe2, 0x86, 0x90, 0xe2, 0x87, 0xa4, 0xe2, 0x87, 0x86, 0xe2, 0x8c, 0x88, 0xe2, 0x9f, 0xa6, 0xe2, 0xa5, 0xa1, 0xe2, 0x87, 0x83, 0xe2, 0xa5, 0x99, 0xe2, 0x8c, 0x8a, 0xe2, 0x86, 0x94, 0xe2, 0xa5, 0x8e, 0xe2, 0x8a, 0xa3, 0xe2, 0x86, 0xa4, 0xe2, 0xa5, 0x9a, 0xe2, 0x8a, 0xb2, 0xe2, 0xa7, 0x8f, 0xe2, 0x8a, 0xb4, 0xe2, 0xa5, 0x91, 0xe2, 0xa5, 0xa0, 0xe2, 0x86, 0xbf, 0xe2, 0xa5, 0x98, 0xe2, 0x86, 0xbc, 0xe2, 0xa5, 0x92, 0xe2, 0x87, 0x90, 0xe2, 0x87, 0x94, 0xe2, 0x8b, 0x9a, 0xe2, 0x89, 0xa6, 0xe2, 0x89, 0xb6, 0xe2, 0xaa, 0xa1, 0xe2, 0xa9, 0xbd, 0xe2, 0x89, 0xb2, 0xf0, 0x9d, 0x94, 0x8f, 0xe2, 0x8b, 0x98, 0xe2, 0x87, 0x9a, 0xc4, 0xbf, 0xe2, 0x9f, 0xb5, 0xe2, 0x9f, 0xb7, 0xe2, 0x9f, 0xb6, 0xe2, 0x9f, 0xb8, 0xe2, 0x9f, 0xba, 0xe2, 0x9f, 0xb9, 0xf0, 0x9d, 0x95, 0x83, 0xe2, 0x86, 0x99, 0xe2, 0x86, 0x98, 0xe2, 0x84, 0x92, 0xe2, 0x86, 0xb0, 0xc5, 0x81, 0xe2, 0x89, 0xaa, 0xe2, 0xa4, 0x85, 0xd0, 0x9c, 0xe2, 0x81, 0x9f, 0xe2, 0x84, 0xb3, 0xf0, 0x9d, 0x94, 0x90, 0xe2, 0x88, 0x93, 0xf0, 0x9d, 0x95, 0x84, 0xe2, 0x84, 0xb3, 0xce, 0x9c, 0xd0, 0x8a, 0xc5, 0x83, 0xc5, 0x87, 0xc5, 0x85, 0xd0, 0x9d, 0xe2, 0x80, 0x8b, 0xe2, 0x80, 0x8b, 0xe2, 0x80, 0x8b, 0xe2, 0x80, 0x8b, 0xe2, 0x89, 0xab, 0xe2, 0x89, 0xaa, 0xa, 0xf0, 0x9d, 0x94, 0x91, 0xe2, 0x81, 0xa0, 0xc2, 0xa0, 0xe2, 0x84, 0x95, 0xe2, 0xab, 0xac, 0xe2, 0x89, 0xa2, 0xe2, 0x89, 0xad, 0xe2, 0x88, 0xa6, 0xe2, 0x88, 0x89, 0xe2, 0x89, 0xa0, 0xe2, 0x89, 0x82, 0xcc, 0xb8, 0xe2, 0x88, 0x84, 0xe2, 0x89, 0xaf, 0xe2, 0x89, 0xb1, 0xe2, 0x89, 0xa7, 0xcc, 0xb8, 0xe2, 0x89, 0xab, 0xcc, 0xb8, 0xe2, 0x89, 0xb9, 0xe2, 0xa9, 0xbe, 0xcc, 0xb8, 0xe2, 0x89, 0xb5, 0xe2, 0x89, 0x8e, 0xcc, 0xb8, 0xe2, 0x89, 0x8f, 0xcc, 0xb8, 0xe2, 0x8b, 0xaa, 0xe2, 0xa7, 0x8f, 0xcc, 0xb8, 0xe2, 0x8b, 0xac, 0xe2, 0x89, 0xae, 0xe2, 0x89, 0xb0, 0xe2, 0x89, 0xb8, 0xe2, 0x89, 0xaa, 0xcc, 0xb8, 0xe2, 0xa9, 0xbd, 0xcc, 0xb8, 0xe2, 0x89, 0xb4, 0xe2, 0xaa, 0xa2, 0xcc, 0xb8, 0xe2, 0xaa, 0xa1, 0xcc, 0xb8, 0xe2, 0x8a, 0x80, 0xe2, 0xaa, 0xaf, 0xcc, 0xb8, 0xe2, 0x8b, 0xa0, 0xe2, 0x88, 0x8c, 0xe2, 0x8b, 0xab, 0xe2, 0xa7, 0x90, 0xcc, 0xb8, 0xe2, 0x8b, 0xad, 0xe2, 0x8a, 0x8f, 0xcc, 0xb8, 0xe2, 0x8b, 0xa2, 0xe2, 0x8a, 0x90, 0xcc, 0xb8, 0xe2, 0x8b, 0xa3, 0xe2, 0x8a, 0x82, 0xe2, 0x83, 0x92, 0xe2, 0x8a, 0x88, 0xe2, 0x8a, 0x81, 0xe2, 0xaa, 0xb0, 0xcc, 0xb8, 0xe2, 0x8b, 0xa1, 0xe2, 0x89, 0xbf, 0xcc, 0xb8, 0xe2, 0x8a, 0x83, 0xe2, 0x83, 0x92, 0xe2, 0x8a, 0x89, 0xe2, 0x89, 0x81, 0xe2, 0x89, 0x84, 0xe2, 0x89, 0x87, 0xe2, 0x89, 0x89, 0xe2, 0x88, 0xa4, 0xf0, 0x9d, 0x92, 0xa9, 0xc3, 0x91, 0xce, 0x9d, 0xc5, 0x92, 0xc3, 0x93, 0xc3, 0x94, 0xd0, 0x9e, 0xc5, 0x90, 0xf0, 0x9d, 0x94, 0x92, 0xc3, 0x92, 0xc5, 0x8c, 0xce, 0xa9, 0xce, 0x9f, 0xf0, 0x9d, 0x95, 0x86, 0xe2, 0x80, 0x9c, 0xe2, 0x80, 0x98, 0xe2, 0xa9, 0x94, 0xf0, 0x9d, 0x92, 0xaa, 0xc3, 0x98, 0xc3, 0x95, 0xe2, 0xa8, 0xb7, 0xc3, 0x96, 0xe2, 0x80, 0xbe, 0xe2, 0x8f, 0x9e, 0xe2, 0x8e, 0xb4, 0xe2, 0x8f, 0x9c, 0xe2, 0x88, 0x82, 0xd0, 0x9f, 0xf0, 0x9d, 0x94, 0x93, 0xce, 0xa6, 0xce, 0xa0, 0xc2, 0xb1, 0xe2, 0x84, 0x8c, 0xe2, 0x84, 0x99, 0xe2, 0xaa, 0xbb, 0xe2, 0x89, 0xba, 0xe2, 0xaa, 0xaf, 0xe2, 0x89, 0xbc, 0xe2, 0x89, 0xbe, 0xe2, 0x80, 0xb3, 0xe2, 0x88, 0x8f, 0xe2, 0x88, 0xb7, 0xe2, 0x88, 0x9d, 0xf0, 0x9d, 0x92, 0xab, 0xce, 0xa8, 0x22, 0xf0, 0x9d, 0x94, 0x94, 0xe2, 0x84, 0x9a, 0xf0, 0x9d, 0x92, 0xac, 0xe2, 0xa4, 0x90, 0xc2, 0xae, 0xc5, 0x94, 0xe2, 0x9f, 0xab, 0xe2, 0x86, 0xa0, 0xe2, 0xa4, 0x96, 0xc5, 0x98, 0xc5, 0x96, 0xd0, 0xa0, 0xe2, 0x84, 0x9c, 0xe2, 0x88, 0x8b, 0xe2, 0x87, 0x8b, 0xe2, 0xa5, 0xaf, 0xe2, 0x84, 0x9c, 0xce, 0xa1, 0xe2, 0x9f, 0xa9, 0xe2, 0x86, 0x92, 0xe2, 0x87, 0xa5, 0xe2, 0x87, 0x84, 0xe2, 0x8c, 0x89, 0xe2, 0x9f, 0xa7, 0xe2, 0xa5, 0x9d, 0xe2, 0x87, 0x82, 0xe2, 0xa5, 0x95, 0xe2, 0x8c, 0x8b, 0xe2, 0x8a, 0xa2, 0xe2, 0x86, 0xa6, 0xe2, 0xa5, 0x9b, 0xe2, 0x8a, 0xb3, 0xe2, 0xa7, 0x90, 0xe2, 0x8a, 0xb5, 0xe2, 0xa5, 0x8f, 0xe2, 0xa5, 0x9c, 0xe2, 0x86, 0xbe, 0xe2, 0xa5, 0x94, 0xe2, 0x87, 0x80, 0xe2, 0xa5, 0x93, 0xe2, 0x87, 0x92, 0xe2, 0x84, 0x9d, 0xe2, 0xa5, 0xb0, 0xe2, 0x87, 0x9b, 0xe2, 0x84, 0x9b, 0xe2, 0x86, 0xb1, 0xe2, 0xa7, 0xb4, 0xd0, 0xa9, 0xd0, 0xa8, 0xd0, 0xac, 0xc5, 0x9a, 0xe2, 0xaa, 0xbc, 0xc5, 0xa0, 0xc5, 0x9e, 0xc5, 0x9c, 0xd0, 0xa1, 0xf0, 0x9d, 0x94, 0x96, 0xe2, 0x86, 0x93, 0xe2, 0x86, 0x90, 0xe2, 0x86, 0x92, 0xe2, 0x86, 0x91, 0xce, 0xa3, 0xe2, 0x88, 0x98, 0xf0, 0x9d, 0x95, 0x8a, 0xe2, 0x88, 0x9a, 0xe2, 0x96, 0xa1, 0xe2, 0x8a, 0x93, 0xe2, 0x8a, 0x8f, 0xe2, 0x8a, 0x91, 0xe2, 0x8a, 0x90, 0xe2, 0x8a, 0x92, 0xe2, 0x8a, 0x94, 0xf0, 0x9d, 0x92, 0xae, 0xe2, 0x8b, 0x86, 0xe2, 0x8b, 0x90, 0xe2, 0x8b, 0x90, 0xe2, 0x8a, 0x86, 0xe2, 0x89, 0xbb, 0xe2, 0xaa, 0xb0, 0xe2, 0x89, 0xbd, 0xe2, 0x89, 0xbf, 0xe2, 0x88, 0x8b, 0xe2, 0x88, 0x91, 0xe2, 0x8b, 0x91, 0xe2, 0x8a, 0x83, 0xe2, 0x8a, 0x87, 0xe2, 0x8b, 0x91, 0xc3, 0x9e, 0xe2, 0x84, 0xa2, 0xd0, 0x8b, 0xd0, 0xa6, 0x9, 0xce, 0xa4, 0xc5, 0xa4, 0xc5, 0xa2, 0xd0, 0xa2, 0xf0, 0x9d, 0x94, 0x97, 0xe2, 0x88, 0xb4, 0xce, 0x98, 0xe2, 0x81, 0x9f, 0xe2, 0x80, 0x8a, 0xe2, 0x80, 0x89, 0xe2, 0x88, 0xbc, 0xe2, 0x89, 0x83, 0xe2, 0x89, 0x85, 0xe2, 0x89, 0x88, 0xf0, 0x9d, 0x95, 0x8b, 0xe2, 0x83, 0x9b, 0xf0, 0x9d, 0x92, 0xaf, 0xc5, 0xa6, 0xc3, 0x9a, 0xe2, 0x86, 0x9f, 0xe2, 0xa5, 0x89, 0xd0, 0x8e, 0xc5, 0xac, 0xc3, 0x9b, 0xd0, 0xa3, 0xc5, 0xb0, 0xf0, 0x9d, 0x94, 0x98, 0xc3, 0x99, 0xc5, 0xaa, 0x5f, 0xe2, 0x8f, 0x9f, 0xe2, 0x8e, 0xb5, 0xe2, 0x8f, 0x9d, 0xe2, 0x8b, 0x83, 0xe2, 0x8a, 0x8e, 0xc5, 0xb2, 0xf0, 0x9d, 0x95, 0x8c, 0xe2, 0x86, 0x91, 0xe2, 0xa4, 0x92, 0xe2, 0x87, 0x85, 0xe2, 0x86, 0x95, 0xe2, 0xa5, 0xae, 0xe2, 0x8a, 0xa5, 0xe2, 0x86, 0xa5, 0xe2, 0x87, 0x91, 0xe2, 0x87, 0x95, 0xe2, 0x86, 0x96, 0xe2, 0x86, 0x97, 0xcf, 0x92, 0xce, 0xa5, 0xc5, 0xae, 0xf0, 0x9d, 0x92, 0xb0, 0xc5, 0xa8, 0xc3, 0x9c, 0xe2, 0x8a, 0xab, 0xe2, 0xab, 0xab, 0xd0, 0x92, 0xe2, 0x8a, 0xa9, 0xe2, 0xab, 0xa6, 0xe2, 0x8b, 0x81, 0xe2, 0x80, 0x96, 0xe2, 0x80, 0x96, 0xe2, 0x88, 0xa3, 0x7c, 0xe2, 0x9d, 0x98, 0xe2, 0x89, 0x80, 0xe2, 0x80, 0x8a, 0xf0, 0x9d, 0x94, 0x99, 0xf0, 0x9d, 0x95, 0x8d, 0xf0, 0x9d, 0x92, 0xb1, 0xe2, 0x8a, 0xaa, 0xc5, 0xb4, 0xe2, 0x8b, 0x80, 0xf0, 0x9d, 0x94, 0x9a, 0xf0, 0x9d, 0x95, 0x8e, 0xf0, 0x9d, 0x92, 0xb2, 0xf0, 0x9d, 0x94, 0x9b, 0xce, 0x9e, 0xf0, 0x9d, 0x95, 0x8f, 0xf0, 0x9d, 0x92, 0xb3, 0xd0, 0xaf, 0xd0, 0x87, 0xd0, 0xae, 0xc3, 0x9d, 0xc5, 0xb6, 0xd0, 0xab, 0xf0, 0x9d, 0x94, 0x9c, 0xf0, 0x9d, 0x95, 0x90, 0xf0, 0x9d, 0x92, 0xb4, 0xc5, 0xb8, 0xd0, 0x96, 0xc5, 0xb9, 0xc5, 0xbd, 0xd0, 0x97, 0xc5, 0xbb, 0xe2, 0x80, 0x8b, 0xce, 0x96, 0xe2, 0x84, 0xa8, 0xe2, 0x84, 0xa4, 0xf0, 0x9d, 0x92, 0xb5, 0xc3, 0xa1, 0xc4, 0x83, 0xe2, 0x88, 0xbe, 0xe2, 0x88, 0xbe, 0xcc, 0xb3, 0xe2, 0x88, 0xbf, 0xc3, 0xa2, 0xc2, 0xb4, 0xd0, 0xb0, 0xc3, 0xa6, 0xe2, 0x81, 0xa1, 0xf0, 0x9d, 0x94, 0x9e, 0xc3, 0xa0, 0xe2, 0x84, 0xb5, 0xe2, 0x84, 0xb5, 0xce, 0xb1, 0xc4, 0x81, 0xe2, 0xa8, 0xbf, 0x26, 0xe2, 0x88, 0xa7, 0xe2, 0xa9, 0x95, 0xe2, 0xa9, 0x9c, 0xe2, 0xa9, 0x98, 0xe2, 0xa9, 0x9a, 0xe2, 0x88, 0xa0, 0xe2, 0xa6, 0xa4, 0xe2, 0x88, 0xa0, 0xe2, 0x88, 0xa1, 0xe2, 0xa6, 0xa8, 0xe2, 0xa6, 0xa9, 0xe2, 0xa6, 0xaa, 0xe2, 0xa6, 0xab, 0xe2, 0xa6, 0xac, 0xe2, 0xa6, 0xad, 0xe2, 0xa6, 0xae, 0xe2, 0xa6, 0xaf, 0xe2, 0x88, 0x9f, 0xe2, 0x8a, 0xbe, 0xe2, 0xa6, 0x9d, 0xe2, 0x88, 0xa2, 0xc3, 0x85, 0xe2, 0x8d, 0xbc, 0xc4, 0x85, 0xf0, 0x9d, 0x95, 0x92, 0xe2, 0x89, 0x88, 0xe2, 0xa9, 0xb0, 0xe2, 0xa9, 0xaf, 0xe2, 0x89, 0x8a, 0xe2, 0x89, 0x8b, 0x27, 0xe2, 0x89, 0x88, 0xe2, 0x89, 0x8a, 0xc3, 0xa5, 0xf0, 0x9d, 0x92, 0xb6, 0x2a, 0xe2, 0x89, 0x88, 0xe2, 0x89, 0x8d, 0xc3, 0xa3, 0xc3, 0xa4, 0xe2, 0x88, 0xb3, 0xe2, 0xa8, 0x91, 0xe2, 0xab, 0xad, 0xe2, 0x89, 0x8c, 0xcf, 0xb6, 0xe2, 0x80, 0xb5, 0xe2, 0x88, 0xbd, 0xe2, 0x8b, 0x8d, 0xe2, 0x8a, 0xbd, 0xe2, 0x8c, 0x85, 0xe2, 0x8c, 0x85, 0xe2, 0x8e, 0xb5, 0xe2, 0x8e, 0xb6, 0xe2, 0x89, 0x8c, 0xd0, 0xb1, 0xe2, 0x80, 0x9e, 0xe2, 0x88, 0xb5, 0xe2, 0x88, 0xb5, 0xe2, 0xa6, 0xb0, 0xcf, 0xb6, 0xe2, 0x84, 0xac, 0xce, 0xb2, 0xe2, 0x84, 0xb6, 0xe2, 0x89, 0xac, 0xf0, 0x9d, 0x94, 0x9f, 0xe2, 0x8b, 0x82, 0xe2, 0x97, 0xaf, 0xe2, 0x8b, 0x83, 0xe2, 0xa8, 0x80, 0xe2, 0xa8, 0x81, 0xe2, 0xa8, 0x82, 0xe2, 0xa8, 0x86, 0xe2, 0x98, 0x85, 0xe2, 0x96, 0xbd, 0xe2, 0x96, 0xb3, 0xe2, 0xa8, 0x84, 0xe2, 0x8b, 0x81, 0xe2, 0x8b, 0x80, 0xe2, 0xa4, 0x8d, 0xe2, 0xa7, 0xab, 0xe2, 0x96, 0xaa, 0xe2, 0x96, 0xb4, 0xe2, 0x96, 0xbe, 0xe2, 0x97, 0x82, 0xe2, 0x96, 0xb8, 0xe2, 0x90, 0xa3, 0xe2, 0x96, 0x92, 0xe2, 0x96, 0x91, 0xe2, 0x96, 0x93, 0xe2, 0x96, 0x88, 0x3d, 0xe2, 0x83, 0xa5, 0xe2, 0x89, 0xa1, 0xe2, 0x83, 0xa5, 0xe2, 0x8c, 0x90, 0xf0, 0x9d, 0x95, 0x93, 0xe2, 0x8a, 0xa5, 0xe2, 0x8a, 0xa5, 0xe2, 0x8b, 0x88, 0xe2, 0x95, 0x97, 0xe2, 0x95, 0x94, 0xe2, 0x95, 0x96, 0xe2, 0x95, 0x93, 0xe2, 0x95, 0x90, 0xe2, 0x95, 0xa6, 0xe2, 0x95, 0xa9, 0xe2, 0x95, 0xa4, 0xe2, 0x95, 0xa7, 0xe2, 0x95, 0x9d, 0xe2, 0x95, 0x9a, 0xe2, 0x95, 0x9c, 0xe2, 0x95, 0x99, 0xe2, 0x95, 0x91, 0xe2, 0x95, 0xac, 0xe2, 0x95, 0xa3, 0xe2, 0x95, 0xa0, 0xe2, 0x95, 0xab, 0xe2, 0x95, 0xa2, 0xe2, 0x95, 0x9f, 0xe2, 0xa7, 0x89, 0xe2, 0x95, 0x95, 0xe2, 0x95, 0x92, 0xe2, 0x94, 0x90, 0xe2, 0x94, 0x8c, 0xe2, 0x94, 0x80, 0xe2, 0x95, 0xa5, 0xe2, 0x95, 0xa8, 0xe2, 0x94, 0xac, 0xe2, 0x94, 0xb4, 0xe2, 0x8a, 0x9f, 0xe2, 0x8a, 0x9e, 0xe2, 0x8a, 0xa0, 0xe2, 0x95, 0x9b, 0xe2, 0x95, 0x98, 0xe2, 0x94, 0x98, 0xe2, 0x94, 0x94, 0xe2, 0x94, 0x82, 0xe2, 0x95, 0xaa, 0xe2, 0x95, 0xa1, 0xe2, 0x95, 0x9e, 0xe2, 0x94, 0xbc, 0xe2, 0x94, 0xa4, 0xe2, 0x94, 0x9c, 0xe2, 0x80, 0xb5, 0xcb, 0x98, 0xc2, 0xa6, 0xf0, 0x9d, 0x92, 0xb7, 0xe2, 0x81, 0x8f, 0xe2, 0x88, 0xbd, 0xe2, 0x8b, 0x8d, 0x5c, 0xe2, 0xa7, 0x85, 0xe2, 0x9f, 0x88, 0xe2, 0x80, 0xa2, 0xe2, 0x80, 0xa2, 0xe2, 0x89, 0x8e, 0xe2, 0xaa, 0xae, 0xe2, 0x89, 0x8f, 0xe2, 0x89, 0x8f, 0xc4, 0x87, 0xe2, 0x88, 0xa9, 0xe2, 0xa9, 0x84, 0xe2, 0xa9, 0x89, 0xe2, 0xa9, 0x8b, 0xe2, 0xa9, 0x87, 0xe2, 0xa9, 0x80, 0xe2, 0x88, 0xa9, 0xef, 0xb8, 0x80, 0xe2, 0x81, 0x81, 0xcb, 0x87, 0xe2, 0xa9, 0x8d, 0xc4, 0x8d, 0xc3, 0xa7, 0xc4, 0x89, 0xe2, 0xa9, 0x8c, 0xe2, 0xa9, 0x90, 0xc4, 0x8b, 0xc2, 0xb8, 0xe2, 0xa6, 0xb2, 0xc2, 0xa2, 0xc2, 0xb7, 0xf0, 0x9d, 0x94, 0xa0, 0xd1, 0x87, 0xe2, 0x9c, 0x93, 0xe2, 0x9c, 0x93, 0xcf, 0x87, 0xe2, 0x97, 0x8b, 0xe2, 0xa7, 0x83, 0xcb, 0x86, 0xe2, 0x89, 0x97, 0xe2, 0x86, 0xba, 0xe2, 0x86, 0xbb, 0xc2, 0xae, 0xe2, 0x93, 0x88, 0xe2, 0x8a, 0x9b, 0xe2, 0x8a, 0x9a, 0xe2, 0x8a, 0x9d, 0xe2, 0x89, 0x97, 0xe2, 0xa8, 0x90, 0xe2, 0xab, 0xaf, 0xe2, 0xa7, 0x82, 0xe2, 0x99, 0xa3, 0xe2, 0x99, 0xa3, 0x3a, 0xe2, 0x89, 0x94, 0xe2, 0x89, 0x94, 0x2c, 0x40, 0xe2, 0x88, 0x81, 0xe2, 0x88, 0x98, 0xe2, 0x88, 0x81, 0xe2, 0x84, 0x82, 0xe2, 0x89, 0x85, 0xe2, 0xa9, 0xad, 0xe2, 0x88, 0xae, 0xf0, 0x9d, 0x95, 0x94, 0xe2, 0x88, 0x90, 0xc2, 0xa9, 0xe2, 0x84, 0x97, 0xe2, 0x86, 0xb5, 0xe2, 0x9c, 0x97, 0xf0, 0x9d, 0x92, 0xb8, 0xe2, 0xab, 0x8f, 0xe2, 0xab, 0x91, 0xe2, 0xab, 0x90, 0xe2, 0xab, 0x92, 0xe2, 0x8b, 0xaf, 0xe2, 0xa4, 0xb8, 0xe2, 0xa4, 0xb5, 0xe2, 0x8b, 0x9e, 0xe2, 0x8b, 0x9f, 0xe2, 0x86, 0xb6, 0xe2, 0xa4, 0xbd, 0xe2, 0x88, 0xaa, 0xe2, 0xa9, 0x88, 0xe2, 0xa9, 0x86, 0xe2, 0xa9, 0x8a, 0xe2, 0x8a, 0x8d, 0xe2, 0xa9, 0x85, 0xe2, 0x88, 0xaa, 0xef, 0xb8, 0x80, 0xe2, 0x86, 0xb7, 0xe2, 0xa4, 0xbc, 0xe2, 0x8b, 0x9e, 0xe2, 0x8b, 0x9f, 0xe2, 0x8b, 0x8e, 0xe2, 0x8b, 0x8f, 0xc2, 0xa4, 0xe2, 0x86, 0xb6, 0xe2, 0x86, 0xb7, 0xe2, 0x8b, 0x8e, 0xe2, 0x8b, 0x8f, 0xe2, 0x88, 0xb2, 0xe2, 0x88, 0xb1, 0xe2, 0x8c, 0xad, 0xe2, 0x87, 0x93, 0xe2, 0xa5, 0xa5, 0xe2, 0x80, 0xa0, 0xe2, 0x84, 0xb8, 0xe2, 0x86, 0x93, 0xe2, 0x80, 0x90, 0xe2, 0x8a, 0xa3, 0xe2, 0xa4, 0x8f, 0xcb, 0x9d, 0xc4, 0x8f, 0xd0, 0xb4, 0xe2, 0x85, 0x86, 0xe2, 0x80, 0xa1, 0xe2, 0x87, 0x8a, 0xe2, 0xa9, 0xb7, 0xc2, 0xb0, 0xce, 0xb4, 0xe2, 0xa6, 0xb1, 0xe2, 0xa5, 0xbf, 0xf0, 0x9d, 0x94, 0xa1, 0xe2, 0x87, 0x83, 0xe2, 0x87, 0x82, 0xe2, 0x8b, 0x84, 0xe2, 0x8b, 0x84, 0xe2, 0x99, 0xa6, 0xe2, 0x99, 0xa6, 0xc2, 0xa8, 0xcf, 0x9d, 0xe2, 0x8b, 0xb2, 0xc3, 0xb7, 0xc3, 0xb7, 0xe2, 0x8b, 0x87, 0xe2, 0x8b, 0x87, 0xd1, 0x92, 0xe2, 0x8c, 0x9e, 0xe2, 0x8c, 0x8d, 0x24, 0xf0, 0x9d, 0x95, 0x95, 0xcb, 0x99, 0xe2, 0x89, 0x90, 0xe2, 0x89, 0x91, 0xe2, 0x88, 0xb8, 0xe2, 0x88, 0x94, 0xe2, 0x8a, 0xa1, 0xe2, 0x8c, 0x86, 0xe2, 0x86, 0x93, 0xe2, 0x87, 0x8a, 0xe2, 0x87, 0x83, 0xe2, 0x87, 0x82, 0xe2, 0xa4, 0x90, 0xe2, 0x8c, 0x9f, 0xe2, 0x8c, 0x8c, 0xf0, 0x9d, 0x92, 0xb9, 0xd1, 0x95, 0xe2, 0xa7, 0xb6, 0xc4, 0x91, 0xe2, 0x8b, 0xb1, 0xe2, 0x96, 0xbf, 0xe2, 0x96, 0xbe, 0xe2, 0x87, 0xb5, 0xe2, 0xa5, 0xaf, 0xe2, 0xa6, 0xa6, 0xd1, 0x9f, 0xe2, 0x9f, 0xbf, 0xe2, 0xa9, 0xb7, 0xe2, 0x89, 0x91, 0xc3, 0xa9, 0xe2, 0xa9, 0xae, 0xc4, 0x9b, 0xe2, 0x89, 0x96, 0xc3, 0xaa, 0xe2, 0x89, 0x95, 0xd1, 0x8d, 0xc4, 0x97, 0xe2, 0x85, 0x87, 0xe2, 0x89, 0x92, 0xf0, 0x9d, 0x94, 0xa2, 0xe2, 0xaa, 0x9a, 0xc3, 0xa8, 0xe2, 0xaa, 0x96, 0xe2, 0xaa, 0x98, 0xe2, 0xaa, 0x99, 0xe2, 0x8f, 0xa7, 0xe2, 0x84, 0x93, 0xe2, 0xaa, 0x95, 0xe2, 0xaa, 0x97, 0xc4, 0x93, 0xe2, 0x88, 0x85, 0xe2, 0x88, 0x85, 0xe2, 0x88, 0x85, 0xe2, 0x80, 0x83, 0xe2, 0x80, 0x84, 0xe2, 0x80, 0x85, 0xc5, 0x8b, 0xe2, 0x80, 0x82, 0xc4, 0x99, 0xf0, 0x9d, 0x95, 0x96, 0xe2, 0x8b, 0x95, 0xe2, 0xa7, 0xa3, 0xe2, 0xa9, 0xb1, 0xce, 0xb5, 0xce, 0xb5, 0xcf, 0xb5, 0xe2, 0x89, 0x96, 0xe2, 0x89, 0x95, 0xe2, 0x89, 0x82, 0xe2, 0xaa, 0x96, 0xe2, 0xaa, 0x95, 0x3d, 0xe2, 0x89, 0x9f, 0xe2, 0x89, 0xa1, 0xe2, 0xa9, 0xb8, 0xe2, 0xa7, 0xa5, 0xe2, 0x89, 0x93, 0xe2, 0xa5, 0xb1, 0xe2, 0x84, 0xaf, 0xe2, 0x89, 0x90, 0xe2, 0x89, 0x82, 0xce, 0xb7, 0xc3, 0xb0, 0xc3, 0xab, 0xe2, 0x82, 0xac, 0x21, 0xe2, 0x88, 0x83, 0xe2, 0x84, 0xb0, 0xe2, 0x85, 0x87, 0xe2, 0x89, 0x92, 0xd1, 0x84, 0xe2, 0x99, 0x80, 0xef, 0xac, 0x83, 0xef, 0xac, 0x80, 0xef, 0xac, 0x84, 0xf0, 0x9d, 0x94, 0xa3, 0xef, 0xac, 0x81, 0x66, 0x6a, 0xe2, 0x99, 0xad, 0xef, 0xac, 0x82, 0xe2, 0x96, 0xb1, 0xc6, 0x92, 0xf0, 0x9d, 0x95, 0x97, 0xe2, 0x88, 0x80, 0xe2, 0x8b, 0x94, 0xe2, 0xab, 0x99, 0xe2, 0xa8, 0x8d, 0xc2, 0xbd, 0xe2, 0x85, 0x93, 0xc2, 0xbc, 0xe2, 0x85, 0x95, 0xe2, 0x85, 0x99, 0xe2, 0x85, 0x9b, 0xe2, 0x85, 0x94, 0xe2, 0x85, 0x96, 0xc2, 0xbe, 0xe2, 0x85, 0x97, 0xe2, 0x85, 0x9c, 0xe2, 0x85, 0x98, 0xe2, 0x85, 0x9a, 0xe2, 0x85, 0x9d, 0xe2, 0x85, 0x9e, 0xe2, 0x81, 0x84, 0xe2, 0x8c, 0xa2, 0xf0, 0x9d, 0x92, 0xbb, 0xe2, 0x89, 0xa7, 0xe2, 0xaa, 0x8c, 0xc7, 0xb5, 0xce, 0xb3, 0xcf, 0x9d, 0xe2, 0xaa, 0x86, 0xc4, 0x9f, 0xc4, 0x9d, 0xd0, 0xb3, 0xc4, 0xa1, 0xe2, 0x89, 0xa5, 0xe2, 0x8b, 0x9b, 0xe2, 0x89, 0xa5, 0xe2, 0x89, 0xa7, 0xe2, 0xa9, 0xbe, 0xe2, 0xa9, 0xbe, 0xe2, 0xaa, 0xa9, 0xe2, 0xaa, 0x80, 0xe2, 0xaa, 0x82, 0xe2, 0xaa, 0x84, 0xe2, 0x8b, 0x9b, 0xef, 0xb8, 0x80, 0xe2, 0xaa, 0x94, 0xf0, 0x9d, 0x94, 0xa4, 0xe2, 0x89, 0xab, 0xe2, 0x8b, 0x99, 0xe2, 0x84, 0xb7, 0xd1, 0x93, 0xe2, 0x89, 0xb7, 0xe2, 0xaa, 0x92, 0xe2, 0xaa, 0xa5, 0xe2, 0xaa, 0xa4, 0xe2, 0x89, 0xa9, 0xe2, 0xaa, 0x8a, 0xe2, 0xaa, 0x8a, 0xe2, 0xaa, 0x88, 0xe2, 0xaa, 0x88, 0xe2, 0x89, 0xa9, 0xe2, 0x8b, 0xa7, 0xf0, 0x9d, 0x95, 0x98, 0x60, 0xe2, 0x84, 0x8a, 0xe2, 0x89, 0xb3, 0xe2, 0xaa, 0x8e, 0xe2, 0xaa, 0x90, 0x3e, 0xe2, 0xaa, 0xa7, 0xe2, 0xa9, 0xba, 0xe2, 0x8b, 0x97, 0xe2, 0xa6, 0x95, 0xe2, 0xa9, 0xbc, 0xe2, 0xaa, 0x86, 0xe2, 0xa5, 0xb8, 0xe2, 0x8b, 0x97, 0xe2, 0x8b, 0x9b, 0xe2, 0xaa, 0x8c, 0xe2, 0x89, 0xb7, 0xe2, 0x89, 0xb3, 0xe2, 0x89, 0xa9, 0xef, 0xb8, 0x80, 0xe2, 0x89, 0xa9, 0xef, 0xb8, 0x80, 0xe2, 0x87, 0x94, 0xe2, 0x80, 0x8a, 0xc2, 0xbd, 0xe2, 0x84, 0x8b, 0xd1, 0x8a, 0xe2, 0x86, 0x94, 0xe2, 0xa5, 0x88, 0xe2, 0x86, 0xad, 0xe2, 0x84, 0x8f, 0xc4, 0xa5, 0xe2, 0x99, 0xa5, 0xe2, 0x99, 0xa5, 0xe2, 0x80, 0xa6, 0xe2, 0x8a, 0xb9, 0xf0, 0x9d, 0x94, 0xa5, 0xe2, 0xa4, 0xa5, 0xe2, 0xa4, 0xa6, 0xe2, 0x87, 0xbf, 0xe2, 0x88, 0xbb, 0xe2, 0x86, 0xa9, 0xe2, 0x86, 0xaa, 0xf0, 0x9d, 0x95, 0x99, 0xe2, 0x80, 0x95, 0xf0, 0x9d, 0x92, 0xbd, 0xe2, 0x84, 0x8f, 0xc4, 0xa7, 0xe2, 0x81, 0x83, 0xe2, 0x80, 0x90, 0xc3, 0xad, 0xe2, 0x81, 0xa3, 0xc3, 0xae, 0xd0, 0xb8, 0xd0, 0xb5, 0xc2, 0xa1, 0xe2, 0x87, 0x94, 0xf0, 0x9d, 0x94, 0xa6, 0xc3, 0xac, 0xe2, 0x85, 0x88, 0xe2, 0xa8, 0x8c, 0xe2, 0x88, 0xad, 0xe2, 0xa7, 0x9c, 0xe2, 0x84, 0xa9, 0xc4, 0xb3, 0xc4, 0xab, 0xe2, 0x84, 0x91, 0xe2, 0x84, 0x90, 0xe2, 0x84, 0x91, 0xc4, 0xb1, 0xe2, 0x8a, 0xb7, 0xc6, 0xb5, 0xe2, 0x88, 0x88, 0xe2, 0x84, 0x85, 0xe2, 0x88, 0x9e, 0xe2, 0xa7, 0x9d, 0xc4, 0xb1, 0xe2, 0x88, 0xab, 0xe2, 0x8a, 0xba, 0xe2, 0x84, 0xa4, 0xe2, 0x8a, 0xba, 0xe2, 0xa8, 0x97, 0xe2, 0xa8, 0xbc, 0xd1, 0x91, 0xc4, 0xaf, 0xf0, 0x9d, 0x95, 0x9a, 0xce, 0xb9, 0xe2, 0xa8, 0xbc, 0xc2, 0xbf, 0xf0, 0x9d, 0x92, 0xbe, 0xe2, 0x88, 0x88, 0xe2, 0x8b, 0xb9, 0xe2, 0x8b, 0xb5, 0xe2, 0x8b, 0xb4, 0xe2, 0x8b, 0xb3, 0xe2, 0x88, 0x88, 0xe2, 0x81, 0xa2, 0xc4, 0xa9, 0xd1, 0x96, 0xc3, 0xaf, 0xc4, 0xb5, 0xd0, 0xb9, 0xf0, 0x9d, 0x94, 0xa7, 0xc8, 0xb7, 0xf0, 0x9d, 0x95, 0x9b, 0xf0, 0x9d, 0x92, 0xbf, 0xd1, 0x98, 0xd1, 0x94, 0xce, 0xba, 0xcf, 0xb0, 0xc4, 0xb7, 0xd0, 0xba, 0xf0, 0x9d, 0x94, 0xa8, 0xc4, 0xb8, 0xd1, 0x85, 0xd1, 0x9c, 0xf0, 0x9d, 0x95, 0x9c, 0xf0, 0x9d, 0x93, 0x80, 0xe2, 0x87, 0x9a, 0xe2, 0x87, 0x90, 0xe2, 0xa4, 0x9b, 0xe2, 0xa4, 0x8e, 0xe2, 0x89, 0xa6, 0xe2, 0xaa, 0x8b, 0xe2, 0xa5, 0xa2, 0xc4, 0xba, 0xe2, 0xa6, 0xb4, 0xe2, 0x84, 0x92, 0xce, 0xbb, 0xe2, 0x9f, 0xa8, 0xe2, 0xa6, 0x91, 0xe2, 0x9f, 0xa8, 0xe2, 0xaa, 0x85, 0xc2, 0xab, 0xe2, 0x86, 0x90, 0xe2, 0x87, 0xa4, 0xe2, 0xa4, 0x9f, 0xe2, 0xa4, 0x9d, 0xe2, 0x86, 0xa9, 0xe2, 0x86, 0xab, 0xe2, 0xa4, 0xb9, 0xe2, 0xa5, 0xb3, 0xe2, 0x86, 0xa2, 0xe2, 0xaa, 0xab, 0xe2, 0xa4, 0x99, 0xe2, 0xaa, 0xad, 0xe2, 0xaa, 0xad, 0xef, 0xb8, 0x80, 0xe2, 0xa4, 0x8c, 0xe2, 0x9d, 0xb2, 0x7b, 0x5b, 0xe2, 0xa6, 0x8b, 0xe2, 0xa6, 0x8f, 0xe2, 0xa6, 0x8d, 0xc4, 0xbe, 0xc4, 0xbc, 0xe2, 0x8c, 0x88, 0x7b, 0xd0, 0xbb, 0xe2, 0xa4, 0xb6, 0xe2, 0x80, 0x9c, 0xe2, 0x80, 0x9e, 0xe2, 0xa5, 0xa7, 0xe2, 0xa5, 0x8b, 0xe2, 0x86, 0xb2, 0xe2, 0x89, 0xa4, 0xe2, 0x86, 0x90, 0xe2, 0x86, 0xa2, 0xe2, 0x86, 0xbd, 0xe2, 0x86, 0xbc, 0xe2, 0x87, 0x87, 0xe2, 0x86, 0x94, 0xe2, 0x87, 0x86, 0xe2, 0x87, 0x8b, 0xe2, 0x86, 0xad, 0xe2, 0x8b, 0x8b, 0xe2, 0x8b, 0x9a, 0xe2, 0x89, 0xa4, 0xe2, 0x89, 0xa6, 0xe2, 0xa9, 0xbd, 0xe2, 0xa9, 0xbd, 0xe2, 0xaa, 0xa8, 0xe2, 0xa9, 0xbf, 0xe2, 0xaa, 0x81, 0xe2, 0xaa, 0x83, 0xe2, 0x8b, 0x9a, 0xef, 0xb8, 0x80, 0xe2, 0xaa, 0x93, 0xe2, 0xaa, 0x85, 0xe2, 0x8b, 0x96, 0xe2, 0x8b, 0x9a, 0xe2, 0xaa, 0x8b, 0xe2, 0x89, 0xb6, 0xe2, 0x89, 0xb2, 0xe2, 0xa5, 0xbc, 0xe2, 0x8c, 0x8a, 0xf0, 0x9d, 0x94, 0xa9, 0xe2, 0x89, 0xb6, 0xe2, 0xaa, 0x91, 0xe2, 0x86, 0xbd, 0xe2, 0x86, 0xbc, 0xe2, 0xa5, 0xaa, 0xe2, 0x96, 0x84, 0xd1, 0x99, 0xe2, 0x89, 0xaa, 0xe2, 0x87, 0x87, 0xe2, 0x8c, 0x9e, 0xe2, 0xa5, 0xab, 0xe2, 0x97, 0xba, 0xc5, 0x80, 0xe2, 0x8e, 0xb0, 0xe2, 0x8e, 0xb0, 0xe2, 0x89, 0xa8, 0xe2, 0xaa, 0x89, 0xe2, 0xaa, 0x89, 0xe2, 0xaa, 0x87, 0xe2, 0xaa, 0x87, 0xe2, 0x89, 0xa8, 0xe2, 0x8b, 0xa6, 0xe2, 0x9f, 0xac, 0xe2, 0x87, 0xbd, 0xe2, 0x9f, 0xa6, 0xe2, 0x9f, 0xb5, 0xe2, 0x9f, 0xb7, 0xe2, 0x9f, 0xbc, 0xe2, 0x9f, 0xb6, 0xe2, 0x86, 0xab, 0xe2, 0x86, 0xac, 0xe2, 0xa6, 0x85, 0xf0, 0x9d, 0x95, 0x9d, 0xe2, 0xa8, 0xad, 0xe2, 0xa8, 0xb4, 0xe2, 0x88, 0x97, 0x5f, 0xe2, 0x97, 0x8a, 0xe2, 0x97, 0x8a, 0xe2, 0xa7, 0xab, 0x28, 0xe2, 0xa6, 0x93, 0xe2, 0x87, 0x86, 0xe2, 0x8c, 0x9f, 0xe2, 0x87, 0x8b, 0xe2, 0xa5, 0xad, 0xe2, 0x80, 0x8e, 0xe2, 0x8a, 0xbf, 0xe2, 0x80, 0xb9, 0xf0, 0x9d, 0x93, 0x81, 0xe2, 0x86, 0xb0, 0xe2, 0x89, 0xb2, 0xe2, 0xaa, 0x8d, 0xe2, 0xaa, 0x8f, 0x5b, 0xe2, 0x80, 0x98, 0xe2, 0x80, 0x9a, 0xc5, 0x82, 0x3c, 0xe2, 0xaa, 0xa6, 0xe2, 0xa9, 0xb9, 0xe2, 0x8b, 0x96, 0xe2, 0x8b, 0x8b, 0xe2, 0x8b, 0x89, 0xe2, 0xa5, 0xb6, 0xe2, 0xa9, 0xbb, 0xe2, 0xa6, 0x96, 0xe2, 0x97, 0x83, 0xe2, 0x8a, 0xb4, 0xe2, 0x97, 0x82, 0xe2, 0xa5, 0x8a, 0xe2, 0xa5, 0xa6, 0xe2, 0x89, 0xa8, 0xef, 0xb8, 0x80, 0xe2, 0x89, 0xa8, 0xef, 0xb8, 0x80, 0xe2, 0x88, 0xba, 0xc2, 0xaf, 0xe2, 0x99, 0x82, 0xe2, 0x9c, 0xa0, 0xe2, 0x9c, 0xa0, 0xe2, 0x86, 0xa6, 0xe2, 0x86, 0xa6, 0xe2, 0x86, 0xa7, 0xe2, 0x86, 0xa4, 0xe2, 0x86, 0xa5, 0xe2, 0x96, 0xae, 0xe2, 0xa8, 0xa9, 0xd0, 0xbc, 0xe2, 0x80, 0x94, 0xe2, 0x88, 0xa1, 0xf0, 0x9d, 0x94, 0xaa, 0xe2, 0x84, 0xa7, 0xc2, 0xb5, 0xe2, 0x88, 0xa3, 0x2a, 0xe2, 0xab, 0xb0, 0xc2, 0xb7, 0xe2, 0x88, 0x92, 0xe2, 0x8a, 0x9f, 0xe2, 0x88, 0xb8, 0xe2, 0xa8, 0xaa, 0xe2, 0xab, 0x9b, 0xe2, 0x80, 0xa6, 0xe2, 0x88, 0x93, 0xe2, 0x8a, 0xa7, 0xf0, 0x9d, 0x95, 0x9e, 0xe2, 0x88, 0x93, 0xf0, 0x9d, 0x93, 0x82, 0xe2, 0x88, 0xbe, 0xce, 0xbc, 0xe2, 0x8a, 0xb8, 0xe2, 0x8a, 0xb8, 0xe2, 0x8b, 0x99, 0xcc, 0xb8, 0xe2, 0x89, 0xab, 0xe2, 0x83, 0x92, 0xe2, 0x89, 0xab, 0xcc, 0xb8, 0xe2, 0x87, 0x8d, 0xe2, 0x87, 0x8e, 0xe2, 0x8b, 0x98, 0xcc, 0xb8, 0xe2, 0x89, 0xaa, 0xe2, 0x83, 0x92, 0xe2, 0x89, 0xaa, 0xcc, 0xb8, 0xe2, 0x87, 0x8f, 0xe2, 0x8a, 0xaf, 0xe2, 0x8a, 0xae, 0xe2, 0x88, 0x87, 0xc5, 0x84, 0xe2, 0x88, 0xa0, 0xe2, 0x83, 0x92, 0xe2, 0x89, 0x89, 0xe2, 0xa9, 0xb0, 0xcc, 0xb8, 0xe2, 0x89, 0x8b, 0xcc, 0xb8, 0xc5, 0x89, 0xe2, 0x89, 0x89, 0xe2, 0x99, 0xae, 0xe2, 0x99, 0xae, 0xe2, 0x84, 0x95, 0xc2, 0xa0, 0xe2, 0x89, 0x8e, 0xcc, 0xb8, 0xe2, 0x89, 0x8f, 0xcc, 0xb8, 0xe2, 0xa9, 0x83, 0xc5, 0x88, 0xc5, 0x86, 0xe2, 0x89, 0x87, 0xe2, 0xa9, 0xad, 0xcc, 0xb8, 0xe2, 0xa9, 0x82, 0xd0, 0xbd, 0xe2, 0x80, 0x93, 0xe2, 0x89, 0xa0, 0xe2, 0x87, 0x97, 0xe2, 0xa4, 0xa4, 0xe2, 0x86, 0x97, 0xe2, 0x86, 0x97, 0xe2, 0x89, 0x90, 0xcc, 0xb8, 0xe2, 0x89, 0xa2, 0xe2, 0xa4, 0xa8, 0xe2, 0x89, 0x82, 0xcc, 0xb8, 0xe2, 0x88, 0x84, 0xe2, 0x88, 0x84, 0xf0, 0x9d, 0x94, 0xab, 0xe2, 0x89, 0xa7, 0xcc, 0xb8, 0xe2, 0x89, 0xb1, 0xe2, 0x89, 0xb1, 0xe2, 0x89, 0xa7, 0xcc, 0xb8, 0xe2, 0xa9, 0xbe, 0xcc, 0xb8, 0xe2, 0xa9, 0xbe, 0xcc, 0xb8, 0xe2, 0x89, 0xb5, 0xe2, 0x89, 0xaf, 0xe2, 0x89, 0xaf, 0xe2, 0x87, 0x8e, 0xe2, 0x86, 0xae, 0xe2, 0xab, 0xb2, 0xe2, 0x88, 0x8b, 0xe2, 0x8b, 0xbc, 0xe2, 0x8b, 0xba, 0xe2, 0x88, 0x8b, 0xd1, 0x9a, 0xe2, 0x87, 0x8d, 0xe2, 0x89, 0xa6, 0xcc, 0xb8, 0xe2, 0x86, 0x9a, 0xe2, 0x80, 0xa5, 0xe2, 0x89, 0xb0, 0xe2, 0x86, 0x9a, 0xe2, 0x86, 0xae, 0xe2, 0x89, 0xb0, 0xe2, 0x89, 0xa6, 0xcc, 0xb8, 0xe2, 0xa9, 0xbd, 0xcc, 0xb8, 0xe2, 0xa9, 0xbd, 0xcc, 0xb8, 0xe2, 0x89, 0xae, 0xe2, 0x89, 0xb4, 0xe2, 0x89, 0xae, 0xe2, 0x8b, 0xaa, 0xe2, 0x8b, 0xac, 0xe2, 0x88, 0xa4, 0xf0, 0x9d, 0x95, 0x9f, 0xc2, 0xac, 0xe2, 0x88, 0x89, 0xe2, 0x8b, 0xb9, 0xcc, 0xb8, 0xe2, 0x8b, 0xb5, 0xcc, 0xb8, 0xe2, 0x88, 0x89, 0xe2, 0x8b, 0xb7, 0xe2, 0x8b, 0xb6, 0xe2, 0x88, 0x8c, 0xe2, 0x88, 0x8c, 0xe2, 0x8b, 0xbe, 0xe2, 0x8b, 0xbd, 0xe2, 0x88, 0xa6, 0xe2, 0x88, 0xa6, 0xe2, 0xab, 0xbd, 0xe2, 0x83, 0xa5, 0xe2, 0x88, 0x82, 0xcc, 0xb8, 0xe2, 0xa8, 0x94, 0xe2, 0x8a, 0x80, 0xe2, 0x8b, 0xa0, 0xe2, 0xaa, 0xaf, 0xcc, 0xb8, 0xe2, 0x8a, 0x80, 0xe2, 0xaa, 0xaf, 0xcc, 0xb8, 0xe2, 0x87, 0x8f, 0xe2, 0x86, 0x9b, 0xe2, 0xa4, 0xb3, 0xcc, 0xb8, 0xe2, 0x86, 0x9d, 0xcc, 0xb8, 0xe2, 0x86, 0x9b, 0xe2, 0x8b, 0xab, 0xe2, 0x8b, 0xad, 0xe2, 0x8a, 0x81, 0xe2, 0x8b, 0xa1, 0xe2, 0xaa, 0xb0, 0xcc, 0xb8, 0xf0, 0x9d, 0x93, 0x83, 0xe2, 0x88, 0xa4, 0xe2, 0x88, 0xa6, 0xe2, 0x89, 0x81, 0xe2, 0x89, 0x84, 0xe2, 0x89, 0x84, 0xe2, 0x88, 0xa4, 0xe2, 0x88, 0xa6, 0xe2, 0x8b, 0xa2, 0xe2, 0x8b, 0xa3, 0xe2, 0x8a, 0x84, 0xe2, 0xab, 0x85, 0xcc, 0xb8, 0xe2, 0x8a, 0x88, 0xe2, 0x8a, 0x82, 0xe2, 0x83, 0x92, 0xe2, 0x8a, 0x88, 0xe2, 0xab, 0x85, 0xcc, 0xb8, 0xe2, 0x8a, 0x81, 0xe2, 0xaa, 0xb0, 0xcc, 0xb8, 0xe2, 0x8a, 0x85, 0xe2, 0xab, 0x86, 0xcc, 0xb8, 0xe2, 0x8a, 0x89, 0xe2, 0x8a, 0x83, 0xe2, 0x83, 0x92, 0xe2, 0x8a, 0x89, 0xe2, 0xab, 0x86, 0xcc, 0xb8, 0xe2, 0x89, 0xb9, 0xc3, 0xb1, 0xe2, 0x89, 0xb8, 0xe2, 0x8b, 0xaa, 0xe2, 0x8b, 0xac, 0xe2, 0x8b, 0xab, 0xe2, 0x8b, 0xad, 0xce, 0xbd, 0x23, 0xe2, 0x84, 0x96, 0xe2, 0x80, 0x87, 0xe2, 0x8a, 0xad, 0xe2, 0xa4, 0x84, 0xe2, 0x89, 0x8d, 0xe2, 0x83, 0x92, 0xe2, 0x8a, 0xac, 0xe2, 0x89, 0xa5, 0xe2, 0x83, 0x92, 0x3e, 0xe2, 0x83, 0x92, 0xe2, 0xa7, 0x9e, 0xe2, 0xa4, 0x82, 0xe2, 0x89, 0xa4, 0xe2, 0x83, 0x92, 0x3c, 0xe2, 0x83, 0x92, 0xe2, 0x8a, 0xb4, 0xe2, 0x83, 0x92, 0xe2, 0xa4, 0x83, 0xe2, 0x8a, 0xb5, 0xe2, 0x83, 0x92, 0xe2, 0x88, 0xbc, 0xe2, 0x83, 0x92, 0xe2, 0x87, 0x96, 0xe2, 0xa4, 0xa3, 0xe2, 0x86, 0x96, 0xe2, 0x86, 0x96, 0xe2, 0xa4, 0xa7, 0xe2, 0x93, 0x88, 0xc3, 0xb3, 0xe2, 0x8a, 0x9b, 0xe2, 0x8a, 0x9a, 0xc3, 0xb4, 0xd0, 0xbe, 0xe2, 0x8a, 0x9d, 0xc5, 0x91, 0xe2, 0xa8, 0xb8, 0xe2, 0x8a, 0x99, 0xe2, 0xa6, 0xbc, 0xc5, 0x93, 0xe2, 0xa6, 0xbf, 0xf0, 0x9d, 0x94, 0xac, 0xcb, 0x9b, 0xc3, 0xb2, 0xe2, 0xa7, 0x81, 0xe2, 0xa6, 0xb5, 0xce, 0xa9, 0xe2, 0x88, 0xae, 0xe2, 0x86, 0xba, 0xe2, 0xa6, 0xbe, 0xe2, 0xa6, 0xbb, 0xe2, 0x80, 0xbe, 0xe2, 0xa7, 0x80, 0xc5, 0x8d, 0xcf, 0x89, 0xce, 0xbf, 0xe2, 0xa6, 0xb6, 0xe2, 0x8a, 0x96, 0xf0, 0x9d, 0x95, 0xa0, 0xe2, 0xa6, 0xb7, 0xe2, 0xa6, 0xb9, 0xe2, 0x8a, 0x95, 0xe2, 0x88, 0xa8, 0xe2, 0x86, 0xbb, 0xe2, 0xa9, 0x9d, 0xe2, 0x84, 0xb4, 0xe2, 0x84, 0xb4, 0xc2, 0xaa, 0xc2, 0xba, 0xe2, 0x8a, 0xb6, 0xe2, 0xa9, 0x96, 0xe2, 0xa9, 0x97, 0xe2, 0xa9, 0x9b, 0xe2, 0x84, 0xb4, 0xc3, 0xb8, 0xe2, 0x8a, 0x98, 0xc3, 0xb5, 0xe2, 0x8a, 0x97, 0xe2, 0xa8, 0xb6, 0xc3, 0xb6, 0xe2, 0x8c, 0xbd, 0xe2, 0x88, 0xa5, 0xc2, 0xb6, 0xe2, 0x88, 0xa5, 0xe2, 0xab, 0xb3, 0xe2, 0xab, 0xbd, 0xe2, 0x88, 0x82, 0xd0, 0xbf, 0x25, 0x2e, 0xe2, 0x80, 0xb0, 0xe2, 0x8a, 0xa5, 0xe2, 0x80, 0xb1, 0xf0, 0x9d, 0x94, 0xad, 0xcf, 0x86, 0xcf, 0x95, 0xe2, 0x84, 0xb3, 0xe2, 0x98, 0x8e, 0xcf, 0x80, 0xe2, 0x8b, 0x94, 0xcf, 0x96, 0xe2, 0x84, 0x8f, 0xe2, 0x84, 0x8e, 0xe2, 0x84, 0x8f, 0x2b, 0xe2, 0xa8, 0xa3, 0xe2, 0x8a, 0x9e, 0xe2, 0xa8, 0xa2, 0xe2, 0x88, 0x94, 0xe2, 0xa8, 0xa5, 0xe2, 0xa9, 0xb2, 0xc2, 0xb1, 0xe2, 0xa8, 0xa6, 0xe2, 0xa8, 0xa7, 0xc2, 0xb1, 0xe2, 0xa8, 0x95, 0xf0, 0x9d, 0x95, 0xa1, 0xc2, 0xa3, 0xe2, 0x89, 0xba, 0xe2, 0xaa, 0xb3, 0xe2, 0xaa, 0xb7, 0xe2, 0x89, 0xbc, 0xe2, 0xaa, 0xaf, 0xe2, 0x89, 0xba, 0xe2, 0xaa, 0xb7, 0xe2, 0x89, 0xbc, 0xe2, 0xaa, 0xaf, 0xe2, 0xaa, 0xb9, 0xe2, 0xaa, 0xb5, 0xe2, 0x8b, 0xa8, 0xe2, 0x89, 0xbe, 0xe2, 0x80, 0xb2, 0xe2, 0x84, 0x99, 0xe2, 0xaa, 0xb5, 0xe2, 0xaa, 0xb9, 0xe2, 0x8b, 0xa8, 0xe2, 0x88, 0x8f, 0xe2, 0x8c, 0xae, 0xe2, 0x8c, 0x92, 0xe2, 0x8c, 0x93, 0xe2, 0x88, 0x9d, 0xe2, 0x88, 0x9d, 0xe2, 0x89, 0xbe, 0xe2, 0x8a, 0xb0, 0xf0, 0x9d, 0x93, 0x85, 0xcf, 0x88, 0xe2, 0x80, 0x88, 0xf0, 0x9d, 0x94, 0xae, 0xe2, 0xa8, 0x8c, 0xf0, 0x9d, 0x95, 0xa2, 0xe2, 0x81, 0x97, 0xf0, 0x9d, 0x93, 0x86, 0xe2, 0x84, 0x8d, 0xe2, 0xa8, 0x96, 0x3f, 0xe2, 0x89, 0x9f, 0x22, 0xe2, 0x87, 0x9b, 0xe2, 0x87, 0x92, 0xe2, 0xa4, 0x9c, 0xe2, 0xa4, 0x8f, 0xe2, 0xa5, 0xa4, 0xe2, 0x88, 0xbd, 0xcc, 0xb1, 0xc5, 0x95, 0xe2, 0x88, 0x9a, 0xe2, 0xa6, 0xb3, 0xe2, 0x9f, 0xa9, 0xe2, 0xa6, 0x92, 0xe2, 0xa6, 0xa5, 0xe2, 0x9f, 0xa9, 0xc2, 0xbb, 0xe2, 0x86, 0x92, 0xe2, 0xa5, 0xb5, 0xe2, 0x87, 0xa5, 0xe2, 0xa4, 0xa0, 0xe2, 0xa4, 0xb3, 0xe2, 0xa4, 0x9e, 0xe2, 0x86, 0xaa, 0xe2, 0x86, 0xac, 0xe2, 0xa5, 0x85, 0xe2, 0xa5, 0xb4, 0xe2, 0x86, 0xa3, 0xe2, 0x86, 0x9d, 0xe2, 0xa4, 0x9a, 0xe2, 0x88, 0xb6, 0xe2, 0x84, 0x9a, 0xe2, 0xa4, 0x8d, 0xe2, 0x9d, 0xb3, 0x7d, 0x5d, 0xe2, 0xa6, 0x8c, 0xe2, 0xa6, 0x8e, 0xe2, 0xa6, 0x90, 0xc5, 0x99, 0xc5, 0x97, 0xe2, 0x8c, 0x89, 0x7d, 0xd1, 0x80, 0xe2, 0xa4, 0xb7, 0xe2, 0xa5, 0xa9, 0xe2, 0x80, 0x9d, 0xe2, 0x80, 0x9d, 0xe2, 0x86, 0xb3, 0xe2, 0x84, 0x9c, 0xe2, 0x84, 0x9b, 0xe2, 0x84, 0x9c, 0xe2, 0x84, 0x9d, 0xe2, 0x96, 0xad, 0xc2, 0xae, 0xe2, 0xa5, 0xbd, 0xe2, 0x8c, 0x8b, 0xf0, 0x9d, 0x94, 0xaf, 0xe2, 0x87, 0x81, 0xe2, 0x87, 0x80, 0xe2, 0xa5, 0xac, 0xcf, 0x81, 0xcf, 0xb1, 0xe2, 0x86, 0x92, 0xe2, 0x86, 0xa3, 0xe2, 0x87, 0x81, 0xe2, 0x87, 0x80, 0xe2, 0x87, 0x84, 0xe2, 0x87, 0x8c, 0xe2, 0x87, 0x89, 0xe2, 0x86, 0x9d, 0xe2, 0x8b, 0x8c, 0xcb, 0x9a, 0xe2, 0x89, 0x93, 0xe2, 0x87, 0x84, 0xe2, 0x87, 0x8c, 0xe2, 0x80, 0x8f, 0xe2, 0x8e, 0xb1, 0xe2, 0x8e, 0xb1, 0xe2, 0xab, 0xae, 0xe2, 0x9f, 0xad, 0xe2, 0x87, 0xbe, 0xe2, 0x9f, 0xa7, 0xe2, 0xa6, 0x86, 0xf0, 0x9d, 0x95, 0xa3, 0xe2, 0xa8, 0xae, 0xe2, 0xa8, 0xb5, 0x29, 0xe2, 0xa6, 0x94, 0xe2, 0xa8, 0x92, 0xe2, 0x87, 0x89, 0xe2, 0x80, 0xba, 0xf0, 0x9d, 0x93, 0x87, 0xe2, 0x86, 0xb1, 0x5d, 0xe2, 0x80, 0x99, 0xe2, 0x80, 0x99, 0xe2, 0x8b, 0x8c, 0xe2, 0x8b, 0x8a, 0xe2, 0x96, 0xb9, 0xe2, 0x8a, 0xb5, 0xe2, 0x96, 0xb8, 0xe2, 0xa7, 0x8e, 0xe2, 0xa5, 0xa8, 0xe2, 0x84, 0x9e, 0xc5, 0x9b, 0xe2, 0x80, 0x9a, 0xe2, 0x89, 0xbb, 0xe2, 0xaa, 0xb4, 0xe2, 0xaa, 0xb8, 0xc5, 0xa1, 0xe2, 0x89, 0xbd, 0xe2, 0xaa, 0xb0, 0xc5, 0x9f, 0xc5, 0x9d, 0xe2, 0xaa, 0xb6, 0xe2, 0xaa, 0xba, 0xe2, 0x8b, 0xa9, 0xe2, 0xa8, 0x93, 0xe2, 0x89, 0xbf, 0xd1, 0x81, 0xe2, 0x8b, 0x85, 0xe2, 0x8a, 0xa1, 0xe2, 0xa9, 0xa6, 0xe2, 0x87, 0x98, 0xe2, 0xa4, 0xa5, 0xe2, 0x86, 0x98, 0xe2, 0x86, 0x98, 0xc2, 0xa7, 0x3b, 0xe2, 0xa4, 0xa9, 0xe2, 0x88, 0x96, 0xe2, 0x88, 0x96, 0xe2, 0x9c, 0xb6, 0xf0, 0x9d, 0x94, 0xb0, 0xe2, 0x8c, 0xa2, 0xe2, 0x99, 0xaf, 0xd1, 0x89, 0xd1, 0x88, 0xe2, 0x88, 0xa3, 0xe2, 0x88, 0xa5, 0xc2, 0xad, 0xcf, 0x83, 0xcf, 0x82, 0xcf, 0x82, 0xe2, 0x88, 0xbc, 0xe2, 0xa9, 0xaa, 0xe2, 0x89, 0x83, 0xe2, 0x89, 0x83, 0xe2, 0xaa, 0x9e, 0xe2, 0xaa, 0xa0, 0xe2, 0xaa, 0x9d, 0xe2, 0xaa, 0x9f, 0xe2, 0x89, 0x86, 0xe2, 0xa8, 0xa4, 0xe2, 0xa5, 0xb2, 0xe2, 0x86, 0x90, 0xe2, 0x88, 0x96, 0xe2, 0xa8, 0xb3, 0xe2, 0xa7, 0xa4, 0xe2, 0x88, 0xa3, 0xe2, 0x8c, 0xa3, 0xe2, 0xaa, 0xaa, 0xe2, 0xaa, 0xac, 0xe2, 0xaa, 0xac, 0xef, 0xb8, 0x80, 0xd1, 0x8c, 0x2f, 0xe2, 0xa7, 0x84, 0xe2, 0x8c, 0xbf, 0xf0, 0x9d, 0x95, 0xa4, 0xe2, 0x99, 0xa0, 0xe2, 0x99, 0xa0, 0xe2, 0x88, 0xa5, 0xe2, 0x8a, 0x93, 0xe2, 0x8a, 0x93, 0xef, 0xb8, 0x80, 0xe2, 0x8a, 0x94, 0xe2, 0x8a, 0x94, 0xef, 0xb8, 0x80, 0xe2, 0x8a, 0x8f, 0xe2, 0x8a, 0x91, 0xe2, 0x8a, 0x8f, 0xe2, 0x8a, 0x91, 0xe2, 0x8a, 0x90, 0xe2, 0x8a, 0x92, 0xe2, 0x8a, 0x90, 0xe2, 0x8a, 0x92, 0xe2, 0x96, 0xa1, 0xe2, 0x96, 0xa1, 0xe2, 0x96, 0xaa, 0xe2, 0x96, 0xaa, 0xe2, 0x86, 0x92, 0xf0, 0x9d, 0x93, 0x88, 0xe2, 0x88, 0x96, 0xe2, 0x8c, 0xa3, 0xe2, 0x8b, 0x86, 0xe2, 0x98, 0x86, 0xe2, 0x98, 0x85, 0xcf, 0xb5, 0xcf, 0x95, 0xc2, 0xaf, 0xe2, 0x8a, 0x82, 0xe2, 0xab, 0x85, 0xe2, 0xaa, 0xbd, 0xe2, 0x8a, 0x86, 0xe2, 0xab, 0x83, 0xe2, 0xab, 0x81, 0xe2, 0xab, 0x8b, 0xe2, 0x8a, 0x8a, 0xe2, 0xaa, 0xbf, 0xe2, 0xa5, 0xb9, 0xe2, 0x8a, 0x82, 0xe2, 0x8a, 0x86, 0xe2, 0xab, 0x85, 0xe2, 0x8a, 0x8a, 0xe2, 0xab, 0x8b, 0xe2, 0xab, 0x87, 0xe2, 0xab, 0x95, 0xe2, 0xab, 0x93, 0xe2, 0x89, 0xbb, 0xe2, 0xaa, 0xb8, 0xe2, 0x89, 0xbd, 0xe2, 0xaa, 0xb0, 0xe2, 0xaa, 0xba, 0xe2, 0xaa, 0xb6, 0xe2, 0x8b, 0xa9, 0xe2, 0x89, 0xbf, 0xe2, 0x88, 0x91, 0xe2, 0x99, 0xaa, 0xe2, 0x8a, 0x83, 0xc2, 0xb9, 0xc2, 0xb2, 0xc2, 0xb3, 0xe2, 0xab, 0x86, 0xe2, 0xaa, 0xbe, 0xe2, 0xab, 0x98, 0xe2, 0x8a, 0x87, 0xe2, 0xab, 0x84, 0xe2, 0x9f, 0x89, 0xe2, 0xab, 0x97, 0xe2, 0xa5, 0xbb, 0xe2, 0xab, 0x82, 0xe2, 0xab, 0x8c, 0xe2, 0x8a, 0x8b, 0xe2, 0xab, 0x80, 0xe2, 0x8a, 0x83, 0xe2, 0x8a, 0x87, 0xe2, 0xab, 0x86, 0xe2, 0x8a, 0x8b, 0xe2, 0xab, 0x8c, 0xe2, 0xab, 0x88, 0xe2, 0xab, 0x94, 0xe2, 0xab, 0x96, 0xe2, 0x87, 0x99, 0xe2, 0xa4, 0xa6, 0xe2, 0x86, 0x99, 0xe2, 0x86, 0x99, 0xe2, 0xa4, 0xaa, 0xc3, 0x9f, 0xe2, 0x8c, 0x96, 0xcf, 0x84, 0xe2, 0x8e, 0xb4, 0xc5, 0xa5, 0xc5, 0xa3, 0xd1, 0x82, 0xe2, 0x83, 0x9b, 0xe2, 0x8c, 0x95, 0xf0, 0x9d, 0x94, 0xb1, 0xe2, 0x88, 0xb4, 0xe2, 0x88, 0xb4, 0xce, 0xb8, 0xcf, 0x91, 0xcf, 0x91, 0xe2, 0x89, 0x88, 0xe2, 0x88, 0xbc, 0xe2, 0x80, 0x89, 0xe2, 0x89, 0x88, 0xe2, 0x88, 0xbc, 0xc3, 0xbe, 0xcb, 0x9c, 0xc3, 0x97, 0xe2, 0x8a, 0xa0, 0xe2, 0xa8, 0xb1, 0xe2, 0xa8, 0xb0, 0xe2, 0x88, 0xad, 0xe2, 0xa4, 0xa8, 0xe2, 0x8a, 0xa4, 0xe2, 0x8c, 0xb6, 0xe2, 0xab, 0xb1, 0xf0, 0x9d, 0x95, 0xa5, 0xe2, 0xab, 0x9a, 0xe2, 0xa4, 0xa9, 0xe2, 0x80, 0xb4, 0xe2, 0x84, 0xa2, 0xe2, 0x96, 0xb5, 0xe2, 0x96, 0xbf, 0xe2, 0x97, 0x83, 0xe2, 0x8a, 0xb4, 0xe2, 0x89, 0x9c, 0xe2, 0x96, 0xb9, 0xe2, 0x8a, 0xb5, 0xe2, 0x97, 0xac, 0xe2, 0x89, 0x9c, 0xe2, 0xa8, 0xba, 0xe2, 0xa8, 0xb9, 0xe2, 0xa7, 0x8d, 0xe2, 0xa8, 0xbb, 0xe2, 0x8f, 0xa2, 0xf0, 0x9d, 0x93, 0x89, 0xd1, 0x86, 0xd1, 0x9b, 0xc5, 0xa7, 0xe2, 0x89, 0xac, 0xe2, 0x86, 0x9e, 0xe2, 0x86, 0xa0, 0xe2, 0x87, 0x91, 0xe2, 0xa5, 0xa3, 0xc3, 0xba, 0xe2, 0x86, 0x91, 0xd1, 0x9e, 0xc5, 0xad, 0xc3, 0xbb, 0xd1, 0x83, 0xe2, 0x87, 0x85, 0xc5, 0xb1, 0xe2, 0xa5, 0xae, 0xe2, 0xa5, 0xbe, 0xf0, 0x9d, 0x94, 0xb2, 0xc3, 0xb9, 0xe2, 0x86, 0xbf, 0xe2, 0x86, 0xbe, 0xe2, 0x96, 0x80, 0xe2, 0x8c, 0x9c, 0xe2, 0x8c, 0x9c, 0xe2, 0x8c, 0x8f, 0xe2, 0x97, 0xb8, 0xc5, 0xab, 0xc2, 0xa8, 0xc5, 0xb3, 0xf0, 0x9d, 0x95, 0xa6, 0xe2, 0x86, 0x91, 0xe2, 0x86, 0x95, 0xe2, 0x86, 0xbf, 0xe2, 0x86, 0xbe, 0xe2, 0x8a, 0x8e, 0xcf, 0x85, 0xcf, 0x92, 0xcf, 0x85, 0xe2, 0x87, 0x88, 0xe2, 0x8c, 0x9d, 0xe2, 0x8c, 0x9d, 0xe2, 0x8c, 0x8e, 0xc5, 0xaf, 0xe2, 0x97, 0xb9, 0xf0, 0x9d, 0x93, 0x8a, 0xe2, 0x8b, 0xb0, 0xc5, 0xa9, 0xe2, 0x96, 0xb5, 0xe2, 0x96, 0xb4, 0xe2, 0x87, 0x88, 0xc3, 0xbc, 0xe2, 0xa6, 0xa7, 0xe2, 0x87, 0x95, 0xe2, 0xab, 0xa8, 0xe2, 0xab, 0xa9, 0xe2, 0x8a, 0xa8, 0xe2, 0xa6, 0x9c, 0xcf, 0xb5, 0xcf, 0xb0, 0xe2, 0x88, 0x85, 0xcf, 0x95, 0xcf, 0x96, 0xe2, 0x88, 0x9d, 0xe2, 0x86, 0x95, 0xcf, 0xb1, 0xcf, 0x82, 0xe2, 0x8a, 0x8a, 0xef, 0xb8, 0x80, 0xe2, 0xab, 0x8b, 0xef, 0xb8, 0x80, 0xe2, 0x8a, 0x8b, 0xef, 0xb8, 0x80, 0xe2, 0xab, 0x8c, 0xef, 0xb8, 0x80, 0xcf, 0x91, 0xe2, 0x8a, 0xb2, 0xe2, 0x8a, 0xb3, 0xd0, 0xb2, 0xe2, 0x8a, 0xa2, 0xe2, 0x88, 0xa8, 0xe2, 0x8a, 0xbb, 0xe2, 0x89, 0x9a, 0xe2, 0x8b, 0xae, 0x7c, 0x7c, 0xf0, 0x9d, 0x94, 0xb3, 0xe2, 0x8a, 0xb2, 0xe2, 0x8a, 0x82, 0xe2, 0x83, 0x92, 0xe2, 0x8a, 0x83, 0xe2, 0x83, 0x92, 0xf0, 0x9d, 0x95, 0xa7, 0xe2, 0x88, 0x9d, 0xe2, 0x8a, 0xb3, 0xf0, 0x9d, 0x93, 0x8b, 0xe2, 0xab, 0x8b, 0xef, 0xb8, 0x80, 0xe2, 0x8a, 0x8a, 0xef, 0xb8, 0x80, 0xe2, 0xab, 0x8c, 0xef, 0xb8, 0x80, 0xe2, 0x8a, 0x8b, 0xef, 0xb8, 0x80, 0xe2, 0xa6, 0x9a, 0xc5, 0xb5, 0xe2, 0xa9, 0x9f, 0xe2, 0x88, 0xa7, 0xe2, 0x89, 0x99, 0xe2, 0x84, 0x98, 0xf0, 0x9d, 0x94, 0xb4, 0xf0, 0x9d, 0x95, 0xa8, 0xe2, 0x84, 0x98, 0xe2, 0x89, 0x80, 0xe2, 0x89, 0x80, 0xf0, 0x9d, 0x93, 0x8c, 0xe2, 0x8b, 0x82, 0xe2, 0x97, 0xaf, 0xe2, 0x8b, 0x83, 0xe2, 0x96, 0xbd, 0xf0, 0x9d, 0x94, 0xb5, 0xe2, 0x9f, 0xba, 0xe2, 0x9f, 0xb7, 0xce, 0xbe, 0xe2, 0x9f, 0xb8, 0xe2, 0x9f, 0xb5, 0xe2, 0x9f, 0xbc, 0xe2, 0x8b, 0xbb, 0xe2, 0xa8, 0x80, 0xf0, 0x9d, 0x95, 0xa9, 0xe2, 0xa8, 0x81, 0xe2, 0xa8, 0x82, 0xe2, 0x9f, 0xb9, 0xe2, 0x9f, 0xb6, 0xf0, 0x9d, 0x93, 0x8d, 0xe2, 0xa8, 0x86, 0xe2, 0xa8, 0x84, 0xe2, 0x96, 0xb3, 0xe2, 0x8b, 0x81, 0xe2, 0x8b, 0x80, 0xc3, 0xbd, 0xd1, 0x8f, 0xc5, 0xb7, 0xd1, 0x8b, 0xc2, 0xa5, 0xf0, 0x9d, 0x94, 0xb6, 0xd1, 0x97, 0xf0, 0x9d, 0x95, 0xaa, 0xf0, 0x9d, 0x93, 0x8e, 0xd1, 0x8e, 0xc3, 0xbf, 0xc5, 0xba, 0xc5, 0xbe, 0xd0, 0xb7, 0xc5, 0xbc, 0xe2, 0x84, 0xa8, 0xce, 0xb6, 0xf0, 0x9d, 0x94, 0xb7, 0xd0, 0xb6, 0xe2, 0x87, 0x9d, 0xf0, 0x9d, 0x95, 0xab, 0xf0, 0x9d, 0x93, 0x8f, 0xe2, 0x80, 0x8d, 0xe2, 0x80, 0x8c}
+var _html5entitiesCharactersIndex = "\x02\x01\x02\x02\x02\x04\x02\x02\x02\x03\x02\x04\x03\x02\x04\x03\x02\x02\x03\x03\x03\x02\x03\x03\x02\x04\x04\x02\x03\x03\x02\x02\x02\x03\x03\x03\x02\x02\x02\x03\x02\x02\x02\x03\x02\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x04\x03\x03\x03\x03\x02\x02\x02\x03\x03\x03\x02\x02\x03\x02\x04\x02\x02\x02\x01\x02\x03\x03\x04\x02\x03\x03\x03\x02\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x02\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x04\x02\x02\x02\x02\x02\x02\x02\x02\x04\x02\x03\x02\x03\x03\x02\x04\x02\x03\x03\x03\x03\x03\x02\x02\x03\x03\x02\x04\x03\x03\x04\x03\x03\x03\x02\x01\x02\x02\x02\x02\x02\x02\x02\x04\x03\x04\x03\x03\x03\x03\x03\x03\x03\x04\x03\x02\x02\x01\x02\x03\x03\x03\x03\x03\x02\x03\x03\x02\x02\x02\x02\x02\x02\x02\x03\x02\x03\x02\x03\x03\x03\x03\x03\x03\x03\x02\x04\x02\x03\x02\x02\x02\x02\x02\x04\x04\x04\x02\x02\x02\x02\x02\x02\x02\x04\x04\x04\x02\x01\x02\x02\x03\x03\x03\x02\x02\x02\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x04\x03\x03\x02\x03\x03\x03\x03\x03\x03\x04\x03\x03\x03\x03\x02\x03\x03\x02\x03\x03\x04\x03\x04\x03\x02\x02\x02\x02\x02\x02\x03\x03\x03\x03\x03\x03\x01\x04\x03\x02\x03\x03\x03\x03\x03\x03\x03\x05\x03\x03\x03\x05\x05\x03\x05\x03\x05\x05\x03\x05\x03\x03\x03\x03\x05\x05\x03\x05\x05\x03\x05\x03\x03\x03\x05\x03\x05\x03\x05\x03\x06\x03\x03\x05\x03\x05\x06\x03\x03\x03\x03\x03\x03\x04\x02\x02\x02\x02\x02\x02\x02\x04\x02\x02\x02\x02\x04\x03\x03\x03\x04\x02\x02\x03\x02\x03\x03\x03\x03\x03\x02\x04\x02\x02\x02\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x04\x02\x01\x04\x03\x04\x03\x02\x02\x03\x03\x03\x02\x02\x02\x03\x03\x03\x03\x03\x02\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x02\x02\x02\x02\x03\x02\x02\x02\x02\x04\x03\x03\x03\x03\x02\x03\x04\x03\x03\x03\x03\x03\x03\x03\x03\x04\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x02\x03\x02\x02\x01\x02\x02\x02\x02\x04\x03\x02\x06\x03\x03\x03\x03\x03\x04\x03\x04\x02\x02\x03\x03\x02\x02\x02\x02\x02\x04\x02\x02\x01\x03\x03\x03\x03\x03\x02\x04\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x02\x02\x02\x04\x02\x02\x03\x03\x02\x03\x03\x03\x03\x03\x03\x01\x03\x03\x03\x04\x04\x04\x03\x02\x03\x04\x04\x04\x04\x02\x04\x04\x02\x02\x02\x02\x02\x02\x04\x04\x04\x02\x02\x02\x02\x02\x02\x03\x02\x03\x03\x04\x02\x02\x03\x05\x03\x02\x02\x02\x02\x03\x04\x02\x03\x03\x02\x02\x03\x01\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x02\x03\x02\x04\x03\x03\x03\x03\x03\x01\x03\x03\x02\x04\x01\x03\x03\x02\x02\x03\x03\x03\x03\x02\x03\x03\x03\x03\x03\x03\x03\x03\x03\x02\x03\x03\x03\x03\x02\x03\x02\x03\x03\x04\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x04\x06\x03\x04\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x02\x02\x04\x03\x03\x03\x01\x03\x03\x03\x03\x03\x03\x03\x03\x02\x03\x03\x03\x03\x03\x03\x06\x03\x02\x03\x02\x02\x02\x03\x03\x02\x02\x03\x02\x02\x04\x02\x03\x03\x02\x03\x03\x02\x03\x03\x03\x02\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x01\x03\x03\x01\x01\x03\x03\x03\x03\x03\x03\x03\x04\x03\x02\x03\x03\x03\x04\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x06\x03\x03\x03\x03\x03\x03\x02\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x02\x02\x02\x03\x03\x03\x03\x02\x02\x03\x03\x04\x03\x03\x03\x03\x03\x03\x02\x02\x03\x02\x02\x03\x03\x02\x03\x03\x01\x04\x02\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x04\x02\x03\x02\x03\x03\x03\x03\x03\x03\x02\x03\x03\x03\x02\x03\x02\x03\x02\x03\x02\x02\x03\x03\x04\x03\x02\x03\x03\x03\x03\x03\x03\x03\x02\x03\x03\x03\x03\x03\x03\x02\x03\x02\x04\x03\x03\x03\x02\x02\x02\x03\x03\x03\x03\x03\x01\x03\x03\x03\x03\x03\x03\x03\x03\x03\x02\x02\x02\x03\x01\x03\x03\x03\x03\x02\x03\x03\x03\x03\x04\x03\x02\x03\x03\x03\x02\x04\x03\x03\x03\x03\x02\x03\x02\x03\x03\x03\x03\x03\x02\x03\x03\x03\x03\x03\x03\x03\x03\x04\x03\x03\x02\x02\x02\x03\x02\x02\x02\x02\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x06\x03\x04\x03\x03\x03\x02\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x04\x01\x03\x03\x03\x03\x01\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x06\x06\x03\x03\x02\x03\x02\x03\x03\x03\x03\x02\x03\x03\x03\x03\x04\x03\x03\x03\x03\x03\x03\x04\x03\x04\x03\x02\x03\x03\x02\x03\x02\x02\x02\x02\x03\x04\x02\x03\x03\x03\x03\x03\x02\x02\x03\x03\x03\x02\x03\x02\x03\x03\x03\x03\x02\x03\x03\x03\x03\x03\x03\x02\x02\x04\x02\x03\x02\x04\x03\x03\x03\x03\x03\x03\x03\x02\x02\x02\x02\x02\x04\x02\x04\x04\x02\x02\x02\x02\x02\x02\x04\x02\x02\x02\x04\x04\x03\x03\x03\x03\x03\x03\x03\x02\x03\x03\x02\x03\x03\x03\x03\x02\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x06\x03\x03\x01\x01\x03\x03\x03\x02\x02\x03\x01\x02\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x06\x03\x03\x03\x03\x03\x03\x03\x03\x03\x04\x03\x03\x03\x03\x03\x03\x02\x03\x03\x03\x03\x03\x02\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x04\x03\x03\x03\x01\x03\x03\x03\x01\x03\x03\x03\x03\x03\x03\x03\x03\x04\x03\x03\x03\x03\x01\x03\x03\x02\x01\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x06\x06\x03\x02\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x02\x03\x03\x04\x03\x02\x03\x01\x03\x02\x03\x03\x03\x03\x03\x03\x03\x03\x04\x03\x04\x03\x02\x03\x03\x05\x06\x05\x03\x03\x05\x06\x05\x03\x03\x03\x03\x02\x06\x03\x05\x05\x02\x03\x03\x03\x03\x02\x05\x05\x03\x02\x02\x03\x05\x03\x02\x03\x03\x03\x03\x03\x03\x05\x03\x03\x05\x03\x03\x04\x05\x03\x03\x05\x05\x05\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x02\x03\x05\x03\x03\x03\x03\x03\x03\x05\x05\x05\x03\x03\x03\x03\x03\x03\x04\x02\x03\x05\x05\x03\x03\x03\x03\x03\x03\x03\x03\x03\x06\x05\x03\x03\x03\x05\x03\x05\x03\x03\x05\x05\x03\x03\x03\x03\x03\x05\x04\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x05\x03\x06\x03\x05\x03\x05\x03\x05\x03\x06\x03\x05\x03\x02\x03\x03\x03\x03\x03\x02\x01\x03\x03\x03\x03\x06\x03\x06\x04\x03\x03\x06\x04\x06\x03\x06\x06\x03\x03\x03\x03\x03\x03\x02\x03\x03\x02\x02\x03\x02\x03\x03\x03\x02\x03\x04\x02\x02\x03\x03\x02\x03\x03\x03\x03\x03\x03\x02\x02\x02\x03\x03\x04\x03\x03\x03\x03\x03\x03\x03\x03\x02\x02\x03\x03\x03\x03\x03\x02\x03\x02\x03\x03\x02\x03\x03\x02\x03\x03\x03\x03\x02\x01\x01\x03\x03\x03\x04\x02\x02\x03\x03\x02\x03\x02\x03\x03\x03\x01\x03\x03\x03\x03\x03\x03\x02\x03\x03\x02\x03\x04\x02\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x04\x02\x03\x04\x03\x04\x03\x04\x03\x03\x01\x03\x01\x03\x03\x03\x03\x03\x05\x02\x03\x03\x03\x03\x03\x03\x02\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x01\x01\x03\x03\x03\x02\x02\x03\x01\x02\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x02\x03\x03\x04\x03\x03\x03\x02\x02\x03\x03\x03\x03\x03\x03\x03\x03\x03\x02\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x04\x03\x03\x01\x03\x03\x03\x03\x04\x03\x01\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x02\x03\x03\x03\x03\x02\x03\x03\x02\x02\x03\x03\x03\x03\x03\x02\x03\x03\x03\x03\x03\x03\x03\x02\x01\x03\x03\x03\x03\x04\x03\x03\x02\x02\x03\x03\x02\x02\x02\x02\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x06\x02\x01\x03\x03\x04\x03\x03\x03\x03\x06\x03\x06\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x04\x03\x03\x03\x03\x03\x02\x02\x02\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x02\x02\x02\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x02\x03\x02\x03\x02\x02\x02\x03\x03\x04\x03\x03\x02\x02\x02\x03\x03\x03\x03\x03\x02\x02\x02\x03\x03\x03\x03\x03\x03\x03\x03\x04\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x03\x04\x02\x02\x02\x03\x03\x03\x03\x03\x02\x03\x02\x02\x02\x02\x03\x02\x03\x03\x04\x02\x03\x03\x03\x03\x03\x03\x03\x02\x02\x02\x04\x03\x03\x03\x03\x03\x02\x02\x02\x03\x03\x03\x03\x02\x03\x04\x03\x02\x03\x03\x03\x02\x03\x03\x03\x03\x03\x03\x02\x02\x03\x02\x02\x03\x03\x02\x02\x06\x06\x06\x06\x02\x03\x03\x02\x03\x03\x03\x03\x03\x01\x01\x04\x03\x06\x06\x04\x03\x03\x04\x06\x06\x06\x06\x03\x02\x03\x03\x03\x03\x04\x04\x03\x03\x03\x04\x03\x03\x03\x03\x04\x03\x03\x02\x03\x03\x03\x03\x03\x04\x03\x03\x03\x03\x04\x03\x03\x03\x03\x03\x02\x02\x02\x02\x02\x04\x02\x04\x04\x02\x02\x02\x02\x02\x02\x03\x02\x04\x02\x03\x04\x04\x03\x03"
diff --git a/backend/goldmark/util/html5entities.go b/backend/goldmark/util/html5entities.go
new file mode 100644
index 0000000..284dc05
--- /dev/null
+++ b/backend/goldmark/util/html5entities.go
@@ -0,0 +1,47 @@
+package util
+
+import (
+ "sync"
+)
+
+//go:generate go run ../_tools emb-structs -i ../_tools/html5entities.json -o ./html5entities.gen.go
+
+var _html5entitiesOnce sync.Once
+var _html5entitiesMap map[string]*HTML5Entity
+
+func buildHTML5Entities() {
+ _html5entitiesOnce.Do(func() {
+ entities := make([]HTML5Entity, _html5entitiesLength)
+ _html5entitiesMap = make(map[string]*HTML5Entity, _html5entitiesLength)
+
+ cName := 0
+ cCharacters := 0
+ for i := range _html5entitiesLength {
+ tName := cName + int(_html5entitiesNameIndex[i])
+ tCharacters := cCharacters + int(_html5entitiesCharactersIndex[i])
+
+ name := _html5entitiesName[cName:tName]
+ e := &entities[i]
+ e.Name = name
+ e.Characters = _html5entitiesCharacters[cCharacters:tCharacters]
+ _html5entitiesMap[name] = e
+
+ cName = tName
+ cCharacters = tCharacters
+ }
+ })
+}
+
+// HTML5Entity struct represents HTML5 entitites.
+type HTML5Entity struct {
+ Name string
+ Characters []byte
+}
+
+// LookUpHTML5EntityByName returns (an HTML5Entity, true) if an entity named
+// given name is found, otherwise (nil, false).
+func LookUpHTML5EntityByName(name string) (*HTML5Entity, bool) {
+ buildHTML5Entities()
+ v, ok := _html5entitiesMap[name]
+ return v, ok
+}
diff --git a/backend/goldmark/util/unicode_case_folding.gen.go b/backend/goldmark/util/unicode_case_folding.gen.go
new file mode 100644
index 0000000..eb91fb2
--- /dev/null
+++ b/backend/goldmark/util/unicode_case_folding.gen.go
@@ -0,0 +1,6 @@
+// Code generated by _tools; DO NOT EDIT.
+package util
+const _unicodeCaseFoldingLength = 1530
+var _unicodeCaseFoldingFrom = [...]rune{0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a, 0x4b, 0x4c, 0x4d, 0x4e, 0x4f, 0x50, 0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5a, 0xb5, 0xc0, 0xc1, 0xc2, 0xc3, 0xc4, 0xc5, 0xc6, 0xc7, 0xc8, 0xc9, 0xca, 0xcb, 0xcc, 0xcd, 0xce, 0xcf, 0xd0, 0xd1, 0xd2, 0xd3, 0xd4, 0xd5, 0xd6, 0xd8, 0xd9, 0xda, 0xdb, 0xdc, 0xdd, 0xde, 0xdf, 0x100, 0x102, 0x104, 0x106, 0x108, 0x10a, 0x10c, 0x10e, 0x110, 0x112, 0x114, 0x116, 0x118, 0x11a, 0x11c, 0x11e, 0x120, 0x122, 0x124, 0x126, 0x128, 0x12a, 0x12c, 0x12e, 0x130, 0x132, 0x134, 0x136, 0x139, 0x13b, 0x13d, 0x13f, 0x141, 0x143, 0x145, 0x147, 0x149, 0x14a, 0x14c, 0x14e, 0x150, 0x152, 0x154, 0x156, 0x158, 0x15a, 0x15c, 0x15e, 0x160, 0x162, 0x164, 0x166, 0x168, 0x16a, 0x16c, 0x16e, 0x170, 0x172, 0x174, 0x176, 0x178, 0x179, 0x17b, 0x17d, 0x17f, 0x181, 0x182, 0x184, 0x186, 0x187, 0x189, 0x18a, 0x18b, 0x18e, 0x18f, 0x190, 0x191, 0x193, 0x194, 0x196, 0x197, 0x198, 0x19c, 0x19d, 0x19f, 0x1a0, 0x1a2, 0x1a4, 0x1a6, 0x1a7, 0x1a9, 0x1ac, 0x1ae, 0x1af, 0x1b1, 0x1b2, 0x1b3, 0x1b5, 0x1b7, 0x1b8, 0x1bc, 0x1c4, 0x1c5, 0x1c7, 0x1c8, 0x1ca, 0x1cb, 0x1cd, 0x1cf, 0x1d1, 0x1d3, 0x1d5, 0x1d7, 0x1d9, 0x1db, 0x1de, 0x1e0, 0x1e2, 0x1e4, 0x1e6, 0x1e8, 0x1ea, 0x1ec, 0x1ee, 0x1f0, 0x1f1, 0x1f2, 0x1f4, 0x1f6, 0x1f7, 0x1f8, 0x1fa, 0x1fc, 0x1fe, 0x200, 0x202, 0x204, 0x206, 0x208, 0x20a, 0x20c, 0x20e, 0x210, 0x212, 0x214, 0x216, 0x218, 0x21a, 0x21c, 0x21e, 0x220, 0x222, 0x224, 0x226, 0x228, 0x22a, 0x22c, 0x22e, 0x230, 0x232, 0x23a, 0x23b, 0x23d, 0x23e, 0x241, 0x243, 0x244, 0x245, 0x246, 0x248, 0x24a, 0x24c, 0x24e, 0x345, 0x370, 0x372, 0x376, 0x37f, 0x386, 0x388, 0x389, 0x38a, 0x38c, 0x38e, 0x38f, 0x390, 0x391, 0x392, 0x393, 0x394, 0x395, 0x396, 0x397, 0x398, 0x399, 0x39a, 0x39b, 0x39c, 0x39d, 0x39e, 0x39f, 0x3a0, 0x3a1, 0x3a3, 0x3a4, 0x3a5, 0x3a6, 0x3a7, 0x3a8, 0x3a9, 0x3aa, 0x3ab, 0x3b0, 0x3c2, 0x3cf, 0x3d0, 0x3d1, 0x3d5, 0x3d6, 0x3d8, 0x3da, 0x3dc, 0x3de, 0x3e0, 0x3e2, 0x3e4, 0x3e6, 0x3e8, 0x3ea, 0x3ec, 0x3ee, 0x3f0, 0x3f1, 0x3f4, 0x3f5, 0x3f7, 0x3f9, 0x3fa, 0x3fd, 0x3fe, 0x3ff, 0x400, 0x401, 0x402, 0x403, 0x404, 0x405, 0x406, 0x407, 0x408, 0x409, 0x40a, 0x40b, 0x40c, 0x40d, 0x40e, 0x40f, 0x410, 0x411, 0x412, 0x413, 0x414, 0x415, 0x416, 0x417, 0x418, 0x419, 0x41a, 0x41b, 0x41c, 0x41d, 0x41e, 0x41f, 0x420, 0x421, 0x422, 0x423, 0x424, 0x425, 0x426, 0x427, 0x428, 0x429, 0x42a, 0x42b, 0x42c, 0x42d, 0x42e, 0x42f, 0x460, 0x462, 0x464, 0x466, 0x468, 0x46a, 0x46c, 0x46e, 0x470, 0x472, 0x474, 0x476, 0x478, 0x47a, 0x47c, 0x47e, 0x480, 0x48a, 0x48c, 0x48e, 0x490, 0x492, 0x494, 0x496, 0x498, 0x49a, 0x49c, 0x49e, 0x4a0, 0x4a2, 0x4a4, 0x4a6, 0x4a8, 0x4aa, 0x4ac, 0x4ae, 0x4b0, 0x4b2, 0x4b4, 0x4b6, 0x4b8, 0x4ba, 0x4bc, 0x4be, 0x4c0, 0x4c1, 0x4c3, 0x4c5, 0x4c7, 0x4c9, 0x4cb, 0x4cd, 0x4d0, 0x4d2, 0x4d4, 0x4d6, 0x4d8, 0x4da, 0x4dc, 0x4de, 0x4e0, 0x4e2, 0x4e4, 0x4e6, 0x4e8, 0x4ea, 0x4ec, 0x4ee, 0x4f0, 0x4f2, 0x4f4, 0x4f6, 0x4f8, 0x4fa, 0x4fc, 0x4fe, 0x500, 0x502, 0x504, 0x506, 0x508, 0x50a, 0x50c, 0x50e, 0x510, 0x512, 0x514, 0x516, 0x518, 0x51a, 0x51c, 0x51e, 0x520, 0x522, 0x524, 0x526, 0x528, 0x52a, 0x52c, 0x52e, 0x531, 0x532, 0x533, 0x534, 0x535, 0x536, 0x537, 0x538, 0x539, 0x53a, 0x53b, 0x53c, 0x53d, 0x53e, 0x53f, 0x540, 0x541, 0x542, 0x543, 0x544, 0x545, 0x546, 0x547, 0x548, 0x549, 0x54a, 0x54b, 0x54c, 0x54d, 0x54e, 0x54f, 0x550, 0x551, 0x552, 0x553, 0x554, 0x555, 0x556, 0x587, 0x10a0, 0x10a1, 0x10a2, 0x10a3, 0x10a4, 0x10a5, 0x10a6, 0x10a7, 0x10a8, 0x10a9, 0x10aa, 0x10ab, 0x10ac, 0x10ad, 0x10ae, 0x10af, 0x10b0, 0x10b1, 0x10b2, 0x10b3, 0x10b4, 0x10b5, 0x10b6, 0x10b7, 0x10b8, 0x10b9, 0x10ba, 0x10bb, 0x10bc, 0x10bd, 0x10be, 0x10bf, 0x10c0, 0x10c1, 0x10c2, 0x10c3, 0x10c4, 0x10c5, 0x10c7, 0x10cd, 0x13f8, 0x13f9, 0x13fa, 0x13fb, 0x13fc, 0x13fd, 0x1c80, 0x1c81, 0x1c82, 0x1c83, 0x1c84, 0x1c85, 0x1c86, 0x1c87, 0x1c88, 0x1c90, 0x1c91, 0x1c92, 0x1c93, 0x1c94, 0x1c95, 0x1c96, 0x1c97, 0x1c98, 0x1c99, 0x1c9a, 0x1c9b, 0x1c9c, 0x1c9d, 0x1c9e, 0x1c9f, 0x1ca0, 0x1ca1, 0x1ca2, 0x1ca3, 0x1ca4, 0x1ca5, 0x1ca6, 0x1ca7, 0x1ca8, 0x1ca9, 0x1caa, 0x1cab, 0x1cac, 0x1cad, 0x1cae, 0x1caf, 0x1cb0, 0x1cb1, 0x1cb2, 0x1cb3, 0x1cb4, 0x1cb5, 0x1cb6, 0x1cb7, 0x1cb8, 0x1cb9, 0x1cba, 0x1cbd, 0x1cbe, 0x1cbf, 0x1e00, 0x1e02, 0x1e04, 0x1e06, 0x1e08, 0x1e0a, 0x1e0c, 0x1e0e, 0x1e10, 0x1e12, 0x1e14, 0x1e16, 0x1e18, 0x1e1a, 0x1e1c, 0x1e1e, 0x1e20, 0x1e22, 0x1e24, 0x1e26, 0x1e28, 0x1e2a, 0x1e2c, 0x1e2e, 0x1e30, 0x1e32, 0x1e34, 0x1e36, 0x1e38, 0x1e3a, 0x1e3c, 0x1e3e, 0x1e40, 0x1e42, 0x1e44, 0x1e46, 0x1e48, 0x1e4a, 0x1e4c, 0x1e4e, 0x1e50, 0x1e52, 0x1e54, 0x1e56, 0x1e58, 0x1e5a, 0x1e5c, 0x1e5e, 0x1e60, 0x1e62, 0x1e64, 0x1e66, 0x1e68, 0x1e6a, 0x1e6c, 0x1e6e, 0x1e70, 0x1e72, 0x1e74, 0x1e76, 0x1e78, 0x1e7a, 0x1e7c, 0x1e7e, 0x1e80, 0x1e82, 0x1e84, 0x1e86, 0x1e88, 0x1e8a, 0x1e8c, 0x1e8e, 0x1e90, 0x1e92, 0x1e94, 0x1e96, 0x1e97, 0x1e98, 0x1e99, 0x1e9a, 0x1e9b, 0x1e9e, 0x1ea0, 0x1ea2, 0x1ea4, 0x1ea6, 0x1ea8, 0x1eaa, 0x1eac, 0x1eae, 0x1eb0, 0x1eb2, 0x1eb4, 0x1eb6, 0x1eb8, 0x1eba, 0x1ebc, 0x1ebe, 0x1ec0, 0x1ec2, 0x1ec4, 0x1ec6, 0x1ec8, 0x1eca, 0x1ecc, 0x1ece, 0x1ed0, 0x1ed2, 0x1ed4, 0x1ed6, 0x1ed8, 0x1eda, 0x1edc, 0x1ede, 0x1ee0, 0x1ee2, 0x1ee4, 0x1ee6, 0x1ee8, 0x1eea, 0x1eec, 0x1eee, 0x1ef0, 0x1ef2, 0x1ef4, 0x1ef6, 0x1ef8, 0x1efa, 0x1efc, 0x1efe, 0x1f08, 0x1f09, 0x1f0a, 0x1f0b, 0x1f0c, 0x1f0d, 0x1f0e, 0x1f0f, 0x1f18, 0x1f19, 0x1f1a, 0x1f1b, 0x1f1c, 0x1f1d, 0x1f28, 0x1f29, 0x1f2a, 0x1f2b, 0x1f2c, 0x1f2d, 0x1f2e, 0x1f2f, 0x1f38, 0x1f39, 0x1f3a, 0x1f3b, 0x1f3c, 0x1f3d, 0x1f3e, 0x1f3f, 0x1f48, 0x1f49, 0x1f4a, 0x1f4b, 0x1f4c, 0x1f4d, 0x1f50, 0x1f52, 0x1f54, 0x1f56, 0x1f59, 0x1f5b, 0x1f5d, 0x1f5f, 0x1f68, 0x1f69, 0x1f6a, 0x1f6b, 0x1f6c, 0x1f6d, 0x1f6e, 0x1f6f, 0x1f80, 0x1f81, 0x1f82, 0x1f83, 0x1f84, 0x1f85, 0x1f86, 0x1f87, 0x1f88, 0x1f89, 0x1f8a, 0x1f8b, 0x1f8c, 0x1f8d, 0x1f8e, 0x1f8f, 0x1f90, 0x1f91, 0x1f92, 0x1f93, 0x1f94, 0x1f95, 0x1f96, 0x1f97, 0x1f98, 0x1f99, 0x1f9a, 0x1f9b, 0x1f9c, 0x1f9d, 0x1f9e, 0x1f9f, 0x1fa0, 0x1fa1, 0x1fa2, 0x1fa3, 0x1fa4, 0x1fa5, 0x1fa6, 0x1fa7, 0x1fa8, 0x1fa9, 0x1faa, 0x1fab, 0x1fac, 0x1fad, 0x1fae, 0x1faf, 0x1fb2, 0x1fb3, 0x1fb4, 0x1fb6, 0x1fb7, 0x1fb8, 0x1fb9, 0x1fba, 0x1fbb, 0x1fbc, 0x1fbe, 0x1fc2, 0x1fc3, 0x1fc4, 0x1fc6, 0x1fc7, 0x1fc8, 0x1fc9, 0x1fca, 0x1fcb, 0x1fcc, 0x1fd2, 0x1fd3, 0x1fd6, 0x1fd7, 0x1fd8, 0x1fd9, 0x1fda, 0x1fdb, 0x1fe2, 0x1fe3, 0x1fe4, 0x1fe6, 0x1fe7, 0x1fe8, 0x1fe9, 0x1fea, 0x1feb, 0x1fec, 0x1ff2, 0x1ff3, 0x1ff4, 0x1ff6, 0x1ff7, 0x1ff8, 0x1ff9, 0x1ffa, 0x1ffb, 0x1ffc, 0x2126, 0x212a, 0x212b, 0x2132, 0x2160, 0x2161, 0x2162, 0x2163, 0x2164, 0x2165, 0x2166, 0x2167, 0x2168, 0x2169, 0x216a, 0x216b, 0x216c, 0x216d, 0x216e, 0x216f, 0x2183, 0x24b6, 0x24b7, 0x24b8, 0x24b9, 0x24ba, 0x24bb, 0x24bc, 0x24bd, 0x24be, 0x24bf, 0x24c0, 0x24c1, 0x24c2, 0x24c3, 0x24c4, 0x24c5, 0x24c6, 0x24c7, 0x24c8, 0x24c9, 0x24ca, 0x24cb, 0x24cc, 0x24cd, 0x24ce, 0x24cf, 0x2c00, 0x2c01, 0x2c02, 0x2c03, 0x2c04, 0x2c05, 0x2c06, 0x2c07, 0x2c08, 0x2c09, 0x2c0a, 0x2c0b, 0x2c0c, 0x2c0d, 0x2c0e, 0x2c0f, 0x2c10, 0x2c11, 0x2c12, 0x2c13, 0x2c14, 0x2c15, 0x2c16, 0x2c17, 0x2c18, 0x2c19, 0x2c1a, 0x2c1b, 0x2c1c, 0x2c1d, 0x2c1e, 0x2c1f, 0x2c20, 0x2c21, 0x2c22, 0x2c23, 0x2c24, 0x2c25, 0x2c26, 0x2c27, 0x2c28, 0x2c29, 0x2c2a, 0x2c2b, 0x2c2c, 0x2c2d, 0x2c2e, 0x2c2f, 0x2c60, 0x2c62, 0x2c63, 0x2c64, 0x2c67, 0x2c69, 0x2c6b, 0x2c6d, 0x2c6e, 0x2c6f, 0x2c70, 0x2c72, 0x2c75, 0x2c7e, 0x2c7f, 0x2c80, 0x2c82, 0x2c84, 0x2c86, 0x2c88, 0x2c8a, 0x2c8c, 0x2c8e, 0x2c90, 0x2c92, 0x2c94, 0x2c96, 0x2c98, 0x2c9a, 0x2c9c, 0x2c9e, 0x2ca0, 0x2ca2, 0x2ca4, 0x2ca6, 0x2ca8, 0x2caa, 0x2cac, 0x2cae, 0x2cb0, 0x2cb2, 0x2cb4, 0x2cb6, 0x2cb8, 0x2cba, 0x2cbc, 0x2cbe, 0x2cc0, 0x2cc2, 0x2cc4, 0x2cc6, 0x2cc8, 0x2cca, 0x2ccc, 0x2cce, 0x2cd0, 0x2cd2, 0x2cd4, 0x2cd6, 0x2cd8, 0x2cda, 0x2cdc, 0x2cde, 0x2ce0, 0x2ce2, 0x2ceb, 0x2ced, 0x2cf2, 0xa640, 0xa642, 0xa644, 0xa646, 0xa648, 0xa64a, 0xa64c, 0xa64e, 0xa650, 0xa652, 0xa654, 0xa656, 0xa658, 0xa65a, 0xa65c, 0xa65e, 0xa660, 0xa662, 0xa664, 0xa666, 0xa668, 0xa66a, 0xa66c, 0xa680, 0xa682, 0xa684, 0xa686, 0xa688, 0xa68a, 0xa68c, 0xa68e, 0xa690, 0xa692, 0xa694, 0xa696, 0xa698, 0xa69a, 0xa722, 0xa724, 0xa726, 0xa728, 0xa72a, 0xa72c, 0xa72e, 0xa732, 0xa734, 0xa736, 0xa738, 0xa73a, 0xa73c, 0xa73e, 0xa740, 0xa742, 0xa744, 0xa746, 0xa748, 0xa74a, 0xa74c, 0xa74e, 0xa750, 0xa752, 0xa754, 0xa756, 0xa758, 0xa75a, 0xa75c, 0xa75e, 0xa760, 0xa762, 0xa764, 0xa766, 0xa768, 0xa76a, 0xa76c, 0xa76e, 0xa779, 0xa77b, 0xa77d, 0xa77e, 0xa780, 0xa782, 0xa784, 0xa786, 0xa78b, 0xa78d, 0xa790, 0xa792, 0xa796, 0xa798, 0xa79a, 0xa79c, 0xa79e, 0xa7a0, 0xa7a2, 0xa7a4, 0xa7a6, 0xa7a8, 0xa7aa, 0xa7ab, 0xa7ac, 0xa7ad, 0xa7ae, 0xa7b0, 0xa7b1, 0xa7b2, 0xa7b3, 0xa7b4, 0xa7b6, 0xa7b8, 0xa7ba, 0xa7bc, 0xa7be, 0xa7c0, 0xa7c2, 0xa7c4, 0xa7c5, 0xa7c6, 0xa7c7, 0xa7c9, 0xa7d0, 0xa7d6, 0xa7d8, 0xa7f5, 0xab70, 0xab71, 0xab72, 0xab73, 0xab74, 0xab75, 0xab76, 0xab77, 0xab78, 0xab79, 0xab7a, 0xab7b, 0xab7c, 0xab7d, 0xab7e, 0xab7f, 0xab80, 0xab81, 0xab82, 0xab83, 0xab84, 0xab85, 0xab86, 0xab87, 0xab88, 0xab89, 0xab8a, 0xab8b, 0xab8c, 0xab8d, 0xab8e, 0xab8f, 0xab90, 0xab91, 0xab92, 0xab93, 0xab94, 0xab95, 0xab96, 0xab97, 0xab98, 0xab99, 0xab9a, 0xab9b, 0xab9c, 0xab9d, 0xab9e, 0xab9f, 0xaba0, 0xaba1, 0xaba2, 0xaba3, 0xaba4, 0xaba5, 0xaba6, 0xaba7, 0xaba8, 0xaba9, 0xabaa, 0xabab, 0xabac, 0xabad, 0xabae, 0xabaf, 0xabb0, 0xabb1, 0xabb2, 0xabb3, 0xabb4, 0xabb5, 0xabb6, 0xabb7, 0xabb8, 0xabb9, 0xabba, 0xabbb, 0xabbc, 0xabbd, 0xabbe, 0xabbf, 0xfb00, 0xfb01, 0xfb02, 0xfb03, 0xfb04, 0xfb05, 0xfb06, 0xfb13, 0xfb14, 0xfb15, 0xfb16, 0xfb17, 0xff21, 0xff22, 0xff23, 0xff24, 0xff25, 0xff26, 0xff27, 0xff28, 0xff29, 0xff2a, 0xff2b, 0xff2c, 0xff2d, 0xff2e, 0xff2f, 0xff30, 0xff31, 0xff32, 0xff33, 0xff34, 0xff35, 0xff36, 0xff37, 0xff38, 0xff39, 0xff3a, 0x10400, 0x10401, 0x10402, 0x10403, 0x10404, 0x10405, 0x10406, 0x10407, 0x10408, 0x10409, 0x1040a, 0x1040b, 0x1040c, 0x1040d, 0x1040e, 0x1040f, 0x10410, 0x10411, 0x10412, 0x10413, 0x10414, 0x10415, 0x10416, 0x10417, 0x10418, 0x10419, 0x1041a, 0x1041b, 0x1041c, 0x1041d, 0x1041e, 0x1041f, 0x10420, 0x10421, 0x10422, 0x10423, 0x10424, 0x10425, 0x10426, 0x10427, 0x104b0, 0x104b1, 0x104b2, 0x104b3, 0x104b4, 0x104b5, 0x104b6, 0x104b7, 0x104b8, 0x104b9, 0x104ba, 0x104bb, 0x104bc, 0x104bd, 0x104be, 0x104bf, 0x104c0, 0x104c1, 0x104c2, 0x104c3, 0x104c4, 0x104c5, 0x104c6, 0x104c7, 0x104c8, 0x104c9, 0x104ca, 0x104cb, 0x104cc, 0x104cd, 0x104ce, 0x104cf, 0x104d0, 0x104d1, 0x104d2, 0x104d3, 0x10570, 0x10571, 0x10572, 0x10573, 0x10574, 0x10575, 0x10576, 0x10577, 0x10578, 0x10579, 0x1057a, 0x1057c, 0x1057d, 0x1057e, 0x1057f, 0x10580, 0x10581, 0x10582, 0x10583, 0x10584, 0x10585, 0x10586, 0x10587, 0x10588, 0x10589, 0x1058a, 0x1058c, 0x1058d, 0x1058e, 0x1058f, 0x10590, 0x10591, 0x10592, 0x10594, 0x10595, 0x10c80, 0x10c81, 0x10c82, 0x10c83, 0x10c84, 0x10c85, 0x10c86, 0x10c87, 0x10c88, 0x10c89, 0x10c8a, 0x10c8b, 0x10c8c, 0x10c8d, 0x10c8e, 0x10c8f, 0x10c90, 0x10c91, 0x10c92, 0x10c93, 0x10c94, 0x10c95, 0x10c96, 0x10c97, 0x10c98, 0x10c99, 0x10c9a, 0x10c9b, 0x10c9c, 0x10c9d, 0x10c9e, 0x10c9f, 0x10ca0, 0x10ca1, 0x10ca2, 0x10ca3, 0x10ca4, 0x10ca5, 0x10ca6, 0x10ca7, 0x10ca8, 0x10ca9, 0x10caa, 0x10cab, 0x10cac, 0x10cad, 0x10cae, 0x10caf, 0x10cb0, 0x10cb1, 0x10cb2, 0x118a0, 0x118a1, 0x118a2, 0x118a3, 0x118a4, 0x118a5, 0x118a6, 0x118a7, 0x118a8, 0x118a9, 0x118aa, 0x118ab, 0x118ac, 0x118ad, 0x118ae, 0x118af, 0x118b0, 0x118b1, 0x118b2, 0x118b3, 0x118b4, 0x118b5, 0x118b6, 0x118b7, 0x118b8, 0x118b9, 0x118ba, 0x118bb, 0x118bc, 0x118bd, 0x118be, 0x118bf, 0x16e40, 0x16e41, 0x16e42, 0x16e43, 0x16e44, 0x16e45, 0x16e46, 0x16e47, 0x16e48, 0x16e49, 0x16e4a, 0x16e4b, 0x16e4c, 0x16e4d, 0x16e4e, 0x16e4f, 0x16e50, 0x16e51, 0x16e52, 0x16e53, 0x16e54, 0x16e55, 0x16e56, 0x16e57, 0x16e58, 0x16e59, 0x16e5a, 0x16e5b, 0x16e5c, 0x16e5d, 0x16e5e, 0x16e5f, 0x1e900, 0x1e901, 0x1e902, 0x1e903, 0x1e904, 0x1e905, 0x1e906, 0x1e907, 0x1e908, 0x1e909, 0x1e90a, 0x1e90b, 0x1e90c, 0x1e90d, 0x1e90e, 0x1e90f, 0x1e910, 0x1e911, 0x1e912, 0x1e913, 0x1e914, 0x1e915, 0x1e916, 0x1e917, 0x1e918, 0x1e919, 0x1e91a, 0x1e91b, 0x1e91c, 0x1e91d, 0x1e91e, 0x1e91f, 0x1e920, 0x1e921}
+var _unicodeCaseFoldingTo = [...]rune{97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 956, 224, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239, 240, 241, 242, 243, 244, 245, 246, 248, 249, 250, 251, 252, 253, 254, 115, 115, 257, 259, 261, 263, 265, 267, 269, 271, 273, 275, 277, 279, 281, 283, 285, 287, 289, 291, 293, 295, 297, 299, 301, 303, 105, 775, 307, 309, 311, 314, 316, 318, 320, 322, 324, 326, 328, 700, 110, 331, 333, 335, 337, 339, 341, 343, 345, 347, 349, 351, 353, 355, 357, 359, 361, 363, 365, 367, 369, 371, 373, 375, 255, 378, 380, 382, 115, 595, 387, 389, 596, 392, 598, 599, 396, 477, 601, 603, 402, 608, 611, 617, 616, 409, 623, 626, 629, 417, 419, 421, 640, 424, 643, 429, 648, 432, 650, 651, 436, 438, 658, 441, 445, 454, 454, 457, 457, 460, 460, 462, 464, 466, 468, 470, 472, 474, 476, 479, 481, 483, 485, 487, 489, 491, 493, 495, 106, 780, 499, 499, 501, 405, 447, 505, 507, 509, 511, 513, 515, 517, 519, 521, 523, 525, 527, 529, 531, 533, 535, 537, 539, 541, 543, 414, 547, 549, 551, 553, 555, 557, 559, 561, 563, 11365, 572, 410, 11366, 578, 384, 649, 652, 583, 585, 587, 589, 591, 953, 881, 883, 887, 1011, 940, 941, 942, 943, 972, 973, 974, 953, 776, 769, 945, 946, 947, 948, 949, 950, 951, 952, 953, 954, 955, 956, 957, 958, 959, 960, 961, 963, 964, 965, 966, 967, 968, 969, 970, 971, 965, 776, 769, 963, 983, 946, 952, 966, 960, 985, 987, 989, 991, 993, 995, 997, 999, 1001, 1003, 1005, 1007, 954, 961, 952, 949, 1016, 1010, 1019, 891, 892, 893, 1104, 1105, 1106, 1107, 1108, 1109, 1110, 1111, 1112, 1113, 1114, 1115, 1116, 1117, 1118, 1119, 1072, 1073, 1074, 1075, 1076, 1077, 1078, 1079, 1080, 1081, 1082, 1083, 1084, 1085, 1086, 1087, 1088, 1089, 1090, 1091, 1092, 1093, 1094, 1095, 1096, 1097, 1098, 1099, 1100, 1101, 1102, 1103, 1121, 1123, 1125, 1127, 1129, 1131, 1133, 1135, 1137, 1139, 1141, 1143, 1145, 1147, 1149, 1151, 1153, 1163, 1165, 1167, 1169, 1171, 1173, 1175, 1177, 1179, 1181, 1183, 1185, 1187, 1189, 1191, 1193, 1195, 1197, 1199, 1201, 1203, 1205, 1207, 1209, 1211, 1213, 1215, 1231, 1218, 1220, 1222, 1224, 1226, 1228, 1230, 1233, 1235, 1237, 1239, 1241, 1243, 1245, 1247, 1249, 1251, 1253, 1255, 1257, 1259, 1261, 1263, 1265, 1267, 1269, 1271, 1273, 1275, 1277, 1279, 1281, 1283, 1285, 1287, 1289, 1291, 1293, 1295, 1297, 1299, 1301, 1303, 1305, 1307, 1309, 1311, 1313, 1315, 1317, 1319, 1321, 1323, 1325, 1327, 1377, 1378, 1379, 1380, 1381, 1382, 1383, 1384, 1385, 1386, 1387, 1388, 1389, 1390, 1391, 1392, 1393, 1394, 1395, 1396, 1397, 1398, 1399, 1400, 1401, 1402, 1403, 1404, 1405, 1406, 1407, 1408, 1409, 1410, 1411, 1412, 1413, 1414, 1381, 1410, 11520, 11521, 11522, 11523, 11524, 11525, 11526, 11527, 11528, 11529, 11530, 11531, 11532, 11533, 11534, 11535, 11536, 11537, 11538, 11539, 11540, 11541, 11542, 11543, 11544, 11545, 11546, 11547, 11548, 11549, 11550, 11551, 11552, 11553, 11554, 11555, 11556, 11557, 11559, 11565, 5104, 5105, 5106, 5107, 5108, 5109, 1074, 1076, 1086, 1089, 1090, 1090, 1098, 1123, 42571, 4304, 4305, 4306, 4307, 4308, 4309, 4310, 4311, 4312, 4313, 4314, 4315, 4316, 4317, 4318, 4319, 4320, 4321, 4322, 4323, 4324, 4325, 4326, 4327, 4328, 4329, 4330, 4331, 4332, 4333, 4334, 4335, 4336, 4337, 4338, 4339, 4340, 4341, 4342, 4343, 4344, 4345, 4346, 4349, 4350, 4351, 7681, 7683, 7685, 7687, 7689, 7691, 7693, 7695, 7697, 7699, 7701, 7703, 7705, 7707, 7709, 7711, 7713, 7715, 7717, 7719, 7721, 7723, 7725, 7727, 7729, 7731, 7733, 7735, 7737, 7739, 7741, 7743, 7745, 7747, 7749, 7751, 7753, 7755, 7757, 7759, 7761, 7763, 7765, 7767, 7769, 7771, 7773, 7775, 7777, 7779, 7781, 7783, 7785, 7787, 7789, 7791, 7793, 7795, 7797, 7799, 7801, 7803, 7805, 7807, 7809, 7811, 7813, 7815, 7817, 7819, 7821, 7823, 7825, 7827, 7829, 104, 817, 116, 776, 119, 778, 121, 778, 97, 702, 7777, 115, 115, 7841, 7843, 7845, 7847, 7849, 7851, 7853, 7855, 7857, 7859, 7861, 7863, 7865, 7867, 7869, 7871, 7873, 7875, 7877, 7879, 7881, 7883, 7885, 7887, 7889, 7891, 7893, 7895, 7897, 7899, 7901, 7903, 7905, 7907, 7909, 7911, 7913, 7915, 7917, 7919, 7921, 7923, 7925, 7927, 7929, 7931, 7933, 7935, 7936, 7937, 7938, 7939, 7940, 7941, 7942, 7943, 7952, 7953, 7954, 7955, 7956, 7957, 7968, 7969, 7970, 7971, 7972, 7973, 7974, 7975, 7984, 7985, 7986, 7987, 7988, 7989, 7990, 7991, 8000, 8001, 8002, 8003, 8004, 8005, 965, 787, 965, 787, 768, 965, 787, 769, 965, 787, 834, 8017, 8019, 8021, 8023, 8032, 8033, 8034, 8035, 8036, 8037, 8038, 8039, 7936, 953, 7937, 953, 7938, 953, 7939, 953, 7940, 953, 7941, 953, 7942, 953, 7943, 953, 7936, 953, 7937, 953, 7938, 953, 7939, 953, 7940, 953, 7941, 953, 7942, 953, 7943, 953, 7968, 953, 7969, 953, 7970, 953, 7971, 953, 7972, 953, 7973, 953, 7974, 953, 7975, 953, 7968, 953, 7969, 953, 7970, 953, 7971, 953, 7972, 953, 7973, 953, 7974, 953, 7975, 953, 8032, 953, 8033, 953, 8034, 953, 8035, 953, 8036, 953, 8037, 953, 8038, 953, 8039, 953, 8032, 953, 8033, 953, 8034, 953, 8035, 953, 8036, 953, 8037, 953, 8038, 953, 8039, 953, 8048, 953, 945, 953, 940, 953, 945, 834, 945, 834, 953, 8112, 8113, 8048, 8049, 945, 953, 953, 8052, 953, 951, 953, 942, 953, 951, 834, 951, 834, 953, 8050, 8051, 8052, 8053, 951, 953, 953, 776, 768, 953, 776, 769, 953, 834, 953, 776, 834, 8144, 8145, 8054, 8055, 965, 776, 768, 965, 776, 769, 961, 787, 965, 834, 965, 776, 834, 8160, 8161, 8058, 8059, 8165, 8060, 953, 969, 953, 974, 953, 969, 834, 969, 834, 953, 8056, 8057, 8060, 8061, 969, 953, 969, 107, 229, 8526, 8560, 8561, 8562, 8563, 8564, 8565, 8566, 8567, 8568, 8569, 8570, 8571, 8572, 8573, 8574, 8575, 8580, 9424, 9425, 9426, 9427, 9428, 9429, 9430, 9431, 9432, 9433, 9434, 9435, 9436, 9437, 9438, 9439, 9440, 9441, 9442, 9443, 9444, 9445, 9446, 9447, 9448, 9449, 11312, 11313, 11314, 11315, 11316, 11317, 11318, 11319, 11320, 11321, 11322, 11323, 11324, 11325, 11326, 11327, 11328, 11329, 11330, 11331, 11332, 11333, 11334, 11335, 11336, 11337, 11338, 11339, 11340, 11341, 11342, 11343, 11344, 11345, 11346, 11347, 11348, 11349, 11350, 11351, 11352, 11353, 11354, 11355, 11356, 11357, 11358, 11359, 11361, 619, 7549, 637, 11368, 11370, 11372, 593, 625, 592, 594, 11379, 11382, 575, 576, 11393, 11395, 11397, 11399, 11401, 11403, 11405, 11407, 11409, 11411, 11413, 11415, 11417, 11419, 11421, 11423, 11425, 11427, 11429, 11431, 11433, 11435, 11437, 11439, 11441, 11443, 11445, 11447, 11449, 11451, 11453, 11455, 11457, 11459, 11461, 11463, 11465, 11467, 11469, 11471, 11473, 11475, 11477, 11479, 11481, 11483, 11485, 11487, 11489, 11491, 11500, 11502, 11507, 42561, 42563, 42565, 42567, 42569, 42571, 42573, 42575, 42577, 42579, 42581, 42583, 42585, 42587, 42589, 42591, 42593, 42595, 42597, 42599, 42601, 42603, 42605, 42625, 42627, 42629, 42631, 42633, 42635, 42637, 42639, 42641, 42643, 42645, 42647, 42649, 42651, 42787, 42789, 42791, 42793, 42795, 42797, 42799, 42803, 42805, 42807, 42809, 42811, 42813, 42815, 42817, 42819, 42821, 42823, 42825, 42827, 42829, 42831, 42833, 42835, 42837, 42839, 42841, 42843, 42845, 42847, 42849, 42851, 42853, 42855, 42857, 42859, 42861, 42863, 42874, 42876, 7545, 42879, 42881, 42883, 42885, 42887, 42892, 613, 42897, 42899, 42903, 42905, 42907, 42909, 42911, 42913, 42915, 42917, 42919, 42921, 614, 604, 609, 620, 618, 670, 647, 669, 43859, 42933, 42935, 42937, 42939, 42941, 42943, 42945, 42947, 42900, 642, 7566, 42952, 42954, 42961, 42967, 42969, 42998, 5024, 5025, 5026, 5027, 5028, 5029, 5030, 5031, 5032, 5033, 5034, 5035, 5036, 5037, 5038, 5039, 5040, 5041, 5042, 5043, 5044, 5045, 5046, 5047, 5048, 5049, 5050, 5051, 5052, 5053, 5054, 5055, 5056, 5057, 5058, 5059, 5060, 5061, 5062, 5063, 5064, 5065, 5066, 5067, 5068, 5069, 5070, 5071, 5072, 5073, 5074, 5075, 5076, 5077, 5078, 5079, 5080, 5081, 5082, 5083, 5084, 5085, 5086, 5087, 5088, 5089, 5090, 5091, 5092, 5093, 5094, 5095, 5096, 5097, 5098, 5099, 5100, 5101, 5102, 5103, 102, 102, 102, 105, 102, 108, 102, 102, 105, 102, 102, 108, 115, 116, 115, 116, 1396, 1398, 1396, 1381, 1396, 1387, 1406, 1398, 1396, 1389, 65345, 65346, 65347, 65348, 65349, 65350, 65351, 65352, 65353, 65354, 65355, 65356, 65357, 65358, 65359, 65360, 65361, 65362, 65363, 65364, 65365, 65366, 65367, 65368, 65369, 65370, 66600, 66601, 66602, 66603, 66604, 66605, 66606, 66607, 66608, 66609, 66610, 66611, 66612, 66613, 66614, 66615, 66616, 66617, 66618, 66619, 66620, 66621, 66622, 66623, 66624, 66625, 66626, 66627, 66628, 66629, 66630, 66631, 66632, 66633, 66634, 66635, 66636, 66637, 66638, 66639, 66776, 66777, 66778, 66779, 66780, 66781, 66782, 66783, 66784, 66785, 66786, 66787, 66788, 66789, 66790, 66791, 66792, 66793, 66794, 66795, 66796, 66797, 66798, 66799, 66800, 66801, 66802, 66803, 66804, 66805, 66806, 66807, 66808, 66809, 66810, 66811, 66967, 66968, 66969, 66970, 66971, 66972, 66973, 66974, 66975, 66976, 66977, 66979, 66980, 66981, 66982, 66983, 66984, 66985, 66986, 66987, 66988, 66989, 66990, 66991, 66992, 66993, 66995, 66996, 66997, 66998, 66999, 67000, 67001, 67003, 67004, 68800, 68801, 68802, 68803, 68804, 68805, 68806, 68807, 68808, 68809, 68810, 68811, 68812, 68813, 68814, 68815, 68816, 68817, 68818, 68819, 68820, 68821, 68822, 68823, 68824, 68825, 68826, 68827, 68828, 68829, 68830, 68831, 68832, 68833, 68834, 68835, 68836, 68837, 68838, 68839, 68840, 68841, 68842, 68843, 68844, 68845, 68846, 68847, 68848, 68849, 68850, 71872, 71873, 71874, 71875, 71876, 71877, 71878, 71879, 71880, 71881, 71882, 71883, 71884, 71885, 71886, 71887, 71888, 71889, 71890, 71891, 71892, 71893, 71894, 71895, 71896, 71897, 71898, 71899, 71900, 71901, 71902, 71903, 93792, 93793, 93794, 93795, 93796, 93797, 93798, 93799, 93800, 93801, 93802, 93803, 93804, 93805, 93806, 93807, 93808, 93809, 93810, 93811, 93812, 93813, 93814, 93815, 93816, 93817, 93818, 93819, 93820, 93821, 93822, 93823, 125218, 125219, 125220, 125221, 125222, 125223, 125224, 125225, 125226, 125227, 125228, 125229, 125230, 125231, 125232, 125233, 125234, 125235, 125236, 125237, 125238, 125239, 125240, 125241, 125242, 125243, 125244, 125245, 125246, 125247, 125248, 125249, 125250, 125251}
+var _unicodeCaseFoldingToIndex = "\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x02\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x02\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x02\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x02\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x03\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x03\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x02\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x02\x02\x02\x02\x02\x01\x02\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x02\x03\x03\x03\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x02\x02\x02\x02\x02\x02\x02\x02\x02\x02\x02\x02\x02\x02\x02\x02\x02\x02\x02\x02\x02\x02\x02\x02\x02\x02\x02\x02\x02\x02\x02\x02\x02\x02\x02\x02\x02\x02\x02\x02\x02\x02\x02\x02\x02\x02\x02\x02\x02\x02\x02\x02\x03\x01\x01\x01\x01\x02\x01\x02\x02\x02\x02\x03\x01\x01\x01\x01\x02\x03\x03\x02\x03\x01\x01\x01\x01\x03\x03\x02\x02\x03\x01\x01\x01\x01\x01\x02\x02\x02\x02\x03\x01\x01\x01\x01\x02\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x02\x02\x02\x03\x03\x02\x02\x02\x02\x02\x02\x02\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01"
diff --git a/backend/goldmark/util/unicode_case_folding.go b/backend/goldmark/util/unicode_case_folding.go
new file mode 100644
index 0000000..ecc7e11
--- /dev/null
+++ b/backend/goldmark/util/unicode_case_folding.go
@@ -0,0 +1,17 @@
+package util
+
+//go:generate go run ../_tools unicode-case-folding-map -o ../_tools/unicode-case-folding-map.json
+//go:generate go run ../_tools emb-structs -i ../_tools/unicode-case-folding-map.json -o ./unicode_case_folding.gen.go
+
+var unicodeCaseFoldings map[rune][]rune
+
+func init() {
+ unicodeCaseFoldings = make(map[rune][]rune, _unicodeCaseFoldingLength)
+ cTo := 0
+ for i := range _unicodeCaseFoldingLength {
+ tTo := cTo + int(_unicodeCaseFoldingToIndex[i])
+ to := _unicodeCaseFoldingTo[cTo:tTo]
+ unicodeCaseFoldings[_unicodeCaseFoldingFrom[i]] = to
+ cTo = tTo
+ }
+}
diff --git a/backend/goldmark/util/util.go b/backend/goldmark/util/util.go
new file mode 100644
index 0000000..68a84bf
--- /dev/null
+++ b/backend/goldmark/util/util.go
@@ -0,0 +1,1044 @@
+// Package util provides utility functions for the goldmark.
+package util
+
+import (
+ "bytes"
+ "io"
+ "net/url"
+ "regexp"
+ "slices"
+ "sort"
+ "strconv"
+ "unicode"
+ "unicode/utf8"
+)
+
+// A CopyOnWriteBuffer is a byte buffer that copies buffer when
+// it need to be changed.
+type CopyOnWriteBuffer struct {
+ buffer []byte
+ copied bool
+}
+
+// NewCopyOnWriteBuffer returns a new CopyOnWriteBuffer.
+func NewCopyOnWriteBuffer(buffer []byte) CopyOnWriteBuffer {
+ return CopyOnWriteBuffer{
+ buffer: buffer,
+ copied: false,
+ }
+}
+
+// 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)
+ b.copied = true
+ }
+ 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) error {
+ if !b.copied {
+ b.buffer = make([]byte, 0, len(b.buffer)+20)
+ b.copied = true
+ }
+ b.buffer = append(b.buffer, c)
+ return nil
+}
+
+// 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
+}
+
+// IsCopied returns true if buffer has been copied, otherwise false.
+func (b *CopyOnWriteBuffer) IsCopied() bool {
+ return b.copied
+}
+
+// IsEscapedPunctuation returns true if character at a given index i
+// is an escaped punctuation, otherwise false.
+func IsEscapedPunctuation(source []byte, i int) bool {
+ return source[i] == '\\' && i < len(source)-1 && IsPunct(source[i+1])
+}
+
+// ReadWhile read the given source while pred is true.
+func ReadWhile(source []byte, index [2]int, pred func(byte) bool) (int, bool) {
+ j := index[0]
+ ok := false
+ for ; j < index[1]; j++ {
+ c1 := source[j]
+ if pred(c1) {
+ ok = true
+ continue
+ }
+ break
+ }
+ return j, ok
+}
+
+// IsBlank returns true if the given string is all space characters.
+func IsBlank(bs []byte) bool {
+ for _, b := range bs {
+ if !IsSpace(b) {
+ return false
+ }
+ }
+ return true
+}
+
+// VisualizeSpaces visualize invisible space characters.
+func VisualizeSpaces(bs []byte) []byte {
+ bs = bytes.ReplaceAll(bs, []byte(" "), []byte("[SPACE]"))
+ bs = bytes.ReplaceAll(bs, []byte("\t"), []byte("[TAB]"))
+ bs = bytes.ReplaceAll(bs, []byte("\n"), []byte("[NEWLINE]\n"))
+ bs = bytes.ReplaceAll(bs, []byte("\r"), []byte("[CR]"))
+ bs = bytes.ReplaceAll(bs, []byte("\v"), []byte("[VTAB]"))
+ bs = bytes.ReplaceAll(bs, []byte("\x00"), []byte("[NUL]"))
+ bs = bytes.ReplaceAll(bs, []byte("\ufffd"), []byte("[U+FFFD]"))
+ return bs
+}
+
+// TabWidth calculates actual width of a tab at the given position.
+func TabWidth(currentPos int) int {
+ return 4 - currentPos%4
+}
+
+// IndentPosition searches an indent position with the given width for the given line.
+// If the line contains tab characters, paddings may be not zero.
+// currentPos==0 and width==2:
+//
+// position: 0 1
+// [TAB]aaaa
+// width: 1234 5678
+//
+// width=2 is in the tab character. In this case, IndentPosition returns
+// (pos=1, padding=2).
+func IndentPosition(bs []byte, currentPos, width int) (pos, padding int) {
+ return IndentPositionPadding(bs, currentPos, 0, width)
+}
+
+// IndentPositionPadding searches an indent position with the given width for the given line.
+// This function is mostly same as IndentPosition except this function
+// takes account into additional paddings.
+func IndentPositionPadding(bs []byte, currentPos, paddingv, width int) (pos, padding int) {
+ if width == 0 {
+ return 0, paddingv
+ }
+ w := 0
+ i := 0
+ l := len(bs)
+ p := paddingv
+ for ; i < l; i++ {
+ if p > 0 {
+ p--
+ w++
+ continue
+ }
+ if bs[i] == '\t' && w < width {
+ w += TabWidth(currentPos + w)
+ } else if bs[i] == ' ' && w < width {
+ w++
+ } else {
+ break
+ }
+ }
+ if w >= width {
+ return i - paddingv, w - width
+ }
+ return -1, -1
+}
+
+// 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
+loop:
+ for ; i < l; i++ {
+ switch bs[i] {
+ case '\t':
+ w += TabWidth(currentPos + w)
+ case ' ':
+ w++
+ default:
+ break loop
+ }
+ }
+ 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)
+loop:
+ for ; i < l; i++ {
+ switch bs[i] {
+ case '\t':
+ w += TabWidth(currentPos + w)
+ case ' ':
+ w++
+ default:
+ break loop
+ }
+ }
+ if w >= width {
+ return i - paddingv, w - width
+ }
+ return i - paddingv, 0
+}
+
+// IndentWidth calculate an indent width for the given line.
+func IndentWidth(bs []byte, currentPos int) (width, pos int) {
+ for i := range len(bs) {
+ switch bs[i] {
+ case ' ':
+ width++
+ pos++
+ case '\t':
+ width += TabWidth(currentPos + width)
+ pos++
+ default:
+ return
+ }
+ }
+ return
+}
+
+// FirstNonSpacePosition returns a position line that is a first nonspace
+// character.
+func FirstNonSpacePosition(bs []byte) int {
+ i := 0
+ for ; i < len(bs); i++ {
+ c := bs[i]
+ if c == ' ' || c == '\t' {
+ continue
+ }
+ if c == '\n' {
+ return -1
+ }
+ return i
+ }
+ return -1
+}
+
+// FindClosure returns a position that closes the given opener.
+// If codeSpan is set true, it ignores characters in code spans.
+// If allowNesting is set true, closures correspond to nested opener will be
+// ignored.
+//
+// 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
+ codeSpanOpener := 0
+ for i < len(bs) {
+ c := bs[i]
+ if codeSpan && codeSpanOpener != 0 && c == '`' {
+ codeSpanCloser := 0
+ for ; i < len(bs); i++ {
+ if bs[i] == '`' {
+ codeSpanCloser++
+ } else {
+ i--
+ break
+ }
+ }
+ if codeSpanCloser == codeSpanOpener {
+ codeSpanOpener = 0
+ }
+ } else if codeSpanOpener == 0 && c == '\\' && i < len(bs)-1 && IsPunct(bs[i+1]) {
+ i += 2
+ continue
+ } else if codeSpan && codeSpanOpener == 0 && c == '`' {
+ for ; i < len(bs); i++ {
+ if bs[i] == '`' {
+ codeSpanOpener++
+ } else {
+ i--
+ break
+ }
+ }
+ } else if (codeSpan && codeSpanOpener == 0) || !codeSpan {
+ switch c {
+ case closure:
+ opened--
+ if opened == 0 {
+ return i
+ }
+ case opener:
+ if !allowNesting {
+ return -1
+ }
+ opened++
+ }
+ }
+ i++
+ }
+ return -1
+}
+
+// TrimLeft trims characters in the given s from head of the source.
+// bytes.TrimLeft offers same functionalities, but bytes.TrimLeft
+// allocates new buffer for the result.
+func TrimLeft(source, b []byte) []byte {
+ i := 0
+ for ; i < len(source); i++ {
+ c := source[i]
+ found := false
+ for j := range len(b) {
+ if c == b[j] {
+ found = true
+ break
+ }
+ }
+ if !found {
+ break
+ }
+ }
+ return source[i:]
+}
+
+// TrimRight trims characters in the given s from tail of the source.
+func TrimRight(source, b []byte) []byte {
+ i := len(source) - 1
+ for ; i >= 0; i-- {
+ c := source[i]
+ found := false
+ for j := range len(b) {
+ if c == b[j] {
+ found = true
+ break
+ }
+ }
+ if !found {
+ break
+ }
+ }
+ return source[:i+1]
+}
+
+// TrimLeftLength returns a length of leading specified characters.
+func TrimLeftLength(source, s []byte) int {
+ return len(source) - len(TrimLeft(source, s))
+}
+
+// TrimRightLength returns a length of trailing specified characters.
+func TrimRightLength(source, s []byte) int {
+ return len(source) - len(TrimRight(source, s))
+}
+
+// TrimLeftSpaceLength returns a length of leading space characters.
+func TrimLeftSpaceLength(source []byte) int {
+ i := 0
+ for ; i < len(source); i++ {
+ if !IsSpace(source[i]) {
+ break
+ }
+ }
+ return i
+}
+
+// TrimRightSpaceLength returns a length of trailing space characters.
+func TrimRightSpaceLength(source []byte) int {
+ l := len(source)
+ i := l - 1
+ for ; i >= 0; i-- {
+ if !IsSpace(source[i]) {
+ break
+ }
+ }
+ if i < 0 {
+ return l
+ }
+ return l - 1 - i
+}
+
+// TrimLeftSpace returns a subslice of the given string by slicing off all leading
+// space characters.
+func TrimLeftSpace(source []byte) []byte {
+ return TrimLeft(source, spaces)
+}
+
+// TrimRightSpace returns a subslice of the given string by slicing off all trailing
+// space characters.
+func TrimRightSpace(source []byte) []byte {
+ return TrimRight(source, spaces)
+}
+
+// DoFullUnicodeCaseFolding performs full unicode case folding to given bytes.
+func DoFullUnicodeCaseFolding(v []byte) []byte {
+ var rbuf []byte
+ cob := NewCopyOnWriteBuffer(v)
+ n := 0
+ for i := 0; i < len(v); i++ {
+ c := v[i]
+ if c < 0xb5 {
+ if c >= 0x41 && c <= 0x5a {
+ // A-Z to a-z
+ cob.Write(v[n:i])
+ _ = cob.WriteByte(c + 32)
+ n = i + 1
+ }
+ continue
+ }
+
+ if !utf8.RuneStart(c) {
+ continue
+ }
+ r, length := utf8.DecodeRune(v[i:])
+ if r == utf8.RuneError {
+ continue
+ }
+ folded, ok := unicodeCaseFoldings[r]
+ if !ok {
+ continue
+ }
+
+ cob.Write(v[n:i])
+ if rbuf == nil {
+ rbuf = make([]byte, 4)
+ }
+ for _, f := range folded {
+ l := utf8.EncodeRune(rbuf, f)
+ cob.Write(rbuf[:l])
+ }
+ i += length - 1
+ n = i + 1
+ }
+ if cob.IsCopied() {
+ cob.Write(v[n:])
+ }
+ return cob.Bytes()
+}
+
+// ReplaceSpaces replaces sequence of spaces with the given repl.
+func ReplaceSpaces(source []byte, repl byte) []byte {
+ var ret []byte
+ start := -1
+ for i, c := range source {
+ iss := IsSpace(c)
+ if start < 0 && iss {
+ start = i
+ continue
+ } else if start >= 0 && iss {
+ continue
+ } else if start >= 0 {
+ if ret == nil {
+ ret = make([]byte, 0, len(source))
+ ret = append(ret, source[:start]...)
+ }
+ ret = append(ret, repl)
+ start = -1
+ }
+ if ret != nil {
+ ret = append(ret, c)
+ }
+ }
+ if start >= 0 && ret != nil {
+ ret = append(ret, repl)
+ }
+ if ret == nil {
+ return source
+ }
+ return ret
+}
+
+// ToRune decode given bytes start at pos and returns a rune.
+func ToRune(source []byte, pos int) rune {
+ i := pos
+ for ; i >= 0; i-- {
+ if utf8.RuneStart(source[i]) {
+ break
+ }
+ }
+ r, _ := utf8.DecodeRune(source[i:])
+ return r
+}
+
+// ToValidRune returns 0xFFFD if the given rune is invalid, otherwise v.
+func ToValidRune(v rune) rune {
+ if v == 0 || !utf8.ValidRune(v) {
+ return rune(0xFFFD)
+ }
+ return v
+}
+
+// ToLinkReference converts given bytes into a valid link reference string.
+// ToLinkReference performs unicode case folding, trims leading and trailing spaces, converts into lower
+// case and replace spaces with a single space character.
+func ToLinkReference(v []byte) string {
+ v = TrimLeftSpace(v)
+ v = TrimRightSpace(v)
+ v = DoFullUnicodeCaseFolding(v)
+ return string(ReplaceSpaces(v, ' '))
+}
+
+var htmlQuote = []byte(""")
+var htmlAmp = []byte("&")
+var htmlLess = []byte("<")
+var htmlGreater = []byte(">")
+var htmlNull = []byte("\ufffd")
+
+var htmlEscapeTable = [256]*[]byte{&htmlNull, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, &htmlQuote, nil, nil, nil, &htmlAmp, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, &htmlLess, nil, &htmlGreater, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil} //nolint:golint,lll
+
+// EscapeHTMLByte returns HTML escaped bytes if the given byte should be escaped,
+// otherwise nil.
+func EscapeHTMLByte(b byte) []byte {
+ v := htmlEscapeTable[b]
+ if v != nil {
+ return *v
+ }
+ return nil
+}
+
+// EscapeHTML escapes characters that should be escaped in HTML text.
+func EscapeHTML(v []byte) []byte {
+ cob := NewCopyOnWriteBuffer(v)
+ n := 0
+ for i := range len(v) {
+ c := v[i]
+ escaped := htmlEscapeTable[c]
+ if escaped != nil {
+ cob.Write(v[n:i])
+ cob.Write(*escaped)
+ n = i + 1
+ }
+ }
+ if cob.IsCopied() {
+ cob.Write(v[n:])
+ }
+ return cob.Bytes()
+}
+
+// UnescapePunctuations unescapes blackslash escaped punctuations.
+func UnescapePunctuations(source []byte) []byte {
+ cob := NewCopyOnWriteBuffer(source)
+ limit := len(source)
+ n := 0
+ for i := 0; i < limit; {
+ c := source[i]
+ if i < limit-1 && c == '\\' && IsPunct(source[i+1]) {
+ cob.Write(source[n:i])
+ _ = cob.WriteByte(source[i+1])
+ i += 2
+ n = i
+ continue
+ }
+ i++
+ }
+ if cob.IsCopied() {
+ cob.Write(source[n:])
+ }
+ return cob.Bytes()
+}
+
+// ResolveNumericReferences resolve numeric references like 'Ӓ" .
+func ResolveNumericReferences(source []byte) []byte {
+ cob := NewCopyOnWriteBuffer(source)
+ buf := make([]byte, 6)
+ limit := len(source)
+ var ok bool
+ n := 0
+ for i := 0; i < limit; i++ {
+ if source[i] == '&' {
+ pos := i
+ next := i + 1
+ if next < limit && source[next] == '#' {
+ nnext := next + 1
+ if nnext < limit {
+ nc := source[nnext]
+ // code point like #x22;
+ if nnext < limit && nc == 'x' || nc == 'X' {
+ start := nnext + 1
+ i, ok = ReadWhile(source, [2]int{start, limit}, IsHexDecimal)
+ if ok && i < limit && source[i] == ';' {
+ v, _ := strconv.ParseUint(BytesToReadOnlyString(source[start:i]), 16, 32)
+ cob.Write(source[n:pos])
+ n = i + 1
+ runeSize := utf8.EncodeRune(buf, ToValidRune(rune(v)))
+ cob.Write(buf[:runeSize])
+ continue
+ }
+ // code point like #1234;
+ } else if nc >= '0' && nc <= '9' {
+ start := nnext
+ i, ok = ReadWhile(source, [2]int{start, limit}, IsNumeric)
+ if ok && i < limit && i-start < 8 && source[i] == ';' {
+ v, _ := strconv.ParseUint(BytesToReadOnlyString(source[start:i]), 0, 32)
+ cob.Write(source[n:pos])
+ n = i + 1
+ runeSize := utf8.EncodeRune(buf, ToValidRune(rune(v)))
+ cob.Write(buf[:runeSize])
+ continue
+ }
+ }
+ }
+ }
+ i = next - 1
+ }
+ }
+ if cob.IsCopied() {
+ cob.Write(source[n:])
+ }
+ return cob.Bytes()
+}
+
+// ResolveEntityNames resolve entity references like 'ö" .
+func ResolveEntityNames(source []byte) []byte {
+ cob := NewCopyOnWriteBuffer(source)
+ limit := len(source)
+ var ok bool
+ n := 0
+ for i := 0; i < limit; i++ {
+ if source[i] == '&' {
+ pos := i
+ next := i + 1
+ if !(next < limit && source[next] == '#') {
+ start := next
+ i, ok = ReadWhile(source, [2]int{start, limit}, IsAlphaNumeric)
+ if ok && i < limit && source[i] == ';' {
+ name := BytesToReadOnlyString(source[start:i])
+ entity, ok := LookUpHTML5EntityByName(name)
+ if ok {
+ cob.Write(source[n:pos])
+ n = i + 1
+ cob.Write(entity.Characters)
+ continue
+ }
+ }
+ }
+ i = next - 1
+ }
+ }
+ if cob.IsCopied() {
+ cob.Write(source[n:])
+ }
+ return cob.Bytes()
+}
+
+var htmlSpace = []byte("%20")
+
+// URLEscape escape the given URL.
+// If resolveReference is set true:
+// 1. unescape punctuations
+// 2. resolve numeric references
+// 3. resolve entity references
+//
+// URL encoded values (%xx) are kept as is.
+func URLEscape(v []byte, resolveReference bool) []byte {
+ if resolveReference {
+ v = UnescapePunctuations(v)
+ v = ResolveNumericReferences(v)
+ v = ResolveEntityNames(v)
+ }
+ cob := NewCopyOnWriteBuffer(v)
+ limit := len(v)
+ n := 0
+
+ for i := 0; i < limit; {
+ c := v[i]
+ if urlEscapeTable[c] == 1 {
+ i++
+ continue
+ }
+ if c == '%' && i+2 < limit && IsHexDecimal(v[i+1]) && IsHexDecimal(v[i+1]) {
+ i += 3
+ continue
+ }
+ u8len := utf8lenTable[c]
+ if u8len == 99 { // invalid utf8 leading byte, skip it
+ i++
+ continue
+ }
+ if c == ' ' {
+ cob.Write(v[n:i])
+ cob.Write(htmlSpace)
+ i++
+ n = i
+ continue
+ }
+ if int(u8len) > len(v) {
+ u8len = int8(len(v) - 1)
+ }
+ if u8len == 0 {
+ i++
+ n = i
+ continue
+ }
+ cob.Write(v[n:i])
+ stop := i + int(u8len)
+ if stop > len(v) {
+ i++
+ n = i
+ continue
+ }
+ cob.Write(StringToReadOnlyBytes(url.QueryEscape(string(v[i:stop]))))
+ i += int(u8len)
+ n = i
+ }
+ if cob.IsCopied() && n < limit {
+ cob.Write(v[n:])
+ }
+ return cob.Bytes()
+}
+
+// FindURLIndex returns a stop index value if the given bytes seem an URL.
+// This function is equivalent to [A-Za-z][A-Za-z0-9.+-]{1,31}:[^<>\x00-\x20]* .
+func FindURLIndex(b []byte) int {
+ i := 0
+ if !(len(b) > 0 && urlTable[b[i]]&7 == 7) {
+ return -1
+ }
+ i++
+ for ; i < len(b); i++ {
+ c := b[i]
+ if urlTable[c]&4 != 4 {
+ break
+ }
+ }
+ if i == 1 || i > 33 || i >= len(b) {
+ return -1
+ }
+ if b[i] != ':' {
+ return -1
+ }
+ i++
+ for ; i < len(b); i++ {
+ c := b[i]
+ if urlTable[c]&1 != 1 {
+ break
+ }
+ }
+ return i
+}
+
+var emailDomainRegexp = regexp.MustCompile(`^[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*`) //nolint:golint,lll
+
+// FindEmailIndex returns a stop index value if the given bytes seem an email address.
+func FindEmailIndex(b []byte) int {
+ // TODO: eliminate regexps
+ i := 0
+ for ; i < len(b); i++ {
+ c := b[i]
+ if emailTable[c]&1 != 1 {
+ break
+ }
+ }
+ if i == 0 {
+ return -1
+ }
+ if i >= len(b) || b[i] != '@' {
+ return -1
+ }
+ i++
+ if i >= len(b) {
+ return -1
+ }
+ match := emailDomainRegexp.FindSubmatchIndex(b[i:])
+ if match == nil {
+ return -1
+ }
+ return i + match[1]
+}
+
+var spaces = []byte(" \t\n\x0b\x0c\x0d")
+
+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} //nolint:golint,lll
+
+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} //nolint:golint,lll
+
+// a-zA-Z0-9, ;/?:@&=+$,-_.!~*'()#
+
+var urlEscapeTable = [256]int8{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0} //nolint:golint,lll
+
+var utf8lenTable = [256]int8{1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 4, 99, 99, 99, 99, 99, 99, 99, 99} //nolint:golint,lll
+
+var urlTable = [256]uint8{0, 0, 0, 0, 0, 0, 0, 0, 0, 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, 5, 1, 5, 5, 1, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 1, 1, 0, 1, 0, 1, 1, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 1, 1, 1, 1, 1, 1, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1} //nolint:golint,lll
+
+var emailTable = [256]uint8{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 1, 1, 1, 1, 0, 0, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 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, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0} //nolint:golint,lll
+
+// UTF8Len returns a byte length of the utf-8 character.
+func UTF8Len(b byte) int8 {
+ return utf8lenTable[b]
+}
+
+// IsPunct returns true if the given character is a punctuation, otherwise false.
+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 unicode.IsSymbol(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'
+}
+
+// IsHexDecimal returns true if the given character is a hexdecimal, otherwise false.
+func IsHexDecimal(c byte) bool {
+ return c >= '0' && c <= '9' || c >= 'a' && c <= 'f' || c >= 'A' && c <= 'F'
+}
+
+// IsAlphaNumeric returns true if the given character is a alphabet or a numeric, otherwise false.
+func IsAlphaNumeric(c byte) bool {
+ return c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z' || c >= '0' && c <= '9'
+}
+
+// A BufWriter is a subset of the bufio.Writer .
+type BufWriter interface {
+ io.Writer
+ Available() int
+ Buffered() int
+ Flush() error
+ WriteByte(c byte) error
+ WriteRune(r rune) (size int, err error)
+ WriteString(s string) (int, error)
+}
+
+// A PrioritizedValue struct holds pair of an arbitrary value and a priority.
+type PrioritizedValue struct {
+ // Value is an arbitrary value that you want to prioritize.
+ Value any
+ // Priority is a priority of the value.
+ Priority int
+}
+
+// PrioritizedSlice is a slice of the PrioritizedValues.
+type PrioritizedSlice []PrioritizedValue
+
+// Sort sorts the PrioritizedSlice in ascending order.
+func (s PrioritizedSlice) Sort() {
+ sort.Slice(s, func(i, j int) bool {
+ return s[i].Priority < s[j].Priority
+ })
+}
+
+// Remove removes the given value from this slice.
+func (s PrioritizedSlice) Remove(v any) PrioritizedSlice {
+ i := 0
+ found := false
+ for ; i < len(s); i++ {
+ if s[i].Value == v {
+ found = true
+ break
+ }
+ }
+ if !found {
+ return s
+ }
+ return slices.Delete(s, i, i+1)
+}
+
+// Prioritized returns a new PrioritizedValue.
+func Prioritized(v any, priority int) PrioritizedValue {
+ return PrioritizedValue{v, priority}
+}
+
+func bytesHash(b []byte) uint64 {
+ var hash uint64 = 5381
+ for _, c := range b {
+ hash = ((hash << 5) + hash) + uint64(c)
+ }
+ return hash
+}
+
+// BytesFilter is a efficient data structure for checking whether bytes exist or not.
+// BytesFilter is thread-safe.
+type BytesFilter interface {
+ // Add adds given bytes to this set.
+ Add([]byte)
+
+ // Contains return true if this set contains given bytes, otherwise false.
+ Contains([]byte) bool
+
+ // Extend copies this filter and adds given bytes to new filter.
+ Extend(...[]byte) BytesFilter
+
+ // ExtendString copies this filter and adds given bytes to new filter.
+ // Given string must be separated by a comma.
+ ExtendString(string) BytesFilter
+}
+
+type bytesFilter struct {
+ chars [256]uint8
+ threshold int
+ slots [][][]byte
+}
+
+// NewBytesFilter returns a new BytesFilter.
+func NewBytesFilter(elements ...[]byte) BytesFilter {
+ s := &bytesFilter{
+ threshold: 3,
+ slots: make([][][]byte, 64),
+ }
+ for _, element := range elements {
+ s.Add(element)
+ }
+ return s
+}
+
+// NewBytesFilterString returns a new BytesFilter.
+// Given string must be separated by a comma.
+func NewBytesFilterString(elements string) BytesFilter {
+ s := &bytesFilter{
+ threshold: 3,
+ slots: make([][][]byte, 64),
+ }
+ start := 0
+ for i := range len(elements) {
+ if elements[i] == ',' {
+ s.Add(StringToReadOnlyBytes(elements[start:i]))
+ start = i + 1
+ }
+ }
+ if start < len(elements) {
+ s.Add(StringToReadOnlyBytes(elements[start:]))
+ }
+ return s
+
+}
+
+func (s *bytesFilter) Add(b []byte) {
+ l := len(b)
+ m := min(l, s.threshold)
+ for i := range m {
+ s.chars[b[i]] |= 1 << uint8(i)
+ }
+ h := bytesHash(b) % uint64(len(s.slots))
+ slot := s.slots[h]
+ if slot == nil {
+ slot = [][]byte{}
+ }
+ s.slots[h] = append(slot, b)
+}
+
+func (s *bytesFilter) Extend(bs ...[]byte) BytesFilter {
+ newFilter := NewBytesFilter().(*bytesFilter)
+ newFilter.chars = s.chars
+ newFilter.threshold = s.threshold
+ for k, v := range s.slots {
+ newSlot := make([][]byte, len(v))
+ copy(newSlot, v)
+ newFilter.slots[k] = v
+ }
+ for _, b := range bs {
+ newFilter.Add(b)
+ }
+ return newFilter
+}
+
+func (s *bytesFilter) ExtendString(elements string) BytesFilter {
+ newFilter := NewBytesFilter().(*bytesFilter)
+ newFilter.chars = s.chars
+ newFilter.threshold = s.threshold
+ for k, v := range s.slots {
+ newSlot := make([][]byte, len(v))
+ copy(newSlot, v)
+ newFilter.slots[k] = v
+ }
+ start := 0
+ for i := range len(elements) {
+ if elements[i] == ',' {
+ newFilter.Add(StringToReadOnlyBytes(elements[start:i]))
+ start = i + 1
+ }
+ }
+ if start < len(elements) {
+ newFilter.Add(StringToReadOnlyBytes(elements[start:]))
+ }
+ return newFilter
+}
+
+func (s *bytesFilter) Contains(b []byte) bool {
+ l := len(b)
+ m := min(l, s.threshold)
+ for i := range m {
+ if (s.chars[b[i]] & (1 << uint8(i))) == 0 {
+ return false
+ }
+ }
+ h := bytesHash(b) % uint64(len(s.slots))
+ slot := s.slots[h]
+ if len(slot) == 0 {
+ return false
+ }
+ for _, element := range slot {
+ if bytes.Equal(element, b) {
+ return true
+ }
+ }
+ return false
+}
diff --git a/backend/goldmark/util/util_cjk.go b/backend/goldmark/util/util_cjk.go
new file mode 100644
index 0000000..d758107
--- /dev/null
+++ b/backend/goldmark/util/util_cjk.go
@@ -0,0 +1,469 @@
+package util
+
+import "unicode"
+
+var cjkRadicalsSupplement = &unicode.RangeTable{
+ R16: []unicode.Range16{
+ {0x2E80, 0x2EFF, 1},
+ },
+}
+
+var kangxiRadicals = &unicode.RangeTable{
+ R16: []unicode.Range16{
+ {0x2F00, 0x2FDF, 1},
+ },
+}
+
+var ideographicDescriptionCharacters = &unicode.RangeTable{
+ R16: []unicode.Range16{
+ {0x2FF0, 0x2FFF, 1},
+ },
+}
+
+var cjkSymbolsAndPunctuation = &unicode.RangeTable{
+ R16: []unicode.Range16{
+ {0x3000, 0x303F, 1},
+ },
+}
+
+var hiragana = &unicode.RangeTable{
+ R16: []unicode.Range16{
+ {0x3040, 0x309F, 1},
+ },
+}
+
+var katakana = &unicode.RangeTable{
+ R16: []unicode.Range16{
+ {0x30A0, 0x30FF, 1},
+ },
+}
+
+var kanbun = &unicode.RangeTable{
+ R16: []unicode.Range16{
+ {0x3130, 0x318F, 1},
+ {0x3190, 0x319F, 1},
+ },
+}
+
+var cjkStrokes = &unicode.RangeTable{
+ R16: []unicode.Range16{
+ {0x31C0, 0x31EF, 1},
+ },
+}
+
+var katakanaPhoneticExtensions = &unicode.RangeTable{
+ R16: []unicode.Range16{
+ {0x31F0, 0x31FF, 1},
+ },
+}
+
+var cjkCompatibility = &unicode.RangeTable{
+ R16: []unicode.Range16{
+ {0x3300, 0x33FF, 1},
+ },
+}
+
+var cjkUnifiedIdeographsExtensionA = &unicode.RangeTable{
+ R16: []unicode.Range16{
+ {0x3400, 0x4DBF, 1},
+ },
+}
+
+var cjkUnifiedIdeographs = &unicode.RangeTable{
+ R16: []unicode.Range16{
+ {0x4E00, 0x9FFF, 1},
+ },
+}
+
+var yiSyllables = &unicode.RangeTable{
+ R16: []unicode.Range16{
+ {0xA000, 0xA48F, 1},
+ },
+}
+
+var yiRadicals = &unicode.RangeTable{
+ R16: []unicode.Range16{
+ {0xA490, 0xA4CF, 1},
+ },
+}
+
+var cjkCompatibilityIdeographs = &unicode.RangeTable{
+ R16: []unicode.Range16{
+ {0xF900, 0xFAFF, 1},
+ },
+}
+
+var verticalForms = &unicode.RangeTable{
+ R16: []unicode.Range16{
+ {0xFE10, 0xFE1F, 1},
+ },
+}
+
+var cjkCompatibilityForms = &unicode.RangeTable{
+ R16: []unicode.Range16{
+ {0xFE30, 0xFE4F, 1},
+ },
+}
+
+var smallFormVariants = &unicode.RangeTable{
+ R16: []unicode.Range16{
+ {0xFE50, 0xFE6F, 1},
+ },
+}
+
+var halfwidthAndFullwidthForms = &unicode.RangeTable{
+ R16: []unicode.Range16{
+ {0xFF00, 0xFFEF, 1},
+ },
+}
+
+var kanaSupplement = &unicode.RangeTable{
+ R32: []unicode.Range32{
+ {0x1B000, 0x1B0FF, 1},
+ },
+}
+
+var kanaExtendedA = &unicode.RangeTable{
+ R32: []unicode.Range32{
+ {0x1B100, 0x1B12F, 1},
+ },
+}
+
+var smallKanaExtension = &unicode.RangeTable{
+ R32: []unicode.Range32{
+ {0x1B130, 0x1B16F, 1},
+ },
+}
+
+var cjkUnifiedIdeographsExtensionB = &unicode.RangeTable{
+ R32: []unicode.Range32{
+ {0x20000, 0x2A6DF, 1},
+ },
+}
+
+var cjkUnifiedIdeographsExtensionC = &unicode.RangeTable{
+ R32: []unicode.Range32{
+ {0x2A700, 0x2B73F, 1},
+ },
+}
+
+var cjkUnifiedIdeographsExtensionD = &unicode.RangeTable{
+ R32: []unicode.Range32{
+ {0x2B740, 0x2B81F, 1},
+ },
+}
+
+var cjkUnifiedIdeographsExtensionE = &unicode.RangeTable{
+ R32: []unicode.Range32{
+ {0x2B820, 0x2CEAF, 1},
+ },
+}
+
+var cjkUnifiedIdeographsExtensionF = &unicode.RangeTable{
+ R32: []unicode.Range32{
+ {0x2CEB0, 0x2EBEF, 1},
+ },
+}
+
+var cjkCompatibilityIdeographsSupplement = &unicode.RangeTable{
+ R32: []unicode.Range32{
+ {0x2F800, 0x2FA1F, 1},
+ },
+}
+
+var cjkUnifiedIdeographsExtensionG = &unicode.RangeTable{
+ R32: []unicode.Range32{
+ {0x30000, 0x3134F, 1},
+ },
+}
+
+// IsEastAsianWideRune returns trhe if the given rune is an east asian wide character, otherwise false.
+func IsEastAsianWideRune(r rune) bool {
+ return unicode.Is(unicode.Hiragana, r) ||
+ unicode.Is(unicode.Katakana, r) ||
+ unicode.Is(unicode.Han, r) ||
+ unicode.Is(unicode.Lm, r) ||
+ unicode.Is(unicode.Hangul, r) ||
+ unicode.Is(cjkSymbolsAndPunctuation, r)
+}
+
+// IsSpaceDiscardingUnicodeRune returns true if the given rune is space-discarding unicode character, otherwise false.
+// See https://www.w3.org/TR/2020/WD-css-text-3-20200429/#space-discard-set
+func IsSpaceDiscardingUnicodeRune(r rune) bool {
+ return unicode.Is(cjkRadicalsSupplement, r) ||
+ unicode.Is(kangxiRadicals, r) ||
+ unicode.Is(ideographicDescriptionCharacters, r) ||
+ unicode.Is(cjkSymbolsAndPunctuation, r) ||
+ unicode.Is(hiragana, r) ||
+ unicode.Is(katakana, r) ||
+ unicode.Is(kanbun, r) ||
+ unicode.Is(cjkStrokes, r) ||
+ unicode.Is(katakanaPhoneticExtensions, r) ||
+ unicode.Is(cjkCompatibility, r) ||
+ unicode.Is(cjkUnifiedIdeographsExtensionA, r) ||
+ unicode.Is(cjkUnifiedIdeographs, r) ||
+ unicode.Is(yiSyllables, r) ||
+ unicode.Is(yiRadicals, r) ||
+ unicode.Is(cjkCompatibilityIdeographs, r) ||
+ unicode.Is(verticalForms, r) ||
+ unicode.Is(cjkCompatibilityForms, r) ||
+ unicode.Is(smallFormVariants, r) ||
+ unicode.Is(halfwidthAndFullwidthForms, r) ||
+ unicode.Is(kanaSupplement, r) ||
+ unicode.Is(kanaExtendedA, r) ||
+ unicode.Is(smallKanaExtension, r) ||
+ unicode.Is(cjkUnifiedIdeographsExtensionB, r) ||
+ unicode.Is(cjkUnifiedIdeographsExtensionC, r) ||
+ unicode.Is(cjkUnifiedIdeographsExtensionD, r) ||
+ unicode.Is(cjkUnifiedIdeographsExtensionE, r) ||
+ unicode.Is(cjkUnifiedIdeographsExtensionF, r) ||
+ unicode.Is(cjkCompatibilityIdeographsSupplement, r) ||
+ unicode.Is(cjkUnifiedIdeographsExtensionG, r)
+}
+
+// EastAsianWidth returns the east asian width of the given rune.
+// See https://www.unicode.org/reports/tr11/tr11-36.html
+func EastAsianWidth(r rune) string {
+ switch {
+ case r == 0x3000,
+ (0xFF01 <= r && r <= 0xFF60),
+ (0xFFE0 <= r && r <= 0xFFE6):
+ return "F"
+
+ case r == 0x20A9,
+ (0xFF61 <= r && r <= 0xFFBE),
+ (0xFFC2 <= r && r <= 0xFFC7),
+ (0xFFCA <= r && r <= 0xFFCF),
+ (0xFFD2 <= r && r <= 0xFFD7),
+ (0xFFDA <= r && r <= 0xFFDC),
+ (0xFFE8 <= r && r <= 0xFFEE):
+ return "H"
+
+ case (0x1100 <= r && r <= 0x115F),
+ (0x11A3 <= r && r <= 0x11A7),
+ (0x11FA <= r && r <= 0x11FF),
+ (0x2329 <= r && r <= 0x232A),
+ (0x2E80 <= r && r <= 0x2E99),
+ (0x2E9B <= r && r <= 0x2EF3),
+ (0x2F00 <= r && r <= 0x2FD5),
+ (0x2FF0 <= r && r <= 0x2FFB),
+ (0x3001 <= r && r <= 0x303E),
+ (0x3041 <= r && r <= 0x3096),
+ (0x3099 <= r && r <= 0x30FF),
+ (0x3105 <= r && r <= 0x312D),
+ (0x3131 <= r && r <= 0x318E),
+ (0x3190 <= r && r <= 0x31BA),
+ (0x31C0 <= r && r <= 0x31E3),
+ (0x31F0 <= r && r <= 0x321E),
+ (0x3220 <= r && r <= 0x3247),
+ (0x3250 <= r && r <= 0x32FE),
+ (0x3300 <= r && r <= 0x4DBF),
+ (0x4E00 <= r && r <= 0xA48C),
+ (0xA490 <= r && r <= 0xA4C6),
+ (0xA960 <= r && r <= 0xA97C),
+ (0xAC00 <= r && r <= 0xD7A3),
+ (0xD7B0 <= r && r <= 0xD7C6),
+ (0xD7CB <= r && r <= 0xD7FB),
+ (0xF900 <= r && r <= 0xFAFF),
+ (0xFE10 <= r && r <= 0xFE19),
+ (0xFE30 <= r && r <= 0xFE52),
+ (0xFE54 <= r && r <= 0xFE66),
+ (0xFE68 <= r && r <= 0xFE6B),
+ (0x1B000 <= r && r <= 0x1B001),
+ (0x1F200 <= r && r <= 0x1F202),
+ (0x1F210 <= r && r <= 0x1F23A),
+ (0x1F240 <= r && r <= 0x1F248),
+ (0x1F250 <= r && r <= 0x1F251),
+ (0x20000 <= r && r <= 0x2F73F),
+ (0x2B740 <= r && r <= 0x2FFFD),
+ (0x30000 <= r && r <= 0x3FFFD):
+ return "W"
+
+ case (0x0020 <= r && r <= 0x007E),
+ (0x00A2 <= r && r <= 0x00A3),
+ (0x00A5 <= r && r <= 0x00A6),
+ r == 0x00AC,
+ r == 0x00AF,
+ (0x27E6 <= r && r <= 0x27ED),
+ (0x2985 <= r && r <= 0x2986):
+ return "Na"
+
+ case (0x00A1 == r),
+ (0x00A4 == r),
+ (0x00A7 <= r && r <= 0x00A8),
+ (0x00AA == r),
+ (0x00AD <= r && r <= 0x00AE),
+ (0x00B0 <= r && r <= 0x00B4),
+ (0x00B6 <= r && r <= 0x00BA),
+ (0x00BC <= r && r <= 0x00BF),
+ (0x00C6 == r),
+ (0x00D0 == r),
+ (0x00D7 <= r && r <= 0x00D8),
+ (0x00DE <= r && r <= 0x00E1),
+ (0x00E6 == r),
+ (0x00E8 <= r && r <= 0x00EA),
+ (0x00EC <= r && r <= 0x00ED),
+ (0x00F0 == r),
+ (0x00F2 <= r && r <= 0x00F3),
+ (0x00F7 <= r && r <= 0x00FA),
+ (0x00FC == r),
+ (0x00FE == r),
+ (0x0101 == r),
+ (0x0111 == r),
+ (0x0113 == r),
+ (0x011B == r),
+ (0x0126 <= r && r <= 0x0127),
+ (0x012B == r),
+ (0x0131 <= r && r <= 0x0133),
+ (0x0138 == r),
+ (0x013F <= r && r <= 0x0142),
+ (0x0144 == r),
+ (0x0148 <= r && r <= 0x014B),
+ (0x014D == r),
+ (0x0152 <= r && r <= 0x0153),
+ (0x0166 <= r && r <= 0x0167),
+ (0x016B == r),
+ (0x01CE == r),
+ (0x01D0 == r),
+ (0x01D2 == r),
+ (0x01D4 == r),
+ (0x01D6 == r),
+ (0x01D8 == r),
+ (0x01DA == r),
+ (0x01DC == r),
+ (0x0251 == r),
+ (0x0261 == r),
+ (0x02C4 == r),
+ (0x02C7 == r),
+ (0x02C9 <= r && r <= 0x02CB),
+ (0x02CD == r),
+ (0x02D0 == r),
+ (0x02D8 <= r && r <= 0x02DB),
+ (0x02DD == r),
+ (0x02DF == r),
+ (0x0300 <= r && r <= 0x036F),
+ (0x0391 <= r && r <= 0x03A1),
+ (0x03A3 <= r && r <= 0x03A9),
+ (0x03B1 <= r && r <= 0x03C1),
+ (0x03C3 <= r && r <= 0x03C9),
+ (0x0401 == r),
+ (0x0410 <= r && r <= 0x044F),
+ (0x0451 == r),
+ (0x2010 == r),
+ (0x2013 <= r && r <= 0x2016),
+ (0x2018 <= r && r <= 0x2019),
+ (0x201C <= r && r <= 0x201D),
+ (0x2020 <= r && r <= 0x2022),
+ (0x2024 <= r && r <= 0x2027),
+ (0x2030 == r),
+ (0x2032 <= r && r <= 0x2033),
+ (0x2035 == r),
+ (0x203B == r),
+ (0x203E == r),
+ (0x2074 == r),
+ (0x207F == r),
+ (0x2081 <= r && r <= 0x2084),
+ (0x20AC == r),
+ (0x2103 == r),
+ (0x2105 == r),
+ (0x2109 == r),
+ (0x2113 == r),
+ (0x2116 == r),
+ (0x2121 <= r && r <= 0x2122),
+ (0x2126 == r),
+ (0x212B == r),
+ (0x2153 <= r && r <= 0x2154),
+ (0x215B <= r && r <= 0x215E),
+ (0x2160 <= r && r <= 0x216B),
+ (0x2170 <= r && r <= 0x2179),
+ (0x2189 == r),
+ (0x2190 <= r && r <= 0x2199),
+ (0x21B8 <= r && r <= 0x21B9),
+ (0x21D2 == r),
+ (0x21D4 == r),
+ (0x21E7 == r),
+ (0x2200 == r),
+ (0x2202 <= r && r <= 0x2203),
+ (0x2207 <= r && r <= 0x2208),
+ (0x220B == r),
+ (0x220F == r),
+ (0x2211 == r),
+ (0x2215 == r),
+ (0x221A == r),
+ (0x221D <= r && r <= 0x2220),
+ (0x2223 == r),
+ (0x2225 == r),
+ (0x2227 <= r && r <= 0x222C),
+ (0x222E == r),
+ (0x2234 <= r && r <= 0x2237),
+ (0x223C <= r && r <= 0x223D),
+ (0x2248 == r),
+ (0x224C == r),
+ (0x2252 == r),
+ (0x2260 <= r && r <= 0x2261),
+ (0x2264 <= r && r <= 0x2267),
+ (0x226A <= r && r <= 0x226B),
+ (0x226E <= r && r <= 0x226F),
+ (0x2282 <= r && r <= 0x2283),
+ (0x2286 <= r && r <= 0x2287),
+ (0x2295 == r),
+ (0x2299 == r),
+ (0x22A5 == r),
+ (0x22BF == r),
+ (0x2312 == r),
+ (0x2460 <= r && r <= 0x24E9),
+ (0x24EB <= r && r <= 0x254B),
+ (0x2550 <= r && r <= 0x2573),
+ (0x2580 <= r && r <= 0x258F),
+ (0x2592 <= r && r <= 0x2595),
+ (0x25A0 <= r && r <= 0x25A1),
+ (0x25A3 <= r && r <= 0x25A9),
+ (0x25B2 <= r && r <= 0x25B3),
+ (0x25B6 <= r && r <= 0x25B7),
+ (0x25BC <= r && r <= 0x25BD),
+ (0x25C0 <= r && r <= 0x25C1),
+ (0x25C6 <= r && r <= 0x25C8),
+ (0x25CB == r),
+ (0x25CE <= r && r <= 0x25D1),
+ (0x25E2 <= r && r <= 0x25E5),
+ (0x25EF == r),
+ (0x2605 <= r && r <= 0x2606),
+ (0x2609 == r),
+ (0x260E <= r && r <= 0x260F),
+ (0x2614 <= r && r <= 0x2615),
+ (0x261C == r),
+ (0x261E == r),
+ (0x2640 == r),
+ (0x2642 == r),
+ (0x2660 <= r && r <= 0x2661),
+ (0x2663 <= r && r <= 0x2665),
+ (0x2667 <= r && r <= 0x266A),
+ (0x266C <= r && r <= 0x266D),
+ (0x266F == r),
+ (0x269E <= r && r <= 0x269F),
+ (0x26BE <= r && r <= 0x26BF),
+ (0x26C4 <= r && r <= 0x26CD),
+ (0x26CF <= r && r <= 0x26E1),
+ (0x26E3 == r),
+ (0x26E8 <= r && r <= 0x26FF),
+ (0x273D == r),
+ (0x2757 == r),
+ (0x2776 <= r && r <= 0x277F),
+ (0x2B55 <= r && r <= 0x2B59),
+ (0x3248 <= r && r <= 0x324F),
+ (0xE000 <= r && r <= 0xF8FF),
+ (0xFE00 <= r && r <= 0xFE0F),
+ (0xFFFD == r),
+ (0x1F100 <= r && r <= 0x1F10A),
+ (0x1F110 <= r && r <= 0x1F12D),
+ (0x1F130 <= r && r <= 0x1F169),
+ (0x1F170 <= r && r <= 0x1F19A),
+ (0xE0100 <= r && r <= 0xE01EF),
+ (0xF0000 <= r && r <= 0xFFFFD),
+ (0x100000 <= r && r <= 0x10FFFD):
+ return "A"
+
+ default:
+ return "N"
+ }
+}
diff --git a/backend/goldmark/util/util_safe.go b/backend/goldmark/util/util_safe.go
new file mode 100644
index 0000000..2f6a3fe
--- /dev/null
+++ b/backend/goldmark/util/util_safe.go
@@ -0,0 +1,14 @@
+//go:build appengine || js
+// +build appengine js
+
+package util
+
+// BytesToReadOnlyString returns a string converted from given bytes.
+func BytesToReadOnlyString(b []byte) string {
+ return string(b)
+}
+
+// StringToReadOnlyBytes returns bytes converted from given string.
+func StringToReadOnlyBytes(s string) []byte {
+ return []byte(s)
+}
diff --git a/backend/goldmark/util/util_unsafe_go120.go b/backend/goldmark/util/util_unsafe_go120.go
new file mode 100644
index 0000000..d6be534
--- /dev/null
+++ b/backend/goldmark/util/util_unsafe_go120.go
@@ -0,0 +1,24 @@
+//go:build !appengine && !js && !go1.21
+// +build !appengine,!js,!go1.21
+
+package util
+
+import (
+ "reflect"
+ "unsafe"
+)
+
+// BytesToReadOnlyString returns a string converted from given bytes.
+func BytesToReadOnlyString(b []byte) string {
+ return *(*string)(unsafe.Pointer(&b))
+}
+
+// StringToReadOnlyBytes returns bytes converted from given string.
+func StringToReadOnlyBytes(s string) (bs []byte) {
+ sh := (*reflect.StringHeader)(unsafe.Pointer(&s))
+ bh := (*reflect.SliceHeader)(unsafe.Pointer(&bs))
+ bh.Data = sh.Data
+ bh.Cap = sh.Len
+ bh.Len = sh.Len
+ return
+}
diff --git a/backend/goldmark/util/util_unsafe_go121.go b/backend/goldmark/util/util_unsafe_go121.go
new file mode 100644
index 0000000..50c7fce
--- /dev/null
+++ b/backend/goldmark/util/util_unsafe_go121.go
@@ -0,0 +1,18 @@
+//go:build !appengine && !js && go1.21
+// +build !appengine,!js,go1.21
+
+package util
+
+import (
+ "unsafe"
+)
+
+// BytesToReadOnlyString returns a string converted from given bytes.
+func BytesToReadOnlyString(b []byte) string {
+ return unsafe.String(unsafe.SliceData(b), len(b))
+}
+
+// StringToReadOnlyBytes returns bytes converted from given string.
+func StringToReadOnlyBytes(s string) []byte {
+ return unsafe.Slice(unsafe.StringData(s), len(s))
+}