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