@khanacademy/perseus-linter 1.3.7 → 3.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/es/index.js CHANGED
@@ -4,6 +4,93 @@ import { addLibraryVersionToPerseusDebug } from '@khanacademy/perseus-utils';
4
4
  import PropTypes from 'prop-types';
5
5
 
6
6
  /* eslint-disable no-useless-escape */
7
+ /**
8
+ * The Selector class implements a CSS-like system for matching nodes in a
9
+ * parse tree based on the structure of the tree. Create a Selector object by
10
+ * calling the static Selector.parse() method on a string that describes the
11
+ * tree structure you want to match. For example, if you want to find text
12
+ * nodes that are direct children of paragraph nodes that immediately follow
13
+ * heading nodes, you could create an appropriate selector like this:
14
+ *
15
+ * selector = Selector.parse("heading + paragraph > text");
16
+ *
17
+ * Recall from the TreeTransformer class, that we consider any object with a
18
+ * string-valued `type` property to be a tree node. The words "heading",
19
+ * "paragraph" and "text" in the selector string above specify node types and
20
+ * will match nodes in a parse tree that have `type` properties with those
21
+ * values.
22
+ *
23
+ * Selectors are designed for use during tree traversals done with the
24
+ * TreeTransformer traverse() method. To test whether the node currently being
25
+ * traversed matches a selector, simply pass the TraversalState object to the
26
+ * match() method of the Selector object. If the node does not match the
27
+ * selector, match() returns null. If it does match, then match() returns an
28
+ * array of nodes that match the selector. In the example above the first
29
+ * element of the array would be the node the heading node, the second would
30
+ * be the paragraph node that follows it, and the third would be the text node
31
+ * that is a child of the paragraph. The last element of a returned array of
32
+ * nodes is always equal to the current node of the tree traversal.
33
+ *
34
+ * Code that uses a selector might look like this:
35
+ *
36
+ * matchingNodes = selector.match(state);
37
+ * if (matchingNodes) {
38
+ * let heading = matchingNodes[0];
39
+ * let text = matchingNodes[2];
40
+ * // do something with those nodes
41
+ * }
42
+ *
43
+ * The Selector.parse() method recognizes a grammar that is similar to CSS
44
+ * selectors:
45
+ *
46
+ * selector := treeSelector (, treeSelector)*
47
+ *
48
+ * A selector is one or more comma-separated treeSelectors. A node matches
49
+ * the selector if it matches any of the treeSelectors.
50
+ *
51
+ * treeSelector := (treeSelector combinator)? nodeSelector
52
+ *
53
+ * A treeSelector is a nodeSelector optionally preceeded by a combinator
54
+ * and another tree selector. The tree selector matches if the current node
55
+ * matches the node selector and a sibling or ancestor (depending on the
56
+ * combinator) of the current node matches the optional treeSelector.
57
+ *
58
+ * combinator := ' ' | '>' | '+' | '~' // standard CSS3 combinators
59
+ *
60
+ * A combinator is a space or punctuation character that specifies the
61
+ * relationship between two nodeSelectors. A space between two
62
+ * nodeSelectors means that the first selector much match an ancestor of
63
+ * the node that matches the second selector. A '>' character means that
64
+ * the first selector must match the parent of the node matched by the
65
+ * second. The '~' combinator means that the first selector must match a
66
+ * previous sibling of the node matched by the second. And the '+' selector
67
+ * means that first selector must match the immediate previous sibling of
68
+ * the node that matched the second.
69
+ *
70
+ * nodeSelector := <IDENTIFIER> | '*'
71
+ *
72
+ * A nodeSelector is simply an identifier (a letter followed by any number
73
+ * of letters, digits, hypens, and underscores) or the wildcard asterisk
74
+ * character. A wildcard node selector matches any node. An identifier
75
+ * selector matches any node that has a `type` property whose value matches
76
+ * the identifier.
77
+ *
78
+ * If you call Selector.parse() on a string that does not match this grammar,
79
+ * it will throw an exception
80
+ *
81
+ * TODO(davidflanagan): it might be useful to allow more sophsticated node
82
+ * selector matching with attribute matches and pseudo-classes, like
83
+ * "heading[level=2]" or "paragraph:first-child"
84
+ *
85
+ * Implementation Note: this file exports a very simple Selector class but all
86
+ * the actual work is done in various internal classes. The Parser class
87
+ * parses the string representation of a selector into a parse tree that
88
+ * consists of instances of various subclasses of the Selector class. It is
89
+ * these subclasses that implement the selector matching logic, often
90
+ * depending on features of the TraversalState object from the TreeTransformer
91
+ * traversal.
92
+ */
93
+
7
94
  /**
8
95
  * This is the base class for all Selector types. The key method that all
9
96
  * selector subclasses must implement is match(). It takes a TraversalState
@@ -519,6 +606,7 @@ class SiblingCombinator extends SelectorCombinator {
519
606
  * the Perseus article or exercise that is being linted.
520
607
  */
