@khanacademy/perseus-linter 0.3.9 → 0.3.11
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/dist/es/index.js +1 -1
- package/dist/es/index.js.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/package.json +7 -4
- package/.eslintrc.js +0 -12
- package/CHANGELOG.md +0 -140
- package/src/README.md +0 -41
- package/src/__tests__/matcher.test.ts +0 -498
- package/src/__tests__/rule.test.ts +0 -110
- package/src/__tests__/rules.test.ts +0 -548
- package/src/__tests__/selector-parser.test.ts +0 -51
- package/src/__tests__/tree-transformer.test.ts +0 -444
- package/src/index.ts +0 -281
- package/src/proptypes.ts +0 -19
- package/src/rule.ts +0 -419
- package/src/rules/absolute-url.ts +0 -23
- package/src/rules/all-rules.ts +0 -71
- package/src/rules/blockquoted-math.ts +0 -9
- package/src/rules/blockquoted-widget.ts +0 -9
- package/src/rules/double-spacing-after-terminal.ts +0 -11
- package/src/rules/extra-content-spacing.ts +0 -11
- package/src/rules/heading-level-1.ts +0 -13
- package/src/rules/heading-level-skip.ts +0 -19
- package/src/rules/heading-sentence-case.ts +0 -10
- package/src/rules/heading-title-case.ts +0 -68
- package/src/rules/image-alt-text.ts +0 -20
- package/src/rules/image-in-table.ts +0 -9
- package/src/rules/image-spaces-around-urls.ts +0 -34
- package/src/rules/image-widget.ts +0 -49
- package/src/rules/link-click-here.ts +0 -10
- package/src/rules/lint-utils.ts +0 -47
- package/src/rules/long-paragraph.ts +0 -13
- package/src/rules/math-adjacent.ts +0 -9
- package/src/rules/math-align-extra-break.ts +0 -10
- package/src/rules/math-align-linebreaks.ts +0 -42
- package/src/rules/math-empty.ts +0 -9
- package/src/rules/math-font-size.ts +0 -11
- package/src/rules/math-frac.ts +0 -9
- package/src/rules/math-nested.ts +0 -10
- package/src/rules/math-starts-with-space.ts +0 -11
- package/src/rules/math-text-empty.ts +0 -9
- package/src/rules/math-without-dollars.ts +0 -13
- package/src/rules/nested-lists.ts +0 -10
- package/src/rules/profanity.ts +0 -9
- package/src/rules/table-missing-cells.ts +0 -19
- package/src/rules/unbalanced-code-delimiters.ts +0 -13
- package/src/rules/unescaped-dollar.ts +0 -9
- package/src/rules/widget-in-table.ts +0 -9
- package/src/selector.ts +0 -504
- package/src/tree-transformer.ts +0 -583
- package/src/types.ts +0 -7
- package/src/version.ts +0 -10
- package/tsconfig-build.json +0 -12
- package/tsconfig-build.tsbuildinfo +0 -1
package/src/selector.ts
DELETED
|
@@ -1,504 +0,0 @@
|
|
|
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
|
-
}
|