@khanacademy/perseus-linter 1.3.6 → 2.0.0

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
@@ -1,150 +1,64 @@
1
- 'use strict';
1
+ import _extends from '@babel/runtime/helpers/extends';
2
+ import { PerseusError, Errors } from '@khanacademy/perseus-core';
3
+ import { addLibraryVersionToPerseusDebug } from '@khanacademy/perseus-utils';
4
+ import PropTypes from 'prop-types';
2
5
 
3
- Object.defineProperty(exports, '__esModule', { value: true });
4
-
5
- var perseusCore = require('@khanacademy/perseus-core');
6
- var PropTypes = require('prop-types');
7
-
8
- function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
9
-
10
- var PropTypes__default = /*#__PURE__*/_interopDefaultLegacy(PropTypes);
11
-
12
- /* eslint-disable no-useless-escape */
13
- /**
14
- * This is the base class for all Selector types. The key method that all
15
- * selector subclasses must implement is match(). It takes a TraversalState
16
- * object (from a TreeTransformer traversal) and tests whether the selector
17
- * matches at the current node. See the comment at the start of this file for
18
- * more details on the match() method.
19
- */
20
6
  class Selector {
21
7
  static parse(selectorText) {
22
8
  return new Parser(selectorText).parse();
23
9
  }
24
-
25
- /**
26
- * Return an array of the nodes that matched or null if no match.
27
- * This is the base class so we just throw an exception. All Selector
28
- * subclasses must provide an implementation of this method.
29
- */
30
10
  match(state) {
31
- throw new perseusCore.PerseusError("Selector subclasses must implement match()", perseusCore.Errors.NotAllowed);
11
+ throw new PerseusError("Selector subclasses must implement match()", Errors.NotAllowed);
32
12
  }
33
-
34
- /**
35
- * Selector subclasses all define a toString() method primarily
36
- * because it makes it easy to write parser tests.
37
- */
38
13
  toString() {
39
14
  return "Unknown selector class";
40
15
  }
41
16
  }
