@khanacademy/perseus-linter 0.0.0-PR971-20240207180432 → 0.0.0-PR973-20240207194831

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.
Files changed (93) hide show
  1. package/.eslintrc.js +12 -0
  2. package/CHANGELOG.md +168 -0
  3. package/dist/es/index.js +1 -1
  4. package/dist/index.js +1 -1
  5. package/dist/tsconfig-build.tsbuildinfo +1 -0
  6. package/package.json +5 -7
  7. package/src/README.md +41 -0
  8. package/src/__tests__/matcher.test.ts +498 -0
  9. package/src/__tests__/rule.test.ts +110 -0
  10. package/src/__tests__/rules.test.ts +548 -0
  11. package/src/__tests__/selector-parser.test.ts +51 -0
  12. package/src/__tests__/tree-transformer.test.ts +444 -0
  13. package/src/index.ts +281 -0
  14. package/src/proptypes.ts +19 -0
  15. package/src/rule.ts +419 -0
  16. package/src/rules/absolute-url.ts +23 -0
  17. package/src/rules/all-rules.ts +71 -0
  18. package/src/rules/blockquoted-math.ts +9 -0
  19. package/src/rules/blockquoted-widget.ts +9 -0
  20. package/src/rules/double-spacing-after-terminal.ts +11 -0
  21. package/src/rules/extra-content-spacing.ts +11 -0
  22. package/src/rules/heading-level-1.ts +13 -0
  23. package/src/rules/heading-level-skip.ts +19 -0
  24. package/src/rules/heading-sentence-case.ts +10 -0
  25. package/src/rules/heading-title-case.ts +68 -0
  26. package/src/rules/image-alt-text.ts +20 -0
  27. package/src/rules/image-in-table.ts +9 -0
  28. package/src/rules/image-spaces-around-urls.ts +34 -0
  29. package/src/rules/image-widget.ts +49 -0
  30. package/src/rules/link-click-here.ts +10 -0
  31. package/src/rules/lint-utils.ts +47 -0
  32. package/src/rules/long-paragraph.ts +13 -0
  33. package/src/rules/math-adjacent.ts +9 -0
  34. package/src/rules/math-align-extra-break.ts +10 -0
  35. package/src/rules/math-align-linebreaks.ts +42 -0
  36. package/src/rules/math-empty.ts +9 -0
  37. package/src/rules/math-font-size.ts +11 -0
  38. package/src/rules/math-frac.ts +9 -0
  39. package/src/rules/math-nested.ts +10 -0
  40. package/src/rules/math-starts-with-space.ts +11 -0
  41. package/src/rules/math-text-empty.ts +9 -0
  42. package/src/rules/math-without-dollars.ts +13 -0
  43. package/src/rules/nested-lists.ts +10 -0
  44. package/src/rules/profanity.ts +9 -0
  45. package/src/rules/table-missing-cells.ts +19 -0
  46. package/src/rules/unbalanced-code-delimiters.ts +13 -0
  47. package/src/rules/unescaped-dollar.ts +9 -0
  48. package/src/rules/widget-in-table.ts +9 -0
  49. package/src/selector.ts +504 -0
  50. package/src/tree-transformer.ts +583 -0
  51. package/src/types.ts +7 -0
  52. package/src/version.ts +10 -0
  53. package/tsconfig-build.json +12 -0
  54. /package/dist/{index.d.ts → types/index.d.ts} +0 -0
  55. /package/dist/{proptypes.d.ts → types/proptypes.d.ts} +0 -0
  56. /package/dist/{rule.d.ts → types/rule.d.ts} +0 -0
  57. /package/dist/{rules → types/rules}/absolute-url.d.ts +0 -0
  58. /package/dist/{rules → types/rules}/all-rules.d.ts +0 -0
  59. /package/dist/{rules → types/rules}/blockquoted-math.d.ts +0 -0
  60. /package/dist/{rules → types/rules}/blockquoted-widget.d.ts +0 -0
  61. /package/dist/{rules → types/rules}/double-spacing-after-terminal.d.ts +0 -0
  62. /package/dist/{rules → types/rules}/extra-content-spacing.d.ts +0 -0
  63. /package/dist/{rules → types/rules}/heading-level-1.d.ts +0 -0
  64. /package/dist/{rules → types/rules}/heading-level-skip.d.ts +0 -0
  65. /package/dist/{rules → types/rules}/heading-sentence-case.d.ts +0 -0
  66. /package/dist/{rules → types/rules}/heading-title-case.d.ts +0 -0
  67. /package/dist/{rules → types/rules}/image-alt-text.d.ts +0 -0
  68. /package/dist/{rules → types/rules}/image-in-table.d.ts +0 -0
  69. /package/dist/{rules → types/rules}/image-spaces-around-urls.d.ts +0 -0
  70. /package/dist/{rules → types/rules}/image-widget.d.ts +0 -0
  71. /package/dist/{rules → types/rules}/link-click-here.d.ts +0 -0
  72. /package/dist/{rules → types/rules}/lint-utils.d.ts +0 -0
  73. /package/dist/{rules → types/rules}/long-paragraph.d.ts +0 -0
  74. /package/dist/{rules → types/rules}/math-adjacent.d.ts +0 -0
  75. /package/dist/{rules → types/rules}/math-align-extra-break.d.ts +0 -0
  76. /package/dist/{rules → types/rules}/math-align-linebreaks.d.ts +0 -0
  77. /package/dist/{rules → types/rules}/math-empty.d.ts +0 -0
  78. /package/dist/{rules → types/rules}/math-font-size.d.ts +0 -0
  79. /package/dist/{rules → types/rules}/math-frac.d.ts +0 -0
  80. /package/dist/{rules → types/rules}/math-nested.d.ts +0 -0
  81. /package/dist/{rules → types/rules}/math-starts-with-space.d.ts +0 -0
  82. /package/dist/{rules → types/rules}/math-text-empty.d.ts +0 -0
  83. /package/dist/{rules → types/rules}/math-without-dollars.d.ts +0 -0
  84. /package/dist/{rules → types/rules}/nested-lists.d.ts +0 -0
  85. /package/dist/{rules → types/rules}/profanity.d.ts +0 -0
  86. /package/dist/{rules → types/rules}/table-missing-cells.d.ts +0 -0
  87. /package/dist/{rules → types/rules}/unbalanced-code-delimiters.d.ts +0 -0
  88. /package/dist/{rules → types/rules}/unescaped-dollar.d.ts +0 -0
  89. /package/dist/{rules → types/rules}/widget-in-table.d.ts +0 -0
  90. /package/dist/{selector.d.ts → types/selector.d.ts} +0 -0
  91. /package/dist/{tree-transformer.d.ts → types/tree-transformer.d.ts} +0 -0
  92. /package/dist/{types.d.ts → types/types.d.ts} +0 -0
  93. /package/dist/{version.d.ts → types/version.d.ts} +0 -0
