@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.
- package/CHANGELOG.md +12 -0
- package/dist/es/index.js +3152 -0
- package/dist/es/index.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +3129 -0
- package/dist/index.js.flow +2 -0
- package/dist/index.js.map +1 -0
- package/package.json +31 -0
- package/src/README.md +41 -0
- package/src/__tests__/matcher_test.js +498 -0
- package/src/__tests__/rule_test.js +102 -0
- package/src/__tests__/rules_test.js +488 -0
- package/src/__tests__/selector-parser_test.js +52 -0
- package/src/__tests__/tree-transformer_test.js +446 -0
- package/src/index.js +281 -0
- package/src/proptypes.js +29 -0
- package/src/rule.js +412 -0
- package/src/rules/absolute-url.js +24 -0
- package/src/rules/all-rules.js +72 -0
- package/src/rules/blockquoted-math.js +10 -0
- package/src/rules/blockquoted-widget.js +10 -0
- package/src/rules/double-spacing-after-terminal.js +12 -0
- package/src/rules/extra-content-spacing.js +12 -0
- package/src/rules/heading-level-1.js +14 -0
- package/src/rules/heading-level-skip.js +20 -0
- package/src/rules/heading-sentence-case.js +11 -0
- package/src/rules/heading-title-case.js +63 -0
- package/src/rules/image-alt-text.js +21 -0
- package/src/rules/image-in-table.js +10 -0
- package/src/rules/image-spaces-around-urls.js +35 -0
- package/src/rules/image-widget.js +50 -0
- package/src/rules/link-click-here.js +11 -0
- package/src/rules/lint-utils.js +48 -0
- package/src/rules/long-paragraph.js +14 -0
- package/src/rules/math-adjacent.js +10 -0
- package/src/rules/math-align-extra-break.js +11 -0
- package/src/rules/math-align-linebreaks.js +43 -0
- package/src/rules/math-empty.js +10 -0
- package/src/rules/math-font-size.js +12 -0
- package/src/rules/math-frac.js +10 -0
- package/src/rules/math-nested.js +11 -0
- package/src/rules/math-starts-with-space.js +12 -0
- package/src/rules/math-text-empty.js +10 -0
- package/src/rules/math-without-dollars.js +14 -0
- package/src/rules/nested-lists.js +11 -0
- package/src/rules/profanity.js +10 -0
- package/src/rules/table-missing-cells.js +20 -0
- package/src/rules/unbalanced-code-delimiters.js +14 -0
- package/src/rules/unescaped-dollar.js +10 -0
- package/src/rules/widget-in-table.js +10 -0
- package/src/selector.js +505 -0
- package/src/tree-transformer.js +587 -0
- 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
|
+
"",
|
|
150
|
+
'',
|
|
151
|
+
"![][url-ref]",
|
|
152
|
+
"",
|
|
153
|
+
"", // all whitespace
|
|
154
|
+
"", // too short to be meaningful
|
|
155
|
+
]);
|
|
156
|
+
|
|
157
|
+
expectPass(imageAltTextRule, [
|
|
158
|
+
"",
|
|
159
|
+
'',
|
|
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
|
+
"",
|
|
198
|
+
"",
|
|
199
|
+
"",
|
|
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
|
+
"",
|
|
211
|
+
"",
|
|
212
|
+
"",
|
|
213
|
+
]);
|
|
214
|
+
|
|
215
|
+
expectWarning(imageInTableRule, [
|
|
216
|
+
"|col1|col2|\n|----|----|\n||cell2|",
|
|
217
|
+
]);
|
|
218
|
+
expectPass(imageInTableRule, [
|
|
219
|
+
"\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
|
+
"",
|
|
387
|
+
"",
|
|
388
|
+
"",
|
|
389
|
+
"",
|
|
390
|
+
"",
|
|
391
|
+
"",
|
|
392
|
+
"",
|
|
393
|
+
]);
|
|
394
|
+
expectPass(imageSpacesAroundUrlsRule, [
|
|
395
|
+
"",
|
|
396
|
+
"",
|
|
397
|
+
"",
|
|
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
|
+
});
|