521
608
 
609
+
522
610
  // This represents the type returned by String.match(). It is an
523
611
  // array of strings, but also has index:number and input:string properties.
524
612
  // TypeScript doesn't handle it well, so we punt and just use any.
@@ -628,6 +716,8 @@ class Rule {
628
716
  // If we get here, then the selector and pattern have matched
629
717
  // so now we call the lint function to see if there is lint.
630
718
  const error = this.lint(traversalState, content, selectorMatch, patternMatch, context);
719
+
720
+ // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
631
721
  if (!error) {
632
722
  return null; // No lint; we're done
633
723
  }
@@ -699,6 +789,7 @@ ${e.stack}`,
699
789
  // input "/foo/i" ==> output /foo/i
700
790
  //
701
791
  static makePattern(pattern) {
792
+ // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
702
793
  if (!pattern) {
703
794
  return null;
704
795
  }
@@ -1058,6 +1149,7 @@ var ImageWidget = Rule.makeRule({
1058
1149
  }
1059
1150
 
1060
1151
  // If it can't find a definition for the widget it does nothing
1152
+ // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
1061
1153
  const widget = context && context.widgets && context.widgets[nodeId];
1062
1154
  if (!widget) {
1063
1155
  return;
@@ -1302,6 +1394,11 @@ do not put widgets inside of tables.`
1302
1394
  });
1303
1395
 
1304
1396
  // TODO(davidflanagan):
1397
+ // This should probably be converted to use import and to export
1398
+ // and object that maps rule names to rules. Also, maybe this should
1399
+ // be an auto-generated file with a script that updates it any time
1400
+ // we add a new rule?
1401
+
1305
1402
  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];
1306
1403
 
1307
1404
  /**
@@ -1362,6 +1459,7 @@ var AllRules = [AbsoluteUrl, BlockquotedMath, BlockquotedWidget, DoubleSpacingAf
1362
1459
  * methods are available to the traversal callback.
1363
1460
  **/
1364
1461
 
1462
+
1365
1463
  // TreeNode is the type of a node in a parse tree. The only real requirement is
1366
1464
  // that every node has a string-valued `type` property
1367
1465
 
