@khanacademy/perseus-linter 0.0.0-PR971-20240207180432 → 0.0.0-PR973-20240207194831
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/dist/es/index.js +1 -1
- package/dist/index.js +1 -1
- package/dist/tsconfig-build.tsbuildinfo +1 -0
- package/package.json +5 -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/dist/{index.d.ts → types/index.d.ts} +0 -0
- /package/dist/{proptypes.d.ts → types/proptypes.d.ts} +0 -0
- /package/dist/{rule.d.ts → types/rule.d.ts} +0 -0
- /package/dist/{rules → types/rules}/absolute-url.d.ts +0 -0
- /package/dist/{rules → types/rules}/all-rules.d.ts +0 -0
- /package/dist/{rules → types/rules}/blockquoted-math.d.ts +0 -0
- /package/dist/{rules → types/rules}/blockquoted-widget.d.ts +0 -0
- /package/dist/{rules → types/rules}/double-spacing-after-terminal.d.ts +0 -0
- /package/dist/{rules → types/rules}/extra-content-spacing.d.ts +0 -0
- /package/dist/{rules → types/rules}/heading-level-1.d.ts +0 -0
- /package/dist/{rules → types/rules}/heading-level-skip.d.ts +0 -0
- /package/dist/{rules → types/rules}/heading-sentence-case.d.ts +0 -0
- /package/dist/{rules → types/rules}/heading-title-case.d.ts +0 -0
- /package/dist/{rules → types/rules}/image-alt-text.d.ts +0 -0
- /package/dist/{rules → types/rules}/image-in-table.d.ts +0 -0
- /package/dist/{rules → types/rules}/image-spaces-around-urls.d.ts +0 -0
- /package/dist/{rules → types/rules}/image-widget.d.ts +0 -0
- /package/dist/{rules → types/rules}/link-click-here.d.ts +0 -0
- /package/dist/{rules → types/rules}/lint-utils.d.ts +0 -0
- /package/dist/{rules → types/rules}/long-paragraph.d.ts +0 -0
- /package/dist/{rules → types/rules}/math-adjacent.d.ts +0 -0
- /package/dist/{rules → types/rules}/math-align-extra-break.d.ts +0 -0
- /package/dist/{rules → types/rules}/math-align-linebreaks.d.ts +0 -0
- /package/dist/{rules → types/rules}/math-empty.d.ts +0 -0
- /package/dist/{rules → types/rules}/math-font-size.d.ts +0 -0
- /package/dist/{rules → types/rules}/math-frac.d.ts +0 -0
- /package/dist/{rules → types/rules}/math-nested.d.ts +0 -0
- /package/dist/{rules → types/rules}/math-starts-with-space.d.ts +0 -0
- /package/dist/{rules → types/rules}/math-text-empty.d.ts +0 -0
- /package/dist/{rules → types/rules}/math-without-dollars.d.ts +0 -0
- /package/dist/{rules → types/rules}/nested-lists.d.ts +0 -0
- /package/dist/{rules → types/rules}/profanity.d.ts +0 -0
- /package/dist/{rules → types/rules}/table-missing-cells.d.ts +0 -0
- /package/dist/{rules → types/rules}/unbalanced-code-delimiters.d.ts +0 -0
- /package/dist/{rules → types/rules}/unescaped-dollar.d.ts +0 -0
- /package/dist/{rules → types/rules}/widget-in-table.d.ts +0 -0
- /package/dist/{selector.d.ts → types/selector.d.ts} +0 -0
- /package/dist/{tree-transformer.d.ts → types/tree-transformer.d.ts} +0 -0
- /package/dist/{types.d.ts → types/types.d.ts} +0 -0
- /package/dist/{version.d.ts → types/version.d.ts} +0 -0
|
@@ -0,0 +1,498 @@
|
|
|
1
|
+
/* These tests exercise the Selector.match() method and also test that we
|
|
2
|
+
* can integrate ../../perseus-markdown.js with ../tree-transform.js and
|
|
3
|
+
* ../selector.js
|
|
4
|
+
*/
|
|
5
|
+
import * as PureMarkdown from "@khanacademy/pure-markdown";
|
|
6
|
+
|
|
7
|
+
import Selector from "../selector";
|
|
8
|
+
import TreeTransformer from "../tree-transformer";
|
|
9
|
+
|
|
10
|
+
describe("PerseusLinter selector matching:", () => {
|
|
11
|
+
const markdown = `
|
|
12
|
+
### A
|
|
13
|
+
|
|
14
|
+
B
|
|
15
|
+
|
|
16
|
+
C
|
|
17
|
+
|
|
18
|
+
- D
|
|
19
|
+
- E
|
|
20
|
+
- F
|
|
21
|
+
|
|
22
|
+
*G*H
|
|
23
|
+
`;
|
|
24
|
+
|
|
25
|
+
function parseTree() {
|
|
26
|
+
return PureMarkdown.parse(markdown);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
it("wildcards match every node", () => {
|
|
30
|
+
const tree = parseTree();
|
|
31
|
+
const selector = Selector.parse("*");
|
|
32
|
+
const tt = new TreeTransformer(tree);
|
|
33
|
+
tt.traverse((n, state, content) => {
|
|
34
|
+
// The wildcard selector should match at every node
|
|
35
|
+
// @ts-expect-error - TS2533 - Object is possibly 'null' or 'undefined'.
|
|
36
|
+
expect(selector.match(state)[0]).toEqual(n);
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("type-based matching works", () => {
|
|
41
|
+
const tree = parseTree();
|
|
42
|
+
const selectors: Record<string, any> = {};
|
|
43
|
+
const tt = new TreeTransformer(tree);
|
|
44
|
+
|
|
45
|
+
// Traverse the tree once and create a selector for every type
|
|
46
|
+
// of node we find
|
|
47
|
+
tt.traverse((n, state, content) => {
|
|
48
|
+
if (!selectors[n.type]) {
|
|
49
|
+
selectors[n.type] = Selector.parse(n.type);
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const types = Object.keys(selectors);
|
|
54
|
+
|
|
55
|
+
// Now traverse the tree again. At each node run all of the
|
|
56
|
+
// selectors we've created. Only those with matching types
|
|
57
|
+
// should match.
|
|
58
|
+
tt.traverse((n, state, content) => {
|
|
59
|
+
types.forEach((type) => {
|
|
60
|
+
const selector = selectors[type];
|
|
61
|
+
const match = selector.match(state);
|
|
62
|
+
if (n.type === type) {
|
|
63
|
+
expect(match[0]).toEqual(n);
|
|
64
|
+
} else {
|
|
65
|
+
expect(match).toEqual(null);
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
if (!selectors[n.type]) {
|
|
70
|
+
selectors[n.type] = Selector.parse(n.type);
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("parent combinator", () => {
|
|
76
|
+
const selector = Selector.parse("paragraph > text");
|
|
77
|
+
const tree = parseTree();
|
|
78
|
+
const tt = new TreeTransformer(tree);
|
|
79
|
+
let numMatches = 0;
|
|
80
|
+
let matchedText = "";
|
|
81
|
+
|
|
82
|
+
tt.traverse((n, state, content) => {
|
|
83
|
+
const match = selector.match(state);
|
|
84
|
+
const parent = state.parent();
|
|
85
|
+
// @ts-expect-error - TS2533 - Object is possibly 'null' or 'undefined'.
|
|
86
|
+
if (n.type === "text" && parent.type === "paragraph") {
|
|
87
|
+
expect(Array.isArray(match)).toBeTruthy();
|
|
88
|
+
expect(match).toHaveLength(2);
|
|
89
|
+
// @ts-expect-error - TS2533 - Object is possibly 'null' or 'undefined'.
|
|
90
|
+
expect(match[0]).toEqual(parent);
|
|
91
|
+
// @ts-expect-error - TS2533 - Object is possibly 'null' or 'undefined'.
|
|
92
|
+
expect(match[1]).toEqual(n);
|
|
93
|
+
matchedText += content;
|
|
94
|
+
numMatches++;
|
|
95
|
+
} else {
|
|
96
|
+
expect(match).toEqual(null);
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
expect(numMatches).toEqual(3);
|
|
101
|
+
expect(matchedText).toEqual("BCH");
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("double parent combinator", () => {
|
|
105
|
+
const selector = Selector.parse("paragraph > em > text");
|
|
106
|
+
const tree = parseTree();
|
|
107
|
+
const tt = new TreeTransformer(tree);
|
|
108
|
+
let numMatches = 0;
|
|
109
|
+
let matchedText = "";
|
|
110
|
+
|
|
111
|
+
tt.traverse((n, state, content) => {
|
|
112
|
+
const match = selector.match(state);
|
|
113
|
+
// Make a mutable copy before popping.
|
|
114
|
+
const ancestors = [...state.ancestors()];
|
|
115
|
+
const parent = ancestors.pop();
|
|
116
|
+
const grandparent = ancestors.pop();
|
|
117
|
+
if (
|
|
118
|
+
n.type === "text" &&
|
|
119
|
+
// @ts-expect-error - TS2532 - Object is possibly 'undefined'.
|
|
120
|
+
parent.type === "em" &&
|
|
121
|
+
// @ts-expect-error - TS2532 - Object is possibly 'undefined'.
|
|
122
|
+
grandparent.type === "paragraph"
|
|
123
|
+
) {
|
|
124
|
+
expect(Array.isArray(match)).toBeTruthy();
|
|
125
|
+
expect(match).toHaveLength(3);
|
|
126
|
+
// @ts-expect-error - TS2533 - Object is possibly 'null' or 'undefined'.
|
|
127
|
+
expect(match[0]).toEqual(grandparent);
|
|
128
|
+
// @ts-expect-error - TS2533 - Object is possibly 'null' or 'undefined'.
|
|
129
|
+
expect(match[1]).toEqual(parent);
|
|
130
|
+
// @ts-expect-error - TS2533 - Object is possibly 'null' or 'undefined'.
|
|
131
|
+
expect(match[2]).toEqual(n);
|
|
132
|
+
matchedText += content;
|
|
133
|
+
numMatches++;
|
|
134
|
+
} else {
|
|
135
|
+
expect(match).toEqual(null);
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
expect(numMatches).toEqual(1);
|
|
140
|
+
expect(matchedText).toEqual("G");
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it("ancestor combinator", () => {
|
|
144
|
+
const selector = Selector.parse("paragraph text");
|
|
145
|
+
const tree = parseTree();
|
|
146
|
+
const tt = new TreeTransformer(tree);
|
|
147
|
+
let numMatches = 0;
|
|
148
|
+
let matchedText = "";
|
|
149
|
+
|
|
150
|
+
tt.traverse((n, state, content) => {
|
|
151
|
+
const match = selector.match(state);
|
|
152
|
+
if (match !== null) {
|
|
153
|
+
expect(Array.isArray(match)).toBeTruthy();
|
|
154
|
+
expect(match).toHaveLength(2);
|
|
155
|
+
// @ts-expect-error - TS2532 - Object is possibly 'undefined'.
|
|
156
|
+
expect(match[0].type).toEqual("paragraph");
|
|
157
|
+
// @ts-expect-error - TS2532 - Object is possibly 'undefined'.
|
|
158
|
+
expect(match[1].type).toEqual("text");
|
|
159
|
+
// @ts-expect-error - TS2532 - Object is possibly 'undefined'.
|
|
160
|
+
expect(match[1]).toEqual(n);
|
|
161
|
+
matchedText += content;
|
|
162
|
+
numMatches++;
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
expect(numMatches).toEqual(4);
|
|
167
|
+
expect(matchedText).toEqual("BCGH");
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it("double ancestor combinator", () => {
|
|
171
|
+
const selector = Selector.parse("paragraph em text");
|
|
172
|
+
const tree = parseTree();
|
|
173
|
+
const tt = new TreeTransformer(tree);
|
|
174
|
+
let numMatches = 0;
|
|
175
|
+
let matchedText = "";
|
|
176
|
+
|
|
177
|
+
tt.traverse((n, state, content) => {
|
|
178
|
+
const match = selector.match(state);
|
|
179
|
+
if (match !== null) {
|
|
180
|
+
expect(Array.isArray(match)).toBeTruthy();
|
|
181
|
+
expect(match).toHaveLength(3);
|
|
182
|
+
// @ts-expect-error - TS2532 - Object is possibly 'undefined'.
|
|
183
|
+
expect(match[0].type).toEqual("paragraph");
|
|
184
|
+
// @ts-expect-error - TS2532 - Object is possibly 'undefined'.
|
|
185
|
+
expect(match[1].type).toEqual("em");
|
|
186
|
+
// @ts-expect-error - TS2532 - Object is possibly 'undefined'.
|
|
187
|
+
expect(match[2].type).toEqual("text");
|
|
188
|
+
// @ts-expect-error - TS2532 - Object is possibly 'undefined'.
|
|
189
|
+
expect(match[2]).toEqual(n);
|
|
190
|
+
matchedText += content;
|
|
191
|
+
numMatches++;
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
expect(numMatches).toEqual(1);
|
|
196
|
+
expect(matchedText).toEqual("G");
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it("previous combinator", () => {
|
|
200
|
+
const selector = Selector.parse("heading + paragraph");
|
|
201
|
+
const tree = parseTree();
|
|
202
|
+
const tt = new TreeTransformer(tree);
|
|
203
|
+
let numMatches = 0;
|
|
204
|
+
let matchedText = "";
|
|
205
|
+
|
|
206
|
+
tt.traverse((n, state, content) => {
|
|
207
|
+
const match = selector.match(state);
|
|
208
|
+
if (match !== null) {
|
|
209
|
+
expect(Array.isArray(match)).toBeTruthy();
|
|
210
|
+
expect(match).toHaveLength(2);
|
|
211
|
+
// @ts-expect-error - TS2532 - Object is possibly 'undefined'.
|
|
212
|
+
expect(match[0].type).toEqual("heading");
|
|
213
|
+
// @ts-expect-error - TS2532 - Object is possibly 'undefined'.
|
|
214
|
+
expect(match[0]).toEqual(state.previousSibling());
|
|
215
|
+
// @ts-expect-error - TS2532 - Object is possibly 'undefined'.
|
|
216
|
+
expect(match[1].type).toEqual("paragraph");
|
|
217
|
+
// @ts-expect-error - TS2532 - Object is possibly 'undefined'.
|
|
218
|
+
expect(match[1]).toEqual(n);
|
|
219
|
+
matchedText += content;
|
|
220
|
+
numMatches++;
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
expect(numMatches).toEqual(1);
|
|
225
|
+
expect(matchedText).toEqual("B");
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it("double previous combinator", () => {
|
|
229
|
+
const selector = Selector.parse("heading + paragraph + paragraph");
|
|
230
|
+
const tree = parseTree();
|
|
231
|
+
const tt = new TreeTransformer(tree);
|
|
232
|
+
let numMatches = 0;
|
|
233
|
+
let matchedText = "";
|
|
234
|
+
|
|
235
|
+
tt.traverse((n, state, content) => {
|
|
236
|
+
const match = selector.match(state);
|
|
237
|
+
if (match !== null) {
|
|
238
|
+
expect(Array.isArray(match)).toBeTruthy();
|
|
239
|
+
expect(match).toHaveLength(3);
|
|
240
|
+
// @ts-expect-error - TS2532 - Object is possibly 'undefined'.
|
|
241
|
+
expect(match[0].type).toEqual("heading");
|
|
242
|
+
// @ts-expect-error - TS2532 - Object is possibly 'undefined'.
|
|
243
|
+
expect(match[1].type).toEqual("paragraph");
|
|
244
|
+
// @ts-expect-error - TS2532 - Object is possibly 'undefined'.
|
|
245
|
+
expect(match[1]).toEqual(state.previousSibling());
|
|
246
|
+
// @ts-expect-error - TS2532 - Object is possibly 'undefined'.
|
|
247
|
+
expect(match[2].type).toEqual("paragraph");
|
|
248
|
+
// @ts-expect-error - TS2532 - Object is possibly 'undefined'.
|
|
249
|
+
expect(match[2]).toEqual(n);
|
|
250
|
+
matchedText += content;
|
|
251
|
+
numMatches++;
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
expect(numMatches).toEqual(1);
|
|
256
|
+
expect(matchedText).toEqual("C");
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
it("sibling combinator", () => {
|
|
260
|
+
const selector = Selector.parse("heading ~ paragraph");
|
|
261
|
+
const tree = parseTree();
|
|
262
|
+
const tt = new TreeTransformer(tree);
|
|
263
|
+
let numMatches = 0;
|
|
264
|
+
let matchedText = "";
|
|
265
|
+
|
|
266
|
+
tt.traverse((n, state, content) => {
|
|
267
|
+
const match = selector.match(state);
|
|
268
|
+
if (match !== null) {
|
|
269
|
+
expect(Array.isArray(match)).toBeTruthy();
|
|
270
|
+
expect(match).toHaveLength(2);
|
|
271
|
+
// @ts-expect-error - TS2532 - Object is possibly 'undefined'.
|
|
272
|
+
expect(match[0].type).toEqual("heading");
|
|
273
|
+
// @ts-expect-error - TS2532 - Object is possibly 'undefined'.
|
|
274
|
+
expect(match[1].type).toEqual("paragraph");
|
|
275
|
+
// @ts-expect-error - TS2532 - Object is possibly 'undefined'.
|
|
276
|
+
expect(match[1]).toEqual(n);
|
|
277
|
+
matchedText += content;
|
|
278
|
+
numMatches++;
|
|
279
|
+
}
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
expect(numMatches).toEqual(3);
|
|
283
|
+
expect(matchedText).toEqual("BCGH");
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
it("double sibling combinator", () => {
|
|
287
|
+
const selector = Selector.parse("heading ~ paragraph ~ paragraph");
|
|
288
|
+
const tree = parseTree();
|
|
289
|
+
const tt = new TreeTransformer(tree);
|
|
290
|
+
let numMatches = 0;
|
|
291
|
+
let matchedText = "";
|
|
292
|
+
|
|
293
|
+
tt.traverse((n, state, content) => {
|
|
294
|
+
const match = selector.match(state);
|
|
295
|
+
if (match !== null) {
|
|
296
|
+
expect(Array.isArray(match)).toBeTruthy();
|
|
297
|
+
expect(match).toHaveLength(3);
|
|
298
|
+
// @ts-expect-error - TS2532 - Object is possibly 'undefined'.
|
|
299
|
+
expect(match[0].type).toEqual("heading");
|
|
300
|
+
// @ts-expect-error - TS2532 - Object is possibly 'undefined'.
|
|
301
|
+
expect(match[1].type).toEqual("paragraph");
|
|
302
|
+
// @ts-expect-error - TS2532 - Object is possibly 'undefined'.
|
|
303
|
+
expect(match[2].type).toEqual("paragraph");
|
|
304
|
+
// @ts-expect-error - TS2532 - Object is possibly 'undefined'.
|
|
305
|
+
expect(match[2]).toEqual(n);
|
|
306
|
+
matchedText += content;
|
|
307
|
+
numMatches++;
|
|
308
|
+
}
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
expect(numMatches).toEqual(2);
|
|
312
|
+
expect(matchedText).toEqual("CGH");
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
it("mixed combinators", () => {
|
|
316
|
+
const selector = Selector.parse("list + paragraph > em");
|
|
317
|
+
const tree = parseTree();
|
|
318
|
+
const tt = new TreeTransformer(tree);
|
|
319
|
+
let numMatches = 0;
|
|
320
|
+
let matchedText = "";
|
|
321
|
+
|
|
322
|
+
tt.traverse((n, state, content) => {
|
|
323
|
+
const match = selector.match(state);
|
|
324
|
+
if (match !== null) {
|
|
325
|
+
expect(Array.isArray(match)).toBeTruthy();
|
|
326
|
+
expect(match).toHaveLength(3);
|
|
327
|
+
// @ts-expect-error - TS2532 - Object is possibly 'undefined'.
|
|
328
|
+
expect(match[0].type).toEqual("list");
|
|
329
|
+
// @ts-expect-error - TS2532 - Object is possibly 'undefined'.
|
|
330
|
+
expect(match[1].type).toEqual("paragraph");
|
|
331
|
+
// @ts-expect-error - TS2532 - Object is possibly 'undefined'.
|
|
332
|
+
expect(match[1]).toEqual(state.parent());
|
|
333
|
+
// @ts-expect-error - TS2532 - Object is possibly 'undefined'.
|
|
334
|
+
expect(match[2].type).toEqual("em");
|
|
335
|
+
// @ts-expect-error - TS2532 - Object is possibly 'undefined'.
|
|
336
|
+
expect(match[2]).toEqual(n);
|
|
337
|
+
matchedText += content;
|
|
338
|
+
numMatches++;
|
|
339
|
+
}
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
expect(numMatches).toEqual(1);
|
|
343
|
+
expect(matchedText).toEqual("G");
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
it("mixed combinators 2", () => {
|
|
347
|
+
const selector = Selector.parse("list ~ paragraph em");
|
|
348
|
+
const tree = parseTree();
|
|
349
|
+
const tt = new TreeTransformer(tree);
|
|
350
|
+
let numMatches = 0;
|
|
351
|
+
let matchedText = "";
|
|
352
|
+
|
|
353
|
+
tt.traverse((n, state, content) => {
|
|
354
|
+
const match = selector.match(state);
|
|
355
|
+
if (match !== null) {
|
|
356
|
+
expect(Array.isArray(match)).toBeTruthy();
|
|
357
|
+
expect(match).toHaveLength(3);
|
|
358
|
+
// @ts-expect-error - TS2532 - Object is possibly 'undefined'.
|
|
359
|
+
expect(match[0].type).toEqual("list");
|
|
360
|
+
// @ts-expect-error - TS2532 - Object is possibly 'undefined'.
|
|
361
|
+
expect(match[1].type).toEqual("paragraph");
|
|
362
|
+
// @ts-expect-error - TS2532 - Object is possibly 'undefined'.
|
|
363
|
+
expect(match[1]).toEqual(state.parent());
|
|
364
|
+
// @ts-expect-error - TS2532 - Object is possibly 'undefined'.
|
|
365
|
+
expect(match[2].type).toEqual("em");
|
|
366
|
+
// @ts-expect-error - TS2532 - Object is possibly 'undefined'.
|
|
367
|
+
expect(match[2]).toEqual(n);
|
|
368
|
+
matchedText += content;
|
|
369
|
+
numMatches++;
|
|
370
|
+
}
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
expect(numMatches).toEqual(1);
|
|
374
|
+
expect(matchedText).toEqual("G");
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
it("mixed combinators 3", () => {
|
|
378
|
+
const selector = Selector.parse("paragraph > em + text");
|
|
379
|
+
const tree = parseTree();
|
|
380
|
+
const tt = new TreeTransformer(tree);
|
|
381
|
+
let numMatches = 0;
|
|
382
|
+
let matchedText = "";
|
|
383
|
+
|
|
384
|
+
tt.traverse((n, state, content) => {
|
|
385
|
+
const match = selector.match(state);
|
|
386
|
+
if (match !== null) {
|
|
387
|
+
expect(Array.isArray(match)).toBeTruthy();
|
|
388
|
+
expect(match).toHaveLength(3);
|
|
389
|
+
// @ts-expect-error - TS2532 - Object is possibly 'undefined'.
|
|
390
|
+
expect(match[0].type).toEqual("paragraph");
|
|
391
|
+
// @ts-expect-error - TS2532 - Object is possibly 'undefined'.
|
|
392
|
+
expect(match[0]).toEqual(state.parent());
|
|
393
|
+
// @ts-expect-error - TS2532 - Object is possibly 'undefined'.
|
|
394
|
+
expect(match[1].type).toEqual("em");
|
|
395
|
+
// @ts-expect-error - TS2532 - Object is possibly 'undefined'.
|
|
396
|
+
expect(match[1]).toEqual(state.previousSibling());
|
|
397
|
+
// @ts-expect-error - TS2532 - Object is possibly 'undefined'.
|
|
398
|
+
expect(match[2].type).toEqual("text");
|
|
399
|
+
// @ts-expect-error - TS2532 - Object is possibly 'undefined'.
|
|
400
|
+
expect(match[2]).toEqual(n);
|
|
401
|
+
matchedText += content;
|
|
402
|
+
numMatches++;
|
|
403
|
+
}
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
expect(numMatches).toEqual(1);
|
|
407
|
+
expect(matchedText).toEqual("H");
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
it("mixed combinators 4", () => {
|
|
411
|
+
const selector = Selector.parse("paragraph em ~ text");
|
|
412
|
+
const tree = parseTree();
|
|
413
|
+
const tt = new TreeTransformer(tree);
|
|
414
|
+
let numMatches = 0;
|
|
415
|
+
let matchedText = "";
|
|
416
|
+
|
|
417
|
+
tt.traverse((n, state, content) => {
|
|
418
|
+
const match = selector.match(state);
|
|
419
|
+
if (match !== null) {
|
|
420
|
+
expect(Array.isArray(match)).toBeTruthy();
|
|
421
|
+
expect(match).toHaveLength(3);
|
|
422
|
+
// @ts-expect-error - TS2532 - Object is possibly 'undefined'.
|
|
423
|
+
expect(match[0].type).toEqual("paragraph");
|
|
424
|
+
// @ts-expect-error - TS2532 - Object is possibly 'undefined'.
|
|
425
|
+
expect(match[0]).toEqual(state.parent());
|
|
426
|
+
// @ts-expect-error - TS2532 - Object is possibly 'undefined'.
|
|
427
|
+
expect(match[1].type).toEqual("em");
|
|
428
|
+
// @ts-expect-error - TS2532 - Object is possibly 'undefined'.
|
|
429
|
+
expect(match[1]).toEqual(state.previousSibling());
|
|
430
|
+
// @ts-expect-error - TS2532 - Object is possibly 'undefined'.
|
|
431
|
+
expect(match[2].type).toEqual("text");
|
|
432
|
+
// @ts-expect-error - TS2532 - Object is possibly 'undefined'.
|
|
433
|
+
expect(match[2]).toEqual(n);
|
|
434
|
+
matchedText += content;
|
|
435
|
+
numMatches++;
|
|
436
|
+
}
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
expect(numMatches).toEqual(1);
|
|
440
|
+
expect(matchedText).toEqual("H");
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
it("selector list", () => {
|
|
444
|
+
const selector = Selector.parse("paragraph, list");
|
|
445
|
+
const tree = parseTree();
|
|
446
|
+
const tt = new TreeTransformer(tree);
|
|
447
|
+
let numMatches = 0;
|
|
448
|
+
let matchedText = "";
|
|
449
|
+
|
|
450
|
+
tt.traverse((n, state, content) => {
|
|
451
|
+
const match = selector.match(state);
|
|
452
|
+
if (match !== null) {
|
|
453
|
+
expect(Array.isArray(match)).toBeTruthy();
|
|
454
|
+
expect(match).toHaveLength(1);
|
|
455
|
+
// @ts-expect-error - TS2532 - Object is possibly 'undefined'.
|
|
456
|
+
expect(match[0]).toEqual(n);
|
|
457
|
+
expect(
|
|
458
|
+
n.type === "paragraph" || n.type === "list",
|
|
459
|
+
).toBeTruthy();
|
|
460
|
+
matchedText += content;
|
|
461
|
+
numMatches++;
|
|
462
|
+
}
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
expect(numMatches).toEqual(4);
|
|
466
|
+
expect(matchedText).toEqual("BCDEFGH");
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
it("selector list 2", () => {
|
|
470
|
+
const selector = Selector.parse("heading, paragraph text, list>text");
|
|
471
|
+
const tree = parseTree();
|
|
472
|
+
const tt = new TreeTransformer(tree);
|
|
473
|
+
let numMatches = 0;
|
|
474
|
+
let matchedText = "";
|
|
475
|
+
|
|
476
|
+
tt.traverse((n, state, content) => {
|
|
477
|
+
const match = selector.match(state);
|
|
478
|
+
if (match !== null) {
|
|
479
|
+
expect(Array.isArray(match)).toBeTruthy();
|
|
480
|
+
if (n.type === "heading") {
|
|
481
|
+
expect(match).toHaveLength(1);
|
|
482
|
+
// @ts-expect-error - TS2532 - Object is possibly 'undefined'.
|
|
483
|
+
expect(match[0]).toEqual(n);
|
|
484
|
+
} else {
|
|
485
|
+
expect(match).toHaveLength(2);
|
|
486
|
+
// @ts-expect-error - TS2532 - Object is possibly 'undefined'.
|
|
487
|
+
expect(match[1]).toEqual(n);
|
|
488
|
+
expect(n.type).toEqual("text");
|
|
489
|
+
}
|
|
490
|
+
matchedText += content;
|
|
491
|
+
numMatches++;
|
|
492
|
+
}
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
expect(numMatches).toEqual(8);
|
|
496
|
+
expect(matchedText).toEqual("ABCDEFGH");
|
|
497
|
+
});
|
|
498
|
+
});
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import * as PureMarkdown from "@khanacademy/pure-markdown";
|
|
2
|
+
|
|
3
|
+
import Rule from "../rule";
|
|
4
|
+
import TreeTransformer from "../tree-transformer";
|
|
5
|
+
|
|
6
|
+
describe("PerseusLinter lint Rules class", () => {
|
|
7
|
+
const markdown = `
|
|
8
|
+
## This Heading is in Title Case
|
|
9
|
+
|
|
10
|
+
This paragraph contains forbidden words. Poop!
|
|
11
|
+
|
|
12
|
+
This paragraph contains an unescaped $ sign.
|
|
13
|
+
|
|
14
|
+
#### This heading skipped a level
|
|
15
|
+
`;
|
|
16
|
+
|
|
17
|
+
const ruleDescriptions = [
|
|
18
|
+
{
|
|
19
|
+
name: "heading-title-case",
|
|
20
|
+
selector: "heading",
|
|
21
|
+
pattern: "\\s[A-Z][a-z]",
|
|
22
|
+
message: `Title case in heading:
|
|
23
|
+
Only capitalize the first word of headings.`,
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
name: "profanity",
|
|
27
|
+
pattern: "/poop|crap/i",
|
|
28
|
+
message: `Profanity:
|
|
29
|
+
this is a family website!`,
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
name: "unescaped-dollar",
|
|
33
|
+
selector: "unescapedDollar",
|
|
34
|
+
message: `Unescaped '$':
|
|
35
|
+
If writing math, pair with another $.
|
|
36
|
+
Otherwise escape it by writing \\$.`,
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
name: "heading-level-skip",
|
|
40
|
+
selector: "heading ~ heading",
|
|
41
|
+
lint: function (state, content, nodes) {
|
|
42
|
+
const currentHeading = nodes[1];
|
|
43
|
+
const previousHeading = nodes[0];
|
|
44
|
+
|
|
45
|
+
expect(nodes).toHaveLength(2);
|
|
46
|
+
expect(nodes[1]).toEqual(state.currentNode());
|
|
47
|
+
|
|
48
|
+
// A heading can have a level less than, the same as
|
|
49
|
+
// or one more than the previous heading. But going up
|
|
50
|
+
// by 2 or more levels is not right
|
|
51
|
+
if (currentHeading.level > previousHeading.level + 1) {
|
|
52
|
+
return `Skipped heading level:
|
|
53
|
+
this heading is level ${currentHeading.level} but
|
|
54
|
+
the previous heading was level ${previousHeading.level}`;
|
|
55
|
+
}
|
|
56
|
+
return false;
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
];
|
|
60
|
+
|
|
61
|
+
let rules: Array<never> | Array<Rule | any> = [];
|
|
62
|
+
|
|
63
|
+
function parseTree() {
|
|
64
|
+
return PureMarkdown.parse(markdown);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
it("makeRules() factory method", () => {
|
|
68
|
+
rules = ruleDescriptions.map((r) => Rule.makeRule(r));
|
|
69
|
+
expect(rules).toHaveLength(ruleDescriptions.length);
|
|
70
|
+
rules.forEach((r) => expect(r instanceof Rule).toBeTruthy());
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("check() method", () => {
|
|
74
|
+
const tree = parseTree();
|
|
75
|
+
const tt = new TreeTransformer(tree);
|
|
76
|
+
const warnings: Array<
|
|
77
|
+
| any
|
|
78
|
+
| {
|
|
79
|
+
end: number;
|
|
80
|
+
message: string;
|
|
81
|
+
rule: string;
|
|
82
|
+
severity?: number;
|
|
83
|
+
start: number;
|
|
84
|
+
}
|
|
85
|
+
> = [];
|
|
86
|
+
|
|
87
|
+
tt.traverse((node, state, content) => {
|
|
88
|
+
rules.forEach((r) => {
|
|
89
|
+
const lint = r.check(node, state, content);
|
|
90
|
+
if (lint) {
|
|
91
|
+
warnings.push(lint);
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
expect(warnings).toHaveLength(4);
|
|
97
|
+
expect(warnings[0].rule).toEqual(ruleDescriptions[0].name);
|
|
98
|
+
expect(warnings[0].message).toEqual(ruleDescriptions[0].message);
|
|
99
|
+
|
|
100
|
+
expect(warnings[1].rule).toEqual(ruleDescriptions[1].name);
|
|
101
|
+
expect(warnings[1].message).toEqual(ruleDescriptions[1].message);
|
|
102
|
+
expect(warnings[1].start).toEqual(2);
|
|
103
|
+
expect(warnings[1].end).toEqual(6);
|
|
104
|
+
|
|
105
|
+
expect(warnings[2].rule).toEqual(ruleDescriptions[2].name);
|
|
106
|
+
expect(warnings[2].message).toEqual(ruleDescriptions[2].message);
|
|
107
|
+
|
|
108
|
+
expect(warnings[3].rule).toEqual(ruleDescriptions[3].name);
|
|
109
|
+
});
|
|
110
|
+
});
|