@khanacademy/perseus-linter 0.3.8 → 0.3.10
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 +18 -0
- package/dist/es/index.js +41 -25
- package/dist/es/index.js.map +1 -1
- package/dist/index.js +284 -90
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
- package/tsconfig-build.tsbuildinfo +1 -1
package/dist/index.js
CHANGED
|
@@ -10,35 +10,7 @@ function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'defau
|
|
|
10
10
|
|
|
11
11
|
var PropTypes__default = /*#__PURE__*/_interopDefaultLegacy(PropTypes);
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
key = _toPropertyKey(key);
|
|
15
|
-
if (key in obj) {
|
|
16
|
-
Object.defineProperty(obj, key, {
|
|
17
|
-
value: value,
|
|
18
|
-
enumerable: true,
|
|
19
|
-
configurable: true,
|
|
20
|
-
writable: true
|
|
21
|
-
});
|
|
22
|
-
} else {
|
|
23
|
-
obj[key] = value;
|
|
24
|
-
}
|
|
25
|
-
return obj;
|
|
26
|
-
}
|
|
27
|
-
function _toPrimitive(input, hint) {
|
|
28
|
-
if (typeof input !== "object" || input === null) return input;
|
|
29
|
-
var prim = input[Symbol.toPrimitive];
|
|
30
|
-
if (prim !== undefined) {
|
|
31
|
-
var res = prim.call(input, hint || "default");
|
|
32
|
-
if (typeof res !== "object") return res;
|
|
33
|
-
throw new TypeError("@@toPrimitive must return a primitive value.");
|
|
34
|
-
}
|
|
35
|
-
return (hint === "string" ? String : Number)(input);
|
|
36
|
-
}
|
|
37
|
-
function _toPropertyKey(arg) {
|
|
38
|
-
var key = _toPrimitive(arg, "string");
|
|
39
|
-
return typeof key === "symbol" ? key : String(key);
|
|
40
|
-
}
|
|
41
|
-
|
|
13
|
+
/* eslint-disable no-useless-escape */
|
|
42
14
|
/**
|
|
43
15
|
* This is the base class for all Selector types. The key method that all
|
|
44
16
|
* selector subclasses must implement is match(). It takes a TraversalState
|
|
@@ -84,8 +56,6 @@ class Parser {
|
|
|
84
56
|
// Which token in the array we're looking at now
|
|
85
57
|
|
|
86
58
|
constructor(s) {
|
|
87
|
-
_defineProperty(this, "tokens", void 0);
|
|
88
|
-
_defineProperty(this, "tokenIndex", void 0);
|
|
89
59
|
// Normalize whitespace:
|
|
90
60
|
// - remove leading and trailing whitespace
|
|
91
61
|
// - replace runs of whitespace with single space characters
|
|
@@ -224,7 +194,6 @@ class Parser {
|
|
|
224
194
|
// are identifiers, integers, punctuation and spaces. Note that spaces
|
|
225
195
|
// tokens are only returned when they appear before an identifier or
|
|
226
196
|
// wildcard token and are otherwise omitted.
|
|
227
|
-
_defineProperty(Parser, "TOKENS", void 0);
|
|
228
197
|
Parser.TOKENS = /([a-zA-Z][\w-]*)|(\d+)|[^\s]|(\s(?=[a-zA-Z\*]))/g;
|
|
229
198
|
|
|
230
199
|
/**
|
|
@@ -245,7 +214,6 @@ class ParseError extends Error {
|
|
|
245
214
|
class SelectorList extends Selector {
|
|
246
215
|
constructor(selectors) {
|
|
247
216
|
super();
|
|
248
|
-
_defineProperty(this, "selectors", void 0);
|
|
249
217
|
this.selectors = selectors;
|
|
250
218
|
}
|
|
251
219
|
match(state) {
|
|
@@ -288,7 +256,6 @@ class AnyNode extends Selector {
|
|
|
288
256
|
class TypeSelector extends Selector {
|
|
289
257
|
constructor(type) {
|
|
290
258
|
super();
|
|
291
|
-
_defineProperty(this, "type", void 0);
|
|
292
259
|
this.type = type;
|
|
293
260
|
}
|
|
294
261
|
match(state) {
|
|
@@ -312,8 +279,6 @@ class TypeSelector extends Selector {
|
|
|
312
279
|
class SelectorCombinator extends Selector {
|
|
313
280
|
constructor(left, right) {
|
|
314
281
|
super();
|
|
315
|
-
_defineProperty(this, "left", void 0);
|
|
316
|
-
_defineProperty(this, "right", void 0);
|
|
317
282
|
this.left = left;
|
|
318
283
|
this.right = right;
|
|
319
284
|
}
|
|
@@ -431,6 +396,148 @@ class SiblingCombinator extends SelectorCombinator {
|
|
|
431
396
|
}
|
|
432
397
|
}
|
|
433
398
|
|
|
399
|
+
/**
|
|
400
|
+
* The Rule class represents a Perseus lint rule. A Rule instance has a check()
|
|
401
|
+
* method that takes the same (node, state, content) arguments that a
|
|
402
|
+
* TreeTransformer traversal callback function does. Call the check() method
|
|
403
|
+
* during a tree traversal to determine whether the current node of the tree
|
|
404
|
+
* violates the rule. If there is no violation, then check() returns
|
|
405
|
+
* null. Otherwise, it returns an object that includes the name of the rule,
|
|
406
|
+
* an error message, and the start and end positions within the node's content
|
|
407
|
+
* string of the lint.
|
|
408
|
+
*
|
|
409
|
+
* A Perseus lint rule consists of a name, a severity, a selector, a pattern
|
|
410
|
+
* (RegExp) and two functions. The check() method uses the selector, pattern,
|
|
411
|
+
* and functions as follows:
|
|
412
|
+
*
|
|
413
|
+
* - First, when determining which rules to apply to a particular piece of
|
|
414
|
+
* content, each rule can specify an optional function provided in the fifth
|
|
415
|
+
* parameter to evaluate whether or not we should be applying this rule.
|
|
416
|
+
* If the function returns false, we don't use the rule on this content.
|
|
417
|
+
*
|
|
418
|
+
* - Next, check() tests whether the node currently being traversed matches
|
|
419
|
+
* the selector. If it does not, then the rule does not apply at this node
|
|
420
|
+
* and there is no lint and check() returns null.
|
|
421
|
+
*
|
|
422
|
+
* - If the selector matched, then check() tests the text content of the node
|
|
423
|
+
* (and its children) against the pattern. If the pattern does not match,
|
|
424
|
+
* then there is no lint, and check() returns null.
|
|
425
|
+
*
|
|
426
|
+
* - If both the selector and pattern match, then check() calls the function
|
|
427
|
+
* passing the TraversalState object, the content string for the node, the
|
|
428
|
+
* array of nodes returned by the selector match, and the array of strings
|
|
429
|
+
* returned by the pattern match. This function can use these arguments to
|
|
430
|
+
* implement any kind of lint detection logic it wants. If it determines
|
|
431
|
+
* that there is no lint, then it should return null. Otherwise, it should
|
|
432
|
+
* return an error message as a string, or an object with `message`, `start`
|
|
433
|
+
* and `end` properties. The start and end properties are numbers that mark
|
|
434
|
+
* the beginning and end of the problematic content. Note that these numbers
|
|
435
|
+
* are relative to the content string passed to the traversal callback, not
|
|
436
|
+
* to the entire string that was used to generate the parse tree in the
|
|
437
|
+
* first place. TODO(davidflanagan): modify the simple-markdown library to
|
|
438
|
+
* have an option to add the text offset of each node to the parse
|
|
439
|
+
* tree. This will allows us to pinpoint lint errors within a long string
|
|
440
|
+
* of markdown text.
|
|
441
|
+
*
|
|
442
|
+
* - If the function returns null, then check() returns null. Otherwise,
|
|
443
|
+
* check() returns an object with `rule`, `message`, `start` and `end`
|
|
444
|
+
* properties. The value of the `rule` property is the name of the rule,
|
|
445
|
+
* which is useful for error reporting purposes.
|
|
446
|
+
*
|
|
447
|
+
* The name, severity, selector, pattern and function arguments to the Rule()
|
|
448
|
+
* constructor are optional, but you may not omit both the selector and the
|
|
449
|
+
* pattern. If you do not specify a selector, a default selector that matches
|
|
450
|
+
* any node of type "text" will be used. If you do not specify a pattern, then
|
|
451
|
+
* any node that matches the selector will be assumed to match the pattern as
|
|
452
|
+
* well. If you don't pass a function as the fourth argument to the Rule()
|
|
453
|
+
* constructor, then you must pass an error message string instead. If you do
|
|
454
|
+
* this, you'll get a default function that unconditionally returns an object
|
|
455
|
+
* that includes the error message and the start and end indexes of the
|
|
456
|
+
* portion of the content string that matched the pattern. If you don't pass a
|
|
457
|
+
* function in the fifth parameter, the rule will be applied in any context.
|
|
458
|
+
*
|
|
459
|
+
* One of the design goals of this Rule class is to allow simple lint rules to
|
|
460
|
+
* be described in JSON files without any JavaScript code. So in addition to
|
|
461
|
+
* the Rule() constructor, the class also defines a Rule.makeRule() factory
|
|
462
|
+
* method. This method takes a single object as its argument and expects the
|
|
463
|
+
* object to have four string properties. The `name` property is passed as the
|
|
464
|
+
* first argument to the Rule() construtctor. The optional `selector`
|
|
465
|
+
* property, if specified, is passed to Selector.parse() and the resulting
|
|
466
|
+
* Selector object is used as the second argument to Rule(). The optional
|
|
467
|
+
* `pattern` property is converted to a RegExp before being passed as the
|
|
468
|
+
* third argument to Rule(). (See Rule.makePattern() for details on the string
|
|
469
|
+
* to RegExp conversion). Finally, the `message` property specifies an error
|
|
470
|
+
* message that is passed as the final argument to Rule(). You can also use a
|
|
471
|
+
* real RegExp as the value of the `pattern` property or define a custom lint
|
|
472
|
+
* function on the `lint` property instead of setting the `message`
|
|
473
|
+
* property. Doing either of these things means that your rule description can
|
|
474
|
+
* no longer be saved in a JSON file, however.
|
|
475
|
+
*
|
|
476
|
+
* For example, here are two lint rules defined with Rule.makeRule():
|
|
477
|
+
*
|
|
478
|
+
* let nestedLists = Rule.makeRule({
|
|
479
|
+
* name: "nested-lists",
|
|
480
|
+
* selector: "list list",
|
|
481
|
+
* message: `Nested lists:
|
|
482
|
+
* nested lists are hard to read on mobile devices;
|
|
483
|
+
* do not use additional indentation.`,
|
|
484
|
+
* });
|
|
485
|
+
*
|
|
486
|
+
* let longParagraph = Rule.makeRule({
|
|
487
|
+
* name: "long-paragraph",
|
|
488
|
+
* selector: "paragraph",
|
|
489
|
+
* pattern: /^.{501,}/,
|
|
490
|
+
* lint: function(state, content, nodes, match) {
|
|
491
|
+
* return `Paragraph too long:
|
|
492
|
+
* This paragraph is ${content.length} characters long.
|
|
493
|
+
* Shorten it to 500 characters or fewer.`;
|
|
494
|
+
* },
|
|
495
|
+
* });
|
|
496
|
+
*
|
|
497
|
+
* Certain advanced lint rules need additional information about the content
|
|
498
|
+
* being linted in order to detect lint. For example, a rule to check for
|
|
499
|
+
* whitespace at the start and end of the URL for an image can't use the
|
|
500
|
+
* information in the node or content arguments because the markdown parser
|
|
501
|
+
* strips leading and trailing whitespace when parsing. (Nevertheless, these
|
|
502
|
+
* spaces have been a practical problem for our content translation process so
|
|
503
|
+
* in order to check for them, a lint rule needs access to the original
|
|
504
|
+
* unparsed source text. Similarly, there are various lint rules that check
|
|
505
|
+
* widget usage. For example, it is easy to write a lint rule to ensure that
|
|
506
|
+
* images have alt text for images encoded in markdown. But when images are
|
|
507
|
+
* added to our content via an image widget we also want to be able to check
|
|
508
|
+
* for alt text. In order to do this, the lint rule needs to be able to look
|
|
509
|
+
* widgets up by name in the widgets object associated with the parse tree.
|
|
510
|
+
*
|
|
511
|
+
* In order to support advanced linting rules like these, the check() method
|
|
512
|
+
* takes a context object as its optional fourth argument, and passes this
|
|
513
|
+
* object on to the lint function of each rule. Rules that require extra
|
|
514
|
+
* context should not assume that they will always get it, and should verify
|
|
515
|
+
* that the necessary context has been supplied before using it. Currently the
|
|
516
|
+
* "content" property of the context object is the unparsed source text if
|
|
517
|
+
* available, and the "widgets" property of the context object is the widget
|
|
518
|
+
* object associated with that content string in the JSON object that defines
|
|
519
|
+
* the Perseus article or exercise that is being linted.
|
|
520
|
+
*/
|
|
521
|
+
|
|
522
|
+
// This represents the type returned by String.match(). It is an
|
|
523
|
+
// array of strings, but also has index:number and input:string properties.
|
|
524
|
+
// TypeScript doesn't handle it well, so we punt and just use any.
|
|
525
|
+
// This is the return type of the check() method of a Rule object
|
|
526
|
+
// This is the return type of the lint detection function passed as the 4th
|
|
527
|
+
// argument to the Rule() constructor. It can return null or a string or an
|
|
528
|
+
// object containing a string and two numbers.
|
|
529
|
+
// prettier-ignore
|
|
530
|
+
// (prettier formats this in a way that ka-lint does not like)
|
|
531
|
+
// This is the type of the lint detection function that the Rule() constructor
|
|
532
|
+
// expects as its fourth argument. It is passed the TraversalState object and
|
|
533
|
+
// content string that were passed to check(), and is also passed the array of
|
|
534
|
+
// nodes returned by the selector match and the array of strings returned by
|
|
535
|
+
// the pattern match. It should return null if no lint is detected or an
|
|
536
|
+
// error message or an object contining an error message.
|
|
537
|
+
// An optional check to verify whether or not a particular rule should
|
|
538
|
+
// be checked by context. For example, some rules only apply in exercises,
|
|
539
|
+
// and should never be applied to articles. Defaults to true, so if we
|
|
540
|
+
// omit the applies function in a rule, it'll be tested everywhere.
|
|
434
541
|
/**
|
|
435
542
|
* A Rule object describes a Perseus lint rule. See the comment at the top of
|
|
436
543
|
* this file for detailed description.
|
|
@@ -448,13 +555,6 @@ class Rule {
|
|
|
448
555
|
// this constructor and its arguments
|
|
449
556
|
constructor(name, severity, selector, pattern, lint, applies) {
|
|
450
557
|
var _this = this;
|
|
451
|
-
_defineProperty(this, "name", void 0);
|
|
452
|
-
_defineProperty(this, "severity", void 0);
|
|
453
|
-
_defineProperty(this, "selector", void 0);
|
|
454
|
-
_defineProperty(this, "pattern", void 0);
|
|
455
|
-
_defineProperty(this, "lint", void 0);
|
|
456
|
-
_defineProperty(this, "applies", void 0);
|
|
457
|
-
_defineProperty(this, "message", void 0);
|
|
458
558
|
if (!selector && !pattern) {
|
|
459
559
|
throw new perseusError.PerseusError("Lint rules must have a selector or pattern", perseusError.Errors.InvalidInput, {
|
|
460
560
|
metadata: {
|
|
@@ -553,7 +653,9 @@ class Rule {
|
|
|
553
653
|
// a rule was failing.
|
|
554
654
|
return {
|
|
555
655
|
rule: "lint-rule-failure",
|
|
556
|
-
message:
|
|
656
|
+
message: `Exception in rule ${this.name}: ${e.message}
|
|
657
|
+
Stack trace:
|
|
658
|
+
${e.stack}`,
|
|
557
659
|
start: 0,
|
|
558
660
|
end: content.length
|
|
559
661
|
};
|
|
@@ -617,14 +719,13 @@ class Rule {
|
|
|
617
719
|
result.input = input;
|
|
618
720
|
return result;
|
|
619
721
|
}
|
|
722
|
+
static Severity = {
|
|
723
|
+
ERROR: 1,
|
|
724
|
+
WARNING: 2,
|
|
725
|
+
GUIDELINE: 3,
|
|
726
|
+
BULK_WARNING: 4
|
|
727
|
+
};
|
|
620
728
|
}
|
|
621
|
-
_defineProperty(Rule, "DEFAULT_SELECTOR", void 0);
|
|
622
|
-
_defineProperty(Rule, "Severity", {
|
|
623
|
-
ERROR: 1,
|
|
624
|
-
WARNING: 2,
|
|
625
|
-
GUIDELINE: 3,
|
|
626
|
-
BULK_WARNING: 4
|
|
627
|
-
});
|
|
628
729
|
Rule.DEFAULT_SELECTOR = Selector.parse("text");
|
|
629
730
|
|
|
630
731
|
/* eslint-disable no-useless-escape */
|
|
@@ -652,7 +753,10 @@ var AbsoluteUrl = Rule.makeRule({
|
|
|
652
753
|
const url = nodes[0].target;
|
|
653
754
|
const hostname = getHostname(url);
|
|
654
755
|
if (hostname === "khanacademy.org" || hostname.endsWith(".khanacademy.org")) {
|
|
655
|
-
return
|
|
756
|
+
return `Don't use absolute URLs:
|
|
757
|
+
When linking to KA content or images, omit the
|
|
758
|
+
https://www.khanacademy.org URL prefix.
|
|
759
|
+
Use a relative URL beginning with / instead.`;
|
|
656
760
|
}
|
|
657
761
|
}
|
|
658
762
|
});
|
|
@@ -661,14 +765,16 @@ var BlockquotedMath = Rule.makeRule({
|
|
|
661
765
|
name: "blockquoted-math",
|
|
662
766
|
severity: Rule.Severity.WARNING,
|
|
663
767
|
selector: "blockQuote math, blockQuote blockMath",
|
|
664
|
-
message:
|
|
768
|
+
message: `Blockquoted math:
|
|
769
|
+
math should not be indented.`
|
|
665
770
|
});
|
|
666
771
|
|
|
667
772
|
var BlockquotedWidget = Rule.makeRule({
|
|
668
773
|
name: "blockquoted-widget",
|
|
669
774
|
severity: Rule.Severity.WARNING,
|
|
670
775
|
selector: "blockQuote widget",
|
|
671
|
-
message:
|
|
776
|
+
message: `Blockquoted widget:
|
|
777
|
+
widgets should not be indented.`
|
|
672
778
|
});
|
|
673
779
|
|
|
674
780
|
/* eslint-disable no-useless-escape */
|
|
@@ -677,7 +783,8 @@ var DoubleSpacingAfterTerminal = Rule.makeRule({
|
|
|
677
783
|
severity: Rule.Severity.BULK_WARNING,
|
|
678
784
|
selector: "paragraph",
|
|
679
785
|
pattern: /[.!\?] {2}/i,
|
|
680
|
-
message:
|
|
786
|
+
message: `Use a single space after a sentence-ending period, or
|
|
787
|
+
any other kind of terminal punctuation.`
|
|
681
788
|
});
|
|
682
789
|
|
|
683
790
|
var ExtraContentSpacing = Rule.makeRule({
|
|
@@ -687,7 +794,7 @@ var ExtraContentSpacing = Rule.makeRule({
|
|
|
687
794
|
applies: function (context) {
|
|
688
795
|
return context.contentType === "article";
|
|
689
796
|
},
|
|
690
|
-
message:
|
|
797
|
+
message: `No extra whitespace at the end of content blocks.`
|
|
691
798
|
});
|
|
692
799
|
|
|
693
800
|
var HeadingLevel1 = Rule.makeRule({
|
|
@@ -696,7 +803,8 @@ var HeadingLevel1 = Rule.makeRule({
|
|
|
696
803
|
selector: "heading",
|
|
697
804
|
lint: function (state, content, nodes, match) {
|
|
698
805
|
if (nodes[0].level === 1) {
|
|
699
|
-
return
|
|
806
|
+
return `Don't use level-1 headings:
|
|
807
|
+
Begin headings with two or more # characters.`;
|
|
700
808
|
}
|
|
701
809
|
}
|
|
702
810
|
});
|
|
@@ -712,7 +820,9 @@ var HeadingLevelSkip = Rule.makeRule({
|
|
|
712
820
|
// or one more than the previous heading. But going up
|
|
713
821
|
// by 2 or more levels is not right
|
|
714
822
|
if (currentHeading.level > previousHeading.level + 1) {
|
|
715
|
-
return
|
|
823
|
+
return `Skipped heading level:
|
|
824
|
+
this heading is level ${currentHeading.level} but
|
|
825
|
+
the previous heading was level ${previousHeading.level}`;
|
|
716
826
|
}
|
|
717
827
|
}
|
|
718
828
|
});
|
|
@@ -723,7 +833,8 @@ var HeadingSentenceCase = Rule.makeRule({
|
|
|
723
833
|
selector: "heading",
|
|
724
834
|
pattern: /^\W*[a-z]/,
|
|
725
835
|
// first letter is lowercase
|
|
726
|
-
message:
|
|
836
|
+
message: `First letter is lowercase:
|
|
837
|
+
the first letter of a heading should be capitalized.`
|
|
727
838
|
});
|
|
728
839
|
|
|
729
840
|
// These are 3-letter and longer words that we would not expect to be
|
|
@@ -783,7 +894,9 @@ var HeadingTitleCase = Rule.makeRule({
|
|
|
783
894
|
// If there are at least 3 remaining words and all
|
|
784
895
|
// are capitalized, then the heading is in title case.
|
|
785
896
|
if (words.length >= 3 && words.every(w => isCapitalized(w))) {
|
|
786
|
-
return
|
|
897
|
+
return `Title-case heading:
|
|
898
|
+
This heading appears to be in title-case, but should be sentence-case.
|
|
899
|
+
Only capitalize the first letter and proper nouns.`;
|
|
787
900
|
}
|
|
788
901
|
}
|
|
789
902
|
});
|
|
@@ -795,10 +908,14 @@ var ImageAltText = Rule.makeRule({
|
|
|
795
908
|
lint: function (state, content, nodes, match) {
|
|
796
909
|
const image = nodes[0];
|
|
797
910
|
if (!image.alt || !image.alt.trim()) {
|
|
798
|
-
return
|
|
911
|
+
return `Images should have alt text:
|
|
912
|
+
for accessibility, all images should have alt text.
|
|
913
|
+
Specify alt text inside square brackets after the !.`;
|
|
799
914
|
}
|
|
800
915
|
if (image.alt.length < 8) {
|
|
801
|
-
return
|
|
916
|
+
return `Images should have alt text:
|
|
917
|
+
for accessibility, all images should have descriptive alt text.
|
|
918
|
+
This image's alt text is only ${image.alt.length} characters long.`;
|
|
802
919
|
}
|
|
803
920
|
}
|
|
804
921
|
});
|
|
@@ -807,7 +924,8 @@ var ImageInTable = Rule.makeRule({
|
|
|
807
924
|
name: "image-in-table",
|
|
808
925
|
severity: Rule.Severity.BULK_WARNING,
|
|
809
926
|
selector: "table image",
|
|
810
|
-
message:
|
|
927
|
+
message: `Image in table:
|
|
928
|
+
do not put images inside of tables.`
|
|
811
929
|
});
|
|
812
930
|
|
|
813
931
|
var ImageSpacesAroundUrls = Rule.makeRule({
|
|
@@ -831,7 +949,9 @@ var ImageSpacesAroundUrls = Rule.makeRule({
|
|
|
831
949
|
return;
|
|
832
950
|
}
|
|
833
951
|
if (context.content[index - 1] !== "(" || context.content[index + url.length] !== ")") {
|
|
834
|
-
return
|
|
952
|
+
return `Whitespace before or after image url:
|
|
953
|
+
For images, don't include any space or newlines after '(' or before ')'.
|
|
954
|
+
Whitespace in image URLs causes translation difficulties.`;
|
|
835
955
|
}
|
|
836
956
|
}
|
|
837
957
|
}
|
|
@@ -862,17 +982,22 @@ var ImageWidget = Rule.makeRule({
|
|
|
862
982
|
// Make sure there is alt text
|
|
863
983
|
const alt = widget.options.alt;
|
|
864
984
|
if (!alt) {
|
|
865
|
-
return
|
|
985
|
+
return `Images should have alt text:
|
|
986
|
+
for accessibility, all images should have a text description.
|
|
987
|
+
Add a description in the "Alt Text" box of the image widget.`;
|
|
866
988
|
}
|
|
867
989
|
|
|
868
990
|
// Make sure the alt text it is not trivial
|
|
869
991
|
if (alt.trim().length < 8) {
|
|
870
|
-
return
|
|
992
|
+
return `Images should have alt text:
|
|
993
|
+
for accessibility, all images should have descriptive alt text.
|
|
994
|
+
This image's alt text is only ${alt.trim().length} characters long.`;
|
|
871
995
|
}
|
|
872
996
|
|
|
873
997
|
// Make sure there is no math in the caption
|
|
874
998
|
if (widget.options.caption && widget.options.caption.match(/[^\\]\$/)) {
|
|
875
|
-
return
|
|
999
|
+
return `No math in image captions:
|
|
1000
|
+
Don't include math expressions in image captions.`;
|
|
876
1001
|
}
|
|
877
1002
|
}
|
|
878
1003
|
});
|
|
@@ -882,7 +1007,8 @@ var LinkClickHere = Rule.makeRule({
|
|
|
882
1007
|
severity: Rule.Severity.WARNING,
|
|
883
1008
|
selector: "link",
|
|
884
1009
|
pattern: /click here/i,
|
|
885
|
-
message:
|
|
1010
|
+
message: `Inappropriate link text:
|
|
1011
|
+
Do not use the words "click here" in links.`
|
|
886
1012
|
});
|
|
887
1013
|
|
|
888
1014
|
var LongParagraph = Rule.makeRule({
|
|
@@ -891,7 +1017,9 @@ var LongParagraph = Rule.makeRule({
|
|
|
891
1017
|
selector: "paragraph",
|
|
892
1018
|
pattern: /^.{501,}/,
|
|
893
1019
|
lint: function (state, content, nodes, match) {
|
|
894
|
-
return
|
|
1020
|
+
return `Paragraph too long:
|
|
1021
|
+
This paragraph is ${content.length} characters long.
|
|
1022
|
+
Shorten it to 500 characters or fewer.`;
|
|
895
1023
|
}
|
|
896
1024
|
});
|
|
897
1025
|
|
|
@@ -899,7 +1027,8 @@ var MathAdjacent = Rule.makeRule({
|
|
|
899
1027
|
name: "math-adjacent",
|
|
900
1028
|
severity: Rule.Severity.WARNING,
|
|
901
1029
|
selector: "blockMath+blockMath",
|
|
902
|
-
message:
|
|
1030
|
+
message: `Adjacent math blocks:
|
|
1031
|
+
combine the blocks between \\begin{align} and \\end{align}`
|
|
903
1032
|
});
|
|
904
1033
|
|
|
905
1034
|
var MathAlignExtraBreak = Rule.makeRule({
|
|
@@ -907,7 +1036,8 @@ var MathAlignExtraBreak = Rule.makeRule({
|
|
|
907
1036
|
severity: Rule.Severity.WARNING,
|
|
908
1037
|
selector: "blockMath",
|
|
909
1038
|
pattern: /(\\{2,})\s*\\end{align}/,
|
|
910
|
-
message:
|
|
1039
|
+
message: `Extra space at end of block:
|
|
1040
|
+
Don't end an align block with backslashes`
|
|
911
1041
|
});
|
|
912
1042
|
|
|
913
1043
|
var MathAlignLinebreaks = Rule.makeRule({
|
|
@@ -964,7 +1094,8 @@ var MathFontSize = Rule.makeRule({
|
|
|
964
1094
|
severity: Rule.Severity.GUIDELINE,
|
|
965
1095
|
selector: "math, blockMath",
|
|
966
1096
|
pattern: /\\(tiny|Tiny|small|large|Large|LARGE|huge|Huge|scriptsize|normalsize)\s*{/,
|
|
967
|
-
message:
|
|
1097
|
+
message: `Math font size:
|
|
1098
|
+
Don't change the default font size with \\Large{} or similar commands`
|
|
968
1099
|
});
|
|
969
1100
|
|
|
970
1101
|
var MathFrac = Rule.makeRule({
|
|
@@ -980,7 +1111,8 @@ var MathNested = Rule.makeRule({
|
|
|
980
1111
|
severity: Rule.Severity.ERROR,
|
|
981
1112
|
selector: "math, blockMath",
|
|
982
1113
|
pattern: /\\text{[^$}]*\$[^$}]*\$[^}]*}/,
|
|
983
|
-
message:
|
|
1114
|
+
message: `Nested math:
|
|
1115
|
+
Don't nest math expressions inside \\text{} blocks`
|
|
984
1116
|
});
|
|
985
1117
|
|
|
986
1118
|
var MathStartsWithSpace = Rule.makeRule({
|
|
@@ -988,7 +1120,9 @@ var MathStartsWithSpace = Rule.makeRule({
|
|
|
988
1120
|
severity: Rule.Severity.GUIDELINE,
|
|
989
1121
|
selector: "math, blockMath",
|
|
990
1122
|
pattern: /^\s*(~|\\qquad|\\quad|\\,|\\;|\\:|\\ |\\!|\\enspace|\\phantom)/,
|
|
991
|
-
message:
|
|
1123
|
+
message: `Math starts with space:
|
|
1124
|
+
math should not be indented. Do not begin math expressions with
|
|
1125
|
+
LaTeX space commands like ~, \\;, \\quad, or \\phantom`
|
|
992
1126
|
});
|
|
993
1127
|
|
|
994
1128
|
var MathTextEmpty = Rule.makeRule({
|
|
@@ -1007,14 +1141,17 @@ var MathWithoutDollars = Rule.makeRule({
|
|
|
1007
1141
|
name: "math-without-dollars",
|
|
1008
1142
|
severity: Rule.Severity.GUIDELINE,
|
|
1009
1143
|
pattern: /\\\w+{[^}]*}|{|}/,
|
|
1010
|
-
message:
|
|
1144
|
+
message: `This looks like LaTeX:
|
|
1145
|
+
did you mean to put it inside dollar signs?`
|
|
1011
1146
|
});
|
|
1012
1147
|
|
|
1013
1148
|
var NestedLists = Rule.makeRule({
|
|
1014
1149
|
name: "nested-lists",
|
|
1015
1150
|
severity: Rule.Severity.WARNING,
|
|
1016
1151
|
selector: "list list",
|
|
1017
|
-
message:
|
|
1152
|
+
message: `Nested lists:
|
|
1153
|
+
nested lists are hard to read on mobile devices;
|
|
1154
|
+
do not use additional indentation.`
|
|
1018
1155
|
});
|
|
1019
1156
|
|
|
1020
1157
|
var Profanity = Rule.makeRule({
|
|
@@ -1035,7 +1172,9 @@ var TableMissingCells = Rule.makeRule({
|
|
|
1035
1172
|
const rowLengths = table.cells.map(r => r.length);
|
|
1036
1173
|
for (let r = 0; r < rowLengths.length; r++) {
|
|
1037
1174
|
if (rowLengths[r] !== headerLength) {
|
|
1038
|
-
return
|
|
1175
|
+
return `Table rows don't match header:
|
|
1176
|
+
The table header has ${headerLength} cells, but
|
|
1177
|
+
Row ${r + 1} has ${rowLengths[r]} cells.`;
|
|
1039
1178
|
}
|
|
1040
1179
|
}
|
|
1041
1180
|
}
|
|
@@ -1049,35 +1188,97 @@ var UnbalancedCodeDelimiters = Rule.makeRule({
|
|
|
1049
1188
|
name: "unbalanced-code-delimiters",
|
|
1050
1189
|
severity: Rule.Severity.ERROR,
|
|
1051
1190
|
pattern: /[`~]+/,
|
|
1052
|
-
message:
|
|
1191
|
+
message: `Unbalanced code delimiters:
|
|
1192
|
+
code blocks should begin and end with the same type and number of delimiters`
|
|
1053
1193
|
});
|
|
1054
1194
|
|
|
1055
1195
|
var UnescapedDollar = Rule.makeRule({
|
|
1056
1196
|
name: "unescaped-dollar",
|
|
1057
1197
|
severity: Rule.Severity.ERROR,
|
|
1058
1198
|
selector: "unescapedDollar",
|
|
1059
|
-
message:
|
|
1199
|
+
message: `Unescaped dollar sign:
|
|
1200
|
+
Dollar signs must appear in pairs or be escaped as \\$`
|
|
1060
1201
|
});
|
|
1061
1202
|
|
|
1062
1203
|
var WidgetInTable = Rule.makeRule({
|
|
1063
1204
|
name: "widget-in-table",
|
|
1064
1205
|
severity: Rule.Severity.BULK_WARNING,
|
|
1065
1206
|
selector: "table widget",
|
|
1066
|
-
message:
|
|
1207
|
+
message: `Widget in table:
|
|
1208
|
+
do not put widgets inside of tables.`
|
|
1067
1209
|
});
|
|
1068
1210
|
|
|
1069
1211
|
// TODO(davidflanagan):
|
|
1070
1212
|
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];
|
|
1071
1213
|
|
|
1214
|
+
/**
|
|
1215
|
+
* TreeTransformer is a class for traversing and transforming trees. Create a
|
|
1216
|
+
* TreeTransformer by passing the root node of the tree to the
|
|
1217
|
+
* constructor. Then traverse that tree by calling the traverse() method. The
|
|
1218
|
+
* argument to traverse() is a callback function that will be called once for
|
|
1219
|
+
* each node in the tree. This is a post-order depth-first traversal: the
|
|
1220
|
+
* callback is not called on the a way down, but on the way back up. That is,
|
|
1221
|
+
* the children of a node are traversed before the node itself is.
|
|
1222
|
+
*
|
|
1223
|
+
* The traversal callback function is passed three arguments, the node being
|
|
1224
|
+
* traversed, a TraversalState object, and the concatentated text content of
|
|
1225
|
+
* the node and all of its descendants. The TraversalState object is the most
|
|
1226
|
+
* most interesting argument: it has methods for querying the ancestors and
|
|
1227
|
+
* siblings of the node, and for deleting or replacing the node. These
|
|
1228
|
+
* transformation methods are why this class is a tree transformer and not
|
|
1229
|
+
* just a tree traverser.
|
|
1230
|
+
*
|
|
1231
|
+
* A typical tree traversal looks like this:
|
|
1232
|
+
*
|
|
1233
|
+
* new TreeTransformer(root).traverse((node, state, content) => {
|
|
1234
|
+
* let parent = state.parent();
|
|
1235
|
+
* let previous = state.previousSibling();
|
|
1236
|
+
* // etc.
|
|
1237
|
+
* });
|
|
1238
|
+
*
|
|
1239
|
+
* The traverse() method descends through nodes and arrays of nodes and calls
|
|
1240
|
+
* the traverse callback on each node on the way back up to the root of the
|
|
1241
|
+
* tree. (Note that it only calls the callback on the nodes themselves, not
|
|
1242
|
+
* any arrays that contain nodes.) A node is loosely defined as any object
|
|
1243
|
+
* with a string-valued `type` property. Objects that do not have a type
|
|
1244
|
+
* property are assumed to not be part of the tree and are not traversed. When
|
|
1245
|
+
* traversing an array, all elements of the array are examined, and any that
|
|
1246
|
+
* are nodes or arrays are recursively traversed. When traversing a node, all
|
|
1247
|
+
* properties of the object are examined and any node or array values are
|
|
1248
|
+
* recursively traversed. In typical parse trees, the children of a node are
|
|
1249
|
+
* in a `children` or `content` array, but this class is designed to handle
|
|
1250
|
+
* more general trees. The Perseus markdown parser, for example, produces
|
|
1251
|
+
* nodes of type "table" that have children in the `header` and `cells`
|
|
1252
|
+
* properties.
|
|
1253
|
+
*
|
|
1254
|
+
* CAUTION: the traverse() method does not make any attempt to detect
|
|
1255
|
+
* cycles. If you call it on a cyclic graph instead of a tree, it will cause
|
|
1256
|
+
* infinite recursion (or, more likely, a stack overflow).
|
|
1257
|
+
*
|
|
1258
|
+
* TODO(davidflanagan): it probably wouldn't be hard to detect cycles: when
|
|
1259
|
+
* pushing a new node onto the containers stack we could just check that it
|
|
1260
|
+
* isn't already there.
|
|
1261
|
+
*
|
|
1262
|
+
* If a node has a text-valued `content` property, it is taken to be the
|
|
1263
|
+
* plain-text content of the node. The traverse() method concatenates these
|
|
1264
|
+
* content strings and passes them to the traversal callback for each
|
|
1265
|
+
* node. This means that the callback has access the full text content of its
|
|
1266
|
+
* node and all of the nodes descendants.
|
|
1267
|
+
*
|
|
1268
|
+
* See the TraversalState class for more information on what information and
|
|
1269
|
+
* methods are available to the traversal callback.
|
|
1270
|
+
**/
|
|
1271
|
+
|
|
1072
1272
|
// TreeNode is the type of a node in a parse tree. The only real requirement is
|
|
1073
1273
|
// that every node has a string-valued `type` property
|
|
1074
|
-
|
|
1274
|
+
// TraversalCallback is the type of the callback function passed to the
|
|
1275
|
+
// traverse() method. It is invoked with node, state, and content arguments
|
|
1276
|
+
// and is expected to return nothing.
|
|
1075
1277
|
// This is the TreeTransformer class described in detail at the
|
|
1076
1278
|
// top of this file.
|
|
1077
1279
|
class TreeTransformer {
|
|
1078
1280
|
// To create a tree transformer, just pass the root node of the tree
|
|
1079
1281
|
constructor(root) {
|
|
1080
|
-
_defineProperty(this, "root", void 0);
|
|
1081
1282
|
this.root = root;
|
|
1082
1283
|
}
|
|
1083
1284
|
|
|
@@ -1230,14 +1431,8 @@ class TraversalState {
|
|
|
1230
1431
|
// elements, depending on whether we just recursed on an array or on a
|
|
1231
1432
|
// node. This is hard for TypeScript to deal with, so you'll see a number of
|
|
1232
1433
|
// type casts through the any type when working with these two properties.
|
|
1233
|
-
|
|
1234
1434
|
// The constructor just stores the root node and creates empty stacks.
|
|
1235
1435
|
constructor(root) {
|
|
1236
|
-
_defineProperty(this, "root", void 0);
|
|
1237
|
-
_defineProperty(this, "_currentNode", void 0);
|
|
1238
|
-
_defineProperty(this, "_containers", void 0);
|
|
1239
|
-
_defineProperty(this, "_indexes", void 0);
|
|
1240
|
-
_defineProperty(this, "_ancestors", void 0);
|
|
1241
1436
|
this.root = root;
|
|
1242
1437
|
|
|
1243
1438
|
// When the callback is called, this property will hold the
|
|
@@ -1494,7 +1689,6 @@ class TraversalState {
|
|
|
1494
1689
|
*/
|
|
1495
1690
|
class Stack {
|
|
1496
1691
|
constructor(array) {
|
|
1497
|
-
_defineProperty(this, "stack", void 0);
|
|
1498
1692
|
this.stack = array ? array.slice(0) : [];
|
|
1499
1693
|
}
|
|
1500
1694
|
|
|
@@ -1553,7 +1747,7 @@ class Stack {
|
|
|
1553
1747
|
|
|
1554
1748
|
// This file is processed by a Rollup plugin (replace) to inject the production
|
|
1555
1749
|
const libName = "@khanacademy/perseus-linter";
|
|
1556
|
-
const libVersion = "0.3.
|
|
1750
|
+
const libVersion = "0.3.10";
|
|
1557
1751
|
perseusCore.addLibraryVersionToPerseusDebug(libName, libVersion);
|
|
1558
1752
|
|
|
1559
1753
|
// Define the shape of the linter context object that is passed through the
|