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