@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/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 (
|
|
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 = "
|
|
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;
|