@@ -1594,6 +1692,7 @@ class TraversalState {
1594
1692
 
1595
1693
  // If we're at the root of the tree or if the parent is an
1596
1694
  // object instead of an array, then there are no siblings.
1695
+ // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
1597
1696
  if (!siblings || !Array.isArray(siblings)) {
1598
1697
  return null;
1599
1698
  }
@@ -1615,6 +1714,7 @@ class TraversalState {
1615
1714
 
1616
1715
  // If we're at the root of the tree or if the parent is an
1617
1716
  // object instead of an array, then there are no siblings.
1717
+ // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
1618
1718
  if (!siblings || !Array.isArray(siblings)) {
1619
1719
  return null;
1620
1720
  }
@@ -1634,6 +1734,7 @@ class TraversalState {
1634
1734
  */
1635
1735
  removeNextSibling() {
1636
1736
  const siblings = this._containers.top();
1737
+ // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
1637
1738
  if (siblings && Array.isArray(siblings)) {
1638
1739
  // top index is a number because top container is an array
1639
1740
  const index = this._indexes.top();
@@ -1657,6 +1758,7 @@ class TraversalState {
1657
1758
  */
1658
1759
  replace(...replacements) {
1659
1760
  const parent = this._containers.top();
1761
+ // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
1660
1762
  if (!parent) {
1661
1763
  throw new PerseusError("Can't replace the root of the tree", Errors.Internal);
1662
1764
  }
@@ -1746,7 +1848,9 @@ class TraversalState {
1746
1848
  // and more as needed until we restore the invariant that
1747
1849
  // this._containers.top()[this.indexes.top()] === this._currentNode
1748
1850
  //
1749
- while (this._containers.size() && this._containers.top()[this._indexes.top()] !== this._currentNode) {
1851
+ while (
1852
+ // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
1853
+ this._containers.size() && this._containers.top()[this._indexes.top()] !== this._currentNode) {
1750
1854
  this._containers.pop();
1751
1855
  this._indexes.pop();
1752
1856
  }
@@ -1831,6 +1935,7 @@ class Stack {
1831
1935
  * the two arrays are the same.
1832
1936
  */
1833
1937
  equals(that) {
1938
+ // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
1834
1939
  if (!that || !that.stack || that.stack.length !== this.stack.length) {
1835
1940
  return false;
1836
1941
  }
@@ -1844,11 +1949,15 @@ class Stack {
1844
1949
  }
1845
1950
 
1846
1951
  // This file is processed by a Rollup plugin (replace) to inject the production
1952
+ // version number during the release build.
1953
+ // In dev, you'll never see the version number.
1954
+
1847
1955
  const libName = "@khanacademy/perseus-linter";
1848
- const libVersion = "1.3.7";
1956
+ const libVersion = "3.0.0";
1849
1957
  addLibraryVersionToPerseusDebug(libName, libVersion);
1850
1958
 
1851
1959
  // Define the shape of the linter context object that is passed through the
1960
+ // tree with additional information about what we are checking.
1852
1961
  const linterContextProps = PropTypes.shape({
1853
1962
  contentType: PropTypes.string,
1854
1963
  highlightLint: PropTypes.bool,
@@ -1977,6 +2086,7 @@ function runLinter(tree, context, highlight, rules = allLintRules) {
1977
2086
  // If the node we are currently at is a table, and there was lint
1978
2087
  // inside the table, then we want to add that lint here
1979
2088
  if (node.type === "table") {
2089
+ // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
1980
2090
  if (tableWarnings.length) {
1981
2091
  nodeWarnings.push(...tableWarnings);
1982
2092
  }
@@ -1998,6 +2108,7 @@ function runLinter(tree, context, highlight, rules = allLintRules) {
1998
2108
  // If we are inside a table and there were any warnings on
1999
2109
  // this node, then we need to save the warnings for display
2000
2110
  // on the table itself
2111
+ // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
2001
2112
  if (insideTable && nodeWarnings.length) {
2002
2113
  // @ts-expect-error - TS2345 - Argument of type 'any' is not assignable to parameter of type 'never'.
2003
2114
  tableWarnings.push(...nodeWarnings);
@@ -2016,6 +2127,7 @@ function runLinter(tree, context, highlight, rules = allLintRules) {
2016
2127
  // Note that even if we're inside a table, we still reparent the
2017
2128
  // linty node so that it can be highlighted. We just make a note
2018
2129
  // of whether this lint is inside a table or not.
2130
+ // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
2019
2131
  if (nodeWarnings.length) {
2020
2132
  nodeWarnings.sort((a, b) => {
2021
2133
  return a.severity - b.severity;