@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/index.js CHANGED
@@ -6,11 +6,98 @@ var perseusCore = require('@khanacademy/perseus-core');
6
6
  var perseusUtils = require('@khanacademy/perseus-utils');
7
7
  var PropTypes = require('prop-types');
8
8
 
9
- function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
9
+ function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e : { default: e }; }
10
10
 
11
- var PropTypes__default = /*#__PURE__*/_interopDefaultLegacy(PropTypes);
11
+ var PropTypes__default = /*#__PURE__*/_interopDefaultCompat(PropTypes);
12
12
 
13
13
  /* eslint-disable no-useless-escape */
14
+ /**
15
+ * The Selector class implements a CSS-like system for matching nodes in a
16
+ * parse tree based on the structure of the tree. Create a Selector object by
17
+ * calling the static Selector.parse() method on a string that describes the
18
+ * tree structure you want to match. For example, if you want to find text
19
+ * nodes that are direct children of paragraph nodes that immediately follow
20
+ * heading nodes, you could create an appropriate selector like this:
21
+ *
22
+ * selector = Selector.parse("heading + paragraph > text");
23
+ *
24
+ * Recall from the TreeTransformer class, that we consider any object with a
25
+ * string-valued `type` property to be a tree node. The words "heading",
26
+ * "paragraph" and "text" in the selector string above specify node types and
27
+ * will match nodes in a parse tree that have `type` properties with those
28
+ * values.
29
+ *
30
+ * Selectors are designed for use during tree traversals done with the
31
+ * TreeTransformer traverse() method. To test whether the node currently being
32
+ * traversed matches a selector, simply pass the TraversalState object to the
33
+ * match() method of the Selector object. If the node does not match the
34
+ * selector, match() returns null. If it does match, then match() returns an
35
+ * array of nodes that match the selector. In the example above the first
36
+ * element of the array would be the node the heading node, the second would
37
+ * be the paragraph node that follows it, and the third would be the text node
38
+ * that is a child of the paragraph. The last element of a returned array of
39
+ * nodes is always equal to the current node of the tree traversal.
40
+ *
41
+ * Code that uses a selector might look like this:
42
+ *
43
+ * matchingNodes = selector.match(state);
44
+ * if (matchingNodes) {
45
+ * let heading = matchingNodes[0];
46
+ * let text = matchingNodes[2];
47
+ * // do something with those nodes
48
+ * }
49
+ *
50
+ * The Selector.parse() method recognizes a grammar that is similar to CSS
51
+ * selectors:
52
+ *
53
+ * selector := treeSelector (, treeSelector)*
54
+ *
55
+ * A selector is one or more comma-separated treeSelectors. A node matches
56
+ * the selector if it matches any of the treeSelectors.
57
+ *
58
+ * treeSelector := (treeSelector combinator)? nodeSelector
59
+ *
60
+ * A treeSelector is a nodeSelector optionally preceeded by a combinator
61
+ * and another tree selector. The tree selector matches if the current node
62
+ * matches the node selector and a sibling or ancestor (depending on the
63
+ * combinator) of the current node matches the optional treeSelector.
64
+ *
65
+ * combinator := ' ' | '>' | '+' | '~' // standard CSS3 combinators
66
+ *
67
+ * A combinator is a space or punctuation character that specifies the
68
+ * relationship between two nodeSelectors. A space between two
69
+ * nodeSelectors means that the first selector much match an ancestor of
70
+ * the node that matches the second selector. A '>' character means that
71
+ * the first selector must match the parent of the node matched by the
72
+ * second. The '~' combinator means that the first selector must match a
73
+ * previous sibling of the node matched by the second. And the '+' selector
74
+ * means that first selector must match the immediate previous sibling of
75
+ * the node that matched the second.
76
+ *
77
+ * nodeSelector := <IDENTIFIER> | '*'
78
+ *
79
+ * A nodeSelector is simply an identifier (a letter followed by any number
80
+ * of letters, digits, hypens, and underscores) or the wildcard asterisk
81
+ * character. A wildcard node selector matches any node. An identifier
82
+ * selector matches any node that has a `type` property whose value matches
83
+ * the identifier.
84
+ *
85
+ * If you call Selector.parse() on a string that does not match this grammar,
86
+ * it will throw an exception
87
+ *
88
+ * TODO(davidflanagan): it might be useful to allow more sophsticated node
89
+ * selector matching with attribute matches and pseudo-classes, like
90
+ * "heading[level=2]" or "paragraph:first-child"
91
+ *
92
+ * Implementation Note: this file exports a very simple Selector class but all
93
+ * the actual work is done in various internal classes. The Parser class
94
+ * parses the string representation of a selector into a parse tree that
95
+ * consists of instances of various subclasses of the Selector class. It is
96
+ * these subclasses that implement the selector matching logic, often
97
+ * depending on features of the TraversalState object from the TreeTransformer
98
+ * traversal.
99
+ */
100
+
14
101
  /**
15
102
  * This is the base class for all Selector types. The key method that all
16
103
  * selector subclasses must implement is match(). It takes a TraversalState
@@ -523,6 +610,7 @@ class SiblingCombinator extends SelectorCombinator {
523
610
  * the Perseus article or exercise that is being linted.
524
611
  */
