@khanacademy/perseus-linter 0.1.0

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