@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/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
+ })