@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.
Files changed (55) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/dist/es/index.js +420 -240
  3. package/dist/es/index.js.map +1 -1
  4. package/dist/index.js +15 -20
  5. package/dist/index.js.map +1 -1
  6. package/dist/tree-transformer.d.ts +1 -1
  7. package/package.json +3 -3
  8. package/src/__tests__/matcher.test.ts +57 -57
  9. package/src/__tests__/rules.test.ts +62 -62
  10. package/src/__tests__/tree-transformer.test.ts +5 -5
  11. package/src/index.ts +4 -4
  12. package/src/rule.ts +2 -2
  13. package/src/tree-transformer.ts +9 -13
  14. package/tsconfig-build.json +11 -0
  15. package/{tsconfig.tsbuildinfo → tsconfig-build.tsbuildinfo} +1 -1
  16. package/dist/index.js.flow +0 -18
  17. package/dist/proptypes.js.flow +0 -17
  18. package/dist/rule.js.flow +0 -86
  19. package/dist/rules/absolute-url.js.flow +0 -9
  20. package/dist/rules/all-rules.js.flow +0 -9
  21. package/dist/rules/blockquoted-math.js.flow +0 -9
  22. package/dist/rules/blockquoted-widget.js.flow +0 -9
  23. package/dist/rules/double-spacing-after-terminal.js.flow +0 -9
  24. package/dist/rules/extra-content-spacing.js.flow +0 -9
  25. package/dist/rules/heading-level-1.js.flow +0 -9
  26. package/dist/rules/heading-level-skip.js.flow +0 -9
  27. package/dist/rules/heading-sentence-case.js.flow +0 -9
  28. package/dist/rules/heading-title-case.js.flow +0 -9
  29. package/dist/rules/image-alt-text.js.flow +0 -9
  30. package/dist/rules/image-in-table.js.flow +0 -9
  31. package/dist/rules/image-spaces-around-urls.js.flow +0 -9
  32. package/dist/rules/image-widget.js.flow +0 -9
  33. package/dist/rules/link-click-here.js.flow +0 -9
  34. package/dist/rules/lint-utils.js.flow +0 -8
  35. package/dist/rules/long-paragraph.js.flow +0 -9
  36. package/dist/rules/math-adjacent.js.flow +0 -9
  37. package/dist/rules/math-align-extra-break.js.flow +0 -9
  38. package/dist/rules/math-align-linebreaks.js.flow +0 -9
  39. package/dist/rules/math-empty.js.flow +0 -9
  40. package/dist/rules/math-font-size.js.flow +0 -9
  41. package/dist/rules/math-frac.js.flow +0 -9
  42. package/dist/rules/math-nested.js.flow +0 -9
  43. package/dist/rules/math-starts-with-space.js.flow +0 -9
  44. package/dist/rules/math-text-empty.js.flow +0 -9
  45. package/dist/rules/math-without-dollars.js.flow +0 -9
  46. package/dist/rules/nested-lists.js.flow +0 -9
  47. package/dist/rules/profanity.js.flow +0 -9
  48. package/dist/rules/table-missing-cells.js.flow +0 -9
  49. package/dist/rules/unbalanced-code-delimiters.js.flow +0 -9
  50. package/dist/rules/unescaped-dollar.js.flow +0 -9
  51. package/dist/rules/widget-in-table.js.flow +0 -9
  52. package/dist/selector.js.flow +0 -31
  53. package/dist/tree-transformer.js.flow +0 -253
  54. package/dist/types.js.flow +0 -12
  55. 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 ownKeys(object, enumerableOnly) {
5
- var keys = Object.keys(object);
6
- if (Object.getOwnPropertySymbols) {
7
- var symbols = Object.getOwnPropertySymbols(object);
8
- enumerableOnly && (symbols = symbols.filter(function (sym) {
9
- return Object.getOwnPropertyDescriptor(object, sym).enumerable;
10
- })), keys.push.apply(keys, symbols);
11
- }
12
- return keys;
13
- }
14
- function _objectSpread2(target) {
15
- for (var i = 1; i < arguments.length; i++) {
16
- var source = null != arguments[i] ? arguments[i] : {};
17
- i % 2 ? ownKeys(Object(source), !0).forEach(function (key) {
18
- _defineProperty(target, key, source[key]);
19
- }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)) : ownKeys(Object(source)).forEach(function (key) {
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
- _defineProperty(this, "tokens", void 0);
100
- _defineProperty(this, "tokenIndex", void 0);
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
- var c = this.tokens[this.tokenIndex][0];
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
- var ts = this.parseTreeSelector();
112
+ const ts = this.parseTreeSelector();
144
113
 
145
114
  // Now see what's next
146
- var token = this.nextToken();
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
- var treeSelectors = [ts];
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
- var ns = this.parseNodeSelector();
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
- var token = this.nextToken();
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
- var t = this.nextToken();
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
- _defineProperty(Parser, "TOKENS", void 0);
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
- _defineProperty(this, "selectors", void 0);
229
+ this.selectors = void 0;
261
230
  this.selectors = selectors;
262
231
  }
263
232
  match(state) {
264
- for (var i = 0; i < this.selectors.length; i++) {
265
- var s = this.selectors[i];
266
- var result = s.match(state);
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
- var result = "";
275
- for (var i = 0; i < this.selectors.length; i++) {
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
- _defineProperty(this, "type", void 0);
272
+ this.type = void 0;
304
273
  this.type = type;
305
274
  }
306
275
  match(state) {
307
- var node = state.currentNode();
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
- _defineProperty(this, "left", void 0);
328
- _defineProperty(this, "right", void 0);
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
- var rightResult = this.right.match(state);
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
- var leftResult = this.left.match(state);
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
- var rightResult = this.right.match(state);
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
- var leftResult = this.left.match(state);
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
- var rightResult = this.right.match(state);
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
- var leftResult = this.left.match(state);
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
- var rightResult = this.right.match(state);
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
- var leftResult = this.left.match(state);
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
- var _this = this;
463
- _defineProperty(this, "name", void 0);
464
- _defineProperty(this, "severity", void 0);
465
- _defineProperty(this, "selector", void 0);
466
- _defineProperty(this, "pattern", void 0);
467
- _defineProperty(this, "lint", void 0);
468
- _defineProperty(this, "applies", void 0);
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 = function () {
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
- var selectorMatch = this.selector.match(traversalState);
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
- var patternMatch;
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
- var error = this.lint(traversalState, content, selectorMatch, patternMatch, context);
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: "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}`,
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
- var lastSlash = pattern.lastIndexOf("/");
614
- var expression = pattern.substring(1, lastSlash);
615
- var flags = pattern.substring(lastSlash + 1);
616
- // @ts-expect-error [FEI-5003] - 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"]'?
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
- var result = [match];
717
+ const result = [match];
628
718
  result.index = index;
629
719
  result.input = input;
630
720
  return result;
631
721
  }
632
722
  }
633
- _defineProperty(Rule, "DEFAULT_SELECTOR", void 0);
634
- _defineProperty(Rule, "Severity", {
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
- var HOSTNAME = /\/\/([^\/]+)/;
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
- var match = url.match(HOSTNAME);
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 lint(state, content, nodes, match) {
664
- var url = nodes[0].target;
665
- var hostname = getHostname(url);
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 "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.";
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: "Blockquoted math:\nmath should not be indented."
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: "Blockquoted widget:\nwidgets should not be indented."
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: "Use a single space after a sentence-ending period, or\nany other kind of terminal punctuation."
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 applies(context) {
795
+ applies: function (context) {
700
796
  return context.contentType === "article";
701
797
  },
702
- message: "No extra whitespace at the end of content blocks."
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 lint(state, content, nodes, match) {
805
+ lint: function (state, content, nodes, match) {
710
806
  if (nodes[0].level === 1) {
711
- return "Don't use level-1 headings:\nBegin headings with two or more # characters.";
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 lint(state, content, nodes, match) {
721
- var currentHeading = nodes[1];
722
- var previousHeading = nodes[0];
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 "Skipped heading level:\nthis heading is level ".concat(currentHeading.level, " but\nthe previous heading was level ").concat(previousHeading.level);
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: "First letter is lowercase:\nthe first letter of a heading should be capitalized."
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
- var littleWords = {
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
- var c = word[0];
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 lint(state, content, nodes, match) {
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
- var heading = content.trim();
787
- var words = heading.split(/\s+/);
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 "Title-case heading:\nThis heading appears to be in title-case, but should be sentence-case.\nOnly capitalize the first letter and proper nouns.";
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 lint(state, content, nodes, match) {
808
- var image = nodes[0];
909
+ lint: function (state, content, nodes, match) {
910
+ const image = nodes[0];
809
911
  if (!image.alt || !image.alt.trim()) {
810
- return "Images should have alt text:\nfor accessibility, all images should have alt text.\nSpecify alt text inside square brackets after the !.";
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 "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.");
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: "Image in table:\ndo not put images inside of tables."
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 lint(state, content, nodes, match, context) {
830
- var image = nodes[0];
831
- var url = image.target;
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
- var index = context.content.indexOf(url);
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 "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.";
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 lint(state, content, nodes, match, context) {
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
- var widget = context && context.widgets && context.widgets[state.currentNode().id];
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
- var alt = widget.options.alt;
984
+ const alt = widget.options.alt;
876
985
  if (!alt) {
877
- 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.";
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 "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.");
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 "No math in image captions:\nDon't include math expressions in image captions.";
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: "Inappropriate link text:\nDo not use the words \"click here\" in links."
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 lint(state, content, nodes, match) {
906
- return "Paragraph too long:\nThis paragraph is ".concat(content.length, " characters long.\nShorten it to 500 characters or fewer.");
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: "Adjacent math blocks:\ncombine the blocks between \\begin{align} and \\end{align}"
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: "Extra space at end of block:\nDon't end an align block with backslashes"
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 lint(state, content, nodes, match) {
938
- var text = match[0];
1056
+ lint: function (state, content, nodes, match) {
1057
+ let text = match[0];
939
1058
  while (text.length) {
940
- var index = text.indexOf("\\\\");
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
- var nextpair = text.match(/^\s*\\\\\s*(?!\\\\)/);
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: "Math font size:\nDon't change the default font size with \\Large{} or similar commands"
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: "Nested math:\nDon't nest math expressions inside \\text{} blocks"
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: "Math starts with space:\nmath should not be indented. Do not begin math expressions with\nLaTeX space commands like ~, \\;, \\quad, or \\phantom"
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: "This looks like LaTeX:\ndid you mean to put it inside dollar signs?"
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: "Nested lists:\nnested lists are hard to read on mobile devices;\ndo not use additional indentation."
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 lint(state, content, nodes, match) {
1045
- var table = nodes[0];
1046
- var headerLength = table.header.length;
1047
- var rowLengths = table.cells.map(r => r.length);
1048
- for (var r = 0; r < rowLengths.length; r++) {
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 "Table rows don't match header:\nThe table header has ".concat(headerLength, " cells, but\nRow ").concat(r + 1, " has ").concat(rowLengths[r], " cells.");
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: "Unbalanced code delimiters:\ncode blocks should begin and end with the same type and number of delimiters"
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: "Unescaped dollar sign:\nDollar signs must appear in pairs or be escaped as \\$"
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: "Widget in table:\ndo not put widgets inside of tables."
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
- _defineProperty(this, "root", void 0);
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
- var content = "";
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
- var _node = n; // safe cast; we just tested
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(_node);
1135
- state._ancestors.push(_node);
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
- // TODO(mdr): We found a new Flow error when upgrading:
1141
- // "node.content (property `content` is missing in `TreeNode` [1].)"
1142
- // @ts-expect-error [FEI-5003] - TS2339 - Property 'content' does not exist on type 'TreeNode'.
1143
- if (typeof _node.content === "string") {
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
- var keys = Object.keys(_node);
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
- var value = _node[key];
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(_node, state, content);
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
- var nodes = n;
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
- var index = 0;
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 Flow that this is a number
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
- * Flow annotaions.
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 Flow to deal with, so you'll see a number of
1246
- // Flow casts through the any type when working with these two properties.
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
- _defineProperty(this, "root", void 0);
1251
- _defineProperty(this, "_currentNode", void 0);
1252
- _defineProperty(this, "_containers", void 0);
1253
- _defineProperty(this, "_indexes", void 0);
1254
- _defineProperty(this, "_ancestors", void 0);
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
- var siblings = this._containers.top();
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
- var index = this._indexes.top();
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
- var siblings = this._containers.top();
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
- var index = this._indexes.top();
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
- var siblings = this._containers.top();
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
- var index = this._indexes.top();
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
- var parent = this._containers.top();
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 Flow, so we have to do some
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
- var index = this._indexes.top();
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
- var property = this._indexes.top();
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 Flow doesn't know that.
1439
- var index = this._indexes.pop();
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
- var clone = new TraversalState(this.root);
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
- _defineProperty(this, "stack", void 0);
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 [FEI-5003] - TS2322 - Type 'T | undefined' is not assignable to type 'T'.
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 (var i = 0; i < this.stack.length; i++) {
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
- var linterContextProps = PropTypes.shape({
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
- var linterContextDefault = {
1759
+ const linterContextDefault = {
1579
1760
  contentType: "",
1580
1761
  highlightLint: false,
1581
1762
  paths: [],
1582
1763
  stack: []
1583
1764
  };
1584
1765
 
1585
- var allLintRules = AllRules.filter(r => r.severity < Rule.Severity.BULK_WARNING);
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
- var rules = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : allLintRules;
1613
- var warnings = [];
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
- var next = state.nextSibling();
1800
+ let next = state.nextSibling();
1621
1801
  while (TreeTransformer.isTextNode(next)) {
1622
- // @ts-expect-error [FEI-5003] - 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'.
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
- var tableWarnings = [];
1648
- var insideTable = false;
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
- var nodeWarnings = [];
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
- var applicableRules = rules.filter(r => r.applies(context));
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
- var stack = [...context.stack];
1843
+ const stack = [...context.stack];
1664
1844
  stack.push(node.type);
1665
- var nodeContext = _objectSpread2(_objectSpread2({}, context), {}, {
1845
+ const nodeContext = _extends({}, context, {
1666
1846
  stack: stack.join(".")
1667
1847
  });
1668
1848
  applicableRules.forEach(rule => {
1669
- var warning = rule.check(node, state, content, nodeContext);
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 [FEI-5003] - TS2345 - Argument of type 'any' is not assignable to parameter of type 'never'.
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 [FEI-5003] - 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'.
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 [FEI-5003] - TS2339 - Property 'content' does not exist on type 'TreeNode'.
1785
- var _content = node.content; // Text nodes have content
1786
- var warning = nodeWarnings[0]; // There is only one warning.
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
- var start = warning.start || 0;
1789
- var end = warning.end || _content.length;
1790
- var prefix = _content.substring(0, start);
1791
- var lint = _content.substring(start, end);
1792
- var suffix = _content.substring(end);
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
- var replacements = []; // What we'll replace the node with
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
- var stack = context.stack || [];
1835
- return _objectSpread2(_objectSpread2({}, context), {}, {
2014
+ const stack = context.stack || [];
2015
+ return _extends({}, context, {
1836
2016
  stack: stack.concat(name)
1837
2017
  });
1838
2018
  }