@khanacademy/perseus-linter 0.0.0-PR973-20240207204706 → 0.0.0-PR973-20240207213425
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/.eslintrc.js +12 -0
- package/CHANGELOG.md +168 -0
- package/package.json +4 -7
- package/src/README.md +41 -0
- package/src/__tests__/matcher.test.ts +498 -0
- package/src/__tests__/rule.test.ts +110 -0
- package/src/__tests__/rules.test.ts +548 -0
- package/src/__tests__/selector-parser.test.ts +51 -0
- package/src/__tests__/tree-transformer.test.ts +444 -0
- package/src/index.ts +281 -0
- package/src/proptypes.ts +19 -0
- package/src/rule.ts +419 -0
- package/src/rules/absolute-url.ts +23 -0
- package/src/rules/all-rules.ts +71 -0
- package/src/rules/blockquoted-math.ts +9 -0
- package/src/rules/blockquoted-widget.ts +9 -0
- package/src/rules/double-spacing-after-terminal.ts +11 -0
- package/src/rules/extra-content-spacing.ts +11 -0
- package/src/rules/heading-level-1.ts +13 -0
- package/src/rules/heading-level-skip.ts +19 -0
- package/src/rules/heading-sentence-case.ts +10 -0
- package/src/rules/heading-title-case.ts +68 -0
- package/src/rules/image-alt-text.ts +20 -0
- package/src/rules/image-in-table.ts +9 -0
- package/src/rules/image-spaces-around-urls.ts +34 -0
- package/src/rules/image-widget.ts +49 -0
- package/src/rules/link-click-here.ts +10 -0
- package/src/rules/lint-utils.ts +47 -0
- package/src/rules/long-paragraph.ts +13 -0
- package/src/rules/math-adjacent.ts +9 -0
- package/src/rules/math-align-extra-break.ts +10 -0
- package/src/rules/math-align-linebreaks.ts +42 -0
- package/src/rules/math-empty.ts +9 -0
- package/src/rules/math-font-size.ts +11 -0
- package/src/rules/math-frac.ts +9 -0
- package/src/rules/math-nested.ts +10 -0
- package/src/rules/math-starts-with-space.ts +11 -0
- package/src/rules/math-text-empty.ts +9 -0
- package/src/rules/math-without-dollars.ts +13 -0
- package/src/rules/nested-lists.ts +10 -0
- package/src/rules/profanity.ts +9 -0
- package/src/rules/table-missing-cells.ts +19 -0
- package/src/rules/unbalanced-code-delimiters.ts +13 -0
- package/src/rules/unescaped-dollar.ts +9 -0
- package/src/rules/widget-in-table.ts +9 -0
- package/src/selector.ts +504 -0
- package/src/tree-transformer.ts +583 -0
- package/src/types.ts +7 -0
- package/src/version.ts +10 -0
- package/tsconfig-build.json +12 -0
- package/tsconfig-build.tsbuildinfo +1 -0
|
@@ -0,0 +1,583 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TreeTransformer is a class for traversing and transforming trees. Create a
|
|
3
|
+
* TreeTransformer by passing the root node of the tree to the
|
|
4
|
+
* constructor. Then traverse that tree by calling the traverse() method. The
|
|
5
|
+
* argument to traverse() is a callback function that will be called once for
|
|
6
|
+
* each node in the tree. This is a post-order depth-first traversal: the
|
|
7
|
+
* callback is not called on the a way down, but on the way back up. That is,
|
|
8
|
+
* the children of a node are traversed before the node itself is.
|
|
9
|
+
*
|
|
10
|
+
* The traversal callback function is passed three arguments, the node being
|
|
11
|
+
* traversed, a TraversalState object, and the concatentated text content of
|
|
12
|
+
* the node and all of its descendants. The TraversalState object is the most
|
|
13
|
+
* most interesting argument: it has methods for querying the ancestors and
|
|
14
|
+
* siblings of the node, and for deleting or replacing the node. These
|
|
15
|
+
* transformation methods are why this class is a tree transformer and not
|
|
16
|
+
* just a tree traverser.
|
|
17
|
+
*
|
|
18
|
+
* A typical tree traversal looks like this:
|
|
19
|
+
*
|
|
20
|
+
* new TreeTransformer(root).traverse((node, state, content) => {
|
|
21
|
+
* let parent = state.parent();
|
|
22
|
+
* let previous = state.previousSibling();
|
|
23
|
+
* // etc.
|
|
24
|
+
* });
|
|
25
|
+
*
|
|
26
|
+
* The traverse() method descends through nodes and arrays of nodes and calls
|
|
27
|
+
* the traverse callback on each node on the way back up to the root of the
|
|
28
|
+
* tree. (Note that it only calls the callback on the nodes themselves, not
|
|
29
|
+
* any arrays that contain nodes.) A node is loosely defined as any object
|
|
30
|
+
* with a string-valued `type` property. Objects that do not have a type
|
|
31
|
+
* property are assumed to not be part of the tree and are not traversed. When
|
|
32
|
+
* traversing an array, all elements of the array are examined, and any that
|
|
33
|
+
* are nodes or arrays are recursively traversed. When traversing a node, all
|
|
34
|
+
* properties of the object are examined and any node or array values are
|
|
35
|
+
* recursively traversed. In typical parse trees, the children of a node are
|
|
36
|
+
* in a `children` or `content` array, but this class is designed to handle
|
|
37
|
+
* more general trees. The Perseus markdown parser, for example, produces
|
|
38
|
+
* nodes of type "table" that have children in the `header` and `cells`
|
|
39
|
+
* properties.
|
|
40
|
+
*
|
|
41
|
+
* CAUTION: the traverse() method does not make any attempt to detect
|
|
42
|
+
* cycles. If you call it on a cyclic graph instead of a tree, it will cause
|
|
43
|
+
* infinite recursion (or, more likely, a stack overflow).
|
|
44
|
+
*
|
|
45
|
+
* TODO(davidflanagan): it probably wouldn't be hard to detect cycles: when
|
|
46
|
+
* pushing a new node onto the containers stack we could just check that it
|
|
47
|
+
* isn't already there.
|
|
48
|
+
*
|
|
49
|
+
* If a node has a text-valued `content` property, it is taken to be the
|
|
50
|
+
* plain-text content of the node. The traverse() method concatenates these
|
|
51
|
+
* content strings and passes them to the traversal callback for each
|
|
52
|
+
* node. This means that the callback has access the full text content of its
|
|
53
|
+
* node and all of the nodes descendants.
|
|
54
|
+
*
|
|
55
|
+
* See the TraversalState class for more information on what information and
|
|
56
|
+
* methods are available to the traversal callback.
|
|
57
|
+
**/
|
|
58
|
+
|
|
59
|
+
import {Errors, PerseusError} from "@khanacademy/perseus-error";
|
|
60
|
+
|
|
61
|
+
// TreeNode is the type of a node in a parse tree. The only real requirement is
|
|
62
|
+
// that every node has a string-valued `type` property
|
|
63
|
+
export type TreeNode = {
|
|
64
|
+
type: string;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
// TraversalCallback is the type of the callback function passed to the
|
|
68
|
+
// traverse() method. It is invoked with node, state, and content arguments
|
|
69
|
+
// and is expected to return nothing.
|
|
70
|
+
export type TraversalCallback = (
|
|
71
|
+
node: TreeNode,
|
|
72
|
+
state: TraversalState,
|
|
73
|
+
content: string,
|
|
74
|
+
) => void;
|
|
75
|
+
|
|
76
|
+
// This is the TreeTransformer class described in detail at the
|
|
77
|
+
// top of this file.
|
|
78
|
+
export default class TreeTransformer {
|
|
79
|
+
root: TreeNode;
|
|
80
|
+
|
|
81
|
+
// To create a tree transformer, just pass the root node of the tree
|
|
82
|
+
constructor(root: TreeNode) {
|
|
83
|
+
this.root = root;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// A utility function for determing whether an arbitrary value is a node
|
|
87
|
+
static isNode(n: any): boolean {
|
|
88
|
+
return n && typeof n === "object" && typeof n.type === "string";
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Determines whether a value is a node with type "text" and has
|
|
92
|
+
// a text-valued `content` property.
|
|
93
|
+
static isTextNode(n: any): boolean {
|
|
94
|
+
return (
|
|
95
|
+
TreeTransformer.isNode(n) &&
|
|
96
|
+
n.type === "text" &&
|
|
97
|
+
typeof n.content === "string"
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// This is the main entry point for the traverse() method. See the comment
|
|
102
|
+
// at the top of this file for a detailed description. Note that this
|
|
103
|
+
// method just creates a new TraversalState object to use for this
|
|
104
|
+
// traversal and then invokes the internal _traverse() method to begin the
|
|
105
|
+
// recursion.
|
|
106
|
+
traverse(f: TraversalCallback): void {
|
|
107
|
+
this._traverse(this.root, new TraversalState(this.root), f);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Do a post-order traversal of node and its descendants, invoking the
|
|
111
|
+
// callback function f() once for each node and returning the concatenated
|
|
112
|
+
// text content of the node and its descendants. f() is passed three
|
|
113
|
+
// arguments: the current node, a TraversalState object representing the
|
|
114
|
+
// current state of the traversal, and a string that holds the
|
|
115
|
+
// concatenated text of the node and its descendants.
|
|
116
|
+
//
|
|
117
|
+
// This private method holds all the traversal logic and implementation
|
|
118
|
+
// details. Note that this method uses the TraversalState object to store
|
|
119
|
+
// information about the structure of the tree.
|
|
120
|
+
_traverse(
|
|
121
|
+
n: TreeNode | Array<TreeNode>,
|
|
122
|
+
state: TraversalState,
|
|
123
|
+
f: TraversalCallback,
|
|
124
|
+
): string {
|
|
125
|
+
let content = "";
|
|
126
|
+
if (TreeTransformer.isNode(n)) {
|
|
127
|
+
// If we were called on a node object, then we handle it
|
|
128
|
+
// this way.
|
|
129
|
+
const node = n as TreeNode; // safe cast; we just tested
|
|
130
|
+
|
|
131
|
+
// Put the node on the stack before recursing on its children
|
|
132
|
+
state._containers.push(node);
|
|
133
|
+
state._ancestors.push(node);
|
|
134
|
+
|
|
135
|
+
// Record the node's text content if it has any.
|
|
136
|
+
// Usually this is for nodes with a type property of "text",
|
|
137
|
+
// but other nodes types like "math" may also have content.
|
|
138
|
+
// @ts-expect-error - TS2339 - Property 'content' does not exist on type 'TreeNode'.
|
|
139
|
+
if (typeof node.content === "string") {
|
|
140
|
+
// @ts-expect-error - TS2339 - Property 'content' does not exist on type 'TreeNode'.
|
|
141
|
+
content = node.content;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Recurse on the node. If there was content above, then there
|
|
145
|
+
// probably won't be any children to recurse on, but we check
|
|
146
|
+
// anyway.
|
|
147
|
+
//
|
|
148
|
+
// If we wanted to make the traversal completely specific to the
|
|
149
|
+
// actual Perseus parse trees that we'll be dealing with we could
|
|
150
|
+
// put a switch statement here to dispatch on the node type
|
|
151
|
+
// property with specific recursion steps for each known type of
|
|
152
|
+
// node.
|
|
153
|
+
const keys = Object.keys(node);
|
|
154
|
+
keys.forEach((key) => {
|
|
155
|
+
// Never recurse on the type property
|
|
156
|
+
if (key === "type") {
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
// Ignore properties that are null or primitive and only
|
|
160
|
+
// recurse on objects and arrays. Note that we don't do a
|
|
161
|
+
// isNode() check here. That is done in the recursive call to
|
|
162
|
+
// _traverse(). Note that the recursive call on each child
|
|
163
|
+
// returns the text content of the child and we add that
|
|
164
|
+
// content to the content for this node. Also note that we
|
|
165
|
+
// push the name of the property we're recursing over onto a
|
|
166
|
+
// TraversalState stack.
|
|
167
|
+
const value = node[key];
|
|
168
|
+
if (value && typeof value === "object") {
|
|
169
|
+
state._indexes.push(key);
|
|
170
|
+
content += this._traverse(value, state, f);
|
|
171
|
+
state._indexes.pop();
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
// Restore the stacks after recursing on the children
|
|
176
|
+
state._currentNode = state._ancestors.pop();
|
|
177
|
+
state._containers.pop();
|
|
178
|
+
|
|
179
|
+
// And finally call the traversal callback for this node. Note
|
|
180
|
+
// that this is post-order traversal. We call the callback on the
|
|
181
|
+
// way back up the tree, not on the way down. That way we already
|
|
182
|
+
// know all the content contained within the node.
|
|
183
|
+
f(node, state, content);
|
|
184
|
+
} else if (Array.isArray(n)) {
|
|
185
|
+
// If we were called on an array instead of a node, then
|
|
186
|
+
// this is the code we use to recurse.
|
|
187
|
+
const nodes = n;
|
|
188
|
+
|
|
189
|
+
// Push the array onto the stack. This will allow the
|
|
190
|
+
// TraversalState object to locate siblings of this node.
|
|
191
|
+
state._containers.push(nodes);
|
|
192
|
+
|
|
193
|
+
// Now loop through this array and recurse on each element in it.
|
|
194
|
+
// Before recursing on an element, we push its array index on a
|
|
195
|
+
// TraversalState stack so that the TraversalState sibling methods
|
|
196
|
+
// can work. Note that TraversalState methods can alter the length
|
|
197
|
+
// of the array, and change the index of the current node, so we
|
|
198
|
+
// are careful here to test the array length on each iteration and
|
|
199
|
+
// to reset the index when we pop the stack. Also note that we
|
|
200
|
+
// concatentate the text content of the children.
|
|
201
|
+
let index = 0;
|
|
202
|
+
while (index < nodes.length) {
|
|
203
|
+
state._indexes.push(index);
|
|
204
|
+
content += this._traverse(nodes[index], state, f);
|
|
205
|
+
// Casting to convince TypeScript that this is a number
|
|
206
|
+
index = (state._indexes.pop() as number) + 1;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Pop the array off the stack. Note, however, that we do not call
|
|
210
|
+
// the traversal callback on the array. That function is only
|
|
211
|
+
// called for nodes, not arrays of nodes.
|
|
212
|
+
state._containers.pop();
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// The _traverse() method always returns the text content of
|
|
216
|
+
// this node and its children. This is the one piece of state that
|
|
217
|
+
// is not tracked in the TraversalState object.
|
|
218
|
+
return content;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// An instance of this class is passed to the callback function for
|
|
223
|
+
// each node traversed. The class itself is not exported, but its
|
|
224
|
+
// methods define the API available to the traversal callback.
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* This class represents the state of a tree traversal. An instance is created
|
|
228
|
+
* by the traverse() method of the TreeTransformer class to maintain the state
|
|
229
|
+
* for that traversal, and the instance is passed to the traversal callback
|
|
230
|
+
* function for each node that is traversed. This class is not intended to be
|
|
231
|
+
* instantiated directly, but is exported so that its type can be used for
|
|
232
|
+
* type annotaions.
|
|
233
|
+
**/
|
|
234
|
+
export class TraversalState {
|
|
235
|
+
// The root node of the tree being traversed
|
|
236
|
+
root: TreeNode;
|
|
237
|
+
|
|
238
|
+
// These are internal state properties. Use the accessor methods defined
|
|
239
|
+
// below instead of using these properties directly. Note that the
|
|
240
|
+
// _containers and _indexes stacks can have two different types of
|
|
241
|
+
// elements, depending on whether we just recursed on an array or on a
|
|
242
|
+
// node. This is hard for TypeScript to deal with, so you'll see a number of
|
|
243
|
+
// type casts through the any type when working with these two properties.
|
|
244
|
+
_currentNode: TreeNode | null | undefined;
|
|
245
|
+
_containers: Stack<TreeNode | Array<TreeNode>>;
|
|
246
|
+
_indexes: Stack<string | number>;
|
|
247
|
+
_ancestors: Stack<TreeNode>;
|
|
248
|
+
|
|
249
|
+
// The constructor just stores the root node and creates empty stacks.
|
|
250
|
+
constructor(root: TreeNode) {
|
|
251
|
+
this.root = root;
|
|
252
|
+
|
|
253
|
+
// When the callback is called, this property will hold the
|
|
254
|
+
// node that is currently being traversed.
|
|
255
|
+
this._currentNode = null;
|
|
256
|
+
|
|
257
|
+
// This is a stack of the objects and arrays that we've
|
|
258
|
+
// traversed through before reaching the currentNode.
|
|
259
|
+
// It is different than the ancestors array.
|
|
260
|
+
this._containers = new Stack();
|
|
261
|
+
|
|
262
|
+
// This stack has the same number of elements as the _containers
|
|
263
|
+
// stack. The last element of this._indexes[] is the index of
|
|
264
|
+
// the current node in the object or array that is the last element
|
|
265
|
+
// of this._containers[]. If the last element of this._containers[] is
|
|
266
|
+
// an array, then the last element of this stack will be a number.
|
|
267
|
+
// Otherwise if the last container is an object, then the last index
|
|
268
|
+
// will be a string property name.
|
|
269
|
+
this._indexes = new Stack();
|
|
270
|
+
|
|
271
|
+
// This is a stack of the ancestor nodes of the current one.
|
|
272
|
+
// It is different than the containers[] stack because it only
|
|
273
|
+
// includes nodes, not arrays.
|
|
274
|
+
this._ancestors = new Stack();
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Return the current node in the traversal. Any time the traversal
|
|
279
|
+
* callback is called, this method will return the name value as the
|
|
280
|
+
* first argument to the callback.
|
|
281
|
+
*/
|
|
282
|
+
currentNode(): TreeNode {
|
|
283
|
+
return this._currentNode || this.root;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Return the parent of the current node, if there is one, or null.
|
|
288
|
+
*/
|
|
289
|
+
parent(): TreeNode | null | undefined {
|
|
290
|
+
return this._ancestors.top();
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Return an array of ancestor nodes. The first element of this array is
|
|
295
|
+
* the same as this.parent() and the last element is the root node. If we
|
|
296
|
+
* are currently at the root node, the the returned array will be empty.
|
|
297
|
+
* This method makes a copy of the internal state, so modifications to the
|
|
298
|
+
* returned array have no effect on the traversal.
|
|
299
|
+
*/
|
|
300
|
+
ancestors(): ReadonlyArray<TreeNode> {
|
|
301
|
+
return this._ancestors.values();
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Return the next sibling of this node, if it has one, or null otherwise.
|
|
306
|
+
*/
|
|
307
|
+
nextSibling(): TreeNode | null | undefined {
|
|
308
|
+
const siblings = this._containers.top();
|
|
309
|
+
|
|
310
|
+
// If we're at the root of the tree or if the parent is an
|
|
311
|
+
// object instead of an array, then there are no siblings.
|
|
312
|
+
if (!siblings || !Array.isArray(siblings)) {
|
|
313
|
+
return null;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// The top index is a number because the top container is an array
|
|
317
|
+
const index = this._indexes.top() as number;
|
|
318
|
+
if (siblings.length > index + 1) {
|
|
319
|
+
return siblings[index + 1];
|
|
320
|
+
}
|
|
321
|
+
return null; // There is no next sibling
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Return the previous sibling of this node, if it has one, or null
|
|
326
|
+
* otherwise.
|
|
327
|
+
*/
|
|
328
|
+
previousSibling(): TreeNode | null | undefined {
|
|
329
|
+
const siblings = this._containers.top();
|
|
330
|
+
|
|
331
|
+
// If we're at the root of the tree or if the parent is an
|
|
332
|
+
// object instead of an array, then there are no siblings.
|
|
333
|
+
if (!siblings || !Array.isArray(siblings)) {
|
|
334
|
+
return null;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// The top index is a number because the top container is an array
|
|
338
|
+
const index = this._indexes.top() as number;
|
|
339
|
+
if (index > 0) {
|
|
340
|
+
return siblings[index - 1];
|
|
341
|
+
}
|
|
342
|
+
return null; // There is no previous sibling
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Remove the next sibling node (if there is one) from the tree. Returns
|
|
347
|
+
* the removed sibling or null. This method makes it easy to traverse a
|
|
348
|
+
* tree and concatenate adjacent text nodes into a single node.
|
|
349
|
+
*/
|
|
350
|
+
removeNextSibling(): TreeNode | null | undefined {
|
|
351
|
+
const siblings = this._containers.top();
|
|
352
|
+
if (siblings && Array.isArray(siblings)) {
|
|
353
|
+
// top index is a number because top container is an array
|
|
354
|
+
const index = this._indexes.top() as number;
|
|
355
|
+
if (siblings.length > index + 1) {
|
|
356
|
+
return siblings.splice(index + 1, 1)[0];
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
return null;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Replace the current node in the tree with the specified nodes. If no
|
|
364
|
+
* nodes are passed, this is a node deletion. If one node (or array) is
|
|
365
|
+
* passed, this is a 1-for-1 replacement. If more than one node is passed
|
|
366
|
+
* then this is a combination of deletion and insertion. The new node or
|
|
367
|
+
* nodes will not be traversed, so this method can safely be used to
|
|
368
|
+
* reparent the current node node beneath a new parent.
|
|
369
|
+
*
|
|
370
|
+
* This method throws an error if you attempt to replace the root node of
|
|
371
|
+
* the tree.
|
|
372
|
+
*/
|
|
373
|
+
replace(...replacements: ReadonlyArray<TreeNode>): void {
|
|
374
|
+
const parent = this._containers.top();
|
|
375
|
+
if (!parent) {
|
|
376
|
+
throw new PerseusError(
|
|
377
|
+
"Can't replace the root of the tree",
|
|
378
|
+
Errors.Internal,
|
|
379
|
+
);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// The top of the container stack is either an array or an object
|
|
383
|
+
// and the top of the indexes stack is a corresponding array index
|
|
384
|
+
// or object property. This is hard for TypeScript, so we have to do some
|
|
385
|
+
// unsafe casting and be careful when we use which cast version
|
|
386
|
+
if (Array.isArray(parent)) {
|
|
387
|
+
const index = this._indexes.top() as number;
|
|
388
|
+
// For an array parent we just splice the new nodes in
|
|
389
|
+
parent.splice(index, 1, ...replacements);
|
|
390
|
+
// Adjust the index to account for the changed array length.
|
|
391
|
+
// We don't want to traverse any of the newly inserted nodes.
|
|
392
|
+
this._indexes.pop();
|
|
393
|
+
this._indexes.push(index + replacements.length - 1);
|
|
394
|
+
} else {
|
|
395
|
+
const property = this._indexes.top() as string;
|
|
396
|
+
// For an object parent we care how many new nodes there are
|
|
397
|
+
if (replacements.length === 0) {
|
|
398
|
+
// Deletion
|
|
399
|
+
delete parent[property];
|
|
400
|
+
} else if (replacements.length === 1) {
|
|
401
|
+
// Replacement
|
|
402
|
+
parent[property] = replacements[0];
|
|
403
|
+
} else {
|
|
404
|
+
// Replace one node with an array of nodes
|
|
405
|
+
parent[property] = replacements;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* Returns true if the current node has a previous sibling and false
|
|
412
|
+
* otherwise. If this method returns false, then previousSibling() will
|
|
413
|
+
* return null, and goToPreviousSibling() will throw an error.
|
|
414
|
+
*/
|
|
415
|
+
hasPreviousSibling(): boolean {
|
|
416
|
+
return (
|
|
417
|
+
Array.isArray(this._containers.top()) &&
|
|
418
|
+
(this._indexes.top() as number) > 0
|
|
419
|
+
);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* Modify this traversal state object to have the state it would have had
|
|
424
|
+
* when visiting the previous sibling. Note that you may want to use
|
|
425
|
+
* clone() to make a copy before modifying the state object like this.
|
|
426
|
+
* This mutator method is not typically used during ordinary tree
|
|
427
|
+
* traversals, but is used by the Selector class for matching multi-node
|
|
428
|
+
* selectors.
|
|
429
|
+
*/
|
|
430
|
+
goToPreviousSibling(): void {
|
|
431
|
+
if (!this.hasPreviousSibling()) {
|
|
432
|
+
throw new PerseusError(
|
|
433
|
+
"goToPreviousSibling(): node has no previous sibling",
|
|
434
|
+
Errors.Internal,
|
|
435
|
+
);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
this._currentNode = this.previousSibling();
|
|
439
|
+
// Since we know that we have a previous sibling, we know that
|
|
440
|
+
// the value on top of the stack is a number, but we have to do
|
|
441
|
+
// this unsafe cast because TypeScript doesn't know that.
|
|
442
|
+
const index = this._indexes.pop() as number;
|
|
443
|
+
this._indexes.push(index - 1);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* Returns true if the current node has an ancestor and false otherwise.
|
|
448
|
+
* If this method returns false, then the parent() method will return
|
|
449
|
+
* null and goToParent() will throw an error
|
|
450
|
+
*/
|
|
451
|
+
hasParent(): boolean {
|
|
452
|
+
return this._ancestors.size() !== 0;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* Modify this object to look like it will look when we (later) visit the
|
|
457
|
+
* parent node of this node. You should not modify the instance passed to
|
|
458
|
+
* the tree traversal callback. Instead, make a copy with the clone()
|
|
459
|
+
* method and modify that. This mutator method is not typically used
|
|
460
|
+
* during ordinary tree traversals, but is used by the Selector class for
|
|
461
|
+
* matching multi-node selectors that involve parent and ancestor
|
|
462
|
+
* selectors.
|
|
463
|
+
*/
|
|
464
|
+
goToParent(): void {
|
|
465
|
+
if (!this.hasParent()) {
|
|
466
|
+
throw new PerseusError(
|
|
467
|
+
"goToParent(): node has no ancestor",
|
|
468
|
+
Errors.NotAllowed,
|
|
469
|
+
);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
this._currentNode = this._ancestors.pop();
|
|
473
|
+
|
|
474
|
+
// We need to pop the containers and indexes stacks at least once
|
|
475
|
+
// and more as needed until we restore the invariant that
|
|
476
|
+
// this._containers.top()[this.indexes.top()] === this._currentNode
|
|
477
|
+
//
|
|
478
|
+
while (
|
|
479
|
+
this._containers.size() &&
|
|
480
|
+
this._containers.top()[this._indexes.top()] !== this._currentNode
|
|
481
|
+
) {
|
|
482
|
+
this._containers.pop();
|
|
483
|
+
this._indexes.pop();
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
/**
|
|
488
|
+
* Return a new TraversalState object that is a copy of this one.
|
|
489
|
+
* This method is useful in conjunction with the mutating methods
|
|
490
|
+
* goToParent() and goToPreviousSibling().
|
|
491
|
+
*/
|
|
492
|
+
clone(): TraversalState {
|
|
493
|
+
const clone = new TraversalState(this.root);
|
|
494
|
+
clone._currentNode = this._currentNode;
|
|
495
|
+
clone._containers = this._containers.clone();
|
|
496
|
+
clone._indexes = this._indexes.clone();
|
|
497
|
+
clone._ancestors = this._ancestors.clone();
|
|
498
|
+
return clone;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
/**
|
|
502
|
+
* Returns true if this TraversalState object is equal to that
|
|
503
|
+
* TraversalState object, or false otherwise. This method exists
|
|
504
|
+
* primarily for use by our unit tests.
|
|
505
|
+
*/
|
|
506
|
+
equals(that: TraversalState): boolean {
|
|
507
|
+
return (
|
|
508
|
+
this.root === that.root &&
|
|
509
|
+
this._currentNode === that._currentNode &&
|
|
510
|
+
this._containers.equals(that._containers) &&
|
|
511
|
+
this._indexes.equals(that._indexes) &&
|
|
512
|
+
this._ancestors.equals(that._ancestors)
|
|
513
|
+
);
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
/**
|
|
518
|
+
* This class is an internal utility that just treats an array as a stack
|
|
519
|
+
* and gives us a top() method so we don't have to write expressions like
|
|
520
|
+
* `ancestors[ancestors.length-1]`. The values() method automatically
|
|
521
|
+
* copies the internal array so we don't have to worry about client code
|
|
522
|
+
* modifying our internal stacks. The use of this Stack abstraction makes
|
|
523
|
+
* the TraversalState class simpler in a number of places.
|
|
524
|
+
*/
|
|
525
|
+
class Stack<T> {
|
|
526
|
+
stack: Array<T>;
|
|
527
|
+
|
|
528
|
+
constructor(array?: ReadonlyArray<T> | null) {
|
|
529
|
+
this.stack = array ? array.slice(0) : [];
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
/** Push a value onto the stack. */
|
|
533
|
+
push(v: T): void {
|
|
534
|
+
this.stack.push(v);
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
/** Pop a value off of the stack. */
|
|
538
|
+
pop(): T {
|
|
539
|
+
// @ts-expect-error - TS2322 - Type 'T | undefined' is not assignable to type 'T'.
|
|
540
|
+
return this.stack.pop();
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
/** Return the top value of the stack without popping it. */
|
|
544
|
+
top(): T {
|
|
545
|
+
return this.stack[this.stack.length - 1];
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
/** Return a copy of the stack as an array */
|
|
549
|
+
values(): ReadonlyArray<T> {
|
|
550
|
+
return this.stack.slice(0);
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
/** Return the number of elements in the stack */
|
|
554
|
+
size(): number {
|
|
555
|
+
return this.stack.length;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
/** Return a string representation of the stack */
|
|
559
|
+
toString(): string {
|
|
560
|
+
return this.stack.toString();
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
/** Return a shallow copy of the stack */
|
|
564
|
+
clone(): Stack<T> {
|
|
565
|
+
return new Stack(this.stack);
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
/**
|
|
569
|
+
* Compare this stack to another and return true if the contents of
|
|
570
|
+
* the two arrays are the same.
|
|
571
|
+
*/
|
|
572
|
+
equals(that: Stack<T>): boolean {
|
|
573
|
+
if (!that || !that.stack || that.stack.length !== this.stack.length) {
|
|
574
|
+
return false;
|
|
575
|
+
}
|
|
576
|
+
for (let i = 0; i < this.stack.length; i++) {
|
|
577
|
+
if (this.stack[i] !== that.stack[i]) {
|
|
578
|
+
return false;
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
return true;
|
|
582
|
+
}
|
|
583
|
+
}
|
package/src/types.ts
ADDED
package/src/version.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// This file is processed by a Rollup plugin (replace) to inject the production
|
|
2
|
+
// version number during the release build.
|
|
3
|
+
// In dev, you'll never see the version number.
|
|
4
|
+
|
|
5
|
+
import {addLibraryVersionToPerseusDebug} from "@khanacademy/perseus-core";
|
|
6
|
+
|
|
7
|
+
const libName = "@khanacademy/perseus-linter";
|
|
8
|
+
export const libVersion = "__lib_version__";
|
|
9
|
+
|
|
10
|
+
addLibraryVersionToPerseusDebug(libName, libVersion);
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "../tsconfig-shared.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"outDir": "./dist",
|
|
5
|
+
"rootDir": "src",
|
|
6
|
+
},
|
|
7
|
+
"references": [
|
|
8
|
+
{"path": "../perseus-core/tsconfig-build.json"},
|
|
9
|
+
{"path": "../perseus-error/tsconfig-build.json"},
|
|
10
|
+
{"path": "../pure-markdown/tsconfig-build.json"},
|
|
11
|
+
]
|
|
12
|
+
}
|