@@ -0,0 +1,504 @@
1
+ /* eslint-disable no-useless-escape */
2
+ /**
3
+ * The Selector class implements a CSS-like system for matching nodes in a
4
+ * parse tree based on the structure of the tree. Create a Selector object by
5
+ * calling the static Selector.parse() method on a string that describes the
6
+ * tree structure you want to match. For example, if you want to find text
7
+ * nodes that are direct children of paragraph nodes that immediately follow
8
+ * heading nodes, you could create an appropriate selector like this:
9
+ *
10
+ * selector = Selector.parse("heading + paragraph > text");
11
+ *
12
+ * Recall from the TreeTransformer class, that we consider any object with a
13
+ * string-valued `type` property to be a tree node. The words "heading",
14
+ * "paragraph" and "text" in the selector string above specify node types and
15
+ * will match nodes in a parse tree that have `type` properties with those
16
+ * values.
17
+ *
18
+ * Selectors are designed for use during tree traversals done with the
19
+ * TreeTransformer traverse() method. To test whether the node currently being
20
+ * traversed matches a selector, simply pass the TraversalState object to the
21
+ * match() method of the Selector object. If the node does not match the
22
+ * selector, match() returns null. If it does match, then match() returns an
23
+ * array of nodes that match the selector. In the example above the first
24
+ * element of the array would be the node the heading node, the second would
25
+ * be the paragraph node that follows it, and the third would be the text node
26
+ * that is a child of the paragraph. The last element of a returned array of
27
+ * nodes is always equal to the current node of the tree traversal.
28
+ *
29
+ * Code that uses a selector might look like this:
30
+ *
31
+ * matchingNodes = selector.match(state);
32
+ * if (matchingNodes) {
33
+ * let heading = matchingNodes[0];
34
+ * let text = matchingNodes[2];
35
+ * // do something with those nodes
36
+ * }
37
+ *
38
+ * The Selector.parse() method recognizes a grammar that is similar to CSS
39
+ * selectors:
40
+ *
41
+ * selector := treeSelector (, treeSelector)*
42
+ *
43
+ * A selector is one or more comma-separated treeSelectors. A node matches
44
+ * the selector if it matches any of the treeSelectors.
45
+ *
46
+ * treeSelector := (treeSelector combinator)? nodeSelector
47
+ *
48
+ * A treeSelector is a nodeSelector optionally preceeded by a combinator
49
+ * and another tree selector. The tree selector matches if the current node
50
+ * matches the node selector and a sibling or ancestor (depending on the
51
+ * combinator) of the current node matches the optional treeSelector.
52
+ *
53
+ * combinator := ' ' | '>' | '+' | '~' // standard CSS3 combinators
54
+ *
55
+ * A combinator is a space or punctuation character that specifies the
56
+ * relationship between two nodeSelectors. A space between two
57
+ * nodeSelectors means that the first selector much match an ancestor of
58
+ * the node that matches the second selector. A '>' character means that
59
+ * the first selector must match the parent of the node matched by the
60
+ * second. The '~' combinator means that the first selector must match a
61
+ * previous sibling of the node matched by the second. And the '+' selector
62
+ * means that first selector must match the immediate previous sibling of
63
+ * the node that matched the second.
64
+ *
65
+ * nodeSelector := <IDENTIFIER> | '*'
66
+ *
67
+ * A nodeSelector is simply an identifier (a letter followed by any number
68
+ * of letters, digits, hypens, and underscores) or the wildcard asterisk
69
+ * character. A wildcard node selector matches any node. An identifier
70
+ * selector matches any node that has a `type` property whose value matches
71
+ * the identifier.
72
+ *
73
+ * If you call Selector.parse() on a string that does not match this grammar,
74
+ * it will throw an exception
75
+ *
76
+ * TODO(davidflanagan): it might be useful to allow more sophsticated node
77
+ * selector matching with attribute matches and pseudo-classes, like
78
+ * "heading[level=2]" or "paragraph:first-child"
79
+ *
80
+ * Implementation Note: this file exports a very simple Selector class but all
81
+ * the actual work is done in various internal classes. The Parser class
82
+ * parses the string representation of a selector into a parse tree that
83
+ * consists of instances of various subclasses of the Selector class. It is
84
+ * these subclasses that implement the selector matching logic, often
85
+ * depending on features of the TraversalState object from the TreeTransformer
86
+ * traversal.
87
+ */
88
+
89
+ import {Errors, PerseusError} from "@khanacademy/perseus-error";
90
+
91
+ import type {TreeNode, TraversalState} from "./tree-transformer";
92
+
93
+ /**
94
+ * This is the base class for all Selector types. The key method that all
95
+ * selector subclasses must implement is match(). It takes a TraversalState
96
+ * object (from a TreeTransformer traversal) and tests whether the selector
97
+ * matches at the current node. See the comment at the start of this file for
98
+ * more details on the match() method.
99
+ */
100
+ export default class Selector {
101
+ static parse(selectorText: string): Selector {
102
+ return new Parser(selectorText).parse();
103
+ }
104
+
105
+ /**
106
+ * Return an array of the nodes that matched or null if no match.
107
+ * This is the base class so we just throw an exception. All Selector
108
+ * subclasses must provide an implementation of this method.
109
+ */
110
+ match(state: TraversalState): ReadonlyArray<TreeNode> | null | undefined {
111
+ throw new PerseusError(
112
+ "Selector subclasses must implement match()",
113
+ Errors.NotAllowed,
114
+ );
115
+ }
116
+
117
+ /**
118
+ * Selector subclasses all define a toString() method primarily
119
+ * because it makes it easy to write parser tests.
120
+ */
121
+ toString(): string {
122
+ return "Unknown selector class";
123
+ }
124
+ }
125
+
126
+ /**
127
+ * This class implements a parser for the selector grammar. Pass the source
128
+ * text to the Parser() constructor, and then call the parse() method to
129
+ * obtain a corresponding Selector object. parse() throws an exception
130
+ * if there are syntax errors in the selector.
131
+ *
132
+ * This class is not exported, and you don't need to use it directly.
133
+ * Instead call the static Selector.parse() method.
134
+ */
135
+ class Parser {
136
+ static TOKENS: RegExp; // We do lexing with a simple regular expression
137
+ tokens: ReadonlyArray<string>; // The array of tokens
138
+ tokenIndex: number; // Which token in the array we're looking at now
139
+
140
+ constructor(s: string) {
141
+ // Normalize whitespace:
142
+ // - remove leading and trailing whitespace
143
+ // - replace runs of whitespace with single space characters
144
+ s = s.trim().replace(/\s+/g, " ");
145
+ // Convert the string to an array of tokens. Note that the TOKENS
146
+ // pattern ignores spaces that do not appear before identifiers
147
+ // or the * wildcard.
148
+ this.tokens = s.match(Parser.TOKENS) || [];
149
+ this.tokenIndex = 0;
150
+ }
151
+
152
+ // Return the next token or the empty string if there are no more
153
+ nextToken(): string {
154
+ return this.tokens[this.tokenIndex] || "";
155
+ }
156
+
157
+ // Increment the token index to "consume" the token we were looking at
158
+ // and move on to the next one.
159
+ consume(): void {
160
+ this.tokenIndex++;
161
+ }
162
+
163
+ // Return true if the current token is an identifier or false otherwise
164
+ isIdentifier(): boolean {
165
+ // The Parser.TOKENS regexp ensures that we only have to check
166
+ // the first character of a token to know what kind of token it is.
167
+ const c = this.tokens[this.tokenIndex][0];
168
+ return (c >= "a" && c <= "z") || (c >= "A" && c <= "Z");
169
+ }
170
+
171
+ // Consume space tokens until the next token is not a space.
172
+ skipSpace(): void {
173
+ while (this.nextToken() === " ") {
174
+ this.consume();
175
+ }
176
+ }
177
+
178
+ // Parse a comma-separated sequence of tree selectors. This is the
179
+ // entry point for the Parser class and the only method that clients
180
+ // ever need to call.
181
+ parse(): Selector {
182
+ // We expect at least one tree selector
183
+ const ts = this.parseTreeSelector();
184
+
185
+ // Now see what's next
186
+ let token = this.nextToken();
187
+
188
+ // If there is no next token then we're done parsing and can return
189
+ // the tree selector object we got above
190
+ if (!token) {
191
+ return ts;
192
+ }
193
+
194
+ // Otherwise, there is more go come and we're going to need a
195
+ // list of tree selectors
196
+ const treeSelectors = [ts];
197
+ while (token) {
198
+ // The only character we allow after a tree selector is a comma
199
+ if (token === ",") {
200
+ this.consume();
201
+ } else {
202
+ throw new ParseError("Expected comma");
203
+ }
204
+
205
+ // And if we saw a comma, then it must be followed by another
206
+ // tree selector
207
+ treeSelectors.push(this.parseTreeSelector());
208
+ token = this.nextToken();
209
+ }
210
+
211
+ // If we parsed more than one tree selector, return them in a
212
+ // SelectorList object.
213
+ return new SelectorList(treeSelectors);
214
+ }
215
+
216
+ // Parse a sequence of node selectors linked together with
217
+ // hierarchy combinators: space, >, + and ~.
218
+ parseTreeSelector(): Selector {
219
+ this.skipSpace(); // Ignore space after a comma, for example
220
+
221
+ // A tree selector must begin with a node selector
222
+ let ns: Selector = this.parseNodeSelector();
223
+
224
+ for (;;) {
225
+ // Now check the next token. If there is none, or if it is a
226
+ // comma, then we're done with the treeSelector. Otherwise
227
+ // we expect a combinator followed by another node selector.
228
+ // If we don't see a combinator, we throw an error. If we
229
+ // do see a combinator and another node selector then we
230
+ // combine the current node selector with the new node selector
231
+ // using a Selector subclass that depends on the combinator.
232
+ const token = this.nextToken();
233
+
234
+ if (!token || token === ",") {
235
+ break;
236
+ } else if (token === " ") {
237
+ this.consume();
238
+ ns = new AncestorCombinator(ns, this.parseNodeSelector());
239
+ } else if (token === ">") {
240
+ this.consume();
241
+ ns = new ParentCombinator(ns, this.parseNodeSelector());
242
+ } else if (token === "+") {
243
+ this.consume();
244
+ ns = new PreviousCombinator(ns, this.parseNodeSelector());
245
+ } else if (token === "~") {
246
+ this.consume();
247
+ ns = new SiblingCombinator(ns, this.parseNodeSelector());
248
+ } else {
249
+ throw new ParseError("Unexpected token: " + token);
250
+ }
251
+ }
252
+
253
+ return ns;
254
+ }
255
+
256
+ // Parse a single node selector.
257
+ // For now, this is just a node type or a wildcard.
258
+ //
259
+ // TODO(davidflanagan): we may need to extend this with attribute
260
+ // selectors like 'heading[level=3]', or with pseudo-classes like
261
+ // paragraph:first-child
262
+ parseNodeSelector(): Selector {
263
+ // First, skip any whitespace
264
+ this.skipSpace();
265
+
266
+ const t = this.nextToken();
267
+ if (t === "*") {
268
+ this.consume();
269
+ return new AnyNode();
270
+ }
271
+ if (this.isIdentifier()) {
272
+ this.consume();
273
+ return new TypeSelector(t);
274
+ }
275
+
276
+ throw new ParseError("Expected node type");
277
+ }
278
+ }
279
+
280
+ // We break the input string into tokens with this regexp. Token types
281
+ // are identifiers, integers, punctuation and spaces. Note that spaces
282
+ // tokens are only returned when they appear before an identifier or
283
+ // wildcard token and are otherwise omitted.
284
+ Parser.TOKENS = /([a-zA-Z][\w-]*)|(\d+)|[^\s]|(\s(?=[a-zA-Z\*]))/g;
285
+
286
+ /**
287
+ * This is a trivial Error subclass that the Parser uses to signal parse errors
288
+ */
289
+ class ParseError extends Error {
290
+ constructor(message: string) {
291
+ super(message);
292
+ }
293
+ }
294
+
295
+ /**
296
+ * This Selector subclass is a list of selectors. It matches a node if any of
297
+ * the selectors on the list matches the node. It considers the selectors in
298
+ * order, and returns the array of nodes returned by whichever one matches
299
+ * first.
300
+ */
301
+ class SelectorList extends Selector {
302
+ selectors: ReadonlyArray<Selector>;
303
+
304
+ constructor(selectors: ReadonlyArray<Selector>) {
305
+ super();
306
+ this.selectors = selectors;
307
+ }
308
+
309
+ match(state: TraversalState): ReadonlyArray<TreeNode> | null | undefined {
310
+ for (let i = 0; i < this.selectors.length; i++) {
311
+ const s = this.selectors[i];
312
+ const result = s.match(state);
313
+ if (result) {
314
+ return result;
315
+ }
316
+ }
317
+ return null;
318
+ }
319
+
320
+ toString(): string {
321
+ let result = "";
322
+ for (let i = 0; i < this.selectors.length; i++) {
323
+ result += i > 0 ? ", " : "";
324
+ result += this.selectors[i].toString();
325
+ }
326
+ return result;
327
+ }
328
+ }
329
+
330
+ /**
331
+ * This trivial Selector subclass implements the '*' wildcard and
332
+ * matches any node.
333
+ */
334
+ class AnyNode extends Selector {
335
+ match(state: TraversalState): ReadonlyArray<TreeNode> | null | undefined {
336
+ return [state.currentNode()];
337
+ }
338
+
339
+ toString(): string {
340
+ return "*";
341
+ }
342
+ }
343
+
344
+ /**
345
+ * This selector subclass implements the <IDENTIFIER> part of the grammar.
346
+ * it matches any node whose `type` property is a specified string
347
+ */
348
+ class TypeSelector extends Selector {
349
+ type: string;
350
+
351
+ constructor(type: string) {
352
+ super();
353
+ this.type = type;
354
+ }
355
+
356
+ match(state: TraversalState): ReadonlyArray<TreeNode> | null | undefined {
357
+ const node = state.currentNode();
358
+ if (node.type === this.type) {
359
+ return [node];
360
+ }
361
+ return null;
362
+ }
363
+
364
+ toString(): string {
365
+ return this.type;
366
+ }
367
+ }
368
+
369
+ /**
370
+ * This selector subclass is the superclass of the classes that implement
371
+ * matching for the four combinators. It defines left and right properties for
372
+ * the two selectors that are to be combined, but does not define a match
373
+ * method.
374
+ */
375
+ class SelectorCombinator extends Selector {
376
+ left: Selector;
377
+ right: Selector;
378
+
379
+ constructor(left: Selector, right: Selector) {
380
+ super();
381
+ this.left = left;
382
+ this.right = right;
383
+ }
384
+ }
385
+
386
+ /**
387
+ * This Selector subclass implements the space combinator. It matches if the
388
+ * right selector matches the current node and the left selector matches some
389
+ * ancestor of the current node.
390
+ */
391
+ class AncestorCombinator extends SelectorCombinator {
392
+ constructor(left: Selector, right: Selector) {
393
+ super(left, right);
394
+ }
395
+
396
+ match(state: TraversalState): ReadonlyArray<TreeNode> | null | undefined {
397
+ const rightResult = this.right.match(state);
398
+ if (rightResult) {
399
+ state = state.clone();
400
+ while (state.hasParent()) {
401
+ state.goToParent();
402
+ const leftResult = this.left.match(state);
403
+ if (leftResult) {
404
+ return leftResult.concat(rightResult);
405
+ }
406
+ }
407
+ }
408
+ return null;
409
+ }
410
+
411
+ toString(): string {
412
+ return this.left.toString() + " " + this.right.toString();
413
+ }
414
+ }
415
+
416
+ /**
417
+ * This Selector subclass implements the > combinator. It matches if the
418
+ * right selector matches the current node and the left selector matches
419
+ * the parent of the current node.
420
+ */
421
+ class ParentCombinator extends SelectorCombinator {
422
+ constructor(left: Selector, right: Selector) {
423
+ super(left, right);
424
+ }
425
+
426
+ match(state: TraversalState): ReadonlyArray<TreeNode> | null | undefined {
427
+ const rightResult = this.right.match(state);
428
+ if (rightResult) {
429
+ if (state.hasParent()) {
430
+ state = state.clone();
431
+ state.goToParent();
432
+ const leftResult = this.left.match(state);
433
+ if (leftResult) {
434
+ return leftResult.concat(rightResult);
435
+ }
436
+ }
437
+ }
438
+ return null;
439
+ }
440
+
441
+ toString(): string {
442
+ return this.left.toString() + " > " + this.right.toString();
443
+ }
444
+ }
445
+
446
+ /**
447
+ * This Selector subclass implements the + combinator. It matches if the
448
+ * right selector matches the current node and the left selector matches
449
+ * the immediate previous sibling of the current node.
450
+ */
451
+ class PreviousCombinator extends SelectorCombinator {
452
+ constructor(left: Selector, right: Selector) {
453
+ super(left, right);
454
+ }
455
+
456
+ match(state: TraversalState): ReadonlyArray<TreeNode> | null | undefined {
457
+ const rightResult = this.right.match(state);
458
+ if (rightResult) {
459
+ if (state.hasPreviousSibling()) {
460
+ state = state.clone();
461
+ state.goToPreviousSibling();
462
+ const leftResult = this.left.match(state);
463
+ if (leftResult) {
464
+ return leftResult.concat(rightResult);
465
+ }
466
+ }
467
+ }
468
+ return null;
469
+ }
470
+
471
+ toString(): string {
472
+ return this.left.toString() + " + " + this.right.toString();
473
+ }
474
+ }
475
+
476
+ /**
477
+ * This Selector subclass implements the ~ combinator. It matches if the
478
+ * right selector matches the current node and the left selector matches
479
+ * any previous sibling of the current node.
480
+ */
481
+ class SiblingCombinator extends SelectorCombinator {
482
+ constructor(left: Selector, right: Selector) {
483
+ super(left, right);
484
+ }
485
+
486
+ match(state: TraversalState): ReadonlyArray<TreeNode> | null | undefined {
487
+ const rightResult = this.right.match(state);
488
+ if (rightResult) {
489
+ state = state.clone();
490
+ while (state.hasPreviousSibling()) {
491
+ state.goToPreviousSibling();
492
+ const leftResult = this.left.match(state);
493
+ if (leftResult) {
494
+ return leftResult.concat(rightResult);
495
+ }
496
+ }
497
+ }
498
+ return null;
499
+ }
500
+
501
+ toString(): string {
502
+ return this.left.toString() + " ~ " + this.right.toString();
503
+ }
504
+ }