@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 +114 -2
- package/dist/es/index.js.map +1 -1
- package/dist/index.js +121 -9
- package/dist/index.js.map +1 -1
- package/package.json +5 -5
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
|
|
9
|
+
function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e : { default: e }; }
|
|
10
10
|
|
|
11
|
-
var PropTypes__default = /*#__PURE__*/
|
|
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 (
|
|
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 = "
|
|
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
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
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;
|