@khanacademy/perseus-linter 0.3.0 → 0.3.2
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 +14 -0
- package/dist/es/index.js +420 -240
- package/dist/es/index.js.map +1 -1
- package/dist/index.js +15 -20
- package/dist/index.js.map +1 -1
- package/dist/tree-transformer.d.ts +1 -1
- package/package.json +3 -3
- package/src/__tests__/matcher.test.ts +57 -57
- package/src/__tests__/rules.test.ts +62 -62
- package/src/__tests__/tree-transformer.test.ts +5 -5
- package/src/index.ts +4 -4
- package/src/rule.ts +2 -2
- package/src/tree-transformer.ts +9 -13
- package/tsconfig-build.json +11 -0
- package/{tsconfig.tsbuildinfo → tsconfig-build.tsbuildinfo} +1 -1
- package/dist/index.js.flow +0 -18
- package/dist/proptypes.js.flow +0 -17
- package/dist/rule.js.flow +0 -86
- package/dist/rules/absolute-url.js.flow +0 -9
- package/dist/rules/all-rules.js.flow +0 -9
- package/dist/rules/blockquoted-math.js.flow +0 -9
- package/dist/rules/blockquoted-widget.js.flow +0 -9
- package/dist/rules/double-spacing-after-terminal.js.flow +0 -9
- package/dist/rules/extra-content-spacing.js.flow +0 -9
- package/dist/rules/heading-level-1.js.flow +0 -9
- package/dist/rules/heading-level-skip.js.flow +0 -9
- package/dist/rules/heading-sentence-case.js.flow +0 -9
- package/dist/rules/heading-title-case.js.flow +0 -9
- package/dist/rules/image-alt-text.js.flow +0 -9
- package/dist/rules/image-in-table.js.flow +0 -9
- package/dist/rules/image-spaces-around-urls.js.flow +0 -9
- package/dist/rules/image-widget.js.flow +0 -9
- package/dist/rules/link-click-here.js.flow +0 -9
- package/dist/rules/lint-utils.js.flow +0 -8
- package/dist/rules/long-paragraph.js.flow +0 -9
- package/dist/rules/math-adjacent.js.flow +0 -9
- package/dist/rules/math-align-extra-break.js.flow +0 -9
- package/dist/rules/math-align-linebreaks.js.flow +0 -9
- package/dist/rules/math-empty.js.flow +0 -9
- package/dist/rules/math-font-size.js.flow +0 -9
- package/dist/rules/math-frac.js.flow +0 -9
- package/dist/rules/math-nested.js.flow +0 -9
- package/dist/rules/math-starts-with-space.js.flow +0 -9
- package/dist/rules/math-text-empty.js.flow +0 -9
- package/dist/rules/math-without-dollars.js.flow +0 -9
- package/dist/rules/nested-lists.js.flow +0 -9
- package/dist/rules/profanity.js.flow +0 -9
- package/dist/rules/table-missing-cells.js.flow +0 -9
- package/dist/rules/unbalanced-code-delimiters.js.flow +0 -9
- package/dist/rules/unescaped-dollar.js.flow +0 -9
- package/dist/rules/widget-in-table.js.flow +0 -9
- package/dist/selector.js.flow +0 -31
- package/dist/tree-transformer.js.flow +0 -253
- package/dist/types.js.flow +0 -12
- package/tsconfig.json +0 -12
package/dist/es/index.js
CHANGED
|
@@ -1,56 +1,25 @@
|
|
|
1
1
|
import { PerseusError, Errors } from '@khanacademy/perseus-error';
|
|
2
2
|
import PropTypes from 'prop-types';
|
|
3
3
|
|
|
4
|
-
function
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key));
|
|
21
|
-
});
|
|
22
|
-
}
|
|
23
|
-
return target;
|
|
24
|
-
}
|
|
25
|
-
function _defineProperty(obj, key, value) {
|
|
26
|
-
key = _toPropertyKey(key);
|
|
27
|
-
if (key in obj) {
|
|
28
|
-
Object.defineProperty(obj, key, {
|
|
29
|
-
value: value,
|
|
30
|
-
enumerable: true,
|
|
31
|
-
configurable: true,
|
|
32
|
-
writable: true
|
|
33
|
-
});
|
|
34
|
-
} else {
|
|
35
|
-
obj[key] = value;
|
|
36
|
-
}
|
|
37
|
-
return obj;
|
|
38
|
-
}
|
|
39
|
-
function _toPrimitive(input, hint) {
|
|
40
|
-
if (typeof input !== "object" || input === null) return input;
|
|
41
|
-
var prim = input[Symbol.toPrimitive];
|
|
42
|
-
if (prim !== undefined) {
|
|
43
|
-
var res = prim.call(input, hint || "default");
|
|
44
|
-
if (typeof res !== "object") return res;
|
|
45
|
-
throw new TypeError("@@toPrimitive must return a primitive value.");
|
|
46
|
-
}
|
|
47
|
-
return (hint === "string" ? String : Number)(input);
|
|
48
|
-
}
|
|
49
|
-
function _toPropertyKey(arg) {
|
|
50
|
-
var key = _toPrimitive(arg, "string");
|
|
51
|
-
return typeof key === "symbol" ? key : String(key);
|
|
4
|
+
function _extends() {
|
|
5
|
+
_extends = Object.assign || function (target) {
|
|
6
|
+
for (var i = 1; i < arguments.length; i++) {
|
|
7
|
+
var source = arguments[i];
|
|
8
|
+
|
|
9
|
+
for (var key in source) {
|
|
10
|
+
if (Object.prototype.hasOwnProperty.call(source, key)) {
|
|
11
|
+
target[key] = source[key];
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return target;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
return _extends.apply(this, arguments);
|
|
52
20
|
}
|
|
53
21
|
|
|
22
|
+
/* eslint-disable no-useless-escape */
|
|
54
23
|
/**
|
|
55
24
|
* This is the base class for all Selector types. The key method that all
|
|
56
25
|
* selector subclasses must implement is match(). It takes a TraversalState
|
|
@@ -96,8 +65,8 @@ class Parser {
|
|
|
96
65
|
// Which token in the array we're looking at now
|
|
97
66
|
|
|
98
67
|
constructor(s) {
|
|
99
|
-
|
|
100
|
-
|
|
68
|
+
this.tokens = void 0;
|
|
69
|
+
this.tokenIndex = void 0;
|
|
101
70
|
// Normalize whitespace:
|
|
102
71
|
// - remove leading and trailing whitespace
|
|
103
72
|
// - replace runs of whitespace with single space characters
|
|
@@ -124,7 +93,7 @@ class Parser {
|
|
|
124
93
|
isIdentifier() {
|
|
125
94
|
// The Parser.TOKENS regexp ensures that we only have to check
|
|
126
95
|
// the first character of a token to know what kind of token it is.
|
|
127
|
-
|
|
96
|
+
const c = this.tokens[this.tokenIndex][0];
|
|
128
97
|
return c >= "a" && c <= "z" || c >= "A" && c <= "Z";
|
|
129
98
|
}
|
|
130
99
|
|
|
@@ -140,10 +109,10 @@ class Parser {
|
|
|
140
109
|
// ever need to call.
|
|
141
110
|
parse() {
|
|
142
111
|
// We expect at least one tree selector
|
|
143
|
-
|
|
112
|
+
const ts = this.parseTreeSelector();
|
|
144
113
|
|
|
145
114
|
// Now see what's next
|
|
146
|
-
|
|
115
|
+
let token = this.nextToken();
|
|
147
116
|
|
|
148
117
|
// If there is no next token then we're done parsing and can return
|
|
149
118
|
// the tree selector object we got above
|
|
@@ -153,7 +122,7 @@ class Parser {
|
|
|
153
122
|
|
|
154
123
|
// Otherwise, there is more go come and we're going to need a
|
|
155
124
|
// list of tree selectors
|
|
156
|
-
|
|
125
|
+
const treeSelectors = [ts];
|
|
157
126
|
while (token) {
|
|
158
127
|
// The only character we allow after a tree selector is a comma
|
|
159
128
|
if (token === ",") {
|
|
@@ -179,7 +148,7 @@ class Parser {
|
|
|
179
148
|
this.skipSpace(); // Ignore space after a comma, for example
|
|
180
149
|
|
|
181
150
|
// A tree selector must begin with a node selector
|
|
182
|
-
|
|
151
|
+
let ns = this.parseNodeSelector();
|
|
183
152
|
for (;;) {
|
|
184
153
|
// Now check the next token. If there is none, or if it is a
|
|
185
154
|
// comma, then we're done with the treeSelector. Otherwise
|
|
@@ -188,7 +157,7 @@ class Parser {
|
|
|
188
157
|
// do see a combinator and another node selector then we
|
|
189
158
|
// combine the current node selector with the new node selector
|
|
190
159
|
// using a Selector subclass that depends on the combinator.
|
|
191
|
-
|
|
160
|
+
const token = this.nextToken();
|
|
192
161
|
if (!token || token === ",") {
|
|
193
162
|
break;
|
|
194
163
|
} else if (token === " ") {
|
|
@@ -219,7 +188,7 @@ class Parser {
|
|
|
219
188
|
parseNodeSelector() {
|
|
220
189
|
// First, skip any whitespace
|
|
221
190
|
this.skipSpace();
|
|
222
|
-
|
|
191
|
+
const t = this.nextToken();
|
|
223
192
|
if (t === "*") {
|
|
224
193
|
this.consume();
|
|
225
194
|
return new AnyNode();
|
|
@@ -236,7 +205,7 @@ class Parser {
|
|
|
236
205
|
// are identifiers, integers, punctuation and spaces. Note that spaces
|
|
237
206
|
// tokens are only returned when they appear before an identifier or
|
|
238
207
|
// wildcard token and are otherwise omitted.
|
|
239
|
-
|
|
208
|
+
Parser.TOKENS = void 0;
|
|
240
209
|
Parser.TOKENS = /([a-zA-Z][\w-]*)|(\d+)|[^\s]|(\s(?=[a-zA-Z\*]))/g;
|
|
241
210
|
|
|
242
211
|
/**
|
|
@@ -257,13 +226,13 @@ class ParseError extends Error {
|
|
|
257
226
|
class SelectorList extends Selector {
|
|
258
227
|
constructor(selectors) {
|
|
259
228
|
super();
|
|
260
|
-
|
|
229
|
+
this.selectors = void 0;
|
|
261
230
|
this.selectors = selectors;
|
|
262
231
|
}
|
|
263
232
|
match(state) {
|
|
264
|
-
for (
|
|
265
|
-
|
|
266
|
-
|
|
233
|
+
for (let i = 0; i < this.selectors.length; i++) {
|
|
234
|
+
const s = this.selectors[i];
|
|
235
|
+
const result = s.match(state);
|
|
267
236
|
if (result) {
|
|
268
237
|
return result;
|
|
269
238
|
}
|
|
@@ -271,8 +240,8 @@ class SelectorList extends Selector {
|
|
|
271
240
|
return null;
|
|
272
241
|
}
|
|
273
242
|
toString() {
|
|
274
|
-
|
|
275
|
-
for (
|
|
243
|
+
let result = "";
|
|
244
|
+
for (let i = 0; i < this.selectors.length; i++) {
|
|
276
245
|
result += i > 0 ? ", " : "";
|
|
277
246
|
result += this.selectors[i].toString();
|
|
278
247
|
}
|
|
@@ -300,11 +269,11 @@ class AnyNode extends Selector {
|
|
|
300
269
|
class TypeSelector extends Selector {
|
|
301
270
|
constructor(type) {
|
|
302
271
|
super();
|
|
303
|
-
|
|
272
|
+
this.type = void 0;
|
|
304
273
|
this.type = type;
|
|
305
274
|
}
|
|
306
275
|
match(state) {
|
|
307
|
-
|
|
276
|
+
const node = state.currentNode();
|
|
308
277
|
if (node.type === this.type) {
|
|
309
278
|
return [node];
|
|
310
279
|
}
|
|
@@ -324,8 +293,8 @@ class TypeSelector extends Selector {
|
|
|
324
293
|
class SelectorCombinator extends Selector {
|
|
325
294
|
constructor(left, right) {
|
|
326
295
|
super();
|
|
327
|
-
|
|
328
|
-
|
|
296
|
+
this.left = void 0;
|
|
297
|
+
this.right = void 0;
|
|
329
298
|
this.left = left;
|
|
330
299
|
this.right = right;
|
|
331
300
|
}
|
|
@@ -341,12 +310,12 @@ class AncestorCombinator extends SelectorCombinator {
|
|
|
341
310
|
super(left, right);
|
|
342
311
|
}
|
|
343
312
|
match(state) {
|
|
344
|
-
|
|
313
|
+
const rightResult = this.right.match(state);
|
|
345
314
|
if (rightResult) {
|
|
346
315
|
state = state.clone();
|
|
347
316
|
while (state.hasParent()) {
|
|
348
317
|
state.goToParent();
|
|
349
|
-
|
|
318
|
+
const leftResult = this.left.match(state);
|
|
350
319
|
if (leftResult) {
|
|
351
320
|
return leftResult.concat(rightResult);
|
|
352
321
|
}
|
|
@@ -369,12 +338,12 @@ class ParentCombinator extends SelectorCombinator {
|
|
|
369
338
|
super(left, right);
|
|
370
339
|
}
|
|
371
340
|
match(state) {
|
|
372
|
-
|
|
341
|
+
const rightResult = this.right.match(state);
|
|
373
342
|
if (rightResult) {
|
|
374
343
|
if (state.hasParent()) {
|
|
375
344
|
state = state.clone();
|
|
376
345
|
state.goToParent();
|
|
377
|
-
|
|
346
|
+
const leftResult = this.left.match(state);
|
|
378
347
|
if (leftResult) {
|
|
379
348
|
return leftResult.concat(rightResult);
|
|
380
349
|
}
|
|
@@ -397,12 +366,12 @@ class PreviousCombinator extends SelectorCombinator {
|
|
|
397
366
|
super(left, right);
|
|
398
367
|
}
|
|
399
368
|
match(state) {
|
|
400
|
-
|
|
369
|
+
const rightResult = this.right.match(state);
|
|
401
370
|
if (rightResult) {
|
|
402
371
|
if (state.hasPreviousSibling()) {
|
|
403
372
|
state = state.clone();
|
|
404
373
|
state.goToPreviousSibling();
|
|
405
|
-
|
|
374
|
+
const leftResult = this.left.match(state);
|
|
406
375
|
if (leftResult) {
|
|
407
376
|
return leftResult.concat(rightResult);
|
|
408
377
|
}
|
|
@@ -425,12 +394,12 @@ class SiblingCombinator extends SelectorCombinator {
|
|
|
425
394
|
super(left, right);
|
|
426
395
|
}
|
|
427
396
|
match(state) {
|
|
428
|
-
|
|
397
|
+
const rightResult = this.right.match(state);
|
|
429
398
|
if (rightResult) {
|
|
430
399
|
state = state.clone();
|
|
431
400
|
while (state.hasPreviousSibling()) {
|
|
432
401
|
state.goToPreviousSibling();
|
|
433
|
-
|
|
402
|
+
const leftResult = this.left.match(state);
|
|
434
403
|
if (leftResult) {
|
|
435
404
|
return leftResult.concat(rightResult);
|
|
436
405
|
}
|
|
@@ -443,6 +412,128 @@ class SiblingCombinator extends SelectorCombinator {
|
|
|
443
412
|
}
|
|
444
413
|
}
|
|
445
414
|
|
|
415
|
+
/**
|
|
416
|
+
* The Rule class represents a Perseus lint rule. A Rule instance has a check()
|
|
417
|
+
* method that takes the same (node, state, content) arguments that a
|
|
418
|
+
* TreeTransformer traversal callback function does. Call the check() method
|
|
419
|
+
* during a tree traversal to determine whether the current node of the tree
|
|
420
|
+
* violates the rule. If there is no violation, then check() returns
|
|
421
|
+
* null. Otherwise, it returns an object that includes the name of the rule,
|
|
422
|
+
* an error message, and the start and end positions within the node's content
|
|
423
|
+
* string of the lint.
|
|
424
|
+
*
|
|
425
|
+
* A Perseus lint rule consists of a name, a severity, a selector, a pattern
|
|
426
|
+
* (RegExp) and two functions. The check() method uses the selector, pattern,
|
|
427
|
+
* and functions as follows:
|
|
428
|
+
*
|
|
429
|
+
* - First, when determining which rules to apply to a particular piece of
|
|
430
|
+
* content, each rule can specify an optional function provided in the fifth
|
|
431
|
+
* parameter to evaluate whether or not we should be applying this rule.
|
|
432
|
+
* If the function returns false, we don't use the rule on this content.
|
|
433
|
+
*
|
|
434
|
+
* - Next, check() tests whether the node currently being traversed matches
|
|
435
|
+
* the selector. If it does not, then the rule does not apply at this node
|
|
436
|
+
* and there is no lint and check() returns null.
|
|
437
|
+
*
|
|
438
|
+
* - If the selector matched, then check() tests the text content of the node
|
|
439
|
+
* (and its children) against the pattern. If the pattern does not match,
|
|
440
|
+
* then there is no lint, and check() returns null.
|
|
441
|
+
*
|
|
442
|
+
* - If both the selector and pattern match, then check() calls the function
|
|
443
|
+
* passing the TraversalState object, the content string for the node, the
|
|
444
|
+
* array of nodes returned by the selector match, and the array of strings
|
|
445
|
+
* returned by the pattern match. This function can use these arguments to
|
|
446
|
+
* implement any kind of lint detection logic it wants. If it determines
|
|
447
|
+
* that there is no lint, then it should return null. Otherwise, it should
|
|
448
|
+
* return an error message as a string, or an object with `message`, `start`
|
|
449
|
+
* and `end` properties. The start and end properties are numbers that mark
|
|
450
|
+
* the beginning and end of the problematic content. Note that these numbers
|
|
451
|
+
* are relative to the content string passed to the traversal callback, not
|
|
452
|
+
* to the entire string that was used to generate the parse tree in the
|
|
453
|
+
* first place. TODO(davidflanagan): modify the simple-markdown library to
|
|
454
|
+
* have an option to add the text offset of each node to the parse
|
|
455
|
+
* tree. This will allows us to pinpoint lint errors within a long string
|
|
456
|
+
* of markdown text.
|
|
457
|
+
*
|
|
458
|
+
* - If the function returns null, then check() returns null. Otherwise,
|
|
459
|
+
* check() returns an object with `rule`, `message`, `start` and `end`
|
|
460
|
+
* properties. The value of the `rule` property is the name of the rule,
|
|
461
|
+
* which is useful for error reporting purposes.
|
|
462
|
+
*
|
|
463
|
+
* The name, severity, selector, pattern and function arguments to the Rule()
|
|
464
|
+
* constructor are optional, but you may not omit both the selector and the
|
|
465
|
+
* pattern. If you do not specify a selector, a default selector that matches
|
|
466
|
+
* any node of type "text" will be used. If you do not specify a pattern, then
|
|
467
|
+
* any node that matches the selector will be assumed to match the pattern as
|
|
468
|
+
* well. If you don't pass a function as the fourth argument to the Rule()
|
|
469
|
+
* constructor, then you must pass an error message string instead. If you do
|
|
470
|
+
* this, you'll get a default function that unconditionally returns an object
|
|
471
|
+
* that includes the error message and the start and end indexes of the
|
|
472
|
+
* portion of the content string that matched the pattern. If you don't pass a
|
|
473
|
+
* function in the fifth parameter, the rule will be applied in any context.
|
|
474
|
+
*
|
|
475
|
+
* One of the design goals of this Rule class is to allow simple lint rules to
|
|
476
|
+
* be described in JSON files without any JavaScript code. So in addition to
|
|
477
|
+
* the Rule() constructor, the class also defines a Rule.makeRule() factory
|
|
478
|
+
* method. This method takes a single object as its argument and expects the
|
|
479
|
+
* object to have four string properties. The `name` property is passed as the
|
|
480
|
+
* first argument to the Rule() construtctor. The optional `selector`
|
|
481
|
+
* property, if specified, is passed to Selector.parse() and the resulting
|
|
482
|
+
* Selector object is used as the second argument to Rule(). The optional
|
|
483
|
+
* `pattern` property is converted to a RegExp before being passed as the
|
|
484
|
+
* third argument to Rule(). (See Rule.makePattern() for details on the string
|
|
485
|
+
* to RegExp conversion). Finally, the `message` property specifies an error
|
|
486
|
+
* message that is passed as the final argument to Rule(). You can also use a
|
|
487
|
+
* real RegExp as the value of the `pattern` property or define a custom lint
|
|
488
|
+
* function on the `lint` property instead of setting the `message`
|
|
489
|
+
* property. Doing either of these things means that your rule description can
|
|
490
|
+
* no longer be saved in a JSON file, however.
|
|
491
|
+
*
|
|
492
|
+
* For example, here are two lint rules defined with Rule.makeRule():
|
|
493
|
+
*
|
|
494
|
+
* let nestedLists = Rule.makeRule({
|
|
495
|
+
* name: "nested-lists",
|
|
496
|
+
* selector: "list list",
|
|
497
|
+
* message: `Nested lists:
|
|
498
|
+
* nested lists are hard to read on mobile devices;
|
|
499
|
+
* do not use additional indentation.`,
|
|
500
|
+
* });
|
|
501
|
+
*
|
|
502
|
+
* let longParagraph = Rule.makeRule({
|
|
503
|
+
* name: "long-paragraph",
|
|
504
|
+
* selector: "paragraph",
|
|
505
|
+
* pattern: /^.{501,}/,
|
|
506
|
+
* lint: function(state, content, nodes, match) {
|
|
507
|
+
* return `Paragraph too long:
|
|
508
|
+
* This paragraph is ${content.length} characters long.
|
|
509
|
+
* Shorten it to 500 characters or fewer.`;
|
|
510
|
+
* },
|
|
511
|
+
* });
|
|
512
|
+
*
|
|
513
|
+
* Certain advanced lint rules need additional information about the content
|
|
514
|
+
* being linted in order to detect lint. For example, a rule to check for
|
|
515
|
+
* whitespace at the start and end of the URL for an image can't use the
|
|
516
|
+
* information in the node or content arguments because the markdown parser
|
|
517
|
+
* strips leading and trailing whitespace when parsing. (Nevertheless, these
|
|
518
|
+
* spaces have been a practical problem for our content translation process so
|
|
519
|
+
* in order to check for them, a lint rule needs access to the original
|
|
520
|
+
* unparsed source text. Similarly, there are various lint rules that check
|
|
521
|
+
* widget usage. For example, it is easy to write a lint rule to ensure that
|
|
522
|
+
* images have alt text for images encoded in markdown. But when images are
|
|
523
|
+
* added to our content via an image widget we also want to be able to check
|
|
524
|
+
* for alt text. In order to do this, the lint rule needs to be able to look
|
|
525
|
+
* widgets up by name in the widgets object associated with the parse tree.
|
|
526
|
+
*
|
|
527
|
+
* In order to support advanced linting rules like these, the check() method
|
|
528
|
+
* takes a context object as its optional fourth argument, and passes this
|
|
529
|
+
* object on to the lint function of each rule. Rules that require extra
|
|
530
|
+
* context should not assume that they will always get it, and should verify
|
|
531
|
+
* that the necessary context has been supplied before using it. Currently the
|
|
532
|
+
* "content" property of the context object is the unparsed source text if
|
|
533
|
+
* available, and the "widgets" property of the context object is the widget
|
|
534
|
+
* object associated with that content string in the JSON object that defines
|
|
535
|
+
* the Perseus article or exercise that is being linted.
|
|
536
|
+
*/
|
|
446
537
|
/**
|
|
447
538
|
* A Rule object describes a Perseus lint rule. See the comment at the top of
|
|
448
539
|
* this file for detailed description.
|
|
@@ -459,14 +550,13 @@ class Rule {
|
|
|
459
550
|
// The comment at the top of this file has detailed docs for
|
|
460
551
|
// this constructor and its arguments
|
|
461
552
|
constructor(name, severity, selector, pattern, lint, applies) {
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
_defineProperty(this, "message", void 0);
|
|
553
|
+
this.name = void 0;
|
|
554
|
+
this.severity = void 0;
|
|
555
|
+
this.selector = void 0;
|
|
556
|
+
this.pattern = void 0;
|
|
557
|
+
this.lint = void 0;
|
|
558
|
+
this.applies = void 0;
|
|
559
|
+
this.message = void 0;
|
|
470
560
|
if (!selector && !pattern) {
|
|
471
561
|
throw new PerseusError("Lint rules must have a selector or pattern", Errors.InvalidInput, {
|
|
472
562
|
metadata: {
|
|
@@ -485,9 +575,7 @@ class Rule {
|
|
|
485
575
|
this.lint = lint;
|
|
486
576
|
this.message = null;
|
|
487
577
|
} else {
|
|
488
|
-
this.lint =
|
|
489
|
-
return _this._defaultLintFunction(...arguments);
|
|
490
|
-
};
|
|
578
|
+
this.lint = (...args) => this._defaultLintFunction(...args);
|
|
491
579
|
this.message = lint;
|
|
492
580
|
}
|
|
493
581
|
this.applies = applies || function () {
|
|
@@ -508,7 +596,7 @@ class Rule {
|
|
|
508
596
|
// First, see if we match the selector.
|
|
509
597
|
// If no selector was passed to the constructor, we use a
|
|
510
598
|
// default selector that matches text nodes.
|
|
511
|
-
|
|
599
|
+
const selectorMatch = this.selector.match(traversalState);
|
|
512
600
|
|
|
513
601
|
// If the selector did not match, then we're done
|
|
514
602
|
if (!selectorMatch) {
|
|
@@ -516,7 +604,7 @@ class Rule {
|
|
|
516
604
|
}
|
|
517
605
|
|
|
518
606
|
// If the selector matched, then see if the pattern matches
|
|
519
|
-
|
|
607
|
+
let patternMatch;
|
|
520
608
|
if (this.pattern) {
|
|
521
609
|
patternMatch = content.match(this.pattern);
|
|
522
610
|
} else {
|
|
@@ -532,7 +620,7 @@ class Rule {
|
|
|
532
620
|
try {
|
|
533
621
|
// If we get here, then the selector and pattern have matched
|
|
534
622
|
// so now we call the lint function to see if there is lint.
|
|
535
|
-
|
|
623
|
+
const error = this.lint(traversalState, content, selectorMatch, patternMatch, context);
|
|
536
624
|
if (!error) {
|
|
537
625
|
return null; // No lint; we're done
|
|
538
626
|
}
|
|
@@ -565,7 +653,9 @@ class Rule {
|
|
|
565
653
|
// a rule was failing.
|
|
566
654
|
return {
|
|
567
655
|
rule: "lint-rule-failure",
|
|
568
|
-
message:
|
|
656
|
+
message: `Exception in rule ${this.name}: ${e.message}
|
|
657
|
+
Stack trace:
|
|
658
|
+
${e.stack}`,
|
|
569
659
|
start: 0,
|
|
570
660
|
end: content.length
|
|
571
661
|
};
|
|
@@ -610,10 +700,10 @@ class Rule {
|
|
|
610
700
|
return pattern;
|
|
611
701
|
}
|
|
612
702
|
if (pattern[0] === "/") {
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
// @ts-expect-error
|
|
703
|
+
const lastSlash = pattern.lastIndexOf("/");
|
|
704
|
+
const expression = pattern.substring(1, lastSlash);
|
|
705
|
+
const flags = pattern.substring(lastSlash + 1);
|
|
706
|
+
// @ts-expect-error - TS2713 - Cannot access 'RegExp.flags' because 'RegExp' is a type, but not a namespace. Did you mean to retrieve the type of the property 'flags' in 'RegExp' with 'RegExp["flags"]'?
|
|
617
707
|
return new RegExp(expression, flags);
|
|
618
708
|
}
|
|
619
709
|
return new RegExp(pattern);
|
|
@@ -624,19 +714,19 @@ class Rule {
|
|
|
624
714
|
// String.match() method. We use it when a Rule has no pattern and we
|
|
625
715
|
// want to simulate a match on the entire content string.
|
|
626
716
|
static FakePatternMatch(input, match, index) {
|
|
627
|
-
|
|
717
|
+
const result = [match];
|
|
628
718
|
result.index = index;
|
|
629
719
|
result.input = input;
|
|
630
720
|
return result;
|
|
631
721
|
}
|
|
632
722
|
}
|
|
633
|
-
|
|
634
|
-
|
|
723
|
+
Rule.DEFAULT_SELECTOR = void 0;
|
|
724
|
+
Rule.Severity = {
|
|
635
725
|
ERROR: 1,
|
|
636
726
|
WARNING: 2,
|
|
637
727
|
GUIDELINE: 3,
|
|
638
728
|
BULK_WARNING: 4
|
|
639
|
-
}
|
|
729
|
+
};
|
|
640
730
|
Rule.DEFAULT_SELECTOR = Selector.parse("text");
|
|
641
731
|
|
|
642
732
|
/* eslint-disable no-useless-escape */
|
|
@@ -644,7 +734,7 @@ Rule.DEFAULT_SELECTOR = Selector.parse("text");
|
|
|
644
734
|
// portion which is usually just the hostname, but may also include
|
|
645
735
|
// a username, password or port. We don't strip those things out because
|
|
646
736
|
// we typically want to reject any URL that includes them
|
|
647
|
-
|
|
737
|
+
const HOSTNAME = /\/\/([^\/]+)/;
|
|
648
738
|
|
|
649
739
|
// Return the hostname of the URL, with any "www." prefix removed.
|
|
650
740
|
// If this is a relative URL with no hostname, return an empty string.
|
|
@@ -652,7 +742,7 @@ function getHostname(url) {
|
|
|
652
742
|
if (!url) {
|
|
653
743
|
return "";
|
|
654
744
|
}
|
|
655
|
-
|
|
745
|
+
const match = url.match(HOSTNAME);
|
|
656
746
|
return match ? match[1] : "";
|
|
657
747
|
}
|
|
658
748
|
|
|
@@ -660,11 +750,14 @@ var AbsoluteUrl = Rule.makeRule({
|
|
|
660
750
|
name: "absolute-url",
|
|
661
751
|
severity: Rule.Severity.GUIDELINE,
|
|
662
752
|
selector: "link, image",
|
|
663
|
-
lint: function
|
|
664
|
-
|
|
665
|
-
|
|
753
|
+
lint: function (state, content, nodes, match) {
|
|
754
|
+
const url = nodes[0].target;
|
|
755
|
+
const hostname = getHostname(url);
|
|
666
756
|
if (hostname === "khanacademy.org" || hostname.endsWith(".khanacademy.org")) {
|
|
667
|
-
return
|
|
757
|
+
return `Don't use absolute URLs:
|
|
758
|
+
When linking to KA content or images, omit the
|
|
759
|
+
https://www.khanacademy.org URL prefix.
|
|
760
|
+
Use a relative URL beginning with / instead.`;
|
|
668
761
|
}
|
|
669
762
|
}
|
|
670
763
|
});
|
|
@@ -673,14 +766,16 @@ var BlockquotedMath = Rule.makeRule({
|
|
|
673
766
|
name: "blockquoted-math",
|
|
674
767
|
severity: Rule.Severity.WARNING,
|
|
675
768
|
selector: "blockQuote math, blockQuote blockMath",
|
|
676
|
-
message:
|
|
769
|
+
message: `Blockquoted math:
|
|
770
|
+
math should not be indented.`
|
|
677
771
|
});
|
|
678
772
|
|
|
679
773
|
var BlockquotedWidget = Rule.makeRule({
|
|
680
774
|
name: "blockquoted-widget",
|
|
681
775
|
severity: Rule.Severity.WARNING,
|
|
682
776
|
selector: "blockQuote widget",
|
|
683
|
-
message:
|
|
777
|
+
message: `Blockquoted widget:
|
|
778
|
+
widgets should not be indented.`
|
|
684
779
|
});
|
|
685
780
|
|
|
686
781
|
/* eslint-disable no-useless-escape */
|
|
@@ -689,26 +784,28 @@ var DoubleSpacingAfterTerminal = Rule.makeRule({
|
|
|
689
784
|
severity: Rule.Severity.BULK_WARNING,
|
|
690
785
|
selector: "paragraph",
|
|
691
786
|
pattern: /[.!\?] {2}/i,
|
|
692
|
-
message:
|
|
787
|
+
message: `Use a single space after a sentence-ending period, or
|
|
788
|
+
any other kind of terminal punctuation.`
|
|
693
789
|
});
|
|
694
790
|
|
|
695
791
|
var ExtraContentSpacing = Rule.makeRule({
|
|
696
792
|
name: "extra-content-spacing",
|
|
697
793
|
selector: "paragraph",
|
|
698
794
|
pattern: /\s+$/,
|
|
699
|
-
applies: function
|
|
795
|
+
applies: function (context) {
|
|
700
796
|
return context.contentType === "article";
|
|
701
797
|
},
|
|
702
|
-
message:
|
|
798
|
+
message: `No extra whitespace at the end of content blocks.`
|
|
703
799
|
});
|
|
704
800
|
|
|
705
801
|
var HeadingLevel1 = Rule.makeRule({
|
|
706
802
|
name: "heading-level-1",
|
|
707
803
|
severity: Rule.Severity.WARNING,
|
|
708
804
|
selector: "heading",
|
|
709
|
-
lint: function
|
|
805
|
+
lint: function (state, content, nodes, match) {
|
|
710
806
|
if (nodes[0].level === 1) {
|
|
711
|
-
return
|
|
807
|
+
return `Don't use level-1 headings:
|
|
808
|
+
Begin headings with two or more # characters.`;
|
|
712
809
|
}
|
|
713
810
|
}
|
|
714
811
|
});
|
|
@@ -717,14 +814,16 @@ var HeadingLevelSkip = Rule.makeRule({
|
|
|
717
814
|
name: "heading-level-skip",
|
|
718
815
|
severity: Rule.Severity.WARNING,
|
|
719
816
|
selector: "heading ~ heading",
|
|
720
|
-
lint: function
|
|
721
|
-
|
|
722
|
-
|
|
817
|
+
lint: function (state, content, nodes, match) {
|
|
818
|
+
const currentHeading = nodes[1];
|
|
819
|
+
const previousHeading = nodes[0];
|
|
723
820
|
// A heading can have a level less than, the same as
|
|
724
821
|
// or one more than the previous heading. But going up
|
|
725
822
|
// by 2 or more levels is not right
|
|
726
823
|
if (currentHeading.level > previousHeading.level + 1) {
|
|
727
|
-
return
|
|
824
|
+
return `Skipped heading level:
|
|
825
|
+
this heading is level ${currentHeading.level} but
|
|
826
|
+
the previous heading was level ${previousHeading.level}`;
|
|
728
827
|
}
|
|
729
828
|
}
|
|
730
829
|
});
|
|
@@ -735,13 +834,14 @@ var HeadingSentenceCase = Rule.makeRule({
|
|
|
735
834
|
selector: "heading",
|
|
736
835
|
pattern: /^\W*[a-z]/,
|
|
737
836
|
// first letter is lowercase
|
|
738
|
-
message:
|
|
837
|
+
message: `First letter is lowercase:
|
|
838
|
+
the first letter of a heading should be capitalized.`
|
|
739
839
|
});
|
|
740
840
|
|
|
741
841
|
// These are 3-letter and longer words that we would not expect to be
|
|
742
842
|
// capitalized even in a title-case heading. See
|
|
743
843
|
// http://blog.apastyle.org/apastyle/2012/03/title-case-and-sentence-case-capitalization-in-apa-style.html
|
|
744
|
-
|
|
844
|
+
const littleWords = {
|
|
745
845
|
and: true,
|
|
746
846
|
nor: true,
|
|
747
847
|
but: true,
|
|
@@ -749,7 +849,7 @@ var littleWords = {
|
|
|
749
849
|
for: true
|
|
750
850
|
};
|
|
751
851
|
function isCapitalized(word) {
|
|
752
|
-
|
|
852
|
+
const c = word[0];
|
|
753
853
|
return c === c.toUpperCase();
|
|
754
854
|
}
|
|
755
855
|
var HeadingTitleCase = Rule.makeRule({
|
|
@@ -758,7 +858,7 @@ var HeadingTitleCase = Rule.makeRule({
|
|
|
758
858
|
selector: "heading",
|
|
759
859
|
pattern: /[^\s:]\s+[A-Z]+[a-z]/,
|
|
760
860
|
locale: "en",
|
|
761
|
-
lint: function
|
|
861
|
+
lint: function (state, content, nodes, match) {
|
|
762
862
|
// We want to assert that heading text is in sentence case, not
|
|
763
863
|
// title case. The pattern above requires a capital letter at the
|
|
764
864
|
// start of the heading and allows them after a colon, or in
|
|
@@ -783,8 +883,8 @@ var HeadingTitleCase = Rule.makeRule({
|
|
|
783
883
|
//
|
|
784
884
|
// for APA style rules for title case
|
|
785
885
|
|
|
786
|
-
|
|
787
|
-
|
|
886
|
+
const heading = content.trim();
|
|
887
|
+
let words = heading.split(/\s+/);
|
|
788
888
|
|
|
789
889
|
// Remove the first word and the little words
|
|
790
890
|
words.shift();
|
|
@@ -795,7 +895,9 @@ var HeadingTitleCase = Rule.makeRule({
|
|
|
795
895
|
// If there are at least 3 remaining words and all
|
|
796
896
|
// are capitalized, then the heading is in title case.
|
|
797
897
|
if (words.length >= 3 && words.every(w => isCapitalized(w))) {
|
|
798
|
-
return
|
|
898
|
+
return `Title-case heading:
|
|
899
|
+
This heading appears to be in title-case, but should be sentence-case.
|
|
900
|
+
Only capitalize the first letter and proper nouns.`;
|
|
799
901
|
}
|
|
800
902
|
}
|
|
801
903
|
});
|
|
@@ -804,13 +906,17 @@ var ImageAltText = Rule.makeRule({
|
|
|
804
906
|
name: "image-alt-text",
|
|
805
907
|
severity: Rule.Severity.WARNING,
|
|
806
908
|
selector: "image",
|
|
807
|
-
lint: function
|
|
808
|
-
|
|
909
|
+
lint: function (state, content, nodes, match) {
|
|
910
|
+
const image = nodes[0];
|
|
809
911
|
if (!image.alt || !image.alt.trim()) {
|
|
810
|
-
return
|
|
912
|
+
return `Images should have alt text:
|
|
913
|
+
for accessibility, all images should have alt text.
|
|
914
|
+
Specify alt text inside square brackets after the !.`;
|
|
811
915
|
}
|
|
812
916
|
if (image.alt.length < 8) {
|
|
813
|
-
return
|
|
917
|
+
return `Images should have alt text:
|
|
918
|
+
for accessibility, all images should have descriptive alt text.
|
|
919
|
+
This image's alt text is only ${image.alt.length} characters long.`;
|
|
814
920
|
}
|
|
815
921
|
}
|
|
816
922
|
});
|
|
@@ -819,16 +925,17 @@ var ImageInTable = Rule.makeRule({
|
|
|
819
925
|
name: "image-in-table",
|
|
820
926
|
severity: Rule.Severity.BULK_WARNING,
|
|
821
927
|
selector: "table image",
|
|
822
|
-
message:
|
|
928
|
+
message: `Image in table:
|
|
929
|
+
do not put images inside of tables.`
|
|
823
930
|
});
|
|
824
931
|
|
|
825
932
|
var ImageSpacesAroundUrls = Rule.makeRule({
|
|
826
933
|
name: "image-spaces-around-urls",
|
|
827
934
|
severity: Rule.Severity.ERROR,
|
|
828
935
|
selector: "image",
|
|
829
|
-
lint: function
|
|
830
|
-
|
|
831
|
-
|
|
936
|
+
lint: function (state, content, nodes, match, context) {
|
|
937
|
+
const image = nodes[0];
|
|
938
|
+
const url = image.target;
|
|
832
939
|
|
|
833
940
|
// The markdown parser strips leading and trailing spaces for us,
|
|
834
941
|
// but they're still a problem for our translation process, so
|
|
@@ -837,13 +944,15 @@ var ImageSpacesAroundUrls = Rule.makeRule({
|
|
|
837
944
|
if (context && context.content) {
|
|
838
945
|
// Find the url in the original content and make sure that the
|
|
839
946
|
// character before is '(' and the character after is ')'
|
|
840
|
-
|
|
947
|
+
const index = context.content.indexOf(url);
|
|
841
948
|
if (index === -1) {
|
|
842
949
|
// It is not an error if we didn't find it.
|
|
843
950
|
return;
|
|
844
951
|
}
|
|
845
952
|
if (context.content[index - 1] !== "(" || context.content[index + url.length] !== ")") {
|
|
846
|
-
return
|
|
953
|
+
return `Whitespace before or after image url:
|
|
954
|
+
For images, don't include any space or newlines after '(' or before ')'.
|
|
955
|
+
Whitespace in image URLs causes translation difficulties.`;
|
|
847
956
|
}
|
|
848
957
|
}
|
|
849
958
|
}
|
|
@@ -859,32 +968,37 @@ var ImageWidget = Rule.makeRule({
|
|
|
859
968
|
name: "image-widget",
|
|
860
969
|
severity: Rule.Severity.WARNING,
|
|
861
970
|
selector: "widget",
|
|
862
|
-
lint: function
|
|
971
|
+
lint: function (state, content, nodes, match, context) {
|
|
863
972
|
// This rule only looks at image widgets
|
|
864
973
|
if (state.currentNode().widgetType !== "image") {
|
|
865
974
|
return;
|
|
866
975
|
}
|
|
867
976
|
|
|
868
977
|
// If it can't find a definition for the widget it does nothing
|
|
869
|
-
|
|
978
|
+
const widget = context && context.widgets && context.widgets[state.currentNode().id];
|
|
870
979
|
if (!widget) {
|
|
871
980
|
return;
|
|
872
981
|
}
|
|
873
982
|
|
|
874
983
|
// Make sure there is alt text
|
|
875
|
-
|
|
984
|
+
const alt = widget.options.alt;
|
|
876
985
|
if (!alt) {
|
|
877
|
-
return
|
|
986
|
+
return `Images should have alt text:
|
|
987
|
+
for accessibility, all images should have a text description.
|
|
988
|
+
Add a description in the "Alt Text" box of the image widget.`;
|
|
878
989
|
}
|
|
879
990
|
|
|
880
991
|
// Make sure the alt text it is not trivial
|
|
881
992
|
if (alt.trim().length < 8) {
|
|
882
|
-
return
|
|
993
|
+
return `Images should have alt text:
|
|
994
|
+
for accessibility, all images should have descriptive alt text.
|
|
995
|
+
This image's alt text is only ${alt.trim().length} characters long.`;
|
|
883
996
|
}
|
|
884
997
|
|
|
885
998
|
// Make sure there is no math in the caption
|
|
886
999
|
if (widget.options.caption && widget.options.caption.match(/[^\\]\$/)) {
|
|
887
|
-
return
|
|
1000
|
+
return `No math in image captions:
|
|
1001
|
+
Don't include math expressions in image captions.`;
|
|
888
1002
|
}
|
|
889
1003
|
}
|
|
890
1004
|
});
|
|
@@ -894,7 +1008,8 @@ var LinkClickHere = Rule.makeRule({
|
|
|
894
1008
|
severity: Rule.Severity.WARNING,
|
|
895
1009
|
selector: "link",
|
|
896
1010
|
pattern: /click here/i,
|
|
897
|
-
message:
|
|
1011
|
+
message: `Inappropriate link text:
|
|
1012
|
+
Do not use the words "click here" in links.`
|
|
898
1013
|
});
|
|
899
1014
|
|
|
900
1015
|
var LongParagraph = Rule.makeRule({
|
|
@@ -902,8 +1017,10 @@ var LongParagraph = Rule.makeRule({
|
|
|
902
1017
|
severity: Rule.Severity.GUIDELINE,
|
|
903
1018
|
selector: "paragraph",
|
|
904
1019
|
pattern: /^.{501,}/,
|
|
905
|
-
lint: function
|
|
906
|
-
return
|
|
1020
|
+
lint: function (state, content, nodes, match) {
|
|
1021
|
+
return `Paragraph too long:
|
|
1022
|
+
This paragraph is ${content.length} characters long.
|
|
1023
|
+
Shorten it to 500 characters or fewer.`;
|
|
907
1024
|
}
|
|
908
1025
|
});
|
|
909
1026
|
|
|
@@ -911,7 +1028,8 @@ var MathAdjacent = Rule.makeRule({
|
|
|
911
1028
|
name: "math-adjacent",
|
|
912
1029
|
severity: Rule.Severity.WARNING,
|
|
913
1030
|
selector: "blockMath+blockMath",
|
|
914
|
-
message:
|
|
1031
|
+
message: `Adjacent math blocks:
|
|
1032
|
+
combine the blocks between \\begin{align} and \\end{align}`
|
|
915
1033
|
});
|
|
916
1034
|
|
|
917
1035
|
var MathAlignExtraBreak = Rule.makeRule({
|
|
@@ -919,7 +1037,8 @@ var MathAlignExtraBreak = Rule.makeRule({
|
|
|
919
1037
|
severity: Rule.Severity.WARNING,
|
|
920
1038
|
selector: "blockMath",
|
|
921
1039
|
pattern: /(\\{2,})\s*\\end{align}/,
|
|
922
|
-
message:
|
|
1040
|
+
message: `Extra space at end of block:
|
|
1041
|
+
Don't end an align block with backslashes`
|
|
923
1042
|
});
|
|
924
1043
|
|
|
925
1044
|
var MathAlignLinebreaks = Rule.makeRule({
|
|
@@ -934,10 +1053,10 @@ var MathAlignLinebreaks = Rule.makeRule({
|
|
|
934
1053
|
// Note that this rule can't know where line breaks belong so
|
|
935
1054
|
// it can't tell whether backslashes are completely missing. It just
|
|
936
1055
|
// enforces that you don't have the wrong number of pairs of backslashes.
|
|
937
|
-
lint: function
|
|
938
|
-
|
|
1056
|
+
lint: function (state, content, nodes, match) {
|
|
1057
|
+
let text = match[0];
|
|
939
1058
|
while (text.length) {
|
|
940
|
-
|
|
1059
|
+
const index = text.indexOf("\\\\");
|
|
941
1060
|
if (index === -1) {
|
|
942
1061
|
// No more backslash pairs, so we found no lint
|
|
943
1062
|
return null;
|
|
@@ -947,7 +1066,7 @@ var MathAlignLinebreaks = Rule.makeRule({
|
|
|
947
1066
|
// Now we expect to find optional spaces, another pair of
|
|
948
1067
|
// backslashes, and more optional spaces not followed immediately
|
|
949
1068
|
// by another pair of backslashes.
|
|
950
|
-
|
|
1069
|
+
const nextpair = text.match(/^\s*\\\\\s*(?!\\\\)/);
|
|
951
1070
|
|
|
952
1071
|
// If that does not match then we either have too few or too
|
|
953
1072
|
// many pairs of backslashes.
|
|
@@ -976,7 +1095,8 @@ var MathFontSize = Rule.makeRule({
|
|
|
976
1095
|
severity: Rule.Severity.GUIDELINE,
|
|
977
1096
|
selector: "math, blockMath",
|
|
978
1097
|
pattern: /\\(tiny|Tiny|small|large|Large|LARGE|huge|Huge|scriptsize|normalsize)\s*{/,
|
|
979
|
-
message:
|
|
1098
|
+
message: `Math font size:
|
|
1099
|
+
Don't change the default font size with \\Large{} or similar commands`
|
|
980
1100
|
});
|
|
981
1101
|
|
|
982
1102
|
var MathFrac = Rule.makeRule({
|
|
@@ -992,7 +1112,8 @@ var MathNested = Rule.makeRule({
|
|
|
992
1112
|
severity: Rule.Severity.ERROR,
|
|
993
1113
|
selector: "math, blockMath",
|
|
994
1114
|
pattern: /\\text{[^$}]*\$[^$}]*\$[^}]*}/,
|
|
995
|
-
message:
|
|
1115
|
+
message: `Nested math:
|
|
1116
|
+
Don't nest math expressions inside \\text{} blocks`
|
|
996
1117
|
});
|
|
997
1118
|
|
|
998
1119
|
var MathStartsWithSpace = Rule.makeRule({
|
|
@@ -1000,7 +1121,9 @@ var MathStartsWithSpace = Rule.makeRule({
|
|
|
1000
1121
|
severity: Rule.Severity.GUIDELINE,
|
|
1001
1122
|
selector: "math, blockMath",
|
|
1002
1123
|
pattern: /^\s*(~|\\qquad|\\quad|\\,|\\;|\\:|\\ |\\!|\\enspace|\\phantom)/,
|
|
1003
|
-
message:
|
|
1124
|
+
message: `Math starts with space:
|
|
1125
|
+
math should not be indented. Do not begin math expressions with
|
|
1126
|
+
LaTeX space commands like ~, \\;, \\quad, or \\phantom`
|
|
1004
1127
|
});
|
|
1005
1128
|
|
|
1006
1129
|
var MathTextEmpty = Rule.makeRule({
|
|
@@ -1019,14 +1142,17 @@ var MathWithoutDollars = Rule.makeRule({
|
|
|
1019
1142
|
name: "math-without-dollars",
|
|
1020
1143
|
severity: Rule.Severity.GUIDELINE,
|
|
1021
1144
|
pattern: /\\\w+{[^}]*}|{|}/,
|
|
1022
|
-
message:
|
|
1145
|
+
message: `This looks like LaTeX:
|
|
1146
|
+
did you mean to put it inside dollar signs?`
|
|
1023
1147
|
});
|
|
1024
1148
|
|
|
1025
1149
|
var NestedLists = Rule.makeRule({
|
|
1026
1150
|
name: "nested-lists",
|
|
1027
1151
|
severity: Rule.Severity.WARNING,
|
|
1028
1152
|
selector: "list list",
|
|
1029
|
-
message:
|
|
1153
|
+
message: `Nested lists:
|
|
1154
|
+
nested lists are hard to read on mobile devices;
|
|
1155
|
+
do not use additional indentation.`
|
|
1030
1156
|
});
|
|
1031
1157
|
|
|
1032
1158
|
var Profanity = Rule.makeRule({
|
|
@@ -1041,13 +1167,15 @@ var TableMissingCells = Rule.makeRule({
|
|
|
1041
1167
|
name: "table-missing-cells",
|
|
1042
1168
|
severity: Rule.Severity.WARNING,
|
|
1043
1169
|
selector: "table",
|
|
1044
|
-
lint: function
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
for (
|
|
1170
|
+
lint: function (state, content, nodes, match) {
|
|
1171
|
+
const table = nodes[0];
|
|
1172
|
+
const headerLength = table.header.length;
|
|
1173
|
+
const rowLengths = table.cells.map(r => r.length);
|
|
1174
|
+
for (let r = 0; r < rowLengths.length; r++) {
|
|
1049
1175
|
if (rowLengths[r] !== headerLength) {
|
|
1050
|
-
return
|
|
1176
|
+
return `Table rows don't match header:
|
|
1177
|
+
The table header has ${headerLength} cells, but
|
|
1178
|
+
Row ${r + 1} has ${rowLengths[r]} cells.`;
|
|
1051
1179
|
}
|
|
1052
1180
|
}
|
|
1053
1181
|
}
|
|
@@ -1061,26 +1189,87 @@ var UnbalancedCodeDelimiters = Rule.makeRule({
|
|
|
1061
1189
|
name: "unbalanced-code-delimiters",
|
|
1062
1190
|
severity: Rule.Severity.ERROR,
|
|
1063
1191
|
pattern: /[`~]+/,
|
|
1064
|
-
message:
|
|
1192
|
+
message: `Unbalanced code delimiters:
|
|
1193
|
+
code blocks should begin and end with the same type and number of delimiters`
|
|
1065
1194
|
});
|
|
1066
1195
|
|
|
1067
1196
|
var UnescapedDollar = Rule.makeRule({
|
|
1068
1197
|
name: "unescaped-dollar",
|
|
1069
1198
|
severity: Rule.Severity.ERROR,
|
|
1070
1199
|
selector: "unescapedDollar",
|
|
1071
|
-
message:
|
|
1200
|
+
message: `Unescaped dollar sign:
|
|
1201
|
+
Dollar signs must appear in pairs or be escaped as \\$`
|
|
1072
1202
|
});
|
|
1073
1203
|
|
|
1074
1204
|
var WidgetInTable = Rule.makeRule({
|
|
1075
1205
|
name: "widget-in-table",
|
|
1076
1206
|
severity: Rule.Severity.BULK_WARNING,
|
|
1077
1207
|
selector: "table widget",
|
|
1078
|
-
message:
|
|
1208
|
+
message: `Widget in table:
|
|
1209
|
+
do not put widgets inside of tables.`
|
|
1079
1210
|
});
|
|
1080
1211
|
|
|
1081
1212
|
// TODO(davidflanagan):
|
|
1082
1213
|
var AllRules = [AbsoluteUrl, BlockquotedMath, BlockquotedWidget, DoubleSpacingAfterTerminal, ExtraContentSpacing, HeadingLevel1, HeadingLevelSkip, HeadingSentenceCase, HeadingTitleCase, ImageAltText, ImageInTable, LinkClickHere, LongParagraph, MathAdjacent, MathAlignExtraBreak, MathAlignLinebreaks, MathEmpty, MathFontSize, MathFrac, MathNested, MathStartsWithSpace, MathTextEmpty, NestedLists, TableMissingCells, UnescapedDollar, WidgetInTable, Profanity, MathWithoutDollars, UnbalancedCodeDelimiters, ImageSpacesAroundUrls, ImageWidget];
|
|
1083
1214
|
|
|
1215
|
+
/**
|
|
1216
|
+
* TreeTransformer is a class for traversing and transforming trees. Create a
|
|
1217
|
+
* TreeTransformer by passing the root node of the tree to the
|
|
1218
|
+
* constructor. Then traverse that tree by calling the traverse() method. The
|
|
1219
|
+
* argument to traverse() is a callback function that will be called once for
|
|
1220
|
+
* each node in the tree. This is a post-order depth-first traversal: the
|
|
1221
|
+
* callback is not called on the a way down, but on the way back up. That is,
|
|
1222
|
+
* the children of a node are traversed before the node itself is.
|
|
1223
|
+
*
|
|
1224
|
+
* The traversal callback function is passed three arguments, the node being
|
|
1225
|
+
* traversed, a TraversalState object, and the concatentated text content of
|
|
1226
|
+
* the node and all of its descendants. The TraversalState object is the most
|
|
1227
|
+
* most interesting argument: it has methods for querying the ancestors and
|
|
1228
|
+
* siblings of the node, and for deleting or replacing the node. These
|
|
1229
|
+
* transformation methods are why this class is a tree transformer and not
|
|
1230
|
+
* just a tree traverser.
|
|
1231
|
+
*
|
|
1232
|
+
* A typical tree traversal looks like this:
|
|
1233
|
+
*
|
|
1234
|
+
* new TreeTransformer(root).traverse((node, state, content) => {
|
|
1235
|
+
* let parent = state.parent();
|
|
1236
|
+
* let previous = state.previousSibling();
|
|
1237
|
+
* // etc.
|
|
1238
|
+
* });
|
|
1239
|
+
*
|
|
1240
|
+
* The traverse() method descends through nodes and arrays of nodes and calls
|
|
1241
|
+
* the traverse callback on each node on the way back up to the root of the
|
|
1242
|
+
* tree. (Note that it only calls the callback on the nodes themselves, not
|
|
1243
|
+
* any arrays that contain nodes.) A node is loosely defined as any object
|
|
1244
|
+
* with a string-valued `type` property. Objects that do not have a type
|
|
1245
|
+
* property are assumed to not be part of the tree and are not traversed. When
|
|
1246
|
+
* traversing an array, all elements of the array are examined, and any that
|
|
1247
|
+
* are nodes or arrays are recursively traversed. When traversing a node, all
|
|
1248
|
+
* properties of the object are examined and any node or array values are
|
|
1249
|
+
* recursively traversed. In typical parse trees, the children of a node are
|
|
1250
|
+
* in a `children` or `content` array, but this class is designed to handle
|
|
1251
|
+
* more general trees. The Perseus markdown parser, for example, produces
|
|
1252
|
+
* nodes of type "table" that have children in the `header` and `cells`
|
|
1253
|
+
* properties.
|
|
1254
|
+
*
|
|
1255
|
+
* CAUTION: the traverse() method does not make any attempt to detect
|
|
1256
|
+
* cycles. If you call it on a cyclic graph instead of a tree, it will cause
|
|
1257
|
+
* infinite recursion (or, more likely, a stack overflow).
|
|
1258
|
+
*
|
|
1259
|
+
* TODO(davidflanagan): it probably wouldn't be hard to detect cycles: when
|
|
1260
|
+
* pushing a new node onto the containers stack we could just check that it
|
|
1261
|
+
* isn't already there.
|
|
1262
|
+
*
|
|
1263
|
+
* If a node has a text-valued `content` property, it is taken to be the
|
|
1264
|
+
* plain-text content of the node. The traverse() method concatenates these
|
|
1265
|
+
* content strings and passes them to the traversal callback for each
|
|
1266
|
+
* node. This means that the callback has access the full text content of its
|
|
1267
|
+
* node and all of the nodes descendants.
|
|
1268
|
+
*
|
|
1269
|
+
* See the TraversalState class for more information on what information and
|
|
1270
|
+
* methods are available to the traversal callback.
|
|
1271
|
+
**/
|
|
1272
|
+
|
|
1084
1273
|
// TreeNode is the type of a node in a parse tree. The only real requirement is
|
|
1085
1274
|
// that every node has a string-valued `type` property
|
|
1086
1275
|
|
|
@@ -1089,7 +1278,7 @@ var AllRules = [AbsoluteUrl, BlockquotedMath, BlockquotedWidget, DoubleSpacingAf
|
|
|
1089
1278
|
class TreeTransformer {
|
|
1090
1279
|
// To create a tree transformer, just pass the root node of the tree
|
|
1091
1280
|
constructor(root) {
|
|
1092
|
-
|
|
1281
|
+
this.root = void 0;
|
|
1093
1282
|
this.root = root;
|
|
1094
1283
|
}
|
|
1095
1284
|
|
|
@@ -1124,25 +1313,23 @@ class TreeTransformer {
|
|
|
1124
1313
|
// details. Note that this method uses the TraversalState object to store
|
|
1125
1314
|
// information about the structure of the tree.
|
|
1126
1315
|
_traverse(n, state, f) {
|
|
1127
|
-
|
|
1316
|
+
let content = "";
|
|
1128
1317
|
if (TreeTransformer.isNode(n)) {
|
|
1129
1318
|
// If we were called on a node object, then we handle it
|
|
1130
1319
|
// this way.
|
|
1131
|
-
|
|
1320
|
+
const node = n; // safe cast; we just tested
|
|
1132
1321
|
|
|
1133
1322
|
// Put the node on the stack before recursing on its children
|
|
1134
|
-
state._containers.push(
|
|
1135
|
-
state._ancestors.push(
|
|
1323
|
+
state._containers.push(node);
|
|
1324
|
+
state._ancestors.push(node);
|
|
1136
1325
|
|
|
1137
1326
|
// Record the node's text content if it has any.
|
|
1138
1327
|
// Usually this is for nodes with a type property of "text",
|
|
1139
1328
|
// but other nodes types like "math" may also have content.
|
|
1140
|
-
//
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
// @ts-expect-error [FEI-5003] - TS2339 - Property 'content' does not exist on type 'TreeNode'.
|
|
1145
|
-
content = _node.content;
|
|
1329
|
+
// @ts-expect-error - TS2339 - Property 'content' does not exist on type 'TreeNode'.
|
|
1330
|
+
if (typeof node.content === "string") {
|
|
1331
|
+
// @ts-expect-error - TS2339 - Property 'content' does not exist on type 'TreeNode'.
|
|
1332
|
+
content = node.content;
|
|
1146
1333
|
}
|
|
1147
1334
|
|
|
1148
1335
|
// Recurse on the node. If there was content above, then there
|
|
@@ -1154,7 +1341,7 @@ class TreeTransformer {
|
|
|
1154
1341
|
// put a switch statement here to dispatch on the node type
|
|
1155
1342
|
// property with specific recursion steps for each known type of
|
|
1156
1343
|
// node.
|
|
1157
|
-
|
|
1344
|
+
const keys = Object.keys(node);
|
|
1158
1345
|
keys.forEach(key => {
|
|
1159
1346
|
// Never recurse on the type property
|
|
1160
1347
|
if (key === "type") {
|
|
@@ -1168,7 +1355,7 @@ class TreeTransformer {
|
|
|
1168
1355
|
// content to the content for this node. Also note that we
|
|
1169
1356
|
// push the name of the property we're recursing over onto a
|
|
1170
1357
|
// TraversalState stack.
|
|
1171
|
-
|
|
1358
|
+
const value = node[key];
|
|
1172
1359
|
if (value && typeof value === "object") {
|
|
1173
1360
|
state._indexes.push(key);
|
|
1174
1361
|
content += this._traverse(value, state, f);
|
|
@@ -1184,11 +1371,11 @@ class TreeTransformer {
|
|
|
1184
1371
|
// that this is post-order traversal. We call the callback on the
|
|
1185
1372
|
// way back up the tree, not on the way down. That way we already
|
|
1186
1373
|
// know all the content contained within the node.
|
|
1187
|
-
f(
|
|
1374
|
+
f(node, state, content);
|
|
1188
1375
|
} else if (Array.isArray(n)) {
|
|
1189
1376
|
// If we were called on an array instead of a node, then
|
|
1190
1377
|
// this is the code we use to recurse.
|
|
1191
|
-
|
|
1378
|
+
const nodes = n;
|
|
1192
1379
|
|
|
1193
1380
|
// Push the array onto the stack. This will allow the
|
|
1194
1381
|
// TraversalState object to locate siblings of this node.
|
|
@@ -1202,11 +1389,11 @@ class TreeTransformer {
|
|
|
1202
1389
|
// are careful here to test the array length on each iteration and
|
|
1203
1390
|
// to reset the index when we pop the stack. Also note that we
|
|
1204
1391
|
// concatentate the text content of the children.
|
|
1205
|
-
|
|
1392
|
+
let index = 0;
|
|
1206
1393
|
while (index < nodes.length) {
|
|
1207
1394
|
state._indexes.push(index);
|
|
1208
1395
|
content += this._traverse(nodes[index], state, f);
|
|
1209
|
-
// Casting to convince
|
|
1396
|
+
// Casting to convince TypeScript that this is a number
|
|
1210
1397
|
index = state._indexes.pop() + 1;
|
|
1211
1398
|
}
|
|
1212
1399
|
|
|
@@ -1233,7 +1420,7 @@ class TreeTransformer {
|
|
|
1233
1420
|
* for that traversal, and the instance is passed to the traversal callback
|
|
1234
1421
|
* function for each node that is traversed. This class is not intended to be
|
|
1235
1422
|
* instantiated directly, but is exported so that its type can be used for
|
|
1236
|
-
*
|
|
1423
|
+
* type annotaions.
|
|
1237
1424
|
**/
|
|
1238
1425
|
class TraversalState {
|
|
1239
1426
|
// The root node of the tree being traversed
|
|
@@ -1242,16 +1429,16 @@ class TraversalState {
|
|
|
1242
1429
|
// below instead of using these properties directly. Note that the
|
|
1243
1430
|
// _containers and _indexes stacks can have two different types of
|
|
1244
1431
|
// elements, depending on whether we just recursed on an array or on a
|
|
1245
|
-
// node. This is hard for
|
|
1246
|
-
//
|
|
1432
|
+
// node. This is hard for TypeScript to deal with, so you'll see a number of
|
|
1433
|
+
// type casts through the any type when working with these two properties.
|
|
1247
1434
|
|
|
1248
1435
|
// The constructor just stores the root node and creates empty stacks.
|
|
1249
1436
|
constructor(root) {
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1437
|
+
this.root = void 0;
|
|
1438
|
+
this._currentNode = void 0;
|
|
1439
|
+
this._containers = void 0;
|
|
1440
|
+
this._indexes = void 0;
|
|
1441
|
+
this._ancestors = void 0;
|
|
1255
1442
|
this.root = root;
|
|
1256
1443
|
|
|
1257
1444
|
// When the callback is called, this property will hold the
|
|
@@ -1309,7 +1496,7 @@ class TraversalState {
|
|
|
1309
1496
|
* Return the next sibling of this node, if it has one, or null otherwise.
|
|
1310
1497
|
*/
|
|
1311
1498
|
nextSibling() {
|
|
1312
|
-
|
|
1499
|
+
const siblings = this._containers.top();
|
|
1313
1500
|
|
|
1314
1501
|
// If we're at the root of the tree or if the parent is an
|
|
1315
1502
|
// object instead of an array, then there are no siblings.
|
|
@@ -1318,7 +1505,7 @@ class TraversalState {
|
|
|
1318
1505
|
}
|
|
1319
1506
|
|
|
1320
1507
|
// The top index is a number because the top container is an array
|
|
1321
|
-
|
|
1508
|
+
const index = this._indexes.top();
|
|
1322
1509
|
if (siblings.length > index + 1) {
|
|
1323
1510
|
return siblings[index + 1];
|
|
1324
1511
|
}
|
|
@@ -1330,7 +1517,7 @@ class TraversalState {
|
|
|
1330
1517
|
* otherwise.
|
|
1331
1518
|
*/
|
|
1332
1519
|
previousSibling() {
|
|
1333
|
-
|
|
1520
|
+
const siblings = this._containers.top();
|
|
1334
1521
|
|
|
1335
1522
|
// If we're at the root of the tree or if the parent is an
|
|
1336
1523
|
// object instead of an array, then there are no siblings.
|
|
@@ -1339,7 +1526,7 @@ class TraversalState {
|
|
|
1339
1526
|
}
|
|
1340
1527
|
|
|
1341
1528
|
// The top index is a number because the top container is an array
|
|
1342
|
-
|
|
1529
|
+
const index = this._indexes.top();
|
|
1343
1530
|
if (index > 0) {
|
|
1344
1531
|
return siblings[index - 1];
|
|
1345
1532
|
}
|
|
@@ -1352,10 +1539,10 @@ class TraversalState {
|
|
|
1352
1539
|
* tree and concatenate adjacent text nodes into a single node.
|
|
1353
1540
|
*/
|
|
1354
1541
|
removeNextSibling() {
|
|
1355
|
-
|
|
1542
|
+
const siblings = this._containers.top();
|
|
1356
1543
|
if (siblings && Array.isArray(siblings)) {
|
|
1357
1544
|
// top index is a number because top container is an array
|
|
1358
|
-
|
|
1545
|
+
const index = this._indexes.top();
|
|
1359
1546
|
if (siblings.length > index + 1) {
|
|
1360
1547
|
return siblings.splice(index + 1, 1)[0];
|
|
1361
1548
|
}
|
|
@@ -1374,21 +1561,18 @@ class TraversalState {
|
|
|
1374
1561
|
* This method throws an error if you attempt to replace the root node of
|
|
1375
1562
|
* the tree.
|
|
1376
1563
|
*/
|
|
1377
|
-
replace() {
|
|
1378
|
-
|
|
1564
|
+
replace(...replacements) {
|
|
1565
|
+
const parent = this._containers.top();
|
|
1379
1566
|
if (!parent) {
|
|
1380
1567
|
throw new PerseusError("Can't replace the root of the tree", Errors.Internal);
|
|
1381
1568
|
}
|
|
1382
1569
|
|
|
1383
1570
|
// The top of the container stack is either an array or an object
|
|
1384
1571
|
// and the top of the indexes stack is a corresponding array index
|
|
1385
|
-
// or object property. This is hard for
|
|
1572
|
+
// or object property. This is hard for TypeScript, so we have to do some
|
|
1386
1573
|
// unsafe casting and be careful when we use which cast version
|
|
1387
|
-
for (var _len = arguments.length, replacements = new Array(_len), _key = 0; _key < _len; _key++) {
|
|
1388
|
-
replacements[_key] = arguments[_key];
|
|
1389
|
-
}
|
|
1390
1574
|
if (Array.isArray(parent)) {
|
|
1391
|
-
|
|
1575
|
+
const index = this._indexes.top();
|
|
1392
1576
|
// For an array parent we just splice the new nodes in
|
|
1393
1577
|
parent.splice(index, 1, ...replacements);
|
|
1394
1578
|
// Adjust the index to account for the changed array length.
|
|
@@ -1396,7 +1580,7 @@ class TraversalState {
|
|
|
1396
1580
|
this._indexes.pop();
|
|
1397
1581
|
this._indexes.push(index + replacements.length - 1);
|
|
1398
1582
|
} else {
|
|
1399
|
-
|
|
1583
|
+
const property = this._indexes.top();
|
|
1400
1584
|
// For an object parent we care how many new nodes there are
|
|
1401
1585
|
if (replacements.length === 0) {
|
|
1402
1586
|
// Deletion
|
|
@@ -1435,8 +1619,8 @@ class TraversalState {
|
|
|
1435
1619
|
this._currentNode = this.previousSibling();
|
|
1436
1620
|
// Since we know that we have a previous sibling, we know that
|
|
1437
1621
|
// the value on top of the stack is a number, but we have to do
|
|
1438
|
-
// this unsafe cast because
|
|
1439
|
-
|
|
1622
|
+
// this unsafe cast because TypeScript doesn't know that.
|
|
1623
|
+
const index = this._indexes.pop();
|
|
1440
1624
|
this._indexes.push(index - 1);
|
|
1441
1625
|
}
|
|
1442
1626
|
|
|
@@ -1468,10 +1652,7 @@ class TraversalState {
|
|
|
1468
1652
|
// and more as needed until we restore the invariant that
|
|
1469
1653
|
// this._containers.top()[this.indexes.top()] === this._currentNode
|
|
1470
1654
|
//
|
|
1471
|
-
while (this._containers.size() &&
|
|
1472
|
-
// This is safe, but easier to just disable flow than do casts
|
|
1473
|
-
// $FlowFixMe[incompatible-use]
|
|
1474
|
-
this._containers.top()[this._indexes.top()] !== this._currentNode) {
|
|
1655
|
+
while (this._containers.size() && this._containers.top()[this._indexes.top()] !== this._currentNode) {
|
|
1475
1656
|
this._containers.pop();
|
|
1476
1657
|
this._indexes.pop();
|
|
1477
1658
|
}
|
|
@@ -1483,7 +1664,7 @@ class TraversalState {
|
|
|
1483
1664
|
* goToParent() and goToPreviousSibling().
|
|
1484
1665
|
*/
|
|
1485
1666
|
clone() {
|
|
1486
|
-
|
|
1667
|
+
const clone = new TraversalState(this.root);
|
|
1487
1668
|
clone._currentNode = this._currentNode;
|
|
1488
1669
|
clone._containers = this._containers.clone();
|
|
1489
1670
|
clone._indexes = this._indexes.clone();
|
|
@@ -1511,7 +1692,7 @@ class TraversalState {
|
|
|
1511
1692
|
*/
|
|
1512
1693
|
class Stack {
|
|
1513
1694
|
constructor(array) {
|
|
1514
|
-
|
|
1695
|
+
this.stack = void 0;
|
|
1515
1696
|
this.stack = array ? array.slice(0) : [];
|
|
1516
1697
|
}
|
|
1517
1698
|
|
|
@@ -1522,7 +1703,7 @@ class Stack {
|
|
|
1522
1703
|
|
|
1523
1704
|
/** Pop a value off of the stack. */
|
|
1524
1705
|
pop() {
|
|
1525
|
-
// @ts-expect-error
|
|
1706
|
+
// @ts-expect-error - TS2322 - Type 'T | undefined' is not assignable to type 'T'.
|
|
1526
1707
|
return this.stack.pop();
|
|
1527
1708
|
}
|
|
1528
1709
|
|
|
@@ -1559,7 +1740,7 @@ class Stack {
|
|
|
1559
1740
|
if (!that || !that.stack || that.stack.length !== this.stack.length) {
|
|
1560
1741
|
return false;
|
|
1561
1742
|
}
|
|
1562
|
-
for (
|
|
1743
|
+
for (let i = 0; i < this.stack.length; i++) {
|
|
1563
1744
|
if (this.stack[i] !== that.stack[i]) {
|
|
1564
1745
|
return false;
|
|
1565
1746
|
}
|
|
@@ -1569,20 +1750,20 @@ class Stack {
|
|
|
1569
1750
|
}
|
|
1570
1751
|
|
|
1571
1752
|
// Define the shape of the linter context object that is passed through the
|
|
1572
|
-
|
|
1753
|
+
const linterContextProps = PropTypes.shape({
|
|
1573
1754
|
contentType: PropTypes.string,
|
|
1574
1755
|
highlightLint: PropTypes.bool,
|
|
1575
1756
|
paths: PropTypes.arrayOf(PropTypes.string),
|
|
1576
1757
|
stack: PropTypes.arrayOf(PropTypes.string)
|
|
1577
1758
|
});
|
|
1578
|
-
|
|
1759
|
+
const linterContextDefault = {
|
|
1579
1760
|
contentType: "",
|
|
1580
1761
|
highlightLint: false,
|
|
1581
1762
|
paths: [],
|
|
1582
1763
|
stack: []
|
|
1583
1764
|
};
|
|
1584
1765
|
|
|
1585
|
-
|
|
1766
|
+
const allLintRules = AllRules.filter(r => r.severity < Rule.Severity.BULK_WARNING);
|
|
1586
1767
|
|
|
1587
1768
|
//
|
|
1588
1769
|
// Run the Perseus linter over the specified markdown parse tree,
|
|
@@ -1608,18 +1789,17 @@ var allLintRules = AllRules.filter(r => r.severity < Rule.Severity.BULK_WARNING)
|
|
|
1608
1789
|
// in that case). This would allow the one function to be used for both
|
|
1609
1790
|
// online linting and batch linting.
|
|
1610
1791
|
//
|
|
1611
|
-
function runLinter(tree, context, highlight) {
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
var tt = new TreeTransformer(tree);
|
|
1792
|
+
function runLinter(tree, context, highlight, rules = allLintRules) {
|
|
1793
|
+
const warnings = [];
|
|
1794
|
+
const tt = new TreeTransformer(tree);
|
|
1615
1795
|
|
|
1616
1796
|
// The markdown parser often outputs adjacent text nodes. We
|
|
1617
1797
|
// coalesce them before linting for efficiency and accuracy.
|
|
1618
1798
|
tt.traverse((node, state, content) => {
|
|
1619
1799
|
if (TreeTransformer.isTextNode(node)) {
|
|
1620
|
-
|
|
1800
|
+
let next = state.nextSibling();
|
|
1621
1801
|
while (TreeTransformer.isTextNode(next)) {
|
|
1622
|
-
// @ts-expect-error
|
|
1802
|
+
// @ts-expect-error - TS2339 - Property 'content' does not exist on type 'TreeNode'. | TS2533 - Object is possibly 'null' or 'undefined'. | TS2339 - Property 'content' does not exist on type 'TreeNode'.
|
|
1623
1803
|
node.content += next.content;
|
|
1624
1804
|
state.removeNextSibling();
|
|
1625
1805
|
next = state.nextSibling();
|
|
@@ -1644,29 +1824,29 @@ function runLinter(tree, context, highlight) {
|
|
|
1644
1824
|
// issue too. But using JavaScript has its own downsides: there is
|
|
1645
1825
|
// risk that the linter JavaScript would interfere with
|
|
1646
1826
|
// widget-related Javascript.
|
|
1647
|
-
|
|
1648
|
-
|
|
1827
|
+
let tableWarnings = [];
|
|
1828
|
+
let insideTable = false;
|
|
1649
1829
|
|
|
1650
1830
|
// Traverse through the nodes of the parse tree. At each node, loop
|
|
1651
1831
|
// through the array of lint rules and check whether there is a
|
|
1652
1832
|
// lint violation at that node.
|
|
1653
1833
|
tt.traverse((node, state, content) => {
|
|
1654
|
-
|
|
1834
|
+
const nodeWarnings = [];
|
|
1655
1835
|
|
|
1656
1836
|
// If our rule is only designed to be tested against a particular
|
|
1657
1837
|
// content type and we're not in that content type, we don't need to
|
|
1658
1838
|
// consider that rule.
|
|
1659
|
-
|
|
1839
|
+
const applicableRules = rules.filter(r => r.applies(context));
|
|
1660
1840
|
|
|
1661
1841
|
// Generate a stack so we can identify our position in the tree in
|
|
1662
1842
|
// lint rules
|
|
1663
|
-
|
|
1843
|
+
const stack = [...context.stack];
|
|
1664
1844
|
stack.push(node.type);
|
|
1665
|
-
|
|
1845
|
+
const nodeContext = _extends({}, context, {
|
|
1666
1846
|
stack: stack.join(".")
|
|
1667
1847
|
});
|
|
1668
1848
|
applicableRules.forEach(rule => {
|
|
1669
|
-
|
|
1849
|
+
const warning = rule.check(node, state, content, nodeContext);
|
|
1670
1850
|
if (warning) {
|
|
1671
1851
|
// The start and end locations are relative to this
|
|
1672
1852
|
// particular node, and so are not generally very useful.
|
|
@@ -1720,7 +1900,7 @@ function runLinter(tree, context, highlight) {
|
|
|
1720
1900
|
// this node, then we need to save the warnings for display
|
|
1721
1901
|
// on the table itself
|
|
1722
1902
|
if (insideTable && nodeWarnings.length) {
|
|
1723
|
-
// @ts-expect-error
|
|
1903
|
+
// @ts-expect-error - TS2345 - Argument of type 'any' is not assignable to parameter of type 'never'.
|
|
1724
1904
|
tableWarnings.push(...nodeWarnings);
|
|
1725
1905
|
}
|
|
1726
1906
|
|
|
@@ -1747,7 +1927,7 @@ function runLinter(tree, context, highlight) {
|
|
|
1747
1927
|
// node under a new lint node and put the warnings there.
|
|
1748
1928
|
state.replace({
|
|
1749
1929
|
type: "lint",
|
|
1750
|
-
// @ts-expect-error
|
|
1930
|
+
// @ts-expect-error - TS2345 - Argument of type '{ type: string; content: TreeNode; message: string; ruleName: any; blockHighlight: any; insideTable: boolean; severity: any; }' is not assignable to parameter of type 'TreeNode'.
|
|
1751
1931
|
content: node,
|
|
1752
1932
|
message: nodeWarnings.map(w => w.message).join("\n\n"),
|
|
1753
1933
|
ruleName: nodeWarnings[0].rule,
|
|
@@ -1781,17 +1961,17 @@ function runLinter(tree, context, highlight) {
|
|
|
1781
1961
|
// single line, so keeping them combined in that case might
|
|
1782
1962
|
// be the best thing, anyway.
|
|
1783
1963
|
//
|
|
1784
|
-
// @ts-expect-error
|
|
1785
|
-
|
|
1786
|
-
|
|
1964
|
+
// @ts-expect-error - TS2339 - Property 'content' does not exist on type 'TreeNode'.
|
|
1965
|
+
const _content = node.content; // Text nodes have content
|
|
1966
|
+
const warning = nodeWarnings[0]; // There is only one warning.
|
|
1787
1967
|
// These are the lint boundaries within the content
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1968
|
+
const start = warning.start || 0;
|
|
1969
|
+
const end = warning.end || _content.length;
|
|
1970
|
+
const prefix = _content.substring(0, start);
|
|
1971
|
+
const lint = _content.substring(start, end);
|
|
1972
|
+
const suffix = _content.substring(end);
|
|
1793
1973
|
// TODO(FEI-5003): Give this a real type.
|
|
1794
|
-
|
|
1974
|
+
const replacements = []; // What we'll replace the node with
|
|
1795
1975
|
|
|
1796
1976
|
// The prefix text node, if there is one
|
|
1797
1977
|
if (prefix) {
|
|
@@ -1831,8 +2011,8 @@ function runLinter(tree, context, highlight) {
|
|
|
1831
2011
|
return warnings;
|
|
1832
2012
|
}
|
|
1833
2013
|
function pushContextStack(context, name) {
|
|
1834
|
-
|
|
1835
|
-
return
|
|
2014
|
+
const stack = context.stack || [];
|
|
2015
|
+
return _extends({}, context, {
|
|
1836
2016
|
stack: stack.concat(name)
|
|
1837
2017
|
});
|
|
1838
2018
|
}
|