42
-
43
- /**
44
- * This class implements a parser for the selector grammar. Pass the source
45
- * text to the Parser() constructor, and then call the parse() method to
46
- * obtain a corresponding Selector object. parse() throws an exception
47
- * if there are syntax errors in the selector.
48
- *
49
- * This class is not exported, and you don't need to use it directly.
50
- * Instead call the static Selector.parse() method.
51
- */
52
17
  class Parser {
53
- static TOKENS; // We do lexing with a simple regular expression
54
- tokens; // The array of tokens
55
- tokenIndex; // Which token in the array we're looking at now
56
-
57
18
  constructor(s) {
58
- // Normalize whitespace:
59
- // - remove leading and trailing whitespace
60
- // - replace runs of whitespace with single space characters
19
+ this.tokens = void 0;
20
+ this.tokenIndex = void 0;
61
21
  s = s.trim().replace(/\s+/g, " ");
62
- // Convert the string to an array of tokens. Note that the TOKENS
63
- // pattern ignores spaces that do not appear before identifiers
64
- // or the * wildcard.
65
22
  this.tokens = s.match(Parser.TOKENS) || [];
66
23
  this.tokenIndex = 0;
67
24
  }
68
-
69
- // Return the next token or the empty string if there are no more
70
25
  nextToken() {
71
26
  return this.tokens[this.tokenIndex] || "";
72
27
  }
73
-
74
- // Increment the token index to "consume" the token we were looking at
75
- // and move on to the next one.
76
28
  consume() {
77
29
  this.tokenIndex++;
78
30
  }
79
-
80
- // Return true if the current token is an identifier or false otherwise
81
31
  isIdentifier() {
82
- // The Parser.TOKENS regexp ensures that we only have to check
83
- // the first character of a token to know what kind of token it is.
84
32
  const c = this.tokens[this.tokenIndex][0];
85
33
  return c >= "a" && c <= "z" || c >= "A" && c <= "Z";
86
34
  }
87
-
88
- // Consume space tokens until the next token is not a space.
89
35
  skipSpace() {
90
36
  while (this.nextToken() === " ") {
91
37
  this.consume();
92
38
  }
93
39
  }
94
-
95
- // Parse a comma-separated sequence of tree selectors. This is the
96
- // entry point for the Parser class and the only method that clients
97
- // ever need to call.
98
40
  parse() {
99
- // We expect at least one tree selector
100
41
  const ts = this.parseTreeSelector();
101
-
102
- // Now see what's next
103
42
  let token = this.nextToken();
104
-
105
- // If there is no next token then we're done parsing and can return
106
- // the tree selector object we got above
107
43
  if (!token) {
108
44
  return ts;
109
45
  }
110
-
111
- // Otherwise, there is more go come and we're going to need a
112
- // list of tree selectors
113
46
  const treeSelectors = [ts];
114
47
  while (token) {
115
- // The only character we allow after a tree selector is a comma
116
48
  if (token === ",") {
117
49
  this.consume();
118
50
  } else {
119
51
  throw new ParseError("Expected comma");
120
52
  }
121
-
122
- // And if we saw a comma, then it must be followed by another
123
- // tree selector
124
53
  treeSelectors.push(this.parseTreeSelector());
125
54
  token = this.nextToken();
126
55
  }
127
-
128
- // If we parsed more than one tree selector, return them in a
129
- // SelectorList object.
130
56
  return new SelectorList(treeSelectors);
131
57
  }
132
-
133
- // Parse a sequence of node selectors linked together with
134
- // hierarchy combinators: space, >, + and ~.
135
58
  parseTreeSelector() {
136
- this.skipSpace(); // Ignore space after a comma, for example
137
-
138
- // A tree selector must begin with a node selector
59
+ this.skipSpace();
139
60
  let ns = this.parseNodeSelector();
140
61
  for (;;) {
141
- // Now check the next token. If there is none, or if it is a
142
- // comma, then we're done with the treeSelector. Otherwise
143
- // we expect a combinator followed by another node selector.
144
- // If we don't see a combinator, we throw an error. If we
145
- // do see a combinator and another node selector then we
146
- // combine the current node selector with the new node selector
147
- // using a Selector subclass that depends on the combinator.
148
62
  const token = this.nextToken();
149
63
  if (!token || token === ",") {
150
64
  break;
@@ -166,15 +80,7 @@ class Parser {
166
80
  }
167
81
  return ns;
168
82
  }
169
-
170
- // Parse a single node selector.
171
- // For now, this is just a node type or a wildcard.
172
- //
173
- // TODO(davidflanagan): we may need to extend this with attribute
174
- // selectors like 'heading[level=3]', or with pseudo-classes like
175
- // paragraph:first-child
176
83
  parseNodeSelector() {
177
- // First, skip any whitespace
178
84
  this.skipSpace();
179
85
  const t = this.nextToken();
180
86
  if (t === "*") {
@@ -188,32 +94,17 @@ class Parser {
188
94
  throw new ParseError("Expected node type");
189
95
  }
190
96
  }
191
-
192
- // We break the input string into tokens with this regexp. Token types
193
- // are identifiers, integers, punctuation and spaces. Note that spaces
194
- // tokens are only returned when they appear before an identifier or
195
- // wildcard token and are otherwise omitted.
97
+ Parser.TOKENS = void 0;
196
98
  Parser.TOKENS = /([a-zA-Z][\w-]*)|(\d+)|[^\s]|(\s(?=[a-zA-Z\*]))/g;
197
-
198
- /**
199
- * This is a trivial Error subclass that the Parser uses to signal parse errors
200
- */
201
99
  class ParseError extends Error {
202
100
  constructor(message) {
203
101
  super(message);
204
102
  }
205
103
  }
206
-
207
- /**
208
- * This Selector subclass is a list of selectors. It matches a node if any of
209
- * the selectors on the list matches the node. It considers the selectors in
210
- * order, and returns the array of nodes returned by whichever one matches
211
- * first.
212
- */
213
104
  class SelectorList extends Selector {
214
- selectors;
215
105
  constructor(selectors) {
216
106
  super();
107
+ this.selectors = void 0;
217
108
  this.selectors = selectors;
218
109
  }
219
110
  match(state) {
@@ -235,11 +126,6 @@ class SelectorList extends Selector {
235
126
  return result;
236
127
  }
237
128
  }
238
-
239
- /**
240
- * This trivial Selector subclass implements the '*' wildcard and
241
- * matches any node.
242
- */
243
129
  class AnyNode extends Selector {
244
130
  match(state) {
245
131
  return [state.currentNode()];
@@ -248,15 +134,10 @@ class AnyNode extends Selector {
248
134
  return "*";
249
135
  }
250
136
  }
251
-
252
- /**
253
- * This selector subclass implements the <IDENTIFIER> part of the grammar.
254
- * it matches any node whose `type` property is a specified string
255
- */
256
137
  class TypeSelector extends Selector {
257
- type;
258
138
  constructor(type) {
259
139
  super();
140
+ this.type = void 0;
260
141
  this.type = type;
261
142
  }
262
143
  match(state) {
@@ -270,28 +151,15 @@ class TypeSelector extends Selector {
270
151
  return this.type;
271
152
  }
272
153
  }
273
-
274
- /**
275
- * This selector subclass is the superclass of the classes that implement
276
- * matching for the four combinators. It defines left and right properties for
277
- * the two selectors that are to be combined, but does not define a match
278
- * method.
279
- */
280
154
  class SelectorCombinator extends Selector {
281
- left;
282
- right;
283
155
  constructor(left, right) {
284
156
  super();
157
+ this.left = void 0;
158
+ this.right = void 0;
285
159
  this.left = left;
286
160
  this.right = right;
287
161
  }
288
162
  }
289
-
290
- /**
291
- * This Selector subclass implements the space combinator. It matches if the
292
- * right selector matches the current node and the left selector matches some
293
- * ancestor of the current node.
294
- */
295
163
  class AncestorCombinator extends SelectorCombinator {
296
164
  constructor(left, right) {
297
165
  super(left, right);
@@ -314,12 +182,6 @@ class AncestorCombinator extends SelectorCombinator {
314
182
  return this.left.toString() + " " + this.right.toString();
315
183
  }
316
184
  }
317
-
318
- /**
319
- * This Selector subclass implements the > combinator. It matches if the
320
- * right selector matches the current node and the left selector matches
321
- * the parent of the current node.
322
- */
323
185
  class ParentCombinator extends SelectorCombinator {
324
186
  constructor(left, right) {
325
187
  super(left, right);
@@ -342,12 +204,6 @@ class ParentCombinator extends SelectorCombinator {
342
204
  return this.left.toString() + " > " + this.right.toString();
343
205
  }
344
206
  }
345
-
346
- /**
347
- * This Selector subclass implements the + combinator. It matches if the
348
- * right selector matches the current node and the left selector matches
349
- * the immediate previous sibling of the current node.
350
- */
351
207
  class PreviousCombinator extends SelectorCombinator {
352
208
  constructor(left, right) {
353
209
  super(left, right);
@@ -370,12 +226,6 @@ class PreviousCombinator extends SelectorCombinator {
370
226
  return this.left.toString() + " + " + this.right.toString();
371
227
  }
372
228
  }
373
-
374
- /**
375
- * This Selector subclass implements the ~ combinator. It matches if the
376
- * right selector matches the current node and the left selector matches
377
- * any previous sibling of the current node.
378
- */
379
229
  class SiblingCombinator extends SelectorCombinator {
380
230
  constructor(left, right) {
381
231
  super(left, right);
@@ -399,173 +249,17 @@ class SiblingCombinator extends SelectorCombinator {
399
249
  }
400
250
  }
401
251
 
402
- /**
403
- * The Rule class represents a Perseus lint rule. A Rule instance has a check()
404
- * method that takes the same (node, state, content) arguments that a
405
- * TreeTransformer traversal callback function does. Call the check() method
406
- * during a tree traversal to determine whether the current node of the tree
407
- * violates the rule. If there is no violation, then check() returns
408
- * null. Otherwise, it returns an object that includes the name of the rule,
409
- * an error message, and the start and end positions within the node's content
410
- * string of the lint.
411
- *
412
- * A Perseus lint rule consists of a name, a severity, a selector, a pattern
413
- * (RegExp) and two functions. The check() method uses the selector, pattern,
414
- * and functions as follows:
415
- *
416
- * - First, when determining which rules to apply to a particular piece of
417
- * content, each rule can specify an optional function provided in the fifth
418
- * parameter to evaluate whether or not we should be applying this rule.
419
- * If the function returns false, we don't use the rule on this content.
420
- *
421
- * - Next, check() tests whether the node currently being traversed matches
422
- * the selector. If it does not, then the rule does not apply at this node
423
- * and there is no lint and check() returns null.
424
- *
425
- * - If the selector matched, then check() tests the text content of the node
426
- * (and its children) against the pattern. If the pattern does not match,
427
- * then there is no lint, and check() returns null.
428
- *
429
- * - If both the selector and pattern match, then check() calls the function
430
- * passing the TraversalState object, the content string for the node, the
431
- * array of nodes returned by the selector match, and the array of strings
432
- * returned by the pattern match. This function can use these arguments to
433
- * implement any kind of lint detection logic it wants. If it determines
434
- * that there is no lint, then it should return null. Otherwise, it should
435
- * return an error message as a string, or an object with `message`, `start`
436
- * and `end` properties. The start and end properties are numbers that mark
437
- * the beginning and end of the problematic content. Note that these numbers
438
- * are relative to the content string passed to the traversal callback, not
439
- * to the entire string that was used to generate the parse tree in the
440
- * first place. TODO(davidflanagan): modify the simple-markdown library to
441
- * have an option to add the text offset of each node to the parse
442
- * tree. This will allows us to pinpoint lint errors within a long string
443
- * of markdown text.
444
- *
445
- * - If the function returns null, then check() returns null. Otherwise,
446
- * check() returns an object with `rule`, `message`, `start` and `end`
447
- * properties. The value of the `rule` property is the name of the rule,
448
- * which is useful for error reporting purposes.
449
- *
450
- * The name, severity, selector, pattern and function arguments to the Rule()
451
- * constructor are optional, but you may not omit both the selector and the
452
- * pattern. If you do not specify a selector, a default selector that matches
453
- * any node of type "text" will be used. If you do not specify a pattern, then
454
- * any node that matches the selector will be assumed to match the pattern as
455
- * well. If you don't pass a function as the fourth argument to the Rule()
456
- * constructor, then you must pass an error message string instead. If you do
457
- * this, you'll get a default function that unconditionally returns an object
458
- * that includes the error message and the start and end indexes of the
459
- * portion of the content string that matched the pattern. If you don't pass a
460
- * function in the fifth parameter, the rule will be applied in any context.
461
- *
462
- * One of the design goals of this Rule class is to allow simple lint rules to
463
- * be described in JSON files without any JavaScript code. So in addition to
464
- * the Rule() constructor, the class also defines a Rule.makeRule() factory
465
- * method. This method takes a single object as its argument and expects the
466
- * object to have four string properties. The `name` property is passed as the
467
- * first argument to the Rule() construtctor. The optional `selector`
468
- * property, if specified, is passed to Selector.parse() and the resulting
469
- * Selector object is used as the second argument to Rule(). The optional
470
- * `pattern` property is converted to a RegExp before being passed as the
471
- * third argument to Rule(). (See Rule.makePattern() for details on the string
472
- * to RegExp conversion). Finally, the `message` property specifies an error
473
- * message that is passed as the final argument to Rule(). You can also use a
474
- * real RegExp as the value of the `pattern` property or define a custom lint
475
- * function on the `lint` property instead of setting the `message`
476
- * property. Doing either of these things means that your rule description can
477
- * no longer be saved in a JSON file, however.
478
- *
479
- * For example, here are two lint rules defined with Rule.makeRule():
480
- *
481
- * let nestedLists = Rule.makeRule({
482
- * name: "nested-lists",
483
- * selector: "list list",
484
- * message: `Nested lists:
485
- * nested lists are hard to read on mobile devices;
486
- * do not use additional indentation.`,
487
- * });
488
- *
489
- * let longParagraph = Rule.makeRule({
490
- * name: "long-paragraph",
491
- * selector: "paragraph",
492
- * pattern: /^.{501,}/,
493
- * lint: function(state, content, nodes, match) {
494
- * return `Paragraph too long:
495
- * This paragraph is ${content.length} characters long.
496
- * Shorten it to 500 characters or fewer.`;
497
- * },
498
- * });
499
- *
500
- * Certain advanced lint rules need additional information about the content
501
- * being linted in order to detect lint. For example, a rule to check for
502
- * whitespace at the start and end of the URL for an image can't use the
503
- * information in the node or content arguments because the markdown parser
504
- * strips leading and trailing whitespace when parsing. (Nevertheless, these
505
- * spaces have been a practical problem for our content translation process so
506
- * in order to check for them, a lint rule needs access to the original
507
- * unparsed source text. Similarly, there are various lint rules that check
508
- * widget usage. For example, it is easy to write a lint rule to ensure that
509
- * images have alt text for images encoded in markdown. But when images are
510
- * added to our content via an image widget we also want to be able to check
511
- * for alt text. In order to do this, the lint rule needs to be able to look
512
- * widgets up by name in the widgets object associated with the parse tree.
513
- *
514
- * In order to support advanced linting rules like these, the check() method
515
- * takes a context object as its optional fourth argument, and passes this
516
- * object on to the lint function of each rule. Rules that require extra
517
- * context should not assume that they will always get it, and should verify
518
- * that the necessary context has been supplied before using it. Currently the
519
- * "content" property of the context object is the unparsed source text if
520
- * available, and the "widgets" property of the context object is the widget
521
- * object associated with that content string in the JSON object that defines
522
- * the Perseus article or exercise that is being linted.
523
- */
524
-
525
- // This represents the type returned by String.match(). It is an
526
- // array of strings, but also has index:number and input:string properties.
527
- // TypeScript doesn't handle it well, so we punt and just use any.
528
-
529
- // This is the return type of the check() method of a Rule object
530
-
531
- // This is the return type of the lint detection function passed as the 4th
532
- // argument to the Rule() constructor. It can return null or a string or an
533
- // object containing a string and two numbers.
534
- // prettier-ignore
535
- // (prettier formats this in a way that ka-lint does not like)
536
-
537
- // This is the type of the lint detection function that the Rule() constructor
538
- // expects as its fourth argument. It is passed the TraversalState object and
539
- // content string that were passed to check(), and is also passed the array of
540
- // nodes returned by the selector match and the array of strings returned by
541
- // the pattern match. It should return null if no lint is detected or an
542
- // error message or an object contining an error message.
543
-
544
- // An optional check to verify whether or not a particular rule should
545
- // be checked by context. For example, some rules only apply in exercises,
546
- // and should never be applied to articles. Defaults to true, so if we
547
- // omit the applies function in a rule, it'll be tested everywhere.
548
-
549
- /**
550
- * A Rule object describes a Perseus lint rule. See the comment at the top of
551
- * this file for detailed description.
552
- */
553
252
  class Rule {
554
- name; // The name of the rule
555
- severity; // The severity of the rule
556
- selector; // The specified selector or the DEFAULT_SELECTOR
557
- pattern; // A regular expression if one was specified
558
- lint; // The lint-testing function or a default
559
- applies; // Checks to see if we should apply a rule or not
560
- message; // The error message for use with the default function
561
- static DEFAULT_SELECTOR;
562
-
563
- // The comment at the top of this file has detailed docs for
564
- // this constructor and its arguments
565
253
  constructor(name, severity, selector, pattern, lint, applies) {
566
- var _this = this;
254
+ this.name = void 0;
255
+ this.severity = void 0;
256
+ this.selector = void 0;
257
+ this.pattern = void 0;
258
+ this.lint = void 0;
259
+ this.applies = void 0;
260
+ this.message = void 0;
567
261
  if (!selector && !pattern) {
568
- throw new perseusCore.PerseusError("Lint rules must have a selector or pattern", perseusCore.Errors.InvalidInput, {
262
+ throw new PerseusError("Lint rules must have a selector or pattern", Errors.InvalidInput, {
569
263
  metadata: {
570
264
  name
571
265
  }
@@ -575,67 +269,40 @@ class Rule {
575
269
  this.severity = severity || Rule.Severity.BULK_WARNING;
576
270
  this.selector = selector || Rule.DEFAULT_SELECTOR;
577
271
  this.pattern = pattern || null;
578
-
579
- // If we're called with an error message instead of a function then
580
- // use a default function that will return the message.
581
272
  if (typeof lint === "function") {
582
273
  this.lint = lint;
583
274
  this.message = null;
584
275
  } else {
585
- this.lint = function () {
586
- return _this._defaultLintFunction(...arguments);
587
- };
276
+ this.lint = (...args) => this._defaultLintFunction(...args);
588
277
  this.message = lint;
589
278
  }
590
279
  this.applies = applies || function () {
591
280
  return true;
592
281
  };
593
282
  }
594
-
595
- // A factory method for use with rules described in JSON files
596
- // See the documentation at the start of this file for details.
597
283
  static makeRule(options) {
598
284
  return new Rule(options.name, options.severity, options.selector ? Selector.parse(options.selector) : null, Rule.makePattern(options.pattern), options.lint || options.message, options.applies);
599
285
  }
600
-
601
- // Check the node n to see if it violates this lint rule. A return value
602
- // of false means there is no lint. A returned object indicates a lint
603
- // error. See the documentation at the top of this file for details.
604
286
  check(node, traversalState, content, context) {
605
- // First, see if we match the selector.
606
- // If no selector was passed to the constructor, we use a
607
- // default selector that matches text nodes.
608
287
  const selectorMatch = this.selector.match(traversalState);
609
-
610
- // If the selector did not match, then we're done
611
288
  if (!selectorMatch) {
612
289
  return null;
613
290
  }
614
-
615
- // If the selector matched, then see if the pattern matches
616
291
  let patternMatch;
617
292
  if (this.pattern) {
618
293
  patternMatch = content.match(this.pattern);
619
294
  } else {
620
- // If there is no pattern, then just match all of the content.
621
- // Use a fake RegExp match object to represent this default match.
622
295
  patternMatch = Rule.FakePatternMatch(content, content, 0);
623
296
  }
624
-
625
- // If there was a pattern and it didn't match, then we're done
626
297
  if (!patternMatch) {
627
298
  return null;
628
299
  }
629
300
  try {
630
- // If we get here, then the selector and pattern have matched
631
- // so now we call the lint function to see if there is lint.
632
301
  const error = this.lint(traversalState, content, selectorMatch, patternMatch, context);
633
302
  if (!error) {
634
- return null; // No lint; we're done
303
+ return null;
635
304
  }
636
305
  if (typeof error === "string") {
637
- // If the lint function returned a string we assume it
638
- // applies to the entire content of the node and return it.
639
306
  return {
640
307
  rule: this.name,
641
308
  severity: this.severity,
@@ -644,8 +311,6 @@ class Rule {
644
311
  end: content.length
645
312
  };
646
313
  }
647
- // If the lint function returned an object, then we just
648
- // add the rule name to the message, start and end.
649
314
  return {
650
315
  rule: this.name,
651
316
  severity: this.severity,
@@ -654,11 +319,6 @@ class Rule {
654
319
  end: error.end
655
320
  };
656
321
  } catch (e) {
657
- // If the lint function threw an exception we handle that as
658
- // a special type of lint. We want the user to see the lint
659
- // warning in this case (even though it is out of their control)
660
- // so that the bug gets reported. Otherwise we'd never know that
661
- // a rule was failing.
662
322
  return {
663
323
  rule: "lint-rule-failure",
664
324
  message: `Exception in rule ${this.name}: ${e.message}
@@ -669,14 +329,6 @@ ${e.stack}`,
669
329
  };
670
330
  }
671
331
  }
672
-
673
- // This internal method is the default lint function that we use when a
674
- // rule is defined without a function. This is useful for rules where the
675
- // selector and/or pattern match are enough to indicate lint. This
676
- // function unconditionally returns the error message that was passed in
677
- // place of a function, but also adds start and end properties that
678
- // specify which particular portion of the node content matched the
679
- // pattern.
680
332
  _defaultLintFunction(state, content, selectorMatch, patternMatch, context) {
681
333
  return {
682
334
  message: this.message || "",
@@ -684,22 +336,6 @@ ${e.stack}`,
684
336
  end: patternMatch.index + patternMatch[0].length
685
337
  };
686
338
  }
687
-
688
- // The makeRule() factory function uses this static method to turn its
689
- // argument into a RegExp. If the argument is already a RegExp, we just
690
- // return it. Otherwise, we compile it into a RegExp and return that.
691
- // The reason this is necessary is that Rule.makeRule() is designed for
692
- // use with data from JSON files and JSON files can't include RegExp
693
- // literals. Strings passed to this function do not need to be delimited
694
- // with / characters unless you want to include flags for the RegExp.
695
- //
696
- // Examples:
697
- //
698
- // input "" ==> output null
699
- // input /foo/ ==> output /foo/
700
- // input "foo" ==> output /foo/
701
- // input "/foo/i" ==> output /foo/i
702
- //
703
339
  static makePattern(pattern) {
704
340
  if (!pattern) {
705
341
  return null;
@@ -711,40 +347,27 @@ ${e.stack}`,
711
347
  const lastSlash = pattern.lastIndexOf("/");
712
348
  const expression = pattern.substring(1, lastSlash);
713
349
  const flags = pattern.substring(lastSlash + 1);
714
- // @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"]'?
715
350
  return new RegExp(expression, flags);
716
351
  }
717
352
  return new RegExp(pattern);
718
353
  }
719
-
720
- // This static method returns an string array with index and input
721
- // properties added, in order to simulate the return value of the
722
- // String.match() method. We use it when a Rule has no pattern and we
723
- // want to simulate a match on the entire content string.
724
354
  static FakePatternMatch(input, match, index) {
725
355
  const result = [match];
726
356
  result.index = index;
727
357
  result.input = input;
728
358
  return result;
729
359
  }
730
- static Severity = {
731
- ERROR: 1,
732
- WARNING: 2,
733
- GUIDELINE: 3,
734
- BULK_WARNING: 4
735
- };
736
360
  }
361
+ Rule.DEFAULT_SELECTOR = void 0;
362
+ Rule.Severity = {
363
+ ERROR: 1,
364
+ WARNING: 2,
365
+ GUIDELINE: 3,
366
+ BULK_WARNING: 4
367
+ };
737
368
  Rule.DEFAULT_SELECTOR = Selector.parse("text");
738
369
 
739
- /* eslint-disable no-useless-escape */
740
- // Return the portion of a URL between // and /. This is the authority
741
- // portion which is usually just the hostname, but may also include
742
- // a username, password or port. We don't strip those things out because
743
- // we typically want to reject any URL that includes them
744
370
  const HOSTNAME = /\/\/([^\/]+)/;
745
-
746
- // Return the hostname of the URL, with any "www." prefix removed.
747
- // If this is a relative URL with no hostname, return an empty string.
748
371
  function getHostname(url) {
749
372
  if (!url) {
750
373
  return "";
@@ -785,7 +408,6 @@ var BlockquotedWidget = Rule.makeRule({
785
408
  widgets should not be indented.`
786
409
  });
787
410
 
788
- /* eslint-disable no-useless-escape */
789
411
  var DoubleSpacingAfterTerminal = Rule.makeRule({
790
412
  name: "double-spacing-after-terminal",
791
413
  severity: Rule.Severity.BULK_WARNING,
@@ -806,19 +428,12 @@ const stringToButtonSet = {
806
428
  "\\log": "logarithms",
807
429
  "\\ln": "logarithms"
808
430
  };
809
-
810
- /**
811
- * Rule to make sure that Expression questions that require
812
- * a specific math symbol to answer have that math symbol
813
- * available in the keypad (desktop learners can use a keyboard,
814
- * but mobile learners must use the MathInput keypad)
815
- */
816
431
  var ExpressionWidget = Rule.makeRule({
817
432
  name: "expression-widget",
818
433
  severity: Rule.Severity.WARNING,
819
434
  selector: "widget",
820
435
  lint: function (state, content, nodes, match, context) {
821
- // This rule only looks at image widgets
436
+ var _context$widgets;
822
437
  if (state.currentNode().widgetType !== "expression") {
823
438
  return;
824
439
  }
@@ -826,9 +441,7 @@ var ExpressionWidget = Rule.makeRule({
826
441
  if (!nodeId) {
827
442
  return;
828
443
  }
829
-
830
- // If it can't find a definition for the widget it does nothing
831
- const widget = context?.widgets?.[nodeId];
444
+ const widget = context == null || (_context$widgets = context.widgets) == null ? void 0 : _context$widgets[nodeId];
832
445
  if (!widget) {
833
446
  return;
834
447
  }
@@ -849,7 +462,7 @@ var ExtraContentSpacing = Rule.makeRule({
849
462
  selector: "paragraph",
850
463
  pattern: /\s+$/,
851
464
  applies: function (context) {
852
- return context?.contentType === "article";
465
+ return (context == null ? void 0 : context.contentType) === "article";
853
466
  },
854
467
  message: `No extra whitespace at the end of content blocks.`
855
468
  });
@@ -873,9 +486,6 @@ var HeadingLevelSkip = Rule.makeRule({
873
486
  lint: function (state, content, nodes, match) {
874
487
  const currentHeading = nodes[1];
875
488
  const previousHeading = nodes[0];
876
- // A heading can have a level less than, the same as
877
- // or one more than the previous heading. But going up
878
- // by 2 or more levels is not right
879
489
  if (currentHeading.level > previousHeading.level + 1) {
880
490
  return `Skipped heading level:
881
491
  this heading is level ${currentHeading.level} but
@@ -889,14 +499,10 @@ var HeadingSentenceCase = Rule.makeRule({
889
499
  severity: Rule.Severity.GUIDELINE,
890
500
  selector: "heading",
891
501
  pattern: /^\W*[a-z]/,
892
- // first letter is lowercase
893
502
  message: `First letter is lowercase:
894
503
  the first letter of a heading should be capitalized.`
895
504
  });
896
505
 
897
- // These are 3-letter and longer words that we would not expect to be
898
- // capitalized even in a title-case heading. See
899
- // http://blog.apastyle.org/apastyle/2012/03/title-case-and-sentence-case-capitalization-in-apa-style.html
900
506
  const littleWords = {
901
507
  and: true,
902
508
  nor: true,
@@ -915,41 +521,10 @@ var HeadingTitleCase = Rule.makeRule({
915
521
  pattern: /[^\s:]\s+[A-Z]+[a-z]/,
916
522
  locale: "en",
917
523
  lint: function (state, content, nodes, match) {
918
- // We want to assert that heading text is in sentence case, not
919
- // title case. The pattern above requires a capital letter at the
920
- // start of the heading and allows them after a colon, or in
921
- // acronyms that are all capitalized.
922
- //
923
- // But we can't warn just because the pattern matched because
924
- // proper nouns are also allowed bo be capitalized. We're not
925
- // going to do dictionary lookup to check for proper nouns, so
926
- // we try a heuristic: if the title is more than 3 words long
927
- // and if all the words are capitalized or are on the list of
928
- // words that don't get capitalized, then we'll assume that
929
- // the heading is incorrectly in title case and will warn.
930
- // But if there is at least one non-capitalized long word then
931
- // we're not in title case and we should not warn.
932
- //
933
- // TODO(davidflanagan): if this rule causes a lot of false
934
- // positives, we should tweak it or remove it. Note that it will
935
- // fail for headings like "World War II in Russia"
936
- //
937
- // TODO(davidflanagan): This rule is specific to English.
938
- // It is marked with a locale property above, but that is NYI
939
- //
940
- // for APA style rules for title case
941
-
942
524
  const heading = content.trim();
943
525
  let words = heading.split(/\s+/);
944
-
945
- // Remove the first word and the little words
946
526
  words.shift();
947
- words = words.filter(
948
- // eslint-disable-next-line no-prototype-builtins
949
- w => w.length > 2 && !littleWords.hasOwnProperty(w));
950
-
951
- // If there are at least 3 remaining words and all
952
- // are capitalized, then the heading is in title case.
527
+ words = words.filter(w => w.length > 2 && !littleWords.hasOwnProperty(w));
953
528
  if (words.length >= 3 && words.every(w => isCapitalized(w))) {
954
529
  return `Title-case heading:
955
530
  This heading appears to be in title-case, but should be sentence-case.
@@ -992,17 +567,9 @@ var ImageSpacesAroundUrls = Rule.makeRule({
992
567
  lint: function (state, content, nodes, match, context) {
993
568
  const image = nodes[0];
994
569
  const url = image.target;
995
-
996
- // The markdown parser strips leading and trailing spaces for us,
997
- // but they're still a problem for our translation process, so
998
- // we need to go check for them in the unparsed source string
999
- // if we have it.
1000
570
  if (context && context.content) {
1001
- // Find the url in the original content and make sure that the
1002
- // character before is '(' and the character after is ')'
1003
571
  const index = context.content.indexOf(url);
1004
572
  if (index === -1) {
1005
- // It is not an error if we didn't find it.
1006
573
  return;
1007
574
  }
1008
575
  if (context.content[index - 1] !== "(" || context.content[index + url.length] !== ")") {
@@ -1021,33 +588,17 @@ var ImageUrlEmpty = Rule.makeRule({
1021
588
  lint: function (state, content, nodes) {
1022
589
  const image = nodes[0];
1023
590
  const url = image.target;
1024
-
1025
- // If no URL is provided, an infinite spinner will be shown in articles
1026
- // overlaying the page where the image should be. This prevents the page
1027
- // from fully loading. As a result, we check for URLS with all images.
1028
591
  if (!url || !url.trim()) {
1029
592
  return "Images should have a URL";
1030
593
  }
1031
-
1032
- // NOTE(TB): Ideally there would be a check to confirm the URL works
1033
- // and leads to a valid resource, but fetching the URL would require
1034
- // linting to be able to handle async functions, which it currently
1035
- // cannot do.
1036
594
  }
1037
595
  });
1038
596
 
1039
- // Normally we have one rule per file. But since our selector class
1040
- // can't match specific widget types directly, this rule implements
1041
- // a number of image widget related rules in one place. This should
1042
- // slightly increase efficiency, but it means that if there is more
1043
- // than one problem with an image widget, the user will only see one
1044
- // problem at a time.
1045
597
  var ImageWidget = Rule.makeRule({
1046
598
  name: "image-widget",
1047
599
  severity: Rule.Severity.WARNING,
1048
600
  selector: "widget",
1049
601
  lint: function (state, content, nodes, match, context) {
1050
- // This rule only looks at image widgets
1051
602
  if (state.currentNode().widgetType !== "image") {
1052
603
  return;
1053
604
  }
@@ -1055,29 +606,21 @@ var ImageWidget = Rule.makeRule({
1055
606
  if (!nodeId) {
1056
607
  return;
1057
608
  }
1058
-
1059
- // If it can't find a definition for the widget it does nothing
1060
609
  const widget = context && context.widgets && context.widgets[nodeId];
1061
610
  if (!widget) {
1062
611
  return;
1063
612
  }
1064
-
1065
- // Make sure there is alt text
1066
613
  const alt = widget.options.alt;
1067
614
  if (!alt) {
1068
615
  return `Images should have alt text:
1069
616
  for accessibility, all images should have a text description.
1070
617
  Add a description in the "Alt Text" box of the image widget.`;
1071
618
  }
1072
-
1073
- // Make sure the alt text it is not trivial
1074
619
  if (alt.trim().length < 8) {
1075
620
  return `Images should have alt text:
1076
621
  for accessibility, all images should have descriptive alt text.
1077
622
  This image's alt text is only ${alt.trim().length} characters long.`;
1078
623
  }
1079
-
1080
- // Make sure there is no math in the caption
1081
624
  if (widget.options.caption && widget.options.caption.match(/[^\\]\$/)) {
1082
625
  return `No math in image captions:
1083
626
  Don't include math expressions in image captions.`;
@@ -1127,38 +670,19 @@ var MathAlignLinebreaks = Rule.makeRule({
1127
670
  name: "math-align-linebreaks",
1128
671
  severity: Rule.Severity.WARNING,
1129
672
  selector: "blockMath",
1130
- // Match any align block with double backslashes in it
1131
- // Use [\s\S]* instead of .* so we match newlines as well.
1132
673
  pattern: /\\begin{align}[\s\S]*\\\\[\s\S]+\\end{align}/,
1133
- // Look for double backslashes and ensure that they are
1134
- // followed by optional space and another pair of backslashes.
1135
- // Note that this rule can't know where line breaks belong so
1136
- // it can't tell whether backslashes are completely missing. It just
1137
- // enforces that you don't have the wrong number of pairs of backslashes.
1138
674
  lint: function (state, content, nodes, match) {
1139
675
  let text = match[0];
1140
676
  while (text.length) {
1141
677
  const index = text.indexOf("\\\\");
1142
678
  if (index === -1) {
1143
- // No more backslash pairs, so we found no lint
1144
679
  return;
1145
680
  }
1146
681
  text = text.substring(index + 2);
1147
-
1148
- // Now we expect to find optional spaces, another pair of
1149
- // backslashes, and more optional spaces not followed immediately
1150
- // by another pair of backslashes.
1151
682
  const nextpair = text.match(/^\s*\\\\\s*(?!\\\\)/);
1152
-
1153
- // If that does not match then we either have too few or too
1154
- // many pairs of backslashes.
1155
683
  if (!nextpair) {
1156
684
  return "Use four backslashes between lines of an align block";
1157
685
  }
1158
-
1159
- // If it did match, then, shorten the string and continue looping
1160
- // (because a single align block may have multiple lines that
1161
- // all must be separated by two sets of double backslashes).
1162
686
  text = text.substring(nextpair[0].length);
1163
687
  }
1164
688
  }
@@ -1207,10 +731,6 @@ var MathTextEmpty = Rule.makeRule({
1207
731
  message: "Empty \\text{} block in math expression"
1208
732
  });
1209
733
 
1210
- // Because no selector is specified, this rule only applies to text nodes.
1211
- // Math and code hold their content directly and do not have text nodes
1212
- // beneath them (unlike the HTML DOM) so this rule automatically does not
1213
- // apply inside $$ or ``.
1214
734
  var MathWithoutDollars = Rule.makeRule({
1215
735
  name: "math-without-dollars",
1216
736
  severity: Rule.Severity.GUIDELINE,
@@ -1233,7 +753,8 @@ var StaticWidgetInQuestionStem = Rule.makeRule({
1233
753
  severity: Rule.Severity.WARNING,
1234
754
  selector: "widget",
1235
755
  lint: (state, content, nodes, match, context) => {
1236
- if (context?.contentType !== "exercise") {
756
+ var _context$widgets;
757
+ if ((context == null ? void 0 : context.contentType) !== "exercise") {
1237
758
  return;
1238
759
  }
1239
760
  if (context.stack.includes("hint")) {
@@ -1243,7 +764,7 @@ var StaticWidgetInQuestionStem = Rule.makeRule({
1243
764
  if (!nodeId) {
1244
765
  return;
1245
766
  }
1246
- const widget = context?.widgets?.[nodeId];
767
+ const widget = context == null || (_context$widgets = context.widgets) == null ? void 0 : _context$widgets[nodeId];
1247
768
  if (!widget) {
1248
769
  return;
1249
770
  }
@@ -1271,10 +792,6 @@ Row ${r + 1} has ${rowLengths[r]} cells.`;
1271
792
  }
1272
793
  });
1273
794
 
1274
- // Because no selector is specified, this rule only applies to text nodes.
1275
- // Math and code hold their content directly and do not have text nodes
1276
- // beneath them (unlike the HTML DOM) so this rule automatically does not
1277
- // apply inside $$ or ``.
1278
795
  var UnbalancedCodeDelimiters = Rule.makeRule({
1279
796
  name: "unbalanced-code-delimiters",
1280
797
  severity: Rule.Severity.ERROR,
@@ -1299,157 +816,36 @@ var WidgetInTable = Rule.makeRule({
1299
816
  do not put widgets inside of tables.`
1300
817
  });
1301
818
 
1302
- // TODO(davidflanagan):
1303
819
  var AllRules = [AbsoluteUrl, BlockquotedMath, BlockquotedWidget, DoubleSpacingAfterTerminal, ImageUrlEmpty, ExpressionWidget, ExtraContentSpacing, HeadingLevel1, HeadingLevelSkip, HeadingSentenceCase, HeadingTitleCase, ImageAltText, ImageInTable, LinkClickHere, LongParagraph, MathAdjacent, MathAlignExtraBreak, MathAlignLinebreaks, MathEmpty, MathFrac, MathNested, MathStartsWithSpace, MathTextEmpty, NestedLists, StaticWidgetInQuestionStem, TableMissingCells, UnescapedDollar, WidgetInTable, MathWithoutDollars, UnbalancedCodeDelimiters, ImageSpacesAroundUrls, ImageWidget];
1304
820
 
1305
- /**
1306
- * TreeTransformer is a class for traversing and transforming trees. Create a
1307
- * TreeTransformer by passing the root node of the tree to the
1308
- * constructor. Then traverse that tree by calling the traverse() method. The
1309
- * argument to traverse() is a callback function that will be called once for
1310
- * each node in the tree. This is a post-order depth-first traversal: the
1311
- * callback is not called on the a way down, but on the way back up. That is,
1312
- * the children of a node are traversed before the node itself is.
1313
- *
1314
- * The traversal callback function is passed three arguments, the node being
1315
- * traversed, a TraversalState object, and the concatentated text content of
1316
- * the node and all of its descendants. The TraversalState object is the most
1317
- * most interesting argument: it has methods for querying the ancestors and
1318
- * siblings of the node, and for deleting or replacing the node. These
1319
- * transformation methods are why this class is a tree transformer and not
1320
- * just a tree traverser.
1321
- *
1322
- * A typical tree traversal looks like this:
1323
- *
1324
- * new TreeTransformer(root).traverse((node, state, content) => {
1325
- * let parent = state.parent();
1326
- * let previous = state.previousSibling();
1327
- * // etc.
1328
- * });
1329
- *
1330
- * The traverse() method descends through nodes and arrays of nodes and calls
1331
- * the traverse callback on each node on the way back up to the root of the
1332
- * tree. (Note that it only calls the callback on the nodes themselves, not
1333
- * any arrays that contain nodes.) A node is loosely defined as any object
1334
- * with a string-valued `type` property. Objects that do not have a type
1335
- * property are assumed to not be part of the tree and are not traversed. When
1336
- * traversing an array, all elements of the array are examined, and any that
1337
- * are nodes or arrays are recursively traversed. When traversing a node, all
1338
- * properties of the object are examined and any node or array values are
1339
- * recursively traversed. In typical parse trees, the children of a node are
1340
- * in a `children` or `content` array, but this class is designed to handle
1341
- * more general trees. The Perseus markdown parser, for example, produces
1342
- * nodes of type "table" that have children in the `header` and `cells`
1343
- * properties.
1344
- *
1345
- * CAUTION: the traverse() method does not make any attempt to detect
1346
- * cycles. If you call it on a cyclic graph instead of a tree, it will cause
1347
- * infinite recursion (or, more likely, a stack overflow).
1348
- *
1349
- * TODO(davidflanagan): it probably wouldn't be hard to detect cycles: when
1350
- * pushing a new node onto the containers stack we could just check that it
1351
- * isn't already there.
1352
- *
1353
- * If a node has a text-valued `content` property, it is taken to be the
1354
- * plain-text content of the node. The traverse() method concatenates these
1355
- * content strings and passes them to the traversal callback for each
1356
- * node. This means that the callback has access the full text content of its
1357
- * node and all of the nodes descendants.
1358
- *
1359
- * See the TraversalState class for more information on what information and
1360
- * methods are available to the traversal callback.
1361
- **/
1362
-
1363
- // TreeNode is the type of a node in a parse tree. The only real requirement is
1364
- // that every node has a string-valued `type` property
1365
-
1366
- // TraversalCallback is the type of the callback function passed to the
1367
- // traverse() method. It is invoked with node, state, and content arguments
1368
- // and is expected to return nothing.
1369
-
1370
- // This is the TreeTransformer class described in detail at the
1371
- // top of this file.
1372
821
  class TreeTransformer {
1373
- root;
1374
-
1375
- // To create a tree transformer, just pass the root node of the tree
1376
822
  constructor(root) {
823
+ this.root = void 0;
1377
824
  this.root = root;
1378
825
  }
1379
-
1380
- // A utility function for determing whether an arbitrary value is a node
1381
826
  static isNode(n) {
1382
827
  return n && typeof n === "object" && typeof n.type === "string";
1383
828
  }
1384
-
1385
- // Determines whether a value is a node with type "text" and has
1386
- // a text-valued `content` property.
1387
829
  static isTextNode(n) {
1388
830
  return TreeTransformer.isNode(n) && n.type === "text" && typeof n.content === "string";
1389
831
  }
1390
-
1391
- // This is the main entry point for the traverse() method. See the comment
1392
- // at the top of this file for a detailed description. Note that this
1393
- // method just creates a new TraversalState object to use for this
1394
- // traversal and then invokes the internal _traverse() method to begin the
1395
- // recursion.
1396
832
  traverse(f) {
1397
833
  this._traverse(this.root, new TraversalState(this.root), f);
1398
834
  }
1399
-
1400
- // Do a post-order traversal of node and its descendants, invoking the
1401
- // callback function f() once for each node and returning the concatenated
1402
- // text content of the node and its descendants. f() is passed three
1403
- // arguments: the current node, a TraversalState object representing the
1404
- // current state of the traversal, and a string that holds the
1405
- // concatenated text of the node and its descendants.
1406
- //
1407
- // This private method holds all the traversal logic and implementation
1408
- // details. Note that this method uses the TraversalState object to store
1409
- // information about the structure of the tree.
1410
835
  _traverse(n, state, f) {
1411
836
  let content = "";
1412
837
  if (TreeTransformer.isNode(n)) {
1413
- // If we were called on a node object, then we handle it
1414
- // this way.
1415
- const node = n; // safe cast; we just tested
1416
-
1417
- // Put the node on the stack before recursing on its children
838
+ const node = n;
1418
839
  state._containers.push(node);
1419
840
  state._ancestors.push(node);
1420
-
1421
- // Record the node's text content if it has any.
1422
- // Usually this is for nodes with a type property of "text",
1423
- // but other nodes types like "math" may also have content.
1424
- // @ts-expect-error - TS2339 - Property 'content' does not exist on type 'TreeNode'.
1425
841
  if (typeof node.content === "string") {
1426
- // @ts-expect-error - TS2339 - Property 'content' does not exist on type 'TreeNode'.
1427
842
  content = node.content;
1428
843
  }
1429
-
1430
- // Recurse on the node. If there was content above, then there
1431
- // probably won't be any children to recurse on, but we check
1432
- // anyway.
1433
- //
1434
- // If we wanted to make the traversal completely specific to the
1435
- // actual Perseus parse trees that we'll be dealing with we could
1436
- // put a switch statement here to dispatch on the node type
1437
- // property with specific recursion steps for each known type of
1438
- // node.
1439
844
  const keys = Object.keys(node);
1440
845
  keys.forEach(key => {
1441
- // Never recurse on the type property
1442
846
  if (key === "type") {
1443
847
  return;
1444
848
  }
1445
- // Ignore properties that are null or primitive and only
1446
- // recurse on objects and arrays. Note that we don't do a
1447
- // isNode() check here. That is done in the recursive call to
1448
- // _traverse(). Note that the recursive call on each child
1449
- // returns the text content of the child and we add that
1450
- // content to the content for this node. Also note that we
1451
- // push the name of the property we're recursing over onto a
1452
- // TraversalState stack.
1453
849
  const value = node[key];
1454
850
  if (value && typeof value === "object") {
1455
851
  state._indexes.push(key);
@@ -1457,186 +853,70 @@ class TreeTransformer {
1457
853
  state._indexes.pop();
1458
854
  }
1459
855
  });
1460
-
1461
- // Restore the stacks after recursing on the children
1462
856
  state._currentNode = state._ancestors.pop();
1463
857
  state._containers.pop();
1464
-
1465
- // And finally call the traversal callback for this node. Note
1466
- // that this is post-order traversal. We call the callback on the
1467
- // way back up the tree, not on the way down. That way we already
1468
- // know all the content contained within the node.
1469
858
  f(node, state, content);
1470
859
  } else if (Array.isArray(n)) {
1471
- // If we were called on an array instead of a node, then
1472
- // this is the code we use to recurse.
1473
860
  const nodes = n;
1474
-
1475
- // Push the array onto the stack. This will allow the
1476
- // TraversalState object to locate siblings of this node.
1477
861
  state._containers.push(nodes);
1478
-
1479
- // Now loop through this array and recurse on each element in it.
1480
- // Before recursing on an element, we push its array index on a
1481
- // TraversalState stack so that the TraversalState sibling methods
1482
- // can work. Note that TraversalState methods can alter the length
1483
- // of the array, and change the index of the current node, so we
1484
- // are careful here to test the array length on each iteration and
1485
- // to reset the index when we pop the stack. Also note that we
1486
- // concatentate the text content of the children.
1487
862
  let index = 0;
1488
863
  while (index < nodes.length) {
1489
864
  state._indexes.push(index);
1490
865
  content += this._traverse(nodes[index], state, f);
1491
- // Casting to convince TypeScript that this is a number
1492
866
  index = state._indexes.pop() + 1;
1493
867
  }
1494
-
1495
- // Pop the array off the stack. Note, however, that we do not call
1496
- // the traversal callback on the array. That function is only
1497
- // called for nodes, not arrays of nodes.
1498
868
  state._containers.pop();
1499
869
  }
1500
-
1501
- // The _traverse() method always returns the text content of
1502
- // this node and its children. This is the one piece of state that
1503
- // is not tracked in the TraversalState object.
1504
870
  return content;
1505
871
  }
1506
872
  }
1507
-
1508
- // An instance of this class is passed to the callback function for
1509
- // each node traversed. The class itself is not exported, but its
1510
- // methods define the API available to the traversal callback.
1511
-
1512
- /**
1513
- * This class represents the state of a tree traversal. An instance is created
1514
- * by the traverse() method of the TreeTransformer class to maintain the state
1515
- * for that traversal, and the instance is passed to the traversal callback
1516
- * function for each node that is traversed. This class is not intended to be
1517
- * instantiated directly, but is exported so that its type can be used for
1518
- * type annotaions.
1519
- **/
1520
873
  class TraversalState {
1521
- // The root node of the tree being traversed
1522
- root;
1523
-
1524
- // These are internal state properties. Use the accessor methods defined
1525
- // below instead of using these properties directly. Note that the
1526
- // _containers and _indexes stacks can have two different types of
1527
- // elements, depending on whether we just recursed on an array or on a
1528
- // node. This is hard for TypeScript to deal with, so you'll see a number of
1529
- // type casts through the any type when working with these two properties.
1530
- _currentNode;
1531
- _containers;
1532
- _indexes;
1533
- _ancestors;
1534
-
1535
- // The constructor just stores the root node and creates empty stacks.
1536
874
  constructor(root) {
875
+ this.root = void 0;
876
+ this._currentNode = void 0;
877
+ this._containers = void 0;
878
+ this._indexes = void 0;
879
+ this._ancestors = void 0;
1537
880
  this.root = root;
1538
-
1539
- // When the callback is called, this property will hold the
1540
- // node that is currently being traversed.
1541
881
  this._currentNode = null;
1542
-
1543
- // This is a stack of the objects and arrays that we've
1544
- // traversed through before reaching the currentNode.
1545
- // It is different than the ancestors array.
1546
882
  this._containers = new Stack();
1547
-
1548
- // This stack has the same number of elements as the _containers
1549
- // stack. The last element of this._indexes[] is the index of
1550
- // the current node in the object or array that is the last element
1551
- // of this._containers[]. If the last element of this._containers[] is
1552
- // an array, then the last element of this stack will be a number.
1553
- // Otherwise if the last container is an object, then the last index
1554
- // will be a string property name.
1555
883
  this._indexes = new Stack();
1556
-
1557
- // This is a stack of the ancestor nodes of the current one.
1558
- // It is different than the containers[] stack because it only
1559
- // includes nodes, not arrays.
1560
884
  this._ancestors = new Stack();
1561
885
  }
1562
-
1563
- /**
1564
- * Return the current node in the traversal. Any time the traversal
1565
- * callback is called, this method will return the name value as the
1566
- * first argument to the callback.
1567
- */
1568
886
  currentNode() {
1569
887
  return this._currentNode || this.root;
1570
888
  }
1571
-
1572
- /**
1573
- * Return the parent of the current node, if there is one, or null.
1574
- */
1575
889
  parent() {
1576
890
  return this._ancestors.top();
1577
891
  }
1578
-
1579
- /**
1580
- * Return an array of ancestor nodes. The first element of this array is
1581
- * the same as this.parent() and the last element is the root node. If we
1582
- * are currently at the root node, the the returned array will be empty.
1583
- * This method makes a copy of the internal state, so modifications to the
1584
- * returned array have no effect on the traversal.
1585
- */
1586
892
  ancestors() {
1587
893
  return this._ancestors.values();
1588
894
  }
1589
-
1590
- /**
1591
- * Return the next sibling of this node, if it has one, or null otherwise.
1592
- */
1593
895
  nextSibling() {
1594
896
  const siblings = this._containers.top();
1595
-
1596
- // If we're at the root of the tree or if the parent is an
1597
- // object instead of an array, then there are no siblings.
1598
897
  if (!siblings || !Array.isArray(siblings)) {
1599
898
  return null;
1600
899
  }
1601
-
1602
- // The top index is a number because the top container is an array
1603
900
  const index = this._indexes.top();
1604
901
  if (siblings.length > index + 1) {
1605
902
  return siblings[index + 1];
1606
903
  }
1607
- return null; // There is no next sibling
904
+ return null;
1608
905
  }
1609
-
1610
- /**
1611
- * Return the previous sibling of this node, if it has one, or null
1612
- * otherwise.
1613
- */
1614
906
  previousSibling() {
1615
907
  const siblings = this._containers.top();
1616
-
1617
- // If we're at the root of the tree or if the parent is an
1618
- // object instead of an array, then there are no siblings.
1619
908
  if (!siblings || !Array.isArray(siblings)) {
1620
909
  return null;
1621
910
  }
1622
-
1623
- // The top index is a number because the top container is an array
1624
911
  const index = this._indexes.top();
1625
912
  if (index > 0) {
1626
913
  return siblings[index - 1];
1627
914
  }
1628
- return null; // There is no previous sibling
915
+ return null;
1629
916
  }
1630
-
1631
- /**
1632
- * Remove the next sibling node (if there is one) from the tree. Returns
1633
- * the removed sibling or null. This method makes it easy to traverse a
1634
- * tree and concatenate adjacent text nodes into a single node.
1635
- */
1636
917
  removeNextSibling() {
1637
918
  const siblings = this._containers.top();
1638
919
  if (siblings && Array.isArray(siblings)) {
1639
- // top index is a number because top container is an array
1640
920
  const index = this._indexes.top();
1641
921
  if (siblings.length > index + 1) {
1642
922
  return siblings.splice(index + 1, 1)[0];
@@ -1644,123 +924,51 @@ class TraversalState {
1644
924
  }
1645
925
  return null;
1646
926
  }
1647
-
1648
- /**
1649
- * Replace the current node in the tree with the specified nodes. If no
1650
- * nodes are passed, this is a node deletion. If one node (or array) is
1651
- * passed, this is a 1-for-1 replacement. If more than one node is passed
1652
- * then this is a combination of deletion and insertion. The new node or
1653
- * nodes will not be traversed, so this method can safely be used to
1654
- * reparent the current node node beneath a new parent.
1655
- *
1656
- * This method throws an error if you attempt to replace the root node of
1657
- * the tree.
1658
- */
1659
- replace() {
927
+ replace(...replacements) {
1660
928
  const parent = this._containers.top();
1661
929
  if (!parent) {
1662
- throw new perseusCore.PerseusError("Can't replace the root of the tree", perseusCore.Errors.Internal);
1663
- }
1664
-
1665
- // The top of the container stack is either an array or an object
1666
- // and the top of the indexes stack is a corresponding array index
1667
- // or object property. This is hard for TypeScript, so we have to do some
1668
- // unsafe casting and be careful when we use which cast version
1669
- for (var _len = arguments.length, replacements = new Array(_len), _key = 0; _key < _len; _key++) {
1670
- replacements[_key] = arguments[_key];
930
+ throw new PerseusError("Can't replace the root of the tree", Errors.Internal);
1671
931
  }
1672
932
  if (Array.isArray(parent)) {
1673
933
  const index = this._indexes.top();
1674
- // For an array parent we just splice the new nodes in
1675
934
  parent.splice(index, 1, ...replacements);
1676
- // Adjust the index to account for the changed array length.
1677
- // We don't want to traverse any of the newly inserted nodes.
1678
935
  this._indexes.pop();
1679
936
  this._indexes.push(index + replacements.length - 1);
1680
937
  } else {
1681
938
  const property = this._indexes.top();
1682
- // For an object parent we care how many new nodes there are
1683
939
  if (replacements.length === 0) {
1684
- // Deletion
1685
940
  delete parent[property];
1686
941
  } else if (replacements.length === 1) {
1687
- // Replacement
1688
942
  parent[property] = replacements[0];
1689
943
  } else {
1690
- // Replace one node with an array of nodes
1691
944
  parent[property] = replacements;
1692
945
  }
1693
946
  }
1694
947
  }
1695
-
1696
- /**
1697
- * Returns true if the current node has a previous sibling and false
1698
- * otherwise. If this method returns false, then previousSibling() will
1699
- * return null, and goToPreviousSibling() will throw an error.
1700
- */
1701
948
  hasPreviousSibling() {
1702
949
  return Array.isArray(this._containers.top()) && this._indexes.top() > 0;
1703
950
  }
1704
-
1705
- /**
1706
- * Modify this traversal state object to have the state it would have had
1707
- * when visiting the previous sibling. Note that you may want to use
1708
- * clone() to make a copy before modifying the state object like this.
1709
- * This mutator method is not typically used during ordinary tree
1710
- * traversals, but is used by the Selector class for matching multi-node
1711
- * selectors.
1712
- */
1713
951
  goToPreviousSibling() {
1714
952
  if (!this.hasPreviousSibling()) {
1715
- throw new perseusCore.PerseusError("goToPreviousSibling(): node has no previous sibling", perseusCore.Errors.Internal);
953
+ throw new PerseusError("goToPreviousSibling(): node has no previous sibling", Errors.Internal);
1716
954
  }
1717
955
  this._currentNode = this.previousSibling();
1718
- // Since we know that we have a previous sibling, we know that
1719
- // the value on top of the stack is a number, but we have to do
1720
- // this unsafe cast because TypeScript doesn't know that.
1721
956
  const index = this._indexes.pop();
1722
957
  this._indexes.push(index - 1);
1723
958
  }
1724
-
1725
- /**
1726
- * Returns true if the current node has an ancestor and false otherwise.
1727
- * If this method returns false, then the parent() method will return
1728
- * null and goToParent() will throw an error
1729
- */
1730
959
  hasParent() {
1731
960
  return this._ancestors.size() !== 0;
1732
961
  }
1733
-
1734
- /**
1735
- * Modify this object to look like it will look when we (later) visit the
1736
- * parent node of this node. You should not modify the instance passed to
1737
- * the tree traversal callback. Instead, make a copy with the clone()
1738
- * method and modify that. This mutator method is not typically used
1739
- * during ordinary tree traversals, but is used by the Selector class for
1740
- * matching multi-node selectors that involve parent and ancestor
1741
- * selectors.
1742
- */
1743
962
  goToParent() {
1744
963
  if (!this.hasParent()) {
1745
- throw new perseusCore.PerseusError("goToParent(): node has no ancestor", perseusCore.Errors.NotAllowed);
964
+ throw new PerseusError("goToParent(): node has no ancestor", Errors.NotAllowed);
1746
965
  }
1747
966
  this._currentNode = this._ancestors.pop();
1748
-
1749
- // We need to pop the containers and indexes stacks at least once
1750
- // and more as needed until we restore the invariant that
1751
- // this._containers.top()[this.indexes.top()] === this._currentNode
1752
- //
1753
967
  while (this._containers.size() && this._containers.top()[this._indexes.top()] !== this._currentNode) {
1754
968
  this._containers.pop();
1755
969
  this._indexes.pop();
1756
970
  }
1757
971
  }
1758
-
1759
- /**
1760
- * Return a new TraversalState object that is a copy of this one.
1761
- * This method is useful in conjunction with the mutating methods
1762
- * goToParent() and goToPreviousSibling().
1763
- */
1764
972
  clone() {
1765
973
  const clone = new TraversalState(this.root);
1766
974
  clone._currentNode = this._currentNode;
@@ -1769,71 +977,36 @@ class TraversalState {
1769
977
  clone._ancestors = this._ancestors.clone();
1770
978
  return clone;
1771
979
  }
1772
-
1773
- /**
1774
- * Returns true if this TraversalState object is equal to that
1775
- * TraversalState object, or false otherwise. This method exists
1776
- * primarily for use by our unit tests.
1777
- */
1778
980
  equals(that) {
1779
981
  return this.root === that.root && this._currentNode === that._currentNode && this._containers.equals(that._containers) && this._indexes.equals(that._indexes) && this._ancestors.equals(that._ancestors);
1780
982
  }
1781
983
  }
1782
-
1783
- /**
1784
- * This class is an internal utility that just treats an array as a stack
1785
- * and gives us a top() method so we don't have to write expressions like
1786
- * `ancestors[ancestors.length-1]`. The values() method automatically
1787
- * copies the internal array so we don't have to worry about client code
1788
- * modifying our internal stacks. The use of this Stack abstraction makes
1789
- * the TraversalState class simpler in a number of places.
1790
- */
1791
984
  class Stack {
1792
- stack;
1793
985
  constructor(array) {
986
+ this.stack = void 0;
1794
987
  this.stack = array ? array.slice(0) : [];
1795
988
  }
1796
-
1797
- /** Push a value onto the stack. */
1798
989
  push(v) {
1799
990
  this.stack.push(v);
1800
991
  }
1801
-
1802
- /** Pop a value off of the stack. */
1803
992
  pop() {
1804
- // @ts-expect-error - TS2322 - Type 'T | undefined' is not assignable to type 'T'.
1805
993
  return this.stack.pop();
1806
994
  }
1807
-
1808
- /** Return the top value of the stack without popping it. */
1809
995
  top() {
1810
996
  return this.stack[this.stack.length - 1];
1811
997
  }
1812
-
1813
- /** Return a copy of the stack as an array */
1814
998
  values() {
1815
999
  return this.stack.slice(0);
1816
1000
  }
1817
-
1818
- /** Return the number of elements in the stack */
1819
1001
  size() {
1820
1002
  return this.stack.length;
1821
1003
  }
1822
-
1823
- /** Return a string representation of the stack */
1824
1004
  toString() {
1825
1005
  return this.stack.toString();
1826
1006
  }
1827
-
1828
- /** Return a shallow copy of the stack */
1829
1007
  clone() {
1830
1008
  return new Stack(this.stack);
1831
1009
  }
1832
-
1833
- /**
1834
- * Compare this stack to another and return true if the contents of
1835
- * the two arrays are the same.
1836
- */
1837
1010
  equals(that) {
1838
1011
  if (!that || !that.stack || that.stack.length !== this.stack.length) {
1839
1012
  return false;
@@ -1847,58 +1020,15 @@ class Stack {
1847
1020
  }
1848
1021
  }
1849
1022
 
1850
- /**
1851
- * Adds the given perseus library version information to the __perseus_debug__
1852
- * object and ensures that the object is attached to `globalThis` (`window` in
1853
- * browser environments).
1854
- *
1855
- * This allows each library to provide runtime version information to assist in
1856
- * debugging in production environments.
1857
- */
1858
- const addLibraryVersionToPerseusDebug = (libraryName, libraryVersion) => {
1859
- // If the library version is the default value, then we don't want to
1860
- // prefix it with a "v" to indicate that it is a version number.
1861
- let prefix = "v";
1862
- if (libraryVersion === "__lib_version__") {
1863
- prefix = "";
1864
- }
1865
- const formattedVersion = `${prefix}${libraryVersion}`;
1866
- if (typeof globalThis !== "undefined") {
1867
- globalThis.__perseus_debug__ = globalThis.__perseus_debug__ ?? {};
1868
- const existingVersionEntry = globalThis.__perseus_debug__[libraryName];
1869
- if (existingVersionEntry) {
1870
- // If we already have an entry and it doesn't match the registered
1871
- // version, we morph the entry into an array and log a warning.
1872
- if (existingVersionEntry !== formattedVersion) {
1873
- // Existing entry might be an array already (oops, at least 2
1874
- // versions of the library already loaded!).
1875
- const allVersions = Array.isArray(existingVersionEntry) ? existingVersionEntry : [existingVersionEntry];
1876
- allVersions.push(formattedVersion);
1877
- globalThis.__perseus_debug__[libraryName] = allVersions;
1878
-
1879
- // eslint-disable-next-line no-console
1880
- console.warn(`Multiple versions of ${libraryName} loaded on this page: ${allVersions.sort().join(", ")}`);
1881
- }
1882
- } else {
1883
- globalThis.__perseus_debug__[libraryName] = formattedVersion;
1884
- }
1885
- } else {
1886
- // eslint-disable-next-line no-console
1887
- console.warn(`globalThis not found found (${formattedVersion})`);
1888
- }
1889
- };
1890
-
1891
- // This file is processed by a Rollup plugin (replace) to inject the production
1892
1023
  const libName = "@khanacademy/perseus-linter";
1893
- const libVersion = "1.3.6";
1024
+ const libVersion = "2.0.0";
1894
1025
  addLibraryVersionToPerseusDebug(libName, libVersion);
1895
1026
 
1896
- // Define the shape of the linter context object that is passed through the
1897
- const linterContextProps = PropTypes__default["default"].shape({
1898
- contentType: PropTypes__default["default"].string,
1899
- highlightLint: PropTypes__default["default"].bool,
1900
- paths: PropTypes__default["default"].arrayOf(PropTypes__default["default"].string),
1901
- stack: PropTypes__default["default"].arrayOf(PropTypes__default["default"].string)
1027
+ const linterContextProps = PropTypes.shape({
1028
+ contentType: PropTypes.string,
1029
+ highlightLint: PropTypes.bool,
1030
+ paths: PropTypes.arrayOf(PropTypes.string),
1031
+ stack: PropTypes.arrayOf(PropTypes.string)
1902
1032
  });
1903
1033
  const linterContextDefault = {
1904
1034
  contentType: "",
@@ -1908,172 +1038,63 @@ const linterContextDefault = {
1908
1038
  };
1909
1039
 
1910
1040
  const allLintRules = AllRules.filter(r => r.severity < Rule.Severity.BULK_WARNING);
1911
-
1912
- /**
1913
- * Run the Perseus linter over the specified markdown parse tree,
1914
- * with the specified context object, and
1915
- * return a (possibly empty) array of lint warning objects. If the
1916
- * highlight argument is true, this function also modifies the parse
1917
- * tree to add "lint" nodes that can be visually rendered,
1918
- * highlighting the problems for the user. The optional rules argument
1919
- * is an array of Rule objects specifying which lint rules should be
1920
- * applied to this parse tree. When omitted, a default set of rules is used.
1921
- *
1922
- * The context object may have additional properties that some lint
1923
- * rules require:
1924
- *
1925
- * context.content is the source content string that was parsed to create
1926
- * the parse tree.
1927
- *
1928
- * context.widgets is the widgets object associated
1929
- * with the content string
1930
- *
1931
- * TODO: to make this even more general, allow the first argument to be
1932
- * a string and run the parser over it in that case? (but ignore highlight
1933
- * in that case). This would allow the one function to be used for both
1934
- * online linting and batch linting.
1935
- */
1936
- function runLinter(tree, context, highlight) {
1937
- let rules = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : allLintRules;
1041
+ function runLinter(tree, context, highlight, rules = allLintRules) {
1938
1042
  const warnings = [];
1939
1043
  const tt = new TreeTransformer(tree);
1940
-
1941
- // The markdown parser often outputs adjacent text nodes. We
1942
- // coalesce them before linting for efficiency and accuracy.
1943
1044
  tt.traverse((node, state, content) => {
1944
1045
  if (TreeTransformer.isTextNode(node)) {
1945
1046
  let next = state.nextSibling();
1946
1047
  while (TreeTransformer.isTextNode(next)) {
1947
- // @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'.
1948
1048
  node.content += next.content;
1949
1049
  state.removeNextSibling();
1950
1050
  next = state.nextSibling();
1951
1051
  }
1952
1052
  }
1953
1053
  });
1954
-
1955
- // HTML tables are complicated, and the CSS we use in
1956
- // ../components/lint.jsx to display lint does not work to
1957
- // correctly position the lint indicators in the margin when the
1958
- // lint is inside a table. So as a workaround we keep track of all
1959
- // the lint that appears within a table and move it up to the
1960
- // table element itself.
1961
- //
1962
- // It is not ideal to have to do this here,
1963
- // but it is cleaner here than fixing up the lint during rendering
1964
- // in perseus-markdown.jsx. If our lint display was simpler and
1965
- // did not require indicators in the margin, this wouldn't be a
1966
- // problem. Or, if we modified the lint display stuff so that
1967
- // indicator positioning and tooltip display were both handled
1968
- // with JavaScript (instead of pure CSS), then we could avoid this
1969
- // issue too. But using JavaScript has its own downsides: there is
1970
- // risk that the linter JavaScript would interfere with
1971
- // widget-related Javascript.
1972
1054
  let tableWarnings = [];
1973
1055
  let insideTable = false;
1974
-
1975
- // Traverse through the nodes of the parse tree. At each node, loop
1976
- // through the array of lint rules and check whether there is a
1977
- // lint violation at that node.
1978
1056
  tt.traverse((node, state, content) => {
1979
1057
  const nodeWarnings = [];
1980
-
1981
- // If our rule is only designed to be tested against a particular
1982
- // content type and we're not in that content type, we don't need to
1983
- // consider that rule.
1984
1058
  const applicableRules = rules.filter(r => r.applies(context));
1985
-
1986
- // Generate a stack so we can identify our position in the tree in
1987
- // lint rules
1988
1059
  const stack = [...context.stack];
1989
1060
  stack.push(node.type);
1990
- const nodeContext = {
1991
- ...context,
1061
+ const nodeContext = _extends({}, context, {
1992
1062
  stack: stack.join(".")
1993
- };
1063
+ });
1994
1064
  applicableRules.forEach(rule => {
1995
1065
  const warning = rule.check(node, state, content, nodeContext);
1996
1066
  if (warning) {
1997
- // The start and end locations are relative to this
1998
- // particular node, and so are not generally very useful.
1999
- // TODO: When the markdown parser saves the node
2000
- // locations in the source string then we can add
2001
- // these numbers to that one and get and absolute
2002
- // character range that will be useful
2003
1067
  if (warning.start || warning.end) {
2004
1068
  warning.target = content.substring(warning.start, warning.end);
2005
1069
  }
2006
-
2007
- // Add the warning to the list of all lint we've found
2008
1070
  warnings.push(warning);
2009
-
2010
- // If we're going to be highlighting lint, then we also
2011
- // need to keep track of warnings specific to this node.
2012
1071
  if (highlight) {
2013
1072
  nodeWarnings.push(warning);
2014
1073
  }
2015
1074
  }
2016
1075
  });
2017
-
2018
- // If we're not highlighting lint in the tree, then we're done
2019
- // traversing this node.
2020
1076
  if (!highlight) {
2021
1077
  return;
2022
1078
  }
2023
-
2024
- // If the node we are currently at is a table, and there was lint
2025
- // inside the table, then we want to add that lint here
2026
1079
  if (node.type === "table") {
2027
1080
  if (tableWarnings.length) {
2028
1081
  nodeWarnings.push(...tableWarnings);
2029
1082
  }
2030
-
2031
- // We're not in a table anymore, and don't have to remember
2032
- // the warnings for the table
2033
1083
  insideTable = false;
2034
1084
  tableWarnings = [];
2035
1085
  } else if (!insideTable) {
2036
- // Otherwise, if we are not already inside a table, check
2037
- // to see if we've entered one. Because this is a post-order
2038
- // traversal we'll see the table contents before the table itself.
2039
- // Note that once we're inside the table, we don't have to
2040
- // do this check each time... We can just wait until we ascend
2041
- // up to the table, then we'll know we're out of it.
2042
1086
  insideTable = state.ancestors().some(n => n.type === "table");
2043
1087
  }
2044
-
2045
- // If we are inside a table and there were any warnings on
2046
- // this node, then we need to save the warnings for display
2047
- // on the table itself
2048
1088
  if (insideTable && nodeWarnings.length) {
2049
- // @ts-expect-error - TS2345 - Argument of type 'any' is not assignable to parameter of type 'never'.
2050
1089
  tableWarnings.push(...nodeWarnings);
2051
1090
  }
2052
-
2053
- // If there were any warnings on this node, and if we're highlighting
2054
- // lint, then reparent the node so we can highlight it. Note that
2055
- // a single node can have multiple warnings. If this happends we
2056
- // concatenate the warnings and newline separate them. (The lint.jsx
2057
- // component that displays the warnings may want to convert the
2058
- // newlines into <br> tags.) We also provide a lint rule name
2059
- // so that lint.jsx can link to a document that provides more details
2060
- // on that particular lint rule. If there is more than one warning
2061
- // we only link to the first rule, however.
2062
- //
2063
- // Note that even if we're inside a table, we still reparent the
2064
- // linty node so that it can be highlighted. We just make a note
2065
- // of whether this lint is inside a table or not.
2066
1091
  if (nodeWarnings.length) {
2067
1092
  nodeWarnings.sort((a, b) => {
2068
1093
  return a.severity - b.severity;
2069
1094
  });
2070
1095
  if (node.type !== "text" || nodeWarnings.length > 1) {
2071
- // If the linty node is not a text node, or if there is more
2072
- // than one warning on a text node, then reparent the entire
2073
- // node under a new lint node and put the warnings there.
2074
1096
  state.replace({
2075
1097
  type: "lint",
2076
- // @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'.
2077
1098
  content: node,
2078
1099
  message: nodeWarnings.map(w => w.message).join("\n\n"),
2079
1100
  ruleName: nodeWarnings[0].rule,
@@ -2082,52 +1103,20 @@ function runLinter(tree, context, highlight) {
2082
1103
  severity: nodeWarnings[0].severity
2083
1104
  });
2084
1105
  } else {
2085
- //
2086
- // Otherwise, it is a single warning on a text node, and we
2087
- // only want to highlight the actual linty part of that string
2088
- // of text. So we want to replace the text node with (in the
2089
- // general case) three nodes:
2090
- //
2091
- // 1) A new text node that holds the non-linty prefix
2092
- //
2093
- // 2) A lint node that is the parent of a new text node
2094
- // that holds the linty part
2095
- //
2096
- // 3) A new text node that holds the non-linty suffix
2097
- //
2098
- // If the lint begins and/or ends at the boundaries of the
2099
- // original text node, then nodes 1 and/or 3 won't exist, of
2100
- // course.
2101
- //
2102
- // Note that we could generalize this to work with multple
2103
- // warnings on a text node as long as the warnings are
2104
- // non-overlapping. Hopefully, though, multiple warnings in a
2105
- // single text node will be rare in practice. Also, we don't
2106
- // have a good way to display multiple lint indicators on a
2107
- // single line, so keeping them combined in that case might
2108
- // be the best thing, anyway.
2109
- //
2110
- // @ts-expect-error - TS2339 - Property 'content' does not exist on type 'TreeNode'.
2111
- const content = node.content; // Text nodes have content
2112
- const warning = nodeWarnings[0]; // There is only one warning.
2113
- // These are the lint boundaries within the content
1106
+ const _content = node.content;
1107
+ const warning = nodeWarnings[0];
2114
1108
  const start = warning.start || 0;
2115
- const end = warning.end || content.length;
2116
- const prefix = content.substring(0, start);
2117
- const lint = content.substring(start, end);
2118
- const suffix = content.substring(end);
2119
- // TODO(FEI-5003): Give this a real type.
2120
- const replacements = []; // What we'll replace the node with
2121
-
2122
- // The prefix text node, if there is one
1109
+ const end = warning.end || _content.length;
1110
+ const prefix = _content.substring(0, start);
1111
+ const lint = _content.substring(start, end);
1112
+ const suffix = _content.substring(end);
1113
+ const replacements = [];
2123
1114
  if (prefix) {
2124
1115
  replacements.push({
2125
1116
  type: "text",
2126
1117
  content: prefix
2127
1118
  });
2128
1119
  }
2129
-
2130
- // The lint node wrapped around the linty text
2131
1120
  replacements.push({
2132
1121
  type: "lint",
2133
1122
  content: {
@@ -2139,17 +1128,12 @@ function runLinter(tree, context, highlight) {
2139
1128
  insideTable: insideTable,
2140
1129
  severity: warning.severity
2141
1130
  });
2142
-
2143
- // The suffix node, if there is one
2144
1131
  if (suffix) {
2145
1132
  replacements.push({
2146
1133
  type: "text",
2147
1134
  content: suffix
2148
1135
  });
2149
1136
  }
2150
-
2151
- // Now replace the lint text node with the one to three
2152
- // nodes in the replacement array
2153
1137
  state.replace(...replacements);
2154
1138
  }
2155
1139
  }
@@ -2158,17 +1142,10 @@ function runLinter(tree, context, highlight) {
2158
1142
  }
2159
1143
  function pushContextStack(context, name) {
2160
1144
  const stack = context.stack || [];
2161
- return {
2162
- ...context,
1145
+ return _extends({}, context, {
2163
1146
  stack: stack.concat(name)
2164
- };
1147
+ });
2165
1148
  }
2166
1149
 
2167
- exports.Rule = Rule;
2168
- exports.libVersion = libVersion;
2169
- exports.linterContextDefault = linterContextDefault;
2170
- exports.linterContextProps = linterContextProps;
2171
- exports.pushContextStack = pushContextStack;
2172
- exports.rules = allLintRules;
2173
- exports.runLinter = runLinter;
1150
+ export { Rule, libVersion, linterContextDefault, linterContextProps, pushContextStack, allLintRules as rules, runLinter };
2174
1151
  //# sourceMappingURL=index.js.map