@farvardin/lezer-parser-markdown 1.6.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +279 -0
- package/LICENSE +21 -0
- package/README.md +719 -0
- package/bin/build-readme.cjs +39 -0
- package/build.js +16 -0
- package/dist/index.cjs +2357 -0
- package/dist/index.d.cts +600 -0
- package/dist/index.d.ts +600 -0
- package/dist/index.js +2340 -0
- package/package.json +37 -0
- package/publish.sh +1 -0
- package/src/README.md +83 -0
- package/src/extension.ts +301 -0
- package/src/index.ts +5 -0
- package/src/markdown.ts +1966 -0
- package/src/nest.ts +46 -0
- package/test/compare-tree.ts +14 -0
- package/test/spec.ts +79 -0
- package/test/test-extension.ts +277 -0
- package/test/test-incremental.ts +265 -0
- package/test/test-markdown.ts +3574 -0
- package/test/test-nesting.ts +86 -0
- package/test/tsconfig.json +12 -0
- package/tsconfig.json +14 -0
package/src/nest.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import {SyntaxNode, Parser, Input, parseMixed, SyntaxNodeRef} from "@lezer/common"
|
|
2
|
+
import {Type, MarkdownExtension} from "./markdown"
|
|
3
|
+
|
|
4
|
+
function leftOverSpace(node: SyntaxNode, from: number, to: number) {
|
|
5
|
+
let ranges = []
|
|
6
|
+
for (let n = node.firstChild, pos = from;; n = n.nextSibling) {
|
|
7
|
+
let nextPos = n ? n.from : to
|
|
8
|
+
if (nextPos > pos) ranges.push({from: pos, to: nextPos})
|
|
9
|
+
if (!n) break
|
|
10
|
+
pos = n.to
|
|
11
|
+
}
|
|
12
|
+
return ranges
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/// Create a Markdown extension to enable nested parsing on code
|
|
16
|
+
/// blocks and/or embedded HTML.
|
|
17
|
+
export function parseCode(config: {
|
|
18
|
+
/// When provided, this will be used to parse the content of code
|
|
19
|
+
/// blocks. `info` is the string after the opening ` ``` ` marker,
|
|
20
|
+
/// or the empty string if there is no such info or this is an
|
|
21
|
+
/// indented code block. If there is a parser available for the
|
|
22
|
+
/// code, it should return a function that can construct the
|
|
23
|
+
/// [parse](https://lezer.codemirror.net/docs/ref/#common.PartialParse).
|
|
24
|
+
codeParser?: (info: string) => null | Parser
|
|
25
|
+
/// The parser used to parse HTML tags (both block and inline).
|
|
26
|
+
htmlParser?: Parser,
|
|
27
|
+
}): MarkdownExtension {
|
|
28
|
+
let {codeParser, htmlParser} = config
|
|
29
|
+
let wrap = parseMixed((node: SyntaxNodeRef, input: Input) => {
|
|
30
|
+
let id = node.type.id
|
|
31
|
+
if (codeParser && (id == Type.CodeBlock || id == Type.FencedCode)) {
|
|
32
|
+
let info = ""
|
|
33
|
+
if (id == Type.FencedCode) {
|
|
34
|
+
let infoNode = node.node.getChild(Type.CodeInfo)
|
|
35
|
+
if (infoNode) info = input.read(infoNode.from, infoNode.to)
|
|
36
|
+
}
|
|
37
|
+
let parser = codeParser(info)
|
|
38
|
+
if (parser)
|
|
39
|
+
return {parser, overlay: node => node.type.id == Type.CodeText, bracketed: id == Type.FencedCode}
|
|
40
|
+
} else if (htmlParser && (id == Type.HTMLBlock || id == Type.HTMLTag || id == Type.CommentBlock)) {
|
|
41
|
+
return {parser: htmlParser, overlay: leftOverSpace(node.node, node.from, node.to)}
|
|
42
|
+
}
|
|
43
|
+
return null
|
|
44
|
+
})
|
|
45
|
+
return {wrap}
|
|
46
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import {Tree} from "@lezer/common"
|
|
2
|
+
|
|
3
|
+
export function compareTree(a: Tree, b: Tree) {
|
|
4
|
+
let curA = a.cursor(), curB = b.cursor()
|
|
5
|
+
for (;;) {
|
|
6
|
+
let mismatch = null, next = false
|
|
7
|
+
if (curA.type != curB.type) mismatch = `Node type mismatch (${curA.name} vs ${curB.name})`
|
|
8
|
+
else if (curA.from != curB.from) mismatch = `Start pos mismatch for ${curA.name}: ${curA.from} vs ${curB.from}`
|
|
9
|
+
else if (curA.to != curB.to) mismatch = `End pos mismatch for ${curA.name}: ${curA.to} vs ${curB.to}`
|
|
10
|
+
else if ((next = curA.next()) != curB.next()) mismatch = `Tree size mismatch`
|
|
11
|
+
if (mismatch) throw new Error(`${mismatch}\n ${a}\n ${b}`)
|
|
12
|
+
if (!next) break
|
|
13
|
+
}
|
|
14
|
+
}
|
package/test/spec.ts
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import {Tree} from "@lezer/common"
|
|
2
|
+
import {MarkdownParser} from ".."
|
|
3
|
+
|
|
4
|
+
const abbrev: {[abbr: string]: string} = {
|
|
5
|
+
__proto__: null as any,
|
|
6
|
+
CB: "CodeBlock",
|
|
7
|
+
FC: "FencedCode",
|
|
8
|
+
Q: "Blockquote",
|
|
9
|
+
HR: "HorizontalRule",
|
|
10
|
+
BL: "BulletList",
|
|
11
|
+
OL: "OrderedList",
|
|
12
|
+
LI: "ListItem",
|
|
13
|
+
H1: "ATXHeading1",
|
|
14
|
+
H2: "ATXHeading2",
|
|
15
|
+
H3: "ATXHeading3",
|
|
16
|
+
H4: "ATXHeading4",
|
|
17
|
+
H5: "ATXHeading5",
|
|
18
|
+
H6: "ATXHeading6",
|
|
19
|
+
SH1: "SetextHeading1",
|
|
20
|
+
SH2: "SetextHeading2",
|
|
21
|
+
HB: "HTMLBlock",
|
|
22
|
+
PI: "ProcessingInstructionBlock",
|
|
23
|
+
CMB: "CommentBlock",
|
|
24
|
+
LR: "LinkReference",
|
|
25
|
+
P: "Paragraph",
|
|
26
|
+
Esc: "Escape",
|
|
27
|
+
Ent: "Entity",
|
|
28
|
+
BR: "HardBreak",
|
|
29
|
+
Em: "Emphasis",
|
|
30
|
+
St: "StrongEmphasis",
|
|
31
|
+
Ln: "Link",
|
|
32
|
+
Al: "Autolink",
|
|
33
|
+
Im: "Image",
|
|
34
|
+
C: "InlineCode",
|
|
35
|
+
HT: "HTMLTag",
|
|
36
|
+
CM: "Comment",
|
|
37
|
+
Pi: "ProcessingInstruction",
|
|
38
|
+
h: "HeaderMark",
|
|
39
|
+
q: "QuoteMark",
|
|
40
|
+
l: "ListMark",
|
|
41
|
+
L: "LinkMark",
|
|
42
|
+
e: "EmphasisMark",
|
|
43
|
+
c: "CodeMark",
|
|
44
|
+
cI: "CodeInfo",
|
|
45
|
+
cT: "CodeText",
|
|
46
|
+
LT: "LinkTitle",
|
|
47
|
+
LL: "LinkLabel"
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export class SpecParser {
|
|
51
|
+
constructor(readonly parser: MarkdownParser, readonly localAbbrev?: {[name: string]: string}) {}
|
|
52
|
+
|
|
53
|
+
type(name: string) {
|
|
54
|
+
name = (this.localAbbrev && this.localAbbrev[name]) || abbrev[name] || name
|
|
55
|
+
return this.parser.nodeSet.types.find(t => t.name == name)?.id
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
parse(spec: string, specName: string) {
|
|
59
|
+
let doc = "", buffer = [], stack: number[] = []
|
|
60
|
+
for (let pos = 0; pos < spec.length; pos++) {
|
|
61
|
+
let ch = spec[pos]
|
|
62
|
+
if (ch == "{") {
|
|
63
|
+
let name = /^(\w+):/.exec(spec.slice(pos + 1)), tag = name && this.type(name[1])
|
|
64
|
+
if (tag == null) throw new Error(`Invalid node opening mark at ${pos} in ${specName}`)
|
|
65
|
+
pos += name![0].length
|
|
66
|
+
stack.push(tag, doc.length, buffer.length)
|
|
67
|
+
} else if (ch == "}") {
|
|
68
|
+
if (!stack.length) throw new Error(`Mismatched node close mark at ${pos} in ${specName}`)
|
|
69
|
+
let bufStart = stack.pop()!, from = stack.pop()!, type = stack.pop()!
|
|
70
|
+
buffer.push(type, from, doc.length, 4 + buffer.length - bufStart)
|
|
71
|
+
} else {
|
|
72
|
+
doc += ch
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
if (stack.length) throw new Error(`Unclosed node in ${specName}`)
|
|
76
|
+
return {tree: Tree.build({buffer, nodeSet: this.parser.nodeSet, topID: this.type("Document")!, length: doc.length}), doc}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
import {parser as cmParser, GFM, Subscript, Superscript, Emoji} from "../dist/index.js"
|
|
2
|
+
import {compareTree} from "./compare-tree.js"
|
|
3
|
+
import {SpecParser} from "./spec.js"
|
|
4
|
+
|
|
5
|
+
const parser = cmParser.configure([GFM, Subscript, Superscript, Emoji])
|
|
6
|
+
|
|
7
|
+
const specParser = new SpecParser(parser, {
|
|
8
|
+
__proto__: null as any,
|
|
9
|
+
Th: "Strikethrough",
|
|
10
|
+
tm: "StrikethroughMark",
|
|
11
|
+
TB: "Table",
|
|
12
|
+
TH: "TableHeader",
|
|
13
|
+
TR: "TableRow",
|
|
14
|
+
TC: "TableCell",
|
|
15
|
+
tb: "TableDelimiter",
|
|
16
|
+
T: "Task",
|
|
17
|
+
t: "TaskMarker",
|
|
18
|
+
Sub: "Subscript",
|
|
19
|
+
sub: "SubscriptMark",
|
|
20
|
+
Sup: "Superscript",
|
|
21
|
+
sup: "SuperscriptMark",
|
|
22
|
+
ji: "Emoji"
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
function test(name: string, spec: string, p = parser) {
|
|
26
|
+
it(name, () => {
|
|
27
|
+
let {tree, doc} = specParser.parse(spec, name)
|
|
28
|
+
compareTree(p.parse(doc), tree)
|
|
29
|
+
})
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
describe("Extension", () => {
|
|
33
|
+
test("Tables (example 198)", `
|
|
34
|
+
{TB:{TH:{tb:|} {TC:foo} {tb:|} {TC:bar} {tb:|}}
|
|
35
|
+
{tb:| --- | --- |}
|
|
36
|
+
{TR:{tb:|} {TC:baz} {tb:|} {TC:bim} {tb:|}}}`)
|
|
37
|
+
|
|
38
|
+
test("Tables (example 199)", `
|
|
39
|
+
{TB:{TH:{tb:|} {TC:abc} {tb:|} {TC:defghi} {tb:|}}
|
|
40
|
+
{tb::-: | -----------:}
|
|
41
|
+
{TR:{TC:bar} {tb:|} {TC:baz}}}`)
|
|
42
|
+
|
|
43
|
+
test("Tables (example 200)", `
|
|
44
|
+
{TB:{TH:{tb:|} {TC:f{Esc:\\|}oo} {tb:|}}
|
|
45
|
+
{tb:| ------ |}
|
|
46
|
+
{TR:{tb:|} {TC:b {C:{c:\`}\\|{c:\`}} az} {tb:|}}
|
|
47
|
+
{TR:{tb:|} {TC:b {St:{e:**}{Esc:\\|}{e:**}} im} {tb:|}}}`)
|
|
48
|
+
|
|
49
|
+
test("Tables (example 201)", `
|
|
50
|
+
{TB:{TH:{tb:|} {TC:abc} {tb:|} {TC:def} {tb:|}}
|
|
51
|
+
{tb:| --- | --- |}
|
|
52
|
+
{TR:{tb:|} {TC:bar} {tb:|} {TC:baz} {tb:|}}}
|
|
53
|
+
{Q:{q:>} {P:bar}}`)
|
|
54
|
+
|
|
55
|
+
test("Tables (example 202)", `
|
|
56
|
+
{TB:{TH:{tb:|} {TC:abc} {tb:|} {TC:def} {tb:|}}
|
|
57
|
+
{tb:| --- | --- |}
|
|
58
|
+
{TR:{tb:|} {TC:bar} {tb:|} {TC:baz} {tb:|}}
|
|
59
|
+
{TR:{TC:bar}}}
|
|
60
|
+
|
|
61
|
+
{P:bar}`)
|
|
62
|
+
|
|
63
|
+
test("Tables (example 203)", `
|
|
64
|
+
{P:| abc | def |
|
|
65
|
+
| --- |
|
|
66
|
+
| bar |}`)
|
|
67
|
+
|
|
68
|
+
test("Tables (example 204)", `
|
|
69
|
+
{TB:{TH:{tb:|} {TC:abc} {tb:|} {TC:def} {tb:|}}
|
|
70
|
+
{tb:| --- | --- |}
|
|
71
|
+
{TR:{tb:|} {TC:bar} {tb:|}}
|
|
72
|
+
{TR:{tb:|} {TC:bar} {tb:|} {TC:baz} {tb:|} {TC:boo} {tb:|}}}`)
|
|
73
|
+
|
|
74
|
+
test("Tables (example 205)", `
|
|
75
|
+
{TB:{TH:{tb:|} {TC:abc} {tb:|} {TC:def} {tb:|}}
|
|
76
|
+
{tb:| --- | --- |}}`)
|
|
77
|
+
|
|
78
|
+
test("Tables (in blockquote)", `
|
|
79
|
+
{Q:{q:>} {TB:{TH:{tb:|} {TC:one} {tb:|} {TC:two} {tb:|}}
|
|
80
|
+
{q:>} {tb:| --- | --- |}
|
|
81
|
+
{q:>} {TR:{tb:|} {TC:123} {tb:|} {TC:456} {tb:|}}}
|
|
82
|
+
{q:>}
|
|
83
|
+
{q:>} {P:Okay}}`)
|
|
84
|
+
|
|
85
|
+
test("Tables (empty header)", `
|
|
86
|
+
{TB:{TH:{tb:|} {tb:|} {tb:|}}
|
|
87
|
+
{tb:| :-: | :-: |}
|
|
88
|
+
{TR:{tb:|} {TC:One} {tb:|} {TC:Two} {tb:|}}}`)
|
|
89
|
+
|
|
90
|
+
test("Tables (nested list)", `
|
|
91
|
+
{BL:{LI:{l:-} {TB:{TH:{tb:|} {TC:Table} {tb:|}}
|
|
92
|
+
{tb:|:--|}}
|
|
93
|
+
{BL:{LI:{l:-} {TB:{TH:{tb:|} {TC:Table} {tb:|}}
|
|
94
|
+
{tb:|:--|}}
|
|
95
|
+
{BL:{LI:{l:-} {TB:{TH:{tb:|} {TC:Table} {tb:|}}
|
|
96
|
+
{tb:|:--|}}}}}}}}`)
|
|
97
|
+
|
|
98
|
+
test("Tables (end paragraph)", `
|
|
99
|
+
{P:Hello}
|
|
100
|
+
{TB:{TH:{tb:|} {TC:foo} {tb:|} {TC:bar} {tb:|}}
|
|
101
|
+
{tb:| --- | --- |}
|
|
102
|
+
{TR:{tb:|} {TC:baz} {tb:|} {TC:bim} {tb:|}}}`)
|
|
103
|
+
|
|
104
|
+
test("Tables (invalid tables don't end paragraph)", `
|
|
105
|
+
{P:Hello
|
|
106
|
+
| abc | def |
|
|
107
|
+
| --- |
|
|
108
|
+
| bar |}`)
|
|
109
|
+
|
|
110
|
+
test("Task list (example 279)", `
|
|
111
|
+
{BL:{LI:{l:-} {T:{t:[ ]} foo}}
|
|
112
|
+
{LI:{l:-} {T:{t:[x]} bar}}}`)
|
|
113
|
+
|
|
114
|
+
test("Task list (example 280)", `
|
|
115
|
+
{BL:{LI:{l:-} {T:{t:[x]} foo}
|
|
116
|
+
{BL:{LI:{l:-} {T:{t:[ ]} bar}}
|
|
117
|
+
{LI:{l:-} {T:{t:[x]} baz}}}}
|
|
118
|
+
{LI:{l:-} {T:{t:[ ]} bim}}}`)
|
|
119
|
+
|
|
120
|
+
test("Autolink (example 622)", `
|
|
121
|
+
{P:{URL:www.commonmark.org}}`)
|
|
122
|
+
|
|
123
|
+
test("Autolink (example 623)", `
|
|
124
|
+
{P:Visit {URL:www.commonmark.org/help} for more information.}`)
|
|
125
|
+
|
|
126
|
+
test("Autolink (example 624)", `
|
|
127
|
+
{P:Visit {URL:www.commonmark.org}.}
|
|
128
|
+
|
|
129
|
+
{P:Visit {URL:www.commonmark.org/a.b}.}`)
|
|
130
|
+
|
|
131
|
+
test("Autolink (example 625)", `
|
|
132
|
+
{P:{URL:www.google.com/search?q=Markup+(business)}}
|
|
133
|
+
|
|
134
|
+
{P:{URL:www.google.com/search?q=Markup+(business)}))}
|
|
135
|
+
|
|
136
|
+
{P:({URL:www.google.com/search?q=Markup+(business)})}
|
|
137
|
+
|
|
138
|
+
{P:({URL:www.google.com/search?q=Markup+(business)}}`)
|
|
139
|
+
|
|
140
|
+
test("Autolink (example 626)", `
|
|
141
|
+
{P:{URL:www.google.com/search?q=(business))+ok}}`)
|
|
142
|
+
|
|
143
|
+
test("Autolink (example 627)", `
|
|
144
|
+
{P:{URL:www.google.com/search?q=commonmark&hl=en}}
|
|
145
|
+
|
|
146
|
+
{P:{URL:www.google.com/search?q=commonmark}{Entity:&hl;}}`)
|
|
147
|
+
|
|
148
|
+
test("Autolink (example 628)", `
|
|
149
|
+
{P:{URL:www.commonmark.org/he}<lp}`)
|
|
150
|
+
|
|
151
|
+
test("Autolink (example 629)", `
|
|
152
|
+
{P:{URL:http://commonmark.org}}
|
|
153
|
+
|
|
154
|
+
{P:(Visit {URL:https://encrypted.google.com/search?q=Markup+(business)})}`)
|
|
155
|
+
|
|
156
|
+
test("Autolink (example 630)", `
|
|
157
|
+
{P:{URL:foo@bar.baz}}`)
|
|
158
|
+
|
|
159
|
+
test("Autolink (example 631)", `
|
|
160
|
+
{P:hello@mail+xyz.example isn't valid, but {URL:hello+xyz@mail.example} is.}`)
|
|
161
|
+
|
|
162
|
+
test("Autolink (example 632)", `
|
|
163
|
+
{P:{URL:a.b-c_d@a.b}}
|
|
164
|
+
|
|
165
|
+
{P:{URL:a.b-c_d@a.b}.}
|
|
166
|
+
|
|
167
|
+
{P:a.b-c_d@a.b-}
|
|
168
|
+
|
|
169
|
+
{P:a.b-c_d@a.b_}`)
|
|
170
|
+
|
|
171
|
+
test("Autolink (example 633)", `
|
|
172
|
+
{P:{URL:mailto:foo@bar.baz}}
|
|
173
|
+
|
|
174
|
+
{P:{URL:mailto:a.b-c_d@a.b}}
|
|
175
|
+
|
|
176
|
+
{P:{URL:mailto:a.b-c_d@a.b}.}
|
|
177
|
+
|
|
178
|
+
{P:{URL:mailto:a.b-c_d@a.b}/}
|
|
179
|
+
|
|
180
|
+
{P:mailto:a.b-c_d@a.b-}
|
|
181
|
+
|
|
182
|
+
{P:mailto:a.b-c_d@a.b_}
|
|
183
|
+
|
|
184
|
+
{P:{URL:xmpp:foo@bar.baz}}
|
|
185
|
+
|
|
186
|
+
{P:{URL:xmpp:foo@bar.baz}.}`)
|
|
187
|
+
|
|
188
|
+
test("Autolink (example 634)", `
|
|
189
|
+
{P:{URL:xmpp:foo@bar.baz/txt}}
|
|
190
|
+
|
|
191
|
+
{P:{URL:xmpp:foo@bar.baz/txt@bin}}
|
|
192
|
+
|
|
193
|
+
{P:{URL:xmpp:foo@bar.baz/txt@bin.com}}`)
|
|
194
|
+
|
|
195
|
+
test("Autolink (example 635)", `
|
|
196
|
+
{P:{URL:xmpp:foo@bar.baz/txt}/bin}`)
|
|
197
|
+
|
|
198
|
+
test("Task list (in ordered list)", `
|
|
199
|
+
{OL:{LI:{l:1.} {T:{t:[X]} Okay}}}`)
|
|
200
|
+
|
|
201
|
+
test("Task list (versus table)", `
|
|
202
|
+
{BL:{LI:{l:-} {TB:{TH:{TC:[ ] foo} {tb:|} {TC:bar}}
|
|
203
|
+
{tb:--- | ---}}}}`)
|
|
204
|
+
|
|
205
|
+
test("Task list (versus setext header)", `
|
|
206
|
+
{OL:{LI:{l:1.} {SH1:{Ln:{L:[}X{L:]}} foo
|
|
207
|
+
{h:===}}}}`)
|
|
208
|
+
|
|
209
|
+
test("Strikethrough (example 491)", `
|
|
210
|
+
{P:{Th:{tm:~~}Hi{tm:~~}} Hello, world!}`)
|
|
211
|
+
|
|
212
|
+
test("Strikethrough (example 492)", `
|
|
213
|
+
{P:This ~~has a}
|
|
214
|
+
|
|
215
|
+
{P:new paragraph~~.}`)
|
|
216
|
+
|
|
217
|
+
test("Strikethrough (nested)", `
|
|
218
|
+
{P:Nesting {St:{e:**}with {Th:{tm:~~}emphasis{tm:~~}}{e:**}}.}`)
|
|
219
|
+
|
|
220
|
+
test("Strikethrough (overlapping)", `
|
|
221
|
+
{P:One {St:{e:**}two ~~three{e:**}} four~~}
|
|
222
|
+
|
|
223
|
+
{P:One {Th:{tm:~~}two **three{tm:~~}} four**}`)
|
|
224
|
+
|
|
225
|
+
test("Strikethrough (escaped)", `
|
|
226
|
+
{P:A {Esc:\\~}~b c~~}`)
|
|
227
|
+
|
|
228
|
+
test("Strikethrough around spaces", `
|
|
229
|
+
{P:One ~~ two~~ three {Th:{tm:~~}.foo.{tm:~~}} a~~.foo.~~a {Th:{tm:~~}blah{tm:~~}}.}`)
|
|
230
|
+
|
|
231
|
+
test("Subscript", `
|
|
232
|
+
{P:One {Sub:{sub:~}two{sub:~}} {Em:{e:*}one {Sub:{sub:~}two{sub:~}}{e:*}}}`)
|
|
233
|
+
|
|
234
|
+
test("Subscript (no spaces)", `
|
|
235
|
+
{P:One ~two three~}`)
|
|
236
|
+
|
|
237
|
+
test("Subscript (escapes)", `
|
|
238
|
+
{P:One {Sub:{sub:~}two{Esc:\\ }th{Esc:\\~}ree{sub:~}}}`)
|
|
239
|
+
|
|
240
|
+
test("Superscript", `
|
|
241
|
+
{P:One {Sup:{sup:^}two{sup:^}} {Em:{e:*}one {Sup:{sup:^}two{sup:^}}{e:*}}}`)
|
|
242
|
+
|
|
243
|
+
test("Superscript (no spaces)", `
|
|
244
|
+
{P:One ^two three^}`)
|
|
245
|
+
|
|
246
|
+
test("Superscript (escapes)", `
|
|
247
|
+
{P:One {Sup:{sup:^}two{Esc:\\ }th{Esc:\\^}ree{sup:^}}}`)
|
|
248
|
+
|
|
249
|
+
test("Emoji", `
|
|
250
|
+
{P:Hello {ji::smile:} {ji::100:}}`)
|
|
251
|
+
|
|
252
|
+
test("Emoji (format)", `
|
|
253
|
+
{P:Hello :smi le: :1.00: ::}`)
|
|
254
|
+
|
|
255
|
+
test("Disable syntax", `
|
|
256
|
+
{BL:{LI:{l:-} {P:List still {Em:{e:*}works{e:*}}}}}
|
|
257
|
+
|
|
258
|
+
{P:> No quote, no ^sup^}
|
|
259
|
+
|
|
260
|
+
{P:No setext either
|
|
261
|
+
===}`, parser.configure({remove: ["Superscript", "Blockquote", "SetextHeading"]}))
|
|
262
|
+
|
|
263
|
+
test("Autolink (.co.uk)", `
|
|
264
|
+
{P:{URL:www.blah.co.uk/path}}`)
|
|
265
|
+
|
|
266
|
+
test("Autolink (email .co.uk)", `
|
|
267
|
+
{P:{URL:foo@bar.co.uk}}`)
|
|
268
|
+
|
|
269
|
+
test("Autolink (http://www.foo-bar.com/)", `
|
|
270
|
+
{P:{URL:http://www.foo-bar.com/}}`)
|
|
271
|
+
|
|
272
|
+
test("Autolink (exclude underscores)", `
|
|
273
|
+
{P:http://www.foo_/ http://foo_.com}`)
|
|
274
|
+
|
|
275
|
+
test("Autolink (in image)", `
|
|
276
|
+
{P:{Im:{L:![}Link: {URL:http://foo.com/}{L:]}{L:(}{URL:x.jpg}{L:)}}}`)
|
|
277
|
+
})
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
import {Tree, TreeFragment} from "@lezer/common"
|
|
2
|
+
import ist from "ist"
|
|
3
|
+
import {parser} from "../dist/index.js"
|
|
4
|
+
import {compareTree} from "./compare-tree.js"
|
|
5
|
+
|
|
6
|
+
let doc1 = `
|
|
7
|
+
Header
|
|
8
|
+
---
|
|
9
|
+
One **two**
|
|
10
|
+
three *four*
|
|
11
|
+
five.
|
|
12
|
+
|
|
13
|
+
> Start of quote
|
|
14
|
+
>
|
|
15
|
+
> 1. Nested list
|
|
16
|
+
>
|
|
17
|
+
> 2. More content
|
|
18
|
+
> inside the [list][link]
|
|
19
|
+
>
|
|
20
|
+
> Continued item
|
|
21
|
+
>
|
|
22
|
+
> ~~~
|
|
23
|
+
> Block of code
|
|
24
|
+
> ~~~
|
|
25
|
+
>
|
|
26
|
+
> 3. And so on
|
|
27
|
+
|
|
28
|
+
[link]: /ref
|
|
29
|
+
[another]: /one
|
|
30
|
+
And a final paragraph.
|
|
31
|
+
***
|
|
32
|
+
The end.
|
|
33
|
+
`
|
|
34
|
+
|
|
35
|
+
type ChangeSpec = {from: number, to?: number, insert?: string}[]
|
|
36
|
+
|
|
37
|
+
class State {
|
|
38
|
+
constructor(readonly doc: string,
|
|
39
|
+
readonly tree: Tree,
|
|
40
|
+
readonly fragments: readonly TreeFragment[]) {}
|
|
41
|
+
|
|
42
|
+
static start(doc: string) {
|
|
43
|
+
let tree = parser.parse(doc)
|
|
44
|
+
return new State(doc, tree, TreeFragment.addTree(tree))
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
update(changes: ChangeSpec, reparse = true) {
|
|
48
|
+
let changed = [], doc = this.doc, off = 0
|
|
49
|
+
for (let {from, to = from, insert = ""} of changes) {
|
|
50
|
+
doc = doc.slice(0, from) + insert + doc.slice(to)
|
|
51
|
+
changed.push({fromA: from - off, toA: to - off, fromB: from, toB: from + insert.length})
|
|
52
|
+
off += insert.length - (to - from)
|
|
53
|
+
}
|
|
54
|
+
let fragments = TreeFragment.applyChanges(this.fragments, changed, 2)
|
|
55
|
+
if (!reparse) return new State(doc, Tree.empty, fragments)
|
|
56
|
+
let tree = parser.parse(doc, fragments)
|
|
57
|
+
return new State(doc, tree, TreeFragment.addTree(tree, fragments))
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
let _state1: State | null = null, state1 = () => _state1 || (_state1 = State.start(doc1))
|
|
62
|
+
|
|
63
|
+
function overlap(a: Tree, b: Tree) {
|
|
64
|
+
let inA = new Set<Tree>(), shared = 0, sharingTo = 0
|
|
65
|
+
for (let cur = a.cursor(); cur.next();) if (cur.tree) inA.add(cur.tree)
|
|
66
|
+
for (let cur = b.cursor(); cur.next();) if (cur.tree && inA.has(cur.tree) && cur.type.is("Block") && cur.from >= sharingTo) {
|
|
67
|
+
shared += cur.to - cur.from
|
|
68
|
+
sharingTo = cur.to
|
|
69
|
+
}
|
|
70
|
+
return Math.round(shared * 100 / b.length)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function testChange(change: ChangeSpec, reuse = 10) {
|
|
74
|
+
let state = state1().update(change)
|
|
75
|
+
compareTree(state.tree, parser.parse(state.doc))
|
|
76
|
+
if (reuse) ist(overlap(state.tree, state1().tree), reuse, ">")
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
describe("Markdown incremental parsing", () => {
|
|
80
|
+
it("can produce the proper tree", () => {
|
|
81
|
+
// Replace 'three' with 'bears'
|
|
82
|
+
let state = state1().update([{from: 24, to: 29, insert: "bears"}])
|
|
83
|
+
compareTree(state.tree, state1().tree)
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
it("reuses nodes from the previous parse", () => {
|
|
87
|
+
// Replace 'three' with 'bears'
|
|
88
|
+
let state = state1().update([{from: 24, to: 29, insert: "bears"}])
|
|
89
|
+
ist(overlap(state1().tree, state.tree), 80, ">")
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
it("can reuse content for a change in a block context", () => {
|
|
93
|
+
// Replace 'content' with 'monkeys'
|
|
94
|
+
let state = state1().update([{from: 92, to: 99, insert: "monkeys"}])
|
|
95
|
+
compareTree(state.tree, state1().tree)
|
|
96
|
+
ist(overlap(state1().tree, state.tree), 20, ">")
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
it("can handle deleting a quote mark", () => testChange([{from: 82, to: 83}]))
|
|
100
|
+
|
|
101
|
+
it("can handle adding to a quoted block", () => testChange([{from: 37, insert: "> "}, {from: 45, insert: "> "}]))
|
|
102
|
+
|
|
103
|
+
it("can handle a change in a post-linkref paragraph", () => testChange([{from: 249, to: 251}]))
|
|
104
|
+
|
|
105
|
+
it("can handle a change in a paragraph-adjacent linkrefs", () => testChange([{from: 230, to: 231}]))
|
|
106
|
+
|
|
107
|
+
it("can deal with multiple changes applied separately", () => {
|
|
108
|
+
let state = state1().update([{from: 190, to: 191}], false).update([{from: 30, insert: "hi\n\nyou"}])
|
|
109
|
+
compareTree(state.tree, parser.parse(state.doc))
|
|
110
|
+
ist(overlap(state.tree, state1().tree), 20, ">")
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
it("works when a change happens directly after a block", () => testChange([{from: 150, to: 167}]))
|
|
114
|
+
|
|
115
|
+
it("works when a change deletes a blank line after a paragraph", () => testChange([{from: 207, to: 213}]))
|
|
116
|
+
|
|
117
|
+
it("doesn't get confused by removing paragraph-breaking markup", () => testChange([{from: 264, to: 265}]))
|
|
118
|
+
|
|
119
|
+
function r(n: number) { return Math.floor(Math.random() * n) }
|
|
120
|
+
function rStr(len: number) {
|
|
121
|
+
let result = "", chars = "\n>x-"
|
|
122
|
+
while (result.length < len) result += chars[r(chars.length)]
|
|
123
|
+
return result
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
it("survives random changes", () => {
|
|
127
|
+
for (let i = 0, l = doc1.length; i < 20; i++) {
|
|
128
|
+
let c = 1 + r(4), changes = []
|
|
129
|
+
for (let i = 0, rFrom = 0; i < c; i++) {
|
|
130
|
+
let rTo = rFrom + Math.floor((l - rFrom) / (c - i))
|
|
131
|
+
let from = rFrom + r(rTo - rFrom - 1), to = r(2) == 1 ? from : from + r(Math.min(rTo - from, 20))
|
|
132
|
+
let iR = r(3), insert = iR == 0 && from != to ? "" : iR == 1 ? "\n\n" : rStr(r(5) + 1)
|
|
133
|
+
changes.push({from, to, insert})
|
|
134
|
+
l += insert.length - (to - from)
|
|
135
|
+
rFrom = to + insert.length
|
|
136
|
+
}
|
|
137
|
+
testChange(changes, 0)
|
|
138
|
+
}
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
it("can handle large documents", () => {
|
|
142
|
+
let doc = doc1.repeat(50)
|
|
143
|
+
let state = State.start(doc)
|
|
144
|
+
let newState = state.update([{from: doc.length >> 1, insert: "a\n\nb"}])
|
|
145
|
+
ist(overlap(state.tree, newState.tree), 90, ">")
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
it("properly re-parses a continued indented code block", () => {
|
|
149
|
+
let state = State.start(`
|
|
150
|
+
One paragraph to create a bit of string length here
|
|
151
|
+
|
|
152
|
+
Code
|
|
153
|
+
Block
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
Another paragraph that is long enough to create a fragment
|
|
158
|
+
`).update([{from: 76, insert: " "}])
|
|
159
|
+
compareTree(state.tree, parser.parse(state.doc))
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
it("properly re-parses a continued list", () => {
|
|
163
|
+
let state = State.start(`
|
|
164
|
+
One paragraph to create a bit of string length here
|
|
165
|
+
|
|
166
|
+
* List
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
More content
|
|
171
|
+
|
|
172
|
+
Another paragraph that is long enough to create a fragment
|
|
173
|
+
`).update([{from: 65, insert: " * "}])
|
|
174
|
+
compareTree(state.tree, parser.parse(state.doc))
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
it("can recover from incremental parses that stop in the middle of a list", () => {
|
|
178
|
+
let doc = `
|
|
179
|
+
1. I am a list item with ***some* emphasized
|
|
180
|
+
content inside** and the parser hopefully stops
|
|
181
|
+
parsing after me.
|
|
182
|
+
|
|
183
|
+
2. Oh no the list continues.
|
|
184
|
+
`
|
|
185
|
+
let parse = parser.startParse(doc), tree
|
|
186
|
+
parse.advance()
|
|
187
|
+
ist(parse.parsedPos, doc.length, "<")
|
|
188
|
+
parse.stopAt(parse.parsedPos)
|
|
189
|
+
while (!(tree = parse.advance())) {}
|
|
190
|
+
let state = new State(doc, tree, TreeFragment.addTree(tree)).update([])
|
|
191
|
+
ist(state.tree.topNode.lastChild!.from, 1)
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
it("can reuse list items", () => {
|
|
195
|
+
let start = State.start(" - List item\n".repeat(100))
|
|
196
|
+
let state = start.update([{from: 18, to: 19}])
|
|
197
|
+
ist(overlap(start.tree, state.tree), 80, ">")
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
it("can reuse regular blocks before a continuable block", () => {
|
|
201
|
+
let start = State.start("A reusable paragraph\n\n".repeat(50) + "- etc\n\n")
|
|
202
|
+
let state = start.update([{from: start.doc.length, insert: "x"}])
|
|
203
|
+
ist(overlap(start.tree, state.tree), 85, ">")
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
it("returns a tree starting at the first range", () => {
|
|
207
|
+
let result = parser.parse("foo\n\nbar", [], [{from: 5, to: 8}])
|
|
208
|
+
ist(result.toString(), "Document(Paragraph)")
|
|
209
|
+
ist(result.length, 3)
|
|
210
|
+
ist(result.positions[0], 0)
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
it("Allows gaps in the input", () => {
|
|
214
|
+
let doc = `
|
|
215
|
+
The first X *y* X<
|
|
216
|
+
|
|
217
|
+
>X paragraph.
|
|
218
|
+
|
|
219
|
+
- And *a X<*>X list*
|
|
220
|
+
`
|
|
221
|
+
let tree = parser.parse(doc, [], [{from: 0, to: 11}, {from: 12, to: 17}, {from: 23, to: 46}, {from: 51, to: 58}])
|
|
222
|
+
ist(tree.toString(),
|
|
223
|
+
"Document(Paragraph(Emphasis(EmphasisMark,EmphasisMark)),BulletList(ListItem(ListMark,Paragraph(Emphasis(EmphasisMark,EmphasisMark)))))")
|
|
224
|
+
ist(tree.length, doc.length)
|
|
225
|
+
let top = tree.topNode, first = top.firstChild!
|
|
226
|
+
ist(first.name, "Paragraph")
|
|
227
|
+
ist(first.from, 1)
|
|
228
|
+
ist(first.to, 34)
|
|
229
|
+
let last = top.lastChild!.lastChild!.lastChild!, em = last.lastChild!
|
|
230
|
+
ist(last.name, "Paragraph")
|
|
231
|
+
ist(last.from, 39)
|
|
232
|
+
ist(last.to, 57)
|
|
233
|
+
ist(em.name, "Emphasis")
|
|
234
|
+
ist(em.from, 43)
|
|
235
|
+
ist(em.to, 57)
|
|
236
|
+
})
|
|
237
|
+
|
|
238
|
+
it("can reuse nodes at the end of the document", () => {
|
|
239
|
+
let doc = `* List item
|
|
240
|
+
|
|
241
|
+
~~~js
|
|
242
|
+
function foo() {
|
|
243
|
+
return false
|
|
244
|
+
}
|
|
245
|
+
~~~
|
|
246
|
+
`
|
|
247
|
+
let tree = parser.parse(doc)
|
|
248
|
+
let ins = 11
|
|
249
|
+
let doc2 = doc.slice(0, ins) + "\n* " + doc.slice(ins)
|
|
250
|
+
let fragments = TreeFragment.applyChanges(TreeFragment.addTree(tree), [{fromA: ins, toA: ins, fromB: ins, toB: ins + 3}])
|
|
251
|
+
let tree2 = parser.parse(doc2, fragments)
|
|
252
|
+
ist(tree2.topNode.lastChild!.tree, tree.topNode.lastChild!.tree)
|
|
253
|
+
})
|
|
254
|
+
|
|
255
|
+
it("places reused nodes at the right position when there are gaps before them", () => {
|
|
256
|
+
let doc = " {{}}\nb\n{{}}"
|
|
257
|
+
let ast1 = parser.parse(doc, undefined, [{from: 0, to: 1}, {from: 5, to: 8}])
|
|
258
|
+
let frag = TreeFragment.applyChanges(TreeFragment.addTree(ast1), [{fromA: 0, toA: 0, fromB: 0, toB: 1}])
|
|
259
|
+
let ast2 = parser.parse(" " + doc, frag, [{from: 0, to: 2}, {from: 6, to: 9}])
|
|
260
|
+
ist(ast2.toString(), "Document(Paragraph)")
|
|
261
|
+
let p = ast2.topNode.firstChild!
|
|
262
|
+
ist(p.from, 7)
|
|
263
|
+
ist(p.to, 8)
|
|
264
|
+
})
|
|
265
|
+
})
|