525
612
 
613
+
526
614
  // This represents the type returned by String.match(). It is an
527
615
  // array of strings, but also has index:number and input:string properties.
528
616
  // TypeScript doesn't handle it well, so we punt and just use any.
@@ -631,6 +719,8 @@ class Rule {
631
719
  // If we get here, then the selector and pattern have matched
632
720
  // so now we call the lint function to see if there is lint.
633
721
  const error = this.lint(traversalState, content, selectorMatch, patternMatch, context);
722
+
723
+ // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
634
724
  if (!error) {
635
725
  return null; // No lint; we're done
636
726
  }
@@ -702,6 +792,7 @@ ${e.stack}`,
702
792
  // input "/foo/i" ==> output /foo/i
703
793
  //
704
794
  static makePattern(pattern) {
795
+ // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
705
796
  if (!pattern) {
706
797
  return null;
707
798
  }
@@ -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;
@@ -1301,6 +1393,11 @@ do not put widgets inside of tables.`
1301
1393
  });
1302
1394
 
1303
1395
  // TODO(davidflanagan):
1396
+ // This should probably be converted to use import and to export
1397
+ // and object that maps rule names to rules. Also, maybe this should
1398
+ // be an auto-generated file with a script that updates it any time
1399
+ // we add a new rule?
1400
+
1304
1401
  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];
1305
1402
 
1306
1403
  /**
@@ -1361,6 +1458,7 @@ var AllRules = [AbsoluteUrl, BlockquotedMath, BlockquotedWidget, DoubleSpacingAf
1361
1458
  * methods are available to the traversal callback.
1362
1459
  **/
1363
1460
 
1461
+
1364
1462
  // TreeNode is the type of a node in a parse tree. The only real requirement is
1365
1463
  // that every node has a string-valued `type` property
1366
1464
 
