@khanacademy/perseus-linter 0.3.7 → 0.3.9

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/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
- function _defineProperty(obj, key, value) {
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: "Exception in rule ".concat(this.name, ": ").concat(e.message, "\nStack trace:\n").concat(e.stack),
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 "Don't use absolute URLs:\nWhen linking to KA content or images, omit the\nhttps://www.khanacademy.org URL prefix.\nUse a relative URL beginning with / instead.";
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: "Blockquoted math:\nmath should not be indented."
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: "Blockquoted widget:\nwidgets should not be indented."
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: "Use a single space after a sentence-ending period, or\nany other kind of terminal punctuation."
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: "No extra whitespace at the end of content blocks."
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 "Don't use level-1 headings:\nBegin headings with two or more # characters.";
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 "Skipped heading level:\nthis heading is level ".concat(currentHeading.level, " but\nthe previous heading was level ").concat(previousHeading.level);
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: "First letter is lowercase:\nthe first letter of a heading should be capitalized."
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 "Title-case heading:\nThis heading appears to be in title-case, but should be sentence-case.\nOnly capitalize the first letter and proper nouns.";
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 "Images should have alt text:\nfor accessibility, all images should have alt text.\nSpecify alt text inside square brackets after the !.";
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 "Images should have alt text:\nfor accessibility, all images should have descriptive alt text.\nThis image's alt text is only ".concat(image.alt.length, " characters long.");
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: "Image in table:\ndo not put images inside of tables."
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 "Whitespace before or after image url:\nFor images, don't include any space or newlines after '(' or before ')'.\nWhitespace in image URLs causes translation difficulties.";
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 "Images should have alt text:\nfor accessibility, all images should have a text description.\nAdd a description in the \"Alt Text\" box of the image widget.";
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 "Images should have alt text:\nfor accessibility, all images should have descriptive alt text.\nThis image's alt text is only ".concat(alt.trim().length, " characters long.");
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 "No math in image captions:\nDon't include math expressions in image captions.";
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: "Inappropriate link text:\nDo not use the words \"click here\" in links."
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 "Paragraph too long:\nThis paragraph is ".concat(content.length, " characters long.\nShorten it to 500 characters or fewer.");
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: "Adjacent math blocks:\ncombine the blocks between \\begin{align} and \\end{align}"
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: "Extra space at end of block:\nDon't end an align block with backslashes"
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: "Math font size:\nDon't change the default font size with \\Large{} or similar commands"
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: "Nested math:\nDon't nest math expressions inside \\text{} blocks"
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: "Math starts with space:\nmath should not be indented. Do not begin math expressions with\nLaTeX space commands like ~, \\;, \\quad, or \\phantom"
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: "This looks like LaTeX:\ndid you mean to put it inside dollar signs?"
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: "Nested lists:\nnested lists are hard to read on mobile devices;\ndo not use additional indentation."
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 "Table rows don't match header:\nThe table header has ".concat(headerLength, " cells, but\nRow ").concat(r + 1, " has ").concat(rowLengths[r], " cells.");
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: "Unbalanced code delimiters:\ncode blocks should begin and end with the same type and number of delimiters"
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: "Unescaped dollar sign:\nDollar signs must appear in pairs or be escaped as \\$"
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: "Widget in table:\ndo not put widgets inside of tables."
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.7";
1750
+ const libVersion = "0.3.9";
1557
1751
  perseusCore.addLibraryVersionToPerseusDebug(libName, libVersion);
1558
1752
 
1559
1753
  // Define the shape of the linter context object that is passed through the