@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,446 @@
1
+ // @flow
2
+ import TreeTransformer from "../tree-transformer.js";
3
+
4
+ describe("gorgon tree transformer", () => {
5
+ function clone(o) {
6
+ return JSON.parse(JSON.stringify(o));
7
+ }
8
+
9
+ const tree1 = {
10
+ id: 0,
11
+ type: "root",
12
+ content: [
13
+ {id: 1, type: "text", content: "Hello, "},
14
+ {
15
+ id: 2,
16
+ type: "em",
17
+ content: {
18
+ id: 3,
19
+ type: "text",
20
+ content: "World!",
21
+ },
22
+ },
23
+ {
24
+ id: 4,
25
+ type: "list",
26
+ items: [
27
+ {id: 5, type: "text", content: "A"},
28
+ {id: 6, type: "text", content: "B"},
29
+ {id: 7, type: "text", content: "C"},
30
+ ],
31
+ },
32
+ ],
33
+ };
34
+
35
+ // These are three variants on the same tree where we use arrays
36
+ // instead of single nodes. The tests below will be run over all
37
+ // four variants, and should work the same for all.
38
+ const tree2 = [clone(tree1)];
39
+ const tree3 = clone(tree1);
40
+ tree3.content[1].content = [tree3.content[1].content];
41
+ const tree4 = clone(tree2);
42
+ tree4[0].content[1].content = [tree4[0].content[1].content];
43
+
44
+ const trees = [tree1, tree2, tree3, tree4];
45
+
46
+ const postOrderTraversalOrder = [1, 3, 2, 5, 6, 7, 4, 0];
47
+ const parentNodeIds = [-1, 0, 0, 2, 0, 4, 4, 4];
48
+ const previousNodeIds = [-1, -1, 1, -1, 2, -1, 5, 6];
49
+ const nextNodeIds = [-1, 2, 4, -1, -1, 6, 7, -1];
50
+
51
+ // The first test will fill in this array mapping numbers to nodes
52
+ // Then subsequent tests can use it
53
+ const nodes = [];
54
+
55
+ function getTraversalOrder(tree) {
56
+ const order = [];
57
+ new TreeTransformer(tree).traverse((n, state) => {
58
+ // $FlowFixMe[prop-missing]
59
+ order.push(n.id);
60
+ });
61
+ return order;
62
+ }
63
+
64
+ trees.forEach((tree: $FlowFixMe, treenum: number) => {
65
+ it(
66
+ "does post-order traversal of each node in the tree " + treenum,
67
+ () => {
68
+ const tt = new TreeTransformer(tree);
69
+ const ids = [];
70
+
71
+ tt.traverse((n: $FlowFixMe) => {
72
+ nodes[n.id] = n; // Remember the nodes by id for later tests
73
+ ids.push(n.id);
74
+ });
75
+
76
+ // Post-order traversal means we visit the nodes on the way
77
+ // back up, not on the way down.
78
+ expect(ids).toEqual(postOrderTraversalOrder);
79
+ },
80
+ );
81
+
82
+ it("tracks the current node " + treenum, () => {
83
+ new TreeTransformer(tree).traverse((n, state) => {
84
+ expect(state.currentNode()).toEqual(n);
85
+ });
86
+ });
87
+
88
+ it("correctly gets the siblings for each node " + treenum, () => {
89
+ new TreeTransformer(tree).traverse((n: $FlowFixMe, state) => {
90
+ const previd = previousNodeIds[n.id];
91
+ expect(state.hasPreviousSibling()).toEqual(previd >= 0);
92
+ expect(state.previousSibling()).toEqual(
93
+ previd >= 0 ? nodes[previd] : null,
94
+ );
95
+
96
+ const nextid = nextNodeIds[n.id];
97
+ expect(state.nextSibling()).toEqual(
98
+ nextid >= 0 ? nodes[nextid] : null,
99
+ );
100
+ });
101
+ });
102
+
103
+ it("knows the ancestors for each node: " + treenum, () => {
104
+ const ancestorsById = [
105
+ [],
106
+ [0],
107
+ [0],
108
+ [0, 2],
109
+ [0],
110
+ [0, 4],
111
+ [0, 4],
112
+ [0, 4],
113
+ ];
114
+ const ancestorTypesById = [
115
+ [],
116
+ ["root"],
117
+ ["root"],
118
+ ["root", "em"],
119
+ ["root"],
120
+ ["root", "list"],
121
+ ["root", "list"],
122
+ ["root", "list"],
123
+ ];
124
+
125
+ new TreeTransformer(tree).traverse((n: $FlowFixMe, state) => {
126
+ expect(state.hasParent()).toEqual(
127
+ ancestorsById[n.id].length > 0,
128
+ );
129
+ const ancestorids = ancestorsById[n.id];
130
+ expect(state.parent()).toEqual(
131
+ nodes[ancestorids[ancestorids.length - 1]],
132
+ );
133
+ expect(state.ancestors()).toEqual(
134
+ ancestorsById[n.id].map((id) => nodes[id]),
135
+ );
136
+ expect(ancestorsById[n.id].map((id) => nodes[id].type)).toEqual(
137
+ ancestorTypesById[n.id],
138
+ );
139
+ });
140
+ });
141
+
142
+ it("computes the textContent for each node " + treenum, () => {
143
+ const textContentForNode = [
144
+ "Hello, World!ABC",
145
+ "Hello, ",
146
+ "World!",
147
+ "World!",
148
+ "ABC",
149
+ "A",
150
+ "B",
151
+ "C",
152
+ ];
153
+
154
+ new TreeTransformer(tree).traverse(
155
+ (n: $FlowFixMe, state, content) => {
156
+ expect(content).toEqual(textContentForNode[n.id]);
157
+ },
158
+ );
159
+ });
160
+
161
+ it("can remove the next sibling " + treenum, () => {
162
+ const expectedTraversals = [
163
+ // if a node has no next sibling, the test is a no-op
164
+ postOrderTraversalOrder,
165
+ [1, 5, 6, 7, 4, 0],
166
+ [1, 3, 2, 0],
167
+ postOrderTraversalOrder,
168
+ postOrderTraversalOrder,
169
+ [1, 3, 2, 5, 7, 4, 0],
170
+ [1, 3, 2, 5, 6, 4, 0],
171
+ postOrderTraversalOrder,
172
+ ];
173
+
174
+ // For each node in the tree
175
+ for (let id = 0; id < nodes.length; id++) {
176
+ // Start with a copy of the tree
177
+ const copy = clone(tree);
178
+
179
+ // Remove the next sibling of the node with this id
180
+ new TreeTransformer(copy).traverse(
181
+ (n: $FlowFixMe, state: $FlowFixMe) => {
182
+ if (n.id === id) {
183
+ state.removeNextSibling();
184
+ }
185
+
186
+ // Ensure that we don't iterate the removed sibling
187
+ expect(n.id).not.toEqual(nextNodeIds[id]);
188
+ },
189
+ );
190
+
191
+ // And then get the traversal order of the resulting tree
192
+ const traversal = getTraversalOrder(copy);
193
+
194
+ // Compare it to the expected value
195
+ expect(traversal).toEqual(expectedTraversals[id]);
196
+ }
197
+ });
198
+
199
+ it("won't try to replace the root of the tree " + treenum, () => {
200
+ const copy = clone(tree);
201
+ new TreeTransformer(copy).traverse((n, state) => {
202
+ if (n === state.root) {
203
+ expect(() => state.replace()).toThrow();
204
+ }
205
+ });
206
+ });
207
+
208
+ it("Can remove nodes " + treenum, () => {
209
+ const expectedTraversals = [
210
+ null,
211
+ [3, 2, 5, 6, 7, 4, 0],
212
+ [1, 5, 6, 7, 4, 0],
213
+ [1, 2, 5, 6, 7, 4, 0],
214
+ [1, 3, 2, 0],
215
+ [1, 3, 2, 6, 7, 4, 0],
216
+ [1, 3, 2, 5, 7, 4, 0],
217
+ [1, 3, 2, 5, 6, 4, 0],
218
+ ];
219
+
220
+ // Loop through all the nodes except the root
221
+ for (let id = 1; id < nodes.length; id++) {
222
+ // Make a copy of the tree
223
+ const copy = clone(tree);
224
+ // Remove this node from it
225
+ new TreeTransformer(copy).traverse(
226
+ (n: $FlowFixMe, state: $FlowFixMe) => {
227
+ if (n.id === id) {
228
+ state.replace();
229
+ }
230
+ },
231
+ );
232
+
233
+ // Traverse what remains and see if we get what is expected
234
+ expect(getTraversalOrder(copy)).toEqual(expectedTraversals[id]);
235
+ }
236
+ });
237
+
238
+ it("Can replace nodes " + treenum, () => {
239
+ const expectedTraversals = [
240
+ null,
241
+ [99, 3, 2, 5, 6, 7, 4, 0],
242
+ [1, 99, 5, 6, 7, 4, 0],
243
+ [1, 99, 2, 5, 6, 7, 4, 0],
244
+ [1, 3, 2, 99, 0],
245
+ [1, 3, 2, 99, 6, 7, 4, 0],
246
+ [1, 3, 2, 5, 99, 7, 4, 0],
247
+ [1, 3, 2, 5, 6, 99, 4, 0],
248
+ ];
249
+
250
+ // Loop through all the nodes except the root
251
+ for (let id = 1; id < nodes.length; id++) {
252
+ // Make a copy of the tree
253
+ const copy = clone(tree);
254
+ // Replace the node with a different one
255
+ new TreeTransformer(copy).traverse((n: $FlowFixMe, state) => {
256
+ if (n.id === id) {
257
+ state.replace({
258
+ id: 99,
259
+ type: "replacement",
260
+ });
261
+ }
262
+
263
+ // Ensure that we don't traverse the new node
264
+ expect(n.id).not.toEqual(99);
265
+ });
266
+
267
+ // Traverse what remains and see if we get what is expected
268
+ expect(getTraversalOrder(copy)).toEqual(expectedTraversals[id]);
269
+ }
270
+ });
271
+
272
+ it("Can reparent nodes " + treenum, () => {
273
+ const expectedTraversals = [
274
+ null,
275
+ [1, 99, 3, 2, 5, 6, 7, 4, 0],
276
+ [1, 3, 2, 99, 5, 6, 7, 4, 0],
277
+ [1, 3, 99, 2, 5, 6, 7, 4, 0],
278
+ [1, 3, 2, 5, 6, 7, 4, 99, 0],
279
+ [1, 3, 2, 5, 99, 6, 7, 4, 0],
280
+ [1, 3, 2, 5, 6, 99, 7, 4, 0],
281
+ [1, 3, 2, 5, 6, 7, 99, 4, 0],
282
+ ];
283
+
284
+ // Loop through all the nodes except the root
285
+ for (let id = 1; id < nodes.length; id++) {
286
+ // Make a copy of the tree
287
+ const copy = clone(tree);
288
+ let count = 0;
289
+ // Replace the node with a different one
290
+ new TreeTransformer(copy).traverse((n: $FlowFixMe, state) => {
291
+ if (n.id === id) {
292
+ // Ensure that we don't traverse the node more than once
293
+ expect(++count).toEqual(1);
294
+ state.replace({
295
+ id: 99,
296
+ type: "reparent",
297
+ content: n,
298
+ });
299
+ }
300
+
301
+ // Ensure that we don't traverse the new node
302
+ expect(n.id).not.toEqual(99);
303
+ });
304
+
305
+ // Traverse what remains and see if we get what is expected
306
+ expect(getTraversalOrder(copy)).toEqual(expectedTraversals[id]);
307
+ }
308
+ });
309
+
310
+ it("Can replace nodes with an array of nodes " + treenum, () => {
311
+ const expectedTraversals = [
312
+ null,
313
+ [99, 101, 100, 3, 2, 5, 6, 7, 4, 0],
314
+ [1, 99, 101, 100, 5, 6, 7, 4, 0],
315
+ [1, 99, 101, 100, 2, 5, 6, 7, 4, 0],
316
+ [1, 3, 2, 99, 101, 100, 0],
317
+ [1, 3, 2, 99, 101, 100, 6, 7, 4, 0],
318
+ [1, 3, 2, 5, 99, 101, 100, 7, 4, 0],
319
+ [1, 3, 2, 5, 6, 99, 101, 100, 4, 0],
320
+ ];
321
+
322
+ // Loop through all the nodes except the root
323
+ for (let id = 1; id < nodes.length; id++) {
324
+ // Make a copy of the tree
325
+ const copy = clone(tree);
326
+ // Replace the node with two new ones
327
+ new TreeTransformer(copy).traverse((n: $FlowFixMe, state) => {
328
+ if (n.id === id) {
329
+ // $FlowFixMe[incompatible-call]
330
+ state.replace([
331
+ {
332
+ id: 99,
333
+ type: "replacement",
334
+ },
335
+ {
336
+ id: 100,
337
+ type: "replacement",
338
+ content: {
339
+ id: 101,
340
+ type: "nested",
341
+ },
342
+ },
343
+ ]);
344
+ }
345
+
346
+ // Ensure that we don't traverse any new nodes
347
+ expect(n.id).not.toEqual(99);
348
+ expect(n.id).not.toEqual(100);
349
+ expect(n.id).not.toEqual(101);
350
+ });
351
+
352
+ // Traverse what remains and see if we get what is expected
353
+ expect(getTraversalOrder(copy)).toEqual(expectedTraversals[id]);
354
+ }
355
+ });
356
+
357
+ it("Can replace nodes with two nodes " + treenum, () => {
358
+ const expectedTraversals = [
359
+ null,
360
+ [99, 101, 100, 3, 2, 5, 6, 7, 4, 0],
361
+ [1, 99, 101, 100, 5, 6, 7, 4, 0],
362
+ [1, 99, 101, 100, 2, 5, 6, 7, 4, 0],
363
+ [1, 3, 2, 99, 101, 100, 0],
364
+ [1, 3, 2, 99, 101, 100, 6, 7, 4, 0],
365
+ [1, 3, 2, 5, 99, 101, 100, 7, 4, 0],
366
+ [1, 3, 2, 5, 6, 99, 101, 100, 4, 0],
367
+ ];
368
+
369
+ // Loop through all the nodes except the root
370
+ for (let id = 1; id < nodes.length; id++) {
371
+ // Make a copy of the tree
372
+ const copy = clone(tree);
373
+ // Replace the node with two new ones
374
+ new TreeTransformer(copy).traverse((n: $FlowFixMe, state) => {
375
+ if (n.id === id) {
376
+ state.replace(
377
+ {
378
+ id: 99,
379
+ type: "replacement",
380
+ },
381
+ {
382
+ id: 100,
383
+ type: "replacement",
384
+ content: {
385
+ id: 101,
386
+ type: "nested",
387
+ },
388
+ },
389
+ );
390
+ }
391
+
392
+ // Ensure that we don't traverse any new nodes
393
+ expect(n.id).not.toEqual(99);
394
+ expect(n.id).not.toEqual(100);
395
+ expect(n.id).not.toEqual(101);
396
+ });
397
+
398
+ // Traverse what remains and see if we get what is expected
399
+ expect(getTraversalOrder(copy)).toEqual(expectedTraversals[id]);
400
+ }
401
+ });
402
+
403
+ it("goToParent() and goToPreviousSibling() work " + treenum, () => {
404
+ // Traverse the tree, saving copies of the state object
405
+ // for each node. Then check that goToParent() and
406
+ // goToPreviousSibling() on each saved state object
407
+ // modify the states in apprpriate ways.
408
+ const states = [];
409
+
410
+ const tt = new TreeTransformer(tree);
411
+ tt.traverse((n: $FlowFixMe, state, content) => {
412
+ states[n.id] = state.clone();
413
+ // Verify that a clone() and equal() work as expected
414
+ expect(state.equals(states[n.id])).toBeTruthy();
415
+ });
416
+
417
+ // Check that goToPreviousSibling() works
418
+ for (let n = 0; n < states.length; n++) {
419
+ const state = states[n].clone();
420
+ const previousNodeId = previousNodeIds[n];
421
+ if (previousNodeId === -1) {
422
+ expect(() => {
423
+ state.goToPreviousSibling();
424
+ }).toThrow();
425
+ } else {
426
+ state.goToPreviousSibling();
427
+ expect(state.equals(states[previousNodeId])).toBeTruthy();
428
+ }
429
+ }
430
+
431
+ // Check that goToParent() works
432
+ for (let n = 0; n < states.length; n++) {
433
+ const state = states[n].clone();
434
+ const parentNodeId = parentNodeIds[n];
435
+ if (parentNodeId === -1) {
436
+ expect(() => {
437
+ state.goToParent();
438
+ }).toThrow();
439
+ } else {
440
+ state.goToParent();
441
+ expect(state.equals(states[parentNodeId])).toBeTruthy();
442
+ }
443
+ }
444
+ });
445
+ });
446
+ });