@@ -1596,6 +1694,7 @@ class TraversalState {
1596
1694
 
1597
1695
  // If we're at the root of the tree or if the parent is an
1598
1696
  // object instead of an array, then there are no siblings.
1697
+ // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
1599
1698
  if (!siblings || !Array.isArray(siblings)) {
1600
1699
  return null;
1601
1700
  }
@@ -1617,6 +1716,7 @@ class TraversalState {
1617
1716
 
1618
1717
  // If we're at the root of the tree or if the parent is an
1619
1718
  // object instead of an array, then there are no siblings.
1719
+ // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
1620
1720
  if (!siblings || !Array.isArray(siblings)) {
1621
1721
  return null;
1622
1722
  }
@@ -1636,6 +1736,7 @@ class TraversalState {
1636
1736
  */
1637
1737
  removeNextSibling() {
1638
1738
  const siblings = this._containers.top();
1739
+ // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
1639
1740
  if (siblings && Array.isArray(siblings)) {
1640
1741
  // top index is a number because top container is an array
1641
1742
  const index = this._indexes.top();
@@ -1659,6 +1760,7 @@ class TraversalState {
1659
1760
  */
1660
1761
  replace() {
1661
1762
  const parent = this._containers.top();
1763
+ // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
1662
1764
  if (!parent) {
1663
1765
  throw new perseusCore.PerseusError("Can't replace the root of the tree", perseusCore.Errors.Internal);
1664
1766
  }
@@ -1751,7 +1853,9 @@ class TraversalState {
1751
1853
  // and more as needed until we restore the invariant that
1752
1854
  // this._containers.top()[this.indexes.top()] === this._currentNode
1753
1855
  //
1754
- while (this._containers.size() && this._containers.top()[this._indexes.top()] !== this._currentNode) {
1856
+ while (
1857
+ // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
1858
+ this._containers.size() && this._containers.top()[this._indexes.top()] !== this._currentNode) {
1755
1859
  this._containers.pop();
1756
1860
  this._indexes.pop();
1757
1861
  }
@@ -1836,6 +1940,7 @@ class Stack {
1836
1940
  * the two arrays are the same.
1837
1941
  */
1838
1942
  equals(that) {
1943
+ // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
1839
1944
  if (!that || !that.stack || that.stack.length !== this.stack.length) {
1840
1945
  return false;
1841
1946
  }
@@ -1849,16 +1954,20 @@ class Stack {
1849
1954
  }
1850
1955
 
1851
1956
  // This file is processed by a Rollup plugin (replace) to inject the production
1957
+ // version number during the release build.
1958
+ // In dev, you'll never see the version number.
1959
+
1852
1960
  const libName = "@khanacademy/perseus-linter";
1853
- const libVersion = "1.3.7";
1961
+ const libVersion = "3.0.0";
1854
1962
  perseusUtils.addLibraryVersionToPerseusDebug(libName, libVersion);
1855
1963
 
1856
1964
  // Define the shape of the linter context object that is passed through the
1857
- const linterContextProps = PropTypes__default["default"].shape({
1858
- contentType: PropTypes__default["default"].string,
1859
- highlightLint: PropTypes__default["default"].bool,
1860
- paths: PropTypes__default["default"].arrayOf(PropTypes__default["default"].string),
1861
- stack: PropTypes__default["default"].arrayOf(PropTypes__default["default"].string)
1965
+ // tree with additional information about what we are checking.
1966
+ const linterContextProps = PropTypes__default.default.shape({
1967
+ contentType: PropTypes__default.default.string,
1968
+ highlightLint: PropTypes__default.default.bool,
1969
+ paths: PropTypes__default.default.arrayOf(PropTypes__default.default.string),
1970
+ stack: PropTypes__default.default.arrayOf(PropTypes__default.default.string)
1862
1971
  });
1863
1972
  const linterContextDefault = {
1864
1973
  contentType: "",
@@ -1984,6 +2093,7 @@ function runLinter(tree, context, highlight) {
1984
2093
  // If the node we are currently at is a table, and there was lint
1985
2094
  // inside the table, then we want to add that lint here
1986
2095
  if (node.type === "table") {
2096
+ // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
1987
2097
  if (tableWarnings.length) {
1988
2098
  nodeWarnings.push(...tableWarnings);
1989
2099
  }
@@ -2005,6 +2115,7 @@ function runLinter(tree, context, highlight) {
2005
2115
  // If we are inside a table and there were any warnings on
2006
2116
  // this node, then we need to save the warnings for display
2007
2117
  // on the table itself
2118
+ // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
2008
2119
  if (insideTable && nodeWarnings.length) {
2009
2120
  // @ts-expect-error - TS2345 - Argument of type 'any' is not assignable to parameter of type 'never'.
2010
2121
  tableWarnings.push(...nodeWarnings);
@@ -2023,6 +2134,7 @@ function runLinter(tree, context, highlight) {
2023
2134
  // Note that even if we're inside a table, we still reparent the
2024
2135
  // linty node so that it can be highlighted. We just make a note
2025
2136
  // of whether this lint is inside a table or not.
2137
+ // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
2026
2138
  if (nodeWarnings.length) {
2027
2139
  nodeWarnings.sort((a, b) => {
2028
2140
  return a.severity - b.severity;