@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,488 @@
1
+ // @flow
2
+ import * as PureMarkdown from "@khanacademy/pure-markdown";
3
+
4
+ import absoluteUrlRule from "../rules/absolute-url.js";
5
+ import blockquotedMathRule from "../rules/blockquoted-math.js";
6
+ import blockquotedWidgetRule from "../rules/blockquoted-widget.js";
7
+ import doubleSpacingAfterTerminalRule from "../rules/double-spacing-after-terminal.js";
8
+ import extraContentSpacingRule from "../rules/extra-content-spacing.js";
9
+ import headingLevel1Rule from "../rules/heading-level-1.js";
10
+ import headingLevelSkipRule from "../rules/heading-level-skip.js";
11
+ import headingSentenceCaseRule from "../rules/heading-sentence-case.js";
12
+ import headingTitleCaseRule from "../rules/heading-title-case.js";
13
+ import imageAltTextRule from "../rules/image-alt-text.js";
14
+ import imageInTableRule from "../rules/image-in-table.js";
15
+ import imageSpacesAroundUrlsRule from "../rules/image-spaces-around-urls.js";
16
+ import imageWidgetRule from "../rules/image-widget.js";
17
+ import linkClickHereRule from "../rules/link-click-here.js";
18
+ import longParagraphRule from "../rules/long-paragraph.js";
19
+ import mathAdjacentRule from "../rules/math-adjacent.js";
20
+ import mathAlignExtraBreakRule from "../rules/math-align-extra-break.js";
21
+ import mathAlignLinebreaksRule from "../rules/math-align-linebreaks.js";
22
+ import mathEmptyRule from "../rules/math-empty.js";
23
+ import mathFontSizeRule from "../rules/math-font-size.js";
24
+ import mathFracRule from "../rules/math-frac.js";
25
+ import mathNestedRule from "../rules/math-nested.js";
26
+ import mathStartsWithSpaceRule from "../rules/math-starts-with-space.js";
27
+ import mathTextEmptyRule from "../rules/math-text-empty.js";
28
+ import mathWithoutDollarsRule from "../rules/math-without-dollars.js";
29
+ import nestedListsRule from "../rules/nested-lists.js";
30
+ import profanityRule from "../rules/profanity.js";
31
+ import tableMissingCellsRule from "../rules/table-missing-cells.js";
32
+ import unbalancedCodeDelimitersRule from "../rules/unbalanced-code-delimiters.js";
33
+ import unescapedDollarRule from "../rules/unescaped-dollar.js";
34
+ import widgetInTableRule from "../rules/widget-in-table.js";
35
+ import TreeTransformer from "../tree-transformer.js";
36
+
37
+ describe("Individual lint rules tests", () => {
38
+ function testRule(rule, markdown, context) {
39
+ const tree = PureMarkdown.parse(markdown);
40
+ const tt = new TreeTransformer(tree);
41
+ const warnings = [];
42
+
43
+ // The markdown parser often outputs adjacent text nodes. We
44
+ // coalesce them before linting for efficiency and accuracy.
45
+ tt.traverse((node, state, content) => {
46
+ if (TreeTransformer.isTextNode(node)) {
47
+ let next = state.nextSibling();
48
+ while (TreeTransformer.isTextNode(next)) {
49
+ // $FlowFixMe[prop-missing]
50
+ // $FlowFixMe[incompatible-use]
51
+ node.content += next.content;
52
+ state.removeNextSibling();
53
+ next = state.nextSibling();
54
+ }
55
+ }
56
+ });
57
+
58
+ if (context) {
59
+ // $FlowFixMe[prop-missing]
60
+ context.content = markdown;
61
+ } else {
62
+ context = {
63
+ content: markdown,
64
+ widgets: {},
65
+ };
66
+ }
67
+ tt.traverse((node, state, content) => {
68
+ const check = rule.check(node, state, content, context);
69
+ if (check) {
70
+ warnings.push(check);
71
+ }
72
+ });
73
+
74
+ return warnings.length === 0 ? null : warnings;
75
+ }
76
+
77
+ function expectWarning(rule, strings, context) {
78
+ if (typeof strings === "string") {
79
+ strings = [strings];
80
+ }
81
+
82
+ it(`Rule ${rule.name} warns`, () => {
83
+ for (const string of strings) {
84
+ expect(testRule(rule, string, context) !== null).toBeTruthy();
85
+ }
86
+ });
87
+ }
88
+
89
+ function expectPass(rule, strings, context) {
90
+ if (typeof strings === "string") {
91
+ strings = [strings];
92
+ }
93
+
94
+ it(`Rule ${rule.name} passes`, () => {
95
+ for (const string of strings) {
96
+ expect(testRule(rule, string, context) === null).toBeTruthy();
97
+ }
98
+ });
99
+ }
100
+
101
+ // 299 characters
102
+ const sentence = new Array(25).fill("lorem ipsum").join(" ");
103
+
104
+ // long-paragraph rule warns about paragraphs over 500 characters
105
+ expectWarning(longParagraphRule, sentence + sentence);
106
+ expectPass(longParagraphRule, [sentence, sentence + "\n\n" + sentence]);
107
+
108
+ expectWarning(headingLevel1Rule, "# Level 1 heading");
109
+ expectPass(headingLevel1Rule, "## Level 1 heading\n\n### Level 3 heading");
110
+
111
+ expectWarning(headingLevelSkipRule, "## heading 1\n\n#### heading 2");
112
+ expectPass(headingLevelSkipRule, [
113
+ "## heading 1\n\n### heading 2\n\n#### heading 3\n\n### heading 4",
114
+ "## heading 1\n\n##heading 2\n\n##heading3",
115
+ ]);
116
+
117
+ expectWarning(
118
+ headingTitleCaseRule,
119
+ "## This Heading is in Title Case and the but nor for Too",
120
+ );
121
+ expectPass(headingTitleCaseRule, [
122
+ "## This heading is in sentence case",
123
+ "## Acronyms: The CIA, NSA, DNI, and FBI",
124
+ "## The Great War",
125
+ ]);
126
+
127
+ expectWarning(headingSentenceCaseRule, [
128
+ "## this heading is uncapitalized",
129
+ "## 'this' heading is uncapitalized",
130
+ "## this heading is uncapitalized",
131
+ ]);
132
+ expectPass(headingSentenceCaseRule, [
133
+ "## This heading is in sentence case",
134
+ "## 'This heading too'",
135
+ "## 2 + 2 = 4",
136
+ ]);
137
+
138
+ expectWarning(nestedListsRule, [
139
+ "1. outer\n * nested\n *nested",
140
+ " + outer\n\n 1. nested",
141
+ ]);
142
+ expectPass(nestedListsRule, [
143
+ "-one\n-two\n-three",
144
+ "1. one\n 2. two\n3. three",
145
+ " * one\n\n * two\n\n * three",
146
+ ]);
147
+
148
+ expectWarning(imageAltTextRule, [
149
+ "![](http://google.com/)",
150
+ '![](http://google.com/ "title")',
151
+ "![][url-ref]",
152
+ "![ ](http://google.com/)",
153
+ "![ \t\n ](http://google.com/)", // all whitespace
154
+ "![blah](http://google.com/)", // too short to be meaningful
155
+ ]);
156
+
157
+ expectPass(imageAltTextRule, [
158
+ "![alt-text](http://google.com)",
159
+ '![alternative text](http://google.com/ "title")',
160
+ "![alt alt alt][url-ref]",
161
+ ]);
162
+
163
+ expectWarning(blockquotedMathRule, ["> $1$", "Quote:\n\n> $x$\n\n"]);
164
+ expectPass(blockquotedMathRule, [
165
+ "$x$",
166
+ "\n$x$\n $y$\n",
167
+ "> bq #1\n\n$x+y=1$\n\n> bq #2",
168
+ ]);
169
+
170
+ expectWarning(blockquotedWidgetRule, ["> [[☃ passage 1]]"]);
171
+ expectPass(blockquotedWidgetRule, [
172
+ "[[☃ passage 1]]",
173
+ "> bq #1\n\nTesting [[☃ passage 1]] testing\n\n> bq #2",
174
+ ]);
175
+
176
+ expectWarning(linkClickHereRule, [
177
+ "[click here](http://google.com)",
178
+ "[Click here, please](http://google.com)",
179
+ "[For a good time, Click Here](http://google.com)",
180
+ ]);
181
+ expectPass(linkClickHereRule, [
182
+ "[click to activate this link here](http://google.com)",
183
+ ]);
184
+
185
+ expectWarning(absoluteUrlRule, [
186
+ // Warn about absolute khanacademy.org urls
187
+ "[target](http://khanacademy.org/about)",
188
+ "[target](https://khanacademy.org/about)",
189
+ "[target](http://www.khanacademy.org/about)",
190
+ "[target](https://www.khanacademy.org/about)",
191
+ "[target](http://es.khanacademy.org/about)",
192
+ "[target](https://es.khanacademy.org/about)",
193
+ "[target](//www.khanacademy.org/about)",
194
+ "[target](//www.khanacademy.org/about)",
195
+
196
+ // We should get the same warnings for images
197
+ "![alt text](http://khanacademy.org/about)",
198
+ "![alt text](https://www.khanacademy.org/about)",
199
+ "![alt text](https://es.khanacademy.org/about)",
200
+ ]);
201
+ expectPass(absoluteUrlRule, [
202
+ "[target](/about)", // relative URLs okay
203
+ "[target](https://kasandbox.org/path)",
204
+ "[target](https://fastly.kastatic.org/path)",
205
+ "[target](https://cdn.kastatic.org/path)",
206
+ "[target](https://ka-perseus-images.s3.amazonaws.com/path)",
207
+ "[target](https://ka-youtube-converted.storage.googleapis.com)",
208
+
209
+ // Same warnings for images
210
+ "![alt text](/about)",
211
+ "![alt text](https://cdn.kastatic.org/path)",
212
+ "![alt text](https://ka-perseus-images.s3.amazonaws.com/path)",
213
+ ]);
214
+
215
+ expectWarning(imageInTableRule, [
216
+ "|col1|col2|\n|----|----|\n|![alt-text](/link.gif)|cell2|",
217
+ ]);
218
+ expectPass(imageInTableRule, [
219
+ "![alt-text](/link.gif)\n|col1|col2|\n|----|----|\n|cell1|cell2|",
220
+ ]);
221
+
222
+ expectWarning(widgetInTableRule, [
223
+ "|col1|col2|\n|----|----|\n|[[☃ passage 1]]|cell2|",
224
+ ]);
225
+ expectPass(widgetInTableRule, [
226
+ "[[☃ passage 1]]\n|col1|col2|\n|----|----|\n|cell1|cell2|",
227
+ ]);
228
+
229
+ expectWarning(tableMissingCellsRule, [
230
+ "|col1|col2|col3|\n|----|----|----|\n|col1|col2|col3|\n|cell1|cell2|",
231
+ "|col1|col2|col3|\n|----|----|----|\n|col1|col2|\n|cell1|cell2|",
232
+ "|col1|col2|\n|----|----|\n|cell1|cell2|\n|cell1|cell2|cell3|",
233
+ "|col1|\n|----|----|\n|col1|\n|cell1|cell2|",
234
+ "|col1|col2|\n|----|----|\n|col1|\n|cell1|cell2|",
235
+ ]);
236
+ expectPass(tableMissingCellsRule, [
237
+ "|col1|col2|\n|----|----|\n|cell1|cell2|\n|cell1|cell2|",
238
+ "|cell1|\n|----|\n|cell2|\n|cell3|",
239
+ ]);
240
+
241
+ expectWarning(unescapedDollarRule, ["It costs $10", "It costs $$10$"]);
242
+
243
+ expectPass(unescapedDollarRule, ["It costs \\$10", "It costs $10x$"]);
244
+
245
+ expectWarning(mathStartsWithSpaceRule, [
246
+ "foo$~ x$bar",
247
+ "$\\qquad x$",
248
+ "$\\quad x$",
249
+ "$\\, x$",
250
+ "$\\; x$",
251
+ "$\\: x$",
252
+ "$\\ x$",
253
+ "$\\! x$",
254
+ "$\\enspace x$",
255
+ "$\\phantom{xyz} x$",
256
+ ]);
257
+ expectPass(mathStartsWithSpaceRule, [
258
+ "$a~ x$",
259
+ "$a\\qquad x$",
260
+ "$a\\quad x$",
261
+ "$a\\, x$",
262
+ "$a\\; x$",
263
+ "$a\\: x$",
264
+ "$a\\ x$",
265
+ "$a\\! x$",
266
+ "$a\\enspace x$",
267
+ "$a\\phantom{xyz} x$",
268
+ ]);
269
+
270
+ expectWarning(mathEmptyRule, [
271
+ "foo $$ bar",
272
+ "foo\n\n$$\n\nbar",
273
+ "$$ | $$ | $$\n- | - | -\ndata 1 | data 2 | data 3",
274
+ ]);
275
+ expectPass(mathEmptyRule, [
276
+ "foo $x$ bar",
277
+ "foo\n\n$x$\n\nbar",
278
+ "$x$ | $y$ | $z$\n- | - | -\ndata 1 | data 2 | data 3",
279
+ ]);
280
+
281
+ expectWarning(mathFracRule, ["$\\frac 12$", "$\\frac{1}{2}$"]);
282
+ expectPass(mathFracRule, [
283
+ "$\\dfrac 12$",
284
+ "$\\dfrac{1}{2}$",
285
+ "$\\fraction 12$",
286
+ ]);
287
+
288
+ expectWarning(mathTextEmptyRule, [
289
+ "$x\\text{}y$",
290
+ "$x\\text{ }y$",
291
+ "$x\\text{\n}y$",
292
+ "$x\\text{\t}y$",
293
+ ]);
294
+ expectPass(mathTextEmptyRule, ["$x\\text{z}y$"]);
295
+
296
+ expectWarning(mathAdjacentRule, ["$x=b+c$\n\n$x-b=c$"]);
297
+ expectPass(mathAdjacentRule, ["$x=b+c$\n\nnew paragraph\n\n$x-b=c$"]);
298
+
299
+ expectWarning(mathAlignLinebreaksRule, [
300
+ "$\\begin{align}x\\\\y\\end{align}$",
301
+ "$\\begin{align} x \\\\ y \\end{align}$",
302
+ "$\\begin{align}x\\\\\\\\\\\\y\\end{align}$",
303
+ "$\\begin{align}\nx\\\\\n\\\\\\\\\ny\n\\end{align}$",
304
+ ]);
305
+ expectPass(mathAlignLinebreaksRule, [
306
+ "$\\begin{align}x\\sqrty\\end{align}$",
307
+ "$\\begin{align}x\\\\\\\\y\\end{align}$",
308
+ "$\\begin{align}x\\\\\n\\\\y\\end{align}$",
309
+ "$\\begin{align}x \\\\ \\\\ y\\end{align}$",
310
+ ]);
311
+
312
+ expectWarning(mathAlignExtraBreakRule, [
313
+ "$\\begin{align}x \\\\\\\\ y \\\\ \\end{align}$",
314
+ "$\\begin{align}x \\\\\\\\ y \\\\\\\\ \\end{align}$",
315
+ ]);
316
+ expectPass(mathAlignExtraBreakRule, [
317
+ "$\\begin{align} x \\\\\\\\ y \\end{align}$",
318
+ ]);
319
+
320
+ expectWarning(mathNestedRule, [
321
+ "$\\text{4$x$}$",
322
+ "inline $\\text{4$x$}$ math",
323
+ "$\\text{$$}$",
324
+ ]);
325
+ expectPass(mathNestedRule, ["$\\text{4}x$", "inline $\\text{4}x$ math"]);
326
+
327
+ expectWarning(mathFontSizeRule, [
328
+ "$\\tiny{x}$",
329
+ "inline $\\Tiny{x}$ math",
330
+ "$a \\small{x} b$",
331
+ "$\\large{ xyz }$",
332
+ "$ \\Large { x } $",
333
+ "$\\LARGE{x}$",
334
+ "$\\huge{x}$",
335
+ "$\\Huge{x}$",
336
+ "$\\normalsize{x}$",
337
+ "$\\scriptsize{x}$",
338
+ ]);
339
+ expectPass(mathFontSizeRule, ["$\\sqrt{x}$", "inline $\\sqrt{x}$ math"]);
340
+
341
+ expectWarning(profanityRule, [
342
+ "Shit",
343
+ "taking a piss",
344
+ "He said 'Fuck that!'",
345
+ "cunt",
346
+ "cocksucker",
347
+ "motherfucker",
348
+ ]);
349
+ expectPass(profanityRule, ["spit", "miss", "duck"]);
350
+
351
+ expectWarning(mathWithoutDollarsRule, [
352
+ "One half: \\frac{1}{2}!",
353
+ "\\Large{BIG}!",
354
+ "This looks like someone's ear: {",
355
+ "Here's the other ear: }. Weird!",
356
+ ]);
357
+ expectPass(mathWithoutDollarsRule, [
358
+ "One half: $\\frac{1}{2}$",
359
+ "$\\Large{BIG}$!",
360
+ "`{`",
361
+ "`\\frac{1}{2}`",
362
+ "``\\frac{1}{2}``",
363
+ "```\n\\frac{1}{2}\n```",
364
+ "~~~\n\\frac{1}{2}\n~~~",
365
+ "\n \\frac{1}{2}\n {\n }\n",
366
+ ]);
367
+
368
+ expectWarning(unbalancedCodeDelimitersRule, [
369
+ "`code``",
370
+ "``code```",
371
+ "```code\n",
372
+ "~~~\ncode\n~~",
373
+ ]);
374
+ expectPass(unbalancedCodeDelimitersRule, [
375
+ "`code`",
376
+ "``code``",
377
+ "```code```",
378
+ "```\ncode\n```",
379
+ "~~~\ncode\n~~~",
380
+ "``co`de``",
381
+ "`co~de`",
382
+ "$`~$",
383
+ ]);
384
+
385
+ expectWarning(imageSpacesAroundUrlsRule, [
386
+ "![alternative]( http://example.com/image.jpg )",
387
+ "![alternative]( http://example.com/image.jpg)",
388
+ "![alternative](http://example.com/image.jpg )",
389
+ "![alternative](\thttp://example.com/image.jpg)",
390
+ "![alternative](http://example.com/image.jpg\t)",
391
+ "![alternative](\nhttp://example.com/image.jpg)",
392
+ "![alternative](http://example.com/image.jpg\n)",
393
+ ]);
394
+ expectPass(imageSpacesAroundUrlsRule, [
395
+ "![alternative](http://example.com/image.jpg)",
396
+ "![alternative](image.jpg)",
397
+ "![alternative](--image.jpg--)",
398
+ ]);
399
+
400
+ // Warn for image widget with no alt text
401
+ expectWarning(imageWidgetRule, "[[☃ image 1]]", {
402
+ widgets: {
403
+ "image 1": {
404
+ options: {},
405
+ },
406
+ },
407
+ });
408
+
409
+ // Warn for image widget with short alt text
410
+ expectWarning(imageWidgetRule, "[[☃ image 1]]", {
411
+ widgets: {
412
+ "image 1": {
413
+ options: {
414
+ alt: "1234567",
415
+ },
416
+ },
417
+ },
418
+ });
419
+
420
+ // Pass for image widget with long alt text
421
+ expectPass(imageWidgetRule, "[[☃ image 1]]", {
422
+ widgets: {
423
+ "image 1": {
424
+ options: {
425
+ alt: "1234567890",
426
+ },
427
+ },
428
+ },
429
+ });
430
+
431
+ // Warn for image widget with math in its caption
432
+ expectWarning(imageWidgetRule, "[[☃ image 1]]", {
433
+ widgets: {
434
+ "image 1": {
435
+ options: {
436
+ alt: "1234567890",
437
+ caption: "Test: $x$",
438
+ },
439
+ },
440
+ },
441
+ });
442
+
443
+ // Pass for image widget with caption and no math
444
+ expectPass(imageWidgetRule, "[[☃ image 1]]", {
445
+ widgets: {
446
+ "image 1": {
447
+ options: {
448
+ alt: "1234567890",
449
+ caption: "Test: x",
450
+ },
451
+ },
452
+ },
453
+ });
454
+
455
+ // Pass for image widget with escaped dollar in its caption
456
+ expectPass(imageWidgetRule, "[[☃ image 1]]", {
457
+ widgets: {
458
+ "image 1": {
459
+ options: {
460
+ alt: "1234567890",
461
+ caption: "Test: \\$10",
462
+ },
463
+ },
464
+ },
465
+ });
466
+
467
+ expectWarning(doubleSpacingAfterTerminalRule, [
468
+ "Good times. Great oldies.",
469
+ "End of the line! ",
470
+ "You? Me!",
471
+ ]);
472
+ expectPass(doubleSpacingAfterTerminalRule, [
473
+ "This is okay.",
474
+ "This is definitely okay. Yeah.",
475
+ "$a == 3. 125$",
476
+ ]);
477
+
478
+ expectWarning(extraContentSpacingRule, [
479
+ "There's extra spaces here. ",
480
+ "There's extra spaces here ",
481
+ " ",
482
+ ]);
483
+ expectPass(extraContentSpacingRule, [
484
+ "This is okay.",
485
+ "This is definitely okay. Yeah.",
486
+ "$a == 3. 125$",
487
+ ]);
488
+ });
@@ -0,0 +1,52 @@
1
+ // @flow
2
+ import Selector from "../selector.js";
3
+
4
+ describe("gorgon selector parser", () => {
5
+ const validExpressions = [
6
+ "*",
7
+ " * ",
8
+ "para",
9
+ "list para",
10
+ "\tlist para\n",
11
+ "list > para",
12
+ "list + para",
13
+ "list ~ para",
14
+ "list list para",
15
+ "para~heading~para~heading",
16
+ "list, para",
17
+ "list > para, list text, heading *, heading+para",
18
+ ];
19
+
20
+ const invalidExpressions = [
21
+ "", // Expected node type
22
+ "", // Expected node type
23
+ "<", // Expected node type
24
+ "+", // Expected node type
25
+ "~", // Expected node type
26
+ "**", // Unexpected token
27
+ "foo*", // Unexpected token
28
+ "*/foo/", // Unexpected token
29
+ "()", // Unexpected token
30
+ ",",
31
+ "list,",
32
+ ",list",
33
+ ];
34
+
35
+ validExpressions.forEach((s) => {
36
+ it("parses '" + s + "'", () => {
37
+ const e = Selector.parse(s);
38
+ expect(e instanceof Selector).toBeTruthy();
39
+ expect(e.toString().replace(/\s/g, "")).toEqual(
40
+ s.replace(/\s/g, ""),
41
+ );
42
+ });
43
+ });
44
+
45
+ invalidExpressions.forEach((s) => {
46
+ it("rejects '" + s + "'", () => {
47
+ expect(() => {
48
+ Selector.parse(s);
49
+ }).toThrow();
50
+ });
51
+ });
52
+ });