markdownlint 0.24.0 → 0.26.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CONTRIBUTING.md +15 -3
- package/LICENSE +1 -1
- package/README.md +138 -87
- package/demo/markdownlint-browser.js +2028 -1364
- package/doc/CustomRules.md +15 -4
- package/doc/Rules.md +217 -26
- package/helpers/LICENSE +1 -1
- package/helpers/helpers.js +541 -188
- package/helpers/package.json +4 -1
- package/lib/cache.js +15 -25
- package/lib/constants.js +7 -0
- package/lib/markdownlint.d.ts +27 -19
- package/lib/markdownlint.js +438 -344
- package/lib/md004.js +4 -4
- package/lib/md005.js +4 -4
- package/lib/md006.js +4 -4
- package/lib/md007.js +8 -6
- package/lib/md009.js +20 -13
- package/lib/md010.js +34 -16
- package/lib/md011.js +4 -4
- package/lib/md013.js +4 -5
- package/lib/md014.js +4 -4
- package/lib/md021.js +3 -1
- package/lib/md026.js +1 -1
- package/lib/md027.js +2 -2
- package/lib/md028.js +2 -2
- package/lib/md029.js +5 -4
- package/lib/md030.js +4 -4
- package/lib/md031.js +2 -2
- package/lib/md032.js +3 -2
- package/lib/md033.js +12 -9
- package/lib/md034.js +2 -2
- package/lib/md035.js +10 -5
- package/lib/md036.js +2 -2
- package/lib/md037.js +5 -3
- package/lib/md038.js +7 -4
- package/lib/md039.js +8 -5
- package/lib/md042.js +17 -10
- package/lib/md043.js +2 -3
- package/lib/md044.js +21 -10
- package/lib/md046.js +14 -13
- package/lib/md048.js +13 -14
- package/lib/md049-md050.js +73 -0
- package/lib/md051.js +84 -0
- package/lib/md052.js +35 -0
- package/lib/md053.js +52 -0
- package/lib/rules.js +8 -6
- package/package.json +37 -32
- package/schema/.markdownlint.jsonc +30 -3
- package/schema/.markdownlint.yaml +26 -1
- package/schema/markdownlint-config-schema.json +159 -683
package/lib/markdownlint.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
const path = require("path");
|
|
6
6
|
const { promisify } = require("util");
|
|
7
7
|
const markdownIt = require("markdown-it");
|
|
8
|
+
const { deprecatedRuleNames } = require("./constants");
|
|
8
9
|
const rules = require("./rules");
|
|
9
10
|
const helpers = require("../helpers");
|
|
10
11
|
const cache = require("./cache");
|
|
@@ -14,47 +15,46 @@ const cache = require("./cache");
|
|
|
14
15
|
const dynamicRequire = (typeof __non_webpack_require__ === "undefined") ? require : /* c8 ignore next */ __non_webpack_require__;
|
|
15
16
|
// Capture native require implementation for dynamic loading of modules
|
|
16
17
|
|
|
17
|
-
const deprecatedRuleNames = [ "MD002", "MD006" ];
|
|
18
|
-
|
|
19
18
|
/**
|
|
20
19
|
* Validate the list of rules for structure and reuse.
|
|
21
20
|
*
|
|
22
21
|
* @param {Rule[]} ruleList List of rules.
|
|
23
|
-
* @
|
|
22
|
+
* @param {boolean} synchronous Whether to execute synchronously.
|
|
23
|
+
* @returns {Error | null} Error message if validation fails.
|
|
24
24
|
*/
|
|
25
|
-
function validateRuleList(ruleList) {
|
|
25
|
+
function validateRuleList(ruleList, synchronous) {
|
|
26
26
|
let result = null;
|
|
27
27
|
if (ruleList.length === rules.length) {
|
|
28
28
|
// No need to validate if only using built-in rules
|
|
29
29
|
return result;
|
|
30
30
|
}
|
|
31
31
|
const allIds = {};
|
|
32
|
-
|
|
32
|
+
for (const [ index, rule ] of ruleList.entries()) {
|
|
33
33
|
const customIndex = index - rules.length;
|
|
34
|
-
// eslint-disable-next-line jsdoc/require-jsdoc
|
|
34
|
+
// eslint-disable-next-line no-inner-declarations, jsdoc/require-jsdoc
|
|
35
35
|
function newError(property) {
|
|
36
36
|
return new Error(
|
|
37
37
|
"Property '" + property + "' of custom rule at index " +
|
|
38
38
|
customIndex + " is incorrect.");
|
|
39
39
|
}
|
|
40
|
-
[ "names", "tags" ]
|
|
40
|
+
for (const property of [ "names", "tags" ]) {
|
|
41
41
|
const value = rule[property];
|
|
42
42
|
if (!result &&
|
|
43
43
|
(!value || !Array.isArray(value) || (value.length === 0) ||
|
|
44
44
|
!value.every(helpers.isString) || value.some(helpers.isEmptyString))) {
|
|
45
45
|
result = newError(property);
|
|
46
46
|
}
|
|
47
|
-
}
|
|
48
|
-
[
|
|
47
|
+
}
|
|
48
|
+
for (const propertyInfo of [
|
|
49
49
|
[ "description", "string" ],
|
|
50
50
|
[ "function", "function" ]
|
|
51
|
-
]
|
|
51
|
+
]) {
|
|
52
52
|
const property = propertyInfo[0];
|
|
53
53
|
const value = rule[property];
|
|
54
54
|
if (!result && (!value || (typeof value !== propertyInfo[1]))) {
|
|
55
55
|
result = newError(property);
|
|
56
56
|
}
|
|
57
|
-
}
|
|
57
|
+
}
|
|
58
58
|
if (
|
|
59
59
|
!result &&
|
|
60
60
|
rule.information &&
|
|
@@ -62,25 +62,38 @@ function validateRuleList(ruleList) {
|
|
|
62
62
|
) {
|
|
63
63
|
result = newError("information");
|
|
64
64
|
}
|
|
65
|
+
if (
|
|
66
|
+
!result &&
|
|
67
|
+
(rule.asynchronous !== undefined) &&
|
|
68
|
+
(typeof rule.asynchronous !== "boolean")
|
|
69
|
+
) {
|
|
70
|
+
result = newError("asynchronous");
|
|
71
|
+
}
|
|
72
|
+
if (!result && rule.asynchronous && synchronous) {
|
|
73
|
+
result = new Error(
|
|
74
|
+
"Custom rule " + rule.names.join("/") + " at index " + customIndex +
|
|
75
|
+
" is asynchronous and can not be used in a synchronous context."
|
|
76
|
+
);
|
|
77
|
+
}
|
|
65
78
|
if (!result) {
|
|
66
|
-
rule.names
|
|
79
|
+
for (const name of rule.names) {
|
|
67
80
|
const nameUpper = name.toUpperCase();
|
|
68
81
|
if (!result && (allIds[nameUpper] !== undefined)) {
|
|
69
82
|
result = new Error("Name '" + name + "' of custom rule at index " +
|
|
70
83
|
customIndex + " is already used as a name or tag.");
|
|
71
84
|
}
|
|
72
85
|
allIds[nameUpper] = true;
|
|
73
|
-
}
|
|
74
|
-
rule.tags
|
|
86
|
+
}
|
|
87
|
+
for (const tag of rule.tags) {
|
|
75
88
|
const tagUpper = tag.toUpperCase();
|
|
76
89
|
if (!result && allIds[tagUpper]) {
|
|
77
90
|
result = new Error("Tag '" + tag + "' of custom rule at index " +
|
|
78
91
|
customIndex + " is already used as a name.");
|
|
79
92
|
}
|
|
80
93
|
allIds[tagUpper] = false;
|
|
81
|
-
}
|
|
94
|
+
}
|
|
82
95
|
}
|
|
83
|
-
}
|
|
96
|
+
}
|
|
84
97
|
return result;
|
|
85
98
|
}
|
|
86
99
|
|
|
@@ -98,10 +111,10 @@ function newResults(ruleList) {
|
|
|
98
111
|
const results = [];
|
|
99
112
|
const keys = Object.keys(lintResults);
|
|
100
113
|
keys.sort();
|
|
101
|
-
|
|
114
|
+
for (const file of keys) {
|
|
102
115
|
const fileResults = lintResults[file];
|
|
103
116
|
if (Array.isArray(fileResults)) {
|
|
104
|
-
|
|
117
|
+
for (const result of fileResults) {
|
|
105
118
|
const ruleMoniker = result.ruleNames ?
|
|
106
119
|
result.ruleNames.join("/") :
|
|
107
120
|
(result.ruleName + "/" + result.ruleAlias);
|
|
@@ -116,30 +129,32 @@ function newResults(ruleList) {
|
|
|
116
129
|
(result.errorContext ?
|
|
117
130
|
" [Context: \"" + result.errorContext + "\"]" :
|
|
118
131
|
""));
|
|
119
|
-
}
|
|
132
|
+
}
|
|
120
133
|
} else {
|
|
121
134
|
if (!ruleNameToRule) {
|
|
122
135
|
ruleNameToRule = {};
|
|
123
|
-
|
|
136
|
+
for (const rule of ruleList) {
|
|
124
137
|
const ruleName = rule.names[0].toUpperCase();
|
|
125
138
|
ruleNameToRule[ruleName] = rule;
|
|
126
|
-
}
|
|
139
|
+
}
|
|
127
140
|
}
|
|
128
|
-
Object.
|
|
141
|
+
for (const [ ruleName, ruleResults ] of Object.entries(fileResults)) {
|
|
129
142
|
const rule = ruleNameToRule[ruleName.toUpperCase()];
|
|
130
|
-
const ruleResults
|
|
131
|
-
|
|
143
|
+
for (const lineNumber of ruleResults) {
|
|
144
|
+
// @ts-ignore
|
|
132
145
|
const nameIndex = Math.min(useAlias ? 1 : 0, rule.names.length - 1);
|
|
133
146
|
const result =
|
|
134
147
|
file + ": " +
|
|
135
148
|
lineNumber + ": " +
|
|
149
|
+
// @ts-ignore
|
|
136
150
|
rule.names[nameIndex] + " " +
|
|
151
|
+
// @ts-ignore
|
|
137
152
|
rule.description;
|
|
138
153
|
results.push(result);
|
|
139
|
-
}
|
|
140
|
-
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
141
156
|
}
|
|
142
|
-
}
|
|
157
|
+
}
|
|
143
158
|
return results.join("\n");
|
|
144
159
|
}
|
|
145
160
|
Object.defineProperty(lintResults, "toString", { "value": toString });
|
|
@@ -175,34 +190,53 @@ function removeFrontMatter(content, frontMatter) {
|
|
|
175
190
|
}
|
|
176
191
|
|
|
177
192
|
/**
|
|
178
|
-
*
|
|
193
|
+
* Freeze all freeze-able members of a token and its children.
|
|
194
|
+
*
|
|
195
|
+
* @param {MarkdownItToken} token A markdown-it token.
|
|
196
|
+
* @returns {void}
|
|
197
|
+
*/
|
|
198
|
+
function freezeToken(token) {
|
|
199
|
+
if (token.attrs) {
|
|
200
|
+
for (const attr of token.attrs) {
|
|
201
|
+
Object.freeze(attr);
|
|
202
|
+
}
|
|
203
|
+
Object.freeze(token.attrs);
|
|
204
|
+
}
|
|
205
|
+
if (token.children) {
|
|
206
|
+
for (const child of token.children) {
|
|
207
|
+
freezeToken(child);
|
|
208
|
+
}
|
|
209
|
+
Object.freeze(token.children);
|
|
210
|
+
}
|
|
211
|
+
if (token.map) {
|
|
212
|
+
Object.freeze(token.map);
|
|
213
|
+
}
|
|
214
|
+
Object.freeze(token);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Annotate tokens with line/lineNumber and freeze them.
|
|
179
219
|
*
|
|
180
220
|
* @param {MarkdownItToken[]} tokens Array of markdown-it tokens.
|
|
181
221
|
* @param {string[]} lines Lines of Markdown content.
|
|
182
222
|
* @returns {void}
|
|
183
223
|
*/
|
|
184
|
-
function
|
|
185
|
-
let
|
|
186
|
-
|
|
187
|
-
//
|
|
188
|
-
if (
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
tableMap = [ ...token.map ];
|
|
193
|
-
} else if (
|
|
194
|
-
(token.type === "tr_close") &&
|
|
195
|
-
tableMap
|
|
196
|
-
) {
|
|
197
|
-
tableMap[0]++;
|
|
198
|
-
} else if (
|
|
199
|
-
(token.type === "thead_close") ||
|
|
200
|
-
(token.type === "tbody_close")
|
|
201
|
-
) {
|
|
202
|
-
tableMap = null;
|
|
224
|
+
function annotateAndFreezeTokens(tokens, lines) {
|
|
225
|
+
let trMap = null;
|
|
226
|
+
for (const token of tokens) {
|
|
227
|
+
// Provide missing maps for table content
|
|
228
|
+
if (token.type === "tr_open") {
|
|
229
|
+
trMap = token.map;
|
|
230
|
+
} else if (token.type === "tr_close") {
|
|
231
|
+
trMap = null;
|
|
203
232
|
}
|
|
204
|
-
if (
|
|
205
|
-
token.map = [ ...
|
|
233
|
+
if (!token.map && trMap) {
|
|
234
|
+
token.map = [ ...trMap ];
|
|
235
|
+
}
|
|
236
|
+
// Adjust maps for math blocks
|
|
237
|
+
if (helpers.isMathBlock(token) && token.map[1]) {
|
|
238
|
+
// markdown-it-texmath plugin does not account for math_block_end
|
|
239
|
+
token.map[1]++;
|
|
206
240
|
}
|
|
207
241
|
// Update token metadata
|
|
208
242
|
if (token.map) {
|
|
@@ -212,16 +246,17 @@ function annotateTokens(tokens, lines) {
|
|
|
212
246
|
while (token.map[1] && !((lines[token.map[1] - 1] || "").trim())) {
|
|
213
247
|
token.map[1]--;
|
|
214
248
|
}
|
|
215
|
-
|
|
216
|
-
|
|
249
|
+
}
|
|
250
|
+
// Annotate children with lineNumber
|
|
251
|
+
if (token.children) {
|
|
217
252
|
const codeSpanExtraLines = [];
|
|
218
|
-
|
|
219
|
-
token.content,
|
|
220
|
-
function handleInlineCodeSpan(code) {
|
|
253
|
+
if (token.children.some((child) => child.type === "code_inline")) {
|
|
254
|
+
helpers.forEachInlineCodeSpan(token.content, (code) => {
|
|
221
255
|
codeSpanExtraLines.push(code.split(helpers.newLineRe).length - 1);
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
let lineNumber = token.lineNumber;
|
|
259
|
+
for (const child of token.children) {
|
|
225
260
|
child.lineNumber = lineNumber;
|
|
226
261
|
child.line = lines[lineNumber - 1];
|
|
227
262
|
if ((child.type === "softbreak") || (child.type === "hardbreak")) {
|
|
@@ -229,9 +264,11 @@ function annotateTokens(tokens, lines) {
|
|
|
229
264
|
} else if (child.type === "code_inline") {
|
|
230
265
|
lineNumber += codeSpanExtraLines.shift();
|
|
231
266
|
}
|
|
232
|
-
}
|
|
267
|
+
}
|
|
233
268
|
}
|
|
234
|
-
|
|
269
|
+
freezeToken(token);
|
|
270
|
+
}
|
|
271
|
+
Object.freeze(tokens);
|
|
235
272
|
}
|
|
236
273
|
|
|
237
274
|
/**
|
|
@@ -243,24 +280,24 @@ function annotateTokens(tokens, lines) {
|
|
|
243
280
|
function mapAliasToRuleNames(ruleList) {
|
|
244
281
|
const aliasToRuleNames = {};
|
|
245
282
|
// const tagToRuleNames = {};
|
|
246
|
-
|
|
283
|
+
for (const rule of ruleList) {
|
|
247
284
|
const ruleName = rule.names[0].toUpperCase();
|
|
248
285
|
// The following is useful for updating README.md:
|
|
249
286
|
// console.log(
|
|
250
287
|
// "* **[" + ruleName + "](doc/Rules.md#" + ruleName.toLowerCase() +
|
|
251
288
|
// ")** *" + rule.names.slice(1).join("/") + "* - " + rule.description);
|
|
252
|
-
rule.names
|
|
289
|
+
for (const name of rule.names) {
|
|
253
290
|
const nameUpper = name.toUpperCase();
|
|
254
291
|
aliasToRuleNames[nameUpper] = [ ruleName ];
|
|
255
|
-
}
|
|
256
|
-
rule.tags
|
|
292
|
+
}
|
|
293
|
+
for (const tag of rule.tags) {
|
|
257
294
|
const tagUpper = tag.toUpperCase();
|
|
258
295
|
const ruleNames = aliasToRuleNames[tagUpper] || [];
|
|
259
296
|
ruleNames.push(ruleName);
|
|
260
297
|
aliasToRuleNames[tagUpper] = ruleNames;
|
|
261
298
|
// tagToRuleNames[tag] = ruleName;
|
|
262
|
-
}
|
|
263
|
-
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
264
301
|
// The following is useful for updating README.md:
|
|
265
302
|
// Object.keys(tagToRuleNames).sort().forEach(function forTag(tag) {
|
|
266
303
|
// console.log("* **" + tag + "** - " +
|
|
@@ -285,14 +322,14 @@ function getEffectiveConfig(ruleList, config, aliasToRuleNames) {
|
|
|
285
322
|
);
|
|
286
323
|
const ruleDefault = (defaultKey.length === 0) || !!config[defaultKey[0]];
|
|
287
324
|
const effectiveConfig = {};
|
|
288
|
-
|
|
325
|
+
for (const rule of ruleList) {
|
|
289
326
|
const ruleName = rule.names[0].toUpperCase();
|
|
290
327
|
effectiveConfig[ruleName] = ruleDefault;
|
|
291
|
-
}
|
|
292
|
-
|
|
328
|
+
}
|
|
329
|
+
for (const ruleName of deprecatedRuleNames) {
|
|
293
330
|
effectiveConfig[ruleName] = false;
|
|
294
|
-
}
|
|
295
|
-
Object.keys(config)
|
|
331
|
+
}
|
|
332
|
+
for (const key of Object.keys(config)) {
|
|
296
333
|
let value = config[key];
|
|
297
334
|
if (value) {
|
|
298
335
|
if (!(value instanceof Object)) {
|
|
@@ -302,13 +339,46 @@ function getEffectiveConfig(ruleList, config, aliasToRuleNames) {
|
|
|
302
339
|
value = false;
|
|
303
340
|
}
|
|
304
341
|
const keyUpper = key.toUpperCase();
|
|
305
|
-
(aliasToRuleNames[keyUpper] || [])
|
|
342
|
+
for (const ruleName of (aliasToRuleNames[keyUpper] || [])) {
|
|
306
343
|
effectiveConfig[ruleName] = value;
|
|
307
|
-
}
|
|
308
|
-
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
309
346
|
return effectiveConfig;
|
|
310
347
|
}
|
|
311
348
|
|
|
349
|
+
/**
|
|
350
|
+
* Parse the content of a configuration file.
|
|
351
|
+
*
|
|
352
|
+
* @param {string} name Name of the configuration file.
|
|
353
|
+
* @param {string} content Configuration content.
|
|
354
|
+
* @param {ConfigurationParser[] | null} [parsers] Parsing function(s).
|
|
355
|
+
* @returns {Object} Configuration object and error message.
|
|
356
|
+
*/
|
|
357
|
+
function parseConfiguration(name, content, parsers) {
|
|
358
|
+
let config = null;
|
|
359
|
+
let message = "";
|
|
360
|
+
const errors = [];
|
|
361
|
+
let index = 0;
|
|
362
|
+
// Try each parser
|
|
363
|
+
(parsers || [ JSON.parse ]).every((parser) => {
|
|
364
|
+
try {
|
|
365
|
+
config = parser(content);
|
|
366
|
+
} catch (error) {
|
|
367
|
+
errors.push(`Parser ${index++}: ${error.message}`);
|
|
368
|
+
}
|
|
369
|
+
return !config;
|
|
370
|
+
});
|
|
371
|
+
// Message if unable to parse
|
|
372
|
+
if (!config) {
|
|
373
|
+
errors.unshift(`Unable to parse '${name}'`);
|
|
374
|
+
message = errors.join("; ");
|
|
375
|
+
}
|
|
376
|
+
return {
|
|
377
|
+
config,
|
|
378
|
+
message
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
|
|
312
382
|
/**
|
|
313
383
|
* Create a mapping of enabled rules per line.
|
|
314
384
|
*
|
|
@@ -317,6 +387,7 @@ function getEffectiveConfig(ruleList, config, aliasToRuleNames) {
|
|
|
317
387
|
* @param {string[]} frontMatterLines List of front matter lines.
|
|
318
388
|
* @param {boolean} noInlineConfig Whether to allow inline configuration.
|
|
319
389
|
* @param {Configuration} config Configuration object.
|
|
390
|
+
* @param {ConfigurationParser[] | null} configParsers Configuration parsers.
|
|
320
391
|
* @param {Object.<string, string[]>} aliasToRuleNames Map of alias to rule
|
|
321
392
|
* names.
|
|
322
393
|
* @returns {Object} Effective configuration and enabled rules per line number.
|
|
@@ -327,6 +398,7 @@ function getEnabledRulesPerLineNumber(
|
|
|
327
398
|
frontMatterLines,
|
|
328
399
|
noInlineConfig,
|
|
329
400
|
config,
|
|
401
|
+
configParsers,
|
|
330
402
|
aliasToRuleNames) {
|
|
331
403
|
// Shared variables
|
|
332
404
|
let enabledRules = {};
|
|
@@ -335,92 +407,101 @@ function getEnabledRulesPerLineNumber(
|
|
|
335
407
|
const enabledRulesPerLineNumber = new Array(1 + frontMatterLines.length);
|
|
336
408
|
// Helper functions
|
|
337
409
|
// eslint-disable-next-line jsdoc/require-jsdoc
|
|
338
|
-
function handleInlineConfig(
|
|
339
|
-
const
|
|
340
|
-
input.forEach((line, lineIndex) => {
|
|
410
|
+
function handleInlineConfig(input, forEachMatch, forEachLine) {
|
|
411
|
+
for (const [ lineIndex, line ] of input.entries()) {
|
|
341
412
|
if (!noInlineConfig) {
|
|
342
413
|
let match = null;
|
|
343
|
-
while ((match = helpers.
|
|
344
|
-
const action =
|
|
345
|
-
const
|
|
414
|
+
while ((match = helpers.inlineCommentStartRe.exec(line))) {
|
|
415
|
+
const action = match[2].toUpperCase();
|
|
416
|
+
const startIndex = match.index + match[1].length;
|
|
417
|
+
const endIndex = line.indexOf("-->", startIndex);
|
|
418
|
+
if (endIndex === -1) {
|
|
419
|
+
break;
|
|
420
|
+
}
|
|
421
|
+
const parameter = line.slice(startIndex, endIndex);
|
|
346
422
|
forEachMatch(action, parameter, lineIndex + 1);
|
|
347
423
|
}
|
|
348
424
|
}
|
|
349
425
|
if (forEachLine) {
|
|
350
426
|
forEachLine();
|
|
351
427
|
}
|
|
352
|
-
}
|
|
428
|
+
}
|
|
353
429
|
}
|
|
354
430
|
// eslint-disable-next-line jsdoc/require-jsdoc
|
|
355
431
|
function configureFile(action, parameter) {
|
|
356
432
|
if (action === "CONFIGURE-FILE") {
|
|
357
|
-
|
|
358
|
-
|
|
433
|
+
const { "config": parsed } = parseConfiguration(
|
|
434
|
+
"CONFIGURE-FILE", parameter, configParsers
|
|
435
|
+
);
|
|
436
|
+
if (parsed) {
|
|
359
437
|
config = {
|
|
360
438
|
...config,
|
|
361
|
-
...
|
|
439
|
+
...parsed
|
|
362
440
|
};
|
|
363
|
-
} catch {
|
|
364
|
-
// Ignore parse errors for inline configuration
|
|
365
441
|
}
|
|
366
442
|
}
|
|
367
443
|
}
|
|
368
444
|
// eslint-disable-next-line jsdoc/require-jsdoc
|
|
369
445
|
function applyEnableDisable(action, parameter, state) {
|
|
446
|
+
state = { ...state };
|
|
370
447
|
const enabled = (action.startsWith("ENABLE"));
|
|
371
|
-
const
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
(aliasToRuleNames[nameUpper] || []).forEach((ruleName) => {
|
|
448
|
+
const trimmed = parameter && parameter.trim();
|
|
449
|
+
const items = trimmed ? trimmed.toUpperCase().split(/\s+/) : allRuleNames;
|
|
450
|
+
for (const nameUpper of items) {
|
|
451
|
+
for (const ruleName of (aliasToRuleNames[nameUpper] || [])) {
|
|
376
452
|
state[ruleName] = enabled;
|
|
377
|
-
}
|
|
378
|
-
}
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
return state;
|
|
379
456
|
}
|
|
380
457
|
// eslint-disable-next-line jsdoc/require-jsdoc
|
|
381
458
|
function enableDisableFile(action, parameter) {
|
|
382
459
|
if ((action === "ENABLE-FILE") || (action === "DISABLE-FILE")) {
|
|
383
|
-
applyEnableDisable(action, parameter, enabledRules);
|
|
460
|
+
enabledRules = applyEnableDisable(action, parameter, enabledRules);
|
|
384
461
|
}
|
|
385
462
|
}
|
|
386
463
|
// eslint-disable-next-line jsdoc/require-jsdoc
|
|
387
464
|
function captureRestoreEnableDisable(action, parameter) {
|
|
388
465
|
if (action === "CAPTURE") {
|
|
389
|
-
capturedRules =
|
|
466
|
+
capturedRules = enabledRules;
|
|
390
467
|
} else if (action === "RESTORE") {
|
|
391
|
-
enabledRules =
|
|
468
|
+
enabledRules = capturedRules;
|
|
392
469
|
} else if ((action === "ENABLE") || (action === "DISABLE")) {
|
|
393
|
-
enabledRules =
|
|
394
|
-
applyEnableDisable(action, parameter, enabledRules);
|
|
470
|
+
enabledRules = applyEnableDisable(action, parameter, enabledRules);
|
|
395
471
|
}
|
|
396
472
|
}
|
|
397
473
|
// eslint-disable-next-line jsdoc/require-jsdoc
|
|
398
474
|
function updateLineState() {
|
|
399
|
-
enabledRulesPerLineNumber.push(
|
|
475
|
+
enabledRulesPerLineNumber.push(enabledRules);
|
|
400
476
|
}
|
|
401
477
|
// eslint-disable-next-line jsdoc/require-jsdoc
|
|
402
|
-
function
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
478
|
+
function disableLineNextLine(action, parameter, lineNumber) {
|
|
479
|
+
const disableLine = (action === "DISABLE-LINE");
|
|
480
|
+
const disableNextLine = (action === "DISABLE-NEXT-LINE");
|
|
481
|
+
if (disableLine || disableNextLine) {
|
|
482
|
+
const nextLineNumber =
|
|
483
|
+
frontMatterLines.length + lineNumber + (disableNextLine ? 1 : 0);
|
|
484
|
+
enabledRulesPerLineNumber[nextLineNumber] =
|
|
485
|
+
applyEnableDisable(
|
|
486
|
+
action,
|
|
487
|
+
parameter,
|
|
488
|
+
enabledRulesPerLineNumber[nextLineNumber] || {}
|
|
489
|
+
);
|
|
409
490
|
}
|
|
410
491
|
}
|
|
411
492
|
// Handle inline comments
|
|
412
|
-
handleInlineConfig(
|
|
493
|
+
handleInlineConfig([ lines.join("\n") ], configureFile);
|
|
413
494
|
const effectiveConfig = getEffectiveConfig(
|
|
414
495
|
ruleList, config, aliasToRuleNames);
|
|
415
|
-
|
|
496
|
+
for (const rule of ruleList) {
|
|
416
497
|
const ruleName = rule.names[0].toUpperCase();
|
|
417
498
|
allRuleNames.push(ruleName);
|
|
418
499
|
enabledRules[ruleName] = !!effectiveConfig[ruleName];
|
|
419
|
-
}
|
|
500
|
+
}
|
|
420
501
|
capturedRules = enabledRules;
|
|
421
|
-
handleInlineConfig(
|
|
422
|
-
handleInlineConfig(
|
|
423
|
-
handleInlineConfig(
|
|
502
|
+
handleInlineConfig(lines, enableDisableFile);
|
|
503
|
+
handleInlineConfig(lines, captureRestoreEnableDisable, updateLineState);
|
|
504
|
+
handleInlineConfig(lines, disableLineNextLine);
|
|
424
505
|
// Return results
|
|
425
506
|
return {
|
|
426
507
|
effectiveConfig,
|
|
@@ -428,38 +509,6 @@ function getEnabledRulesPerLineNumber(
|
|
|
428
509
|
};
|
|
429
510
|
}
|
|
430
511
|
|
|
431
|
-
/**
|
|
432
|
-
* Compare function for Array.prototype.sort for ascending order of errors.
|
|
433
|
-
*
|
|
434
|
-
* @param {LintError} a First error.
|
|
435
|
-
* @param {LintError} b Second error.
|
|
436
|
-
* @returns {number} Positive value if a>b, negative value if b<a, 0 otherwise.
|
|
437
|
-
*/
|
|
438
|
-
function lineNumberComparison(a, b) {
|
|
439
|
-
return a.lineNumber - b.lineNumber;
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
/**
|
|
443
|
-
* Filter function to include everything.
|
|
444
|
-
*
|
|
445
|
-
* @returns {boolean} True.
|
|
446
|
-
*/
|
|
447
|
-
function filterAllValues() {
|
|
448
|
-
return true;
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
/**
|
|
452
|
-
* Function to return unique values from a sorted errors array.
|
|
453
|
-
*
|
|
454
|
-
* @param {LintError} value Error instance.
|
|
455
|
-
* @param {number} index Index in array.
|
|
456
|
-
* @param {LintError[]} array Array of errors.
|
|
457
|
-
* @returns {boolean} Filter value.
|
|
458
|
-
*/
|
|
459
|
-
function uniqueFilterForSortedErrors(value, index, array) {
|
|
460
|
-
return (index === 0) || (value.lineNumber > array[index - 1].lineNumber);
|
|
461
|
-
}
|
|
462
|
-
|
|
463
512
|
/**
|
|
464
513
|
* Lints a string containing Markdown content.
|
|
465
514
|
*
|
|
@@ -468,6 +517,7 @@ function uniqueFilterForSortedErrors(value, index, array) {
|
|
|
468
517
|
* @param {string} content Markdown content.
|
|
469
518
|
* @param {Object} md Instance of markdown-it.
|
|
470
519
|
* @param {Configuration} config Configuration object.
|
|
520
|
+
* @param {ConfigurationParser[] | null} configParsers Configuration parsers.
|
|
471
521
|
* @param {RegExp} frontMatter Regular expression for front matter.
|
|
472
522
|
* @param {boolean} handleRuleFailures Whether to handle exceptions in rules.
|
|
473
523
|
* @param {boolean} noInlineConfig Whether to allow inline configuration.
|
|
@@ -481,6 +531,7 @@ function lintContent(
|
|
|
481
531
|
content,
|
|
482
532
|
md,
|
|
483
533
|
config,
|
|
534
|
+
configParsers,
|
|
484
535
|
frontMatter,
|
|
485
536
|
handleRuleFailures,
|
|
486
537
|
noInlineConfig,
|
|
@@ -490,47 +541,64 @@ function lintContent(
|
|
|
490
541
|
content = content.replace(/^\uFEFF/, "");
|
|
491
542
|
// Remove front matter
|
|
492
543
|
const removeFrontMatterResult = removeFrontMatter(content, frontMatter);
|
|
493
|
-
const frontMatterLines = removeFrontMatterResult
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
// Parse content into tokens and lines
|
|
497
|
-
const tokens = md.parse(content, {});
|
|
498
|
-
const lines = content.split(helpers.newLineRe);
|
|
499
|
-
annotateTokens(tokens, lines);
|
|
500
|
-
const aliasToRuleNames = mapAliasToRuleNames(ruleList);
|
|
544
|
+
const { frontMatterLines } = removeFrontMatterResult;
|
|
545
|
+
content = removeFrontMatterResult.content;
|
|
546
|
+
// Get enabled rules per line (with HTML comments present)
|
|
501
547
|
const { effectiveConfig, enabledRulesPerLineNumber } =
|
|
502
548
|
getEnabledRulesPerLineNumber(
|
|
503
549
|
ruleList,
|
|
504
|
-
|
|
550
|
+
content.split(helpers.newLineRe),
|
|
505
551
|
frontMatterLines,
|
|
506
552
|
noInlineConfig,
|
|
507
553
|
config,
|
|
508
|
-
|
|
554
|
+
configParsers,
|
|
555
|
+
mapAliasToRuleNames(ruleList)
|
|
509
556
|
);
|
|
510
|
-
//
|
|
511
|
-
|
|
557
|
+
// Hide the content of HTML comments from rules, etc.
|
|
558
|
+
content = helpers.clearHtmlCommentText(content);
|
|
559
|
+
// Parse content into tokens and lines
|
|
560
|
+
const tokens = md.parse(content, {});
|
|
561
|
+
const lines = content.split(helpers.newLineRe);
|
|
562
|
+
annotateAndFreezeTokens(tokens, lines);
|
|
563
|
+
// Create (frozen) parameters for rules
|
|
564
|
+
const paramsBase = {
|
|
512
565
|
name,
|
|
513
566
|
tokens,
|
|
514
|
-
lines,
|
|
515
|
-
frontMatterLines
|
|
567
|
+
"lines": Object.freeze(lines),
|
|
568
|
+
"frontMatterLines": Object.freeze(frontMatterLines)
|
|
516
569
|
};
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
570
|
+
const lineMetadata =
|
|
571
|
+
helpers.getLineMetadata(paramsBase);
|
|
572
|
+
const codeBlockAndSpanRanges =
|
|
573
|
+
helpers.codeBlockAndSpanRanges(paramsBase, lineMetadata);
|
|
574
|
+
const flattenedLists =
|
|
575
|
+
helpers.flattenLists(paramsBase.tokens);
|
|
576
|
+
const htmlElementRanges =
|
|
577
|
+
helpers.htmlElementRanges(paramsBase, lineMetadata);
|
|
578
|
+
const referenceLinkImageData =
|
|
579
|
+
helpers.getReferenceLinkImageData(lineMetadata);
|
|
580
|
+
cache.set({
|
|
581
|
+
codeBlockAndSpanRanges,
|
|
582
|
+
flattenedLists,
|
|
583
|
+
htmlElementRanges,
|
|
584
|
+
lineMetadata,
|
|
585
|
+
referenceLinkImageData
|
|
586
|
+
});
|
|
520
587
|
// Function to run for each rule
|
|
521
|
-
|
|
588
|
+
let results = [];
|
|
522
589
|
// eslint-disable-next-line jsdoc/require-jsdoc
|
|
523
590
|
function forRule(rule) {
|
|
524
591
|
// Configure rule
|
|
525
|
-
const
|
|
526
|
-
const
|
|
527
|
-
|
|
592
|
+
const ruleName = rule.names[0].toUpperCase();
|
|
593
|
+
const params = {
|
|
594
|
+
...paramsBase,
|
|
595
|
+
"config": effectiveConfig[ruleName]
|
|
596
|
+
};
|
|
528
597
|
// eslint-disable-next-line jsdoc/require-jsdoc
|
|
529
598
|
function throwError(property) {
|
|
530
599
|
throw new Error(
|
|
531
600
|
"Property '" + property + "' of onError parameter is incorrect.");
|
|
532
601
|
}
|
|
533
|
-
const errors = [];
|
|
534
602
|
// eslint-disable-next-line jsdoc/require-jsdoc
|
|
535
603
|
function onError(errorInfo) {
|
|
536
604
|
if (!errorInfo ||
|
|
@@ -539,6 +607,10 @@ function lintContent(
|
|
|
539
607
|
(errorInfo.lineNumber > lines.length)) {
|
|
540
608
|
throwError("lineNumber");
|
|
541
609
|
}
|
|
610
|
+
const lineNumber = errorInfo.lineNumber + frontMatterLines.length;
|
|
611
|
+
if (!enabledRulesPerLineNumber[lineNumber][ruleName]) {
|
|
612
|
+
return;
|
|
613
|
+
}
|
|
542
614
|
if (errorInfo.detail &&
|
|
543
615
|
!helpers.isString(errorInfo.detail)) {
|
|
544
616
|
throwError("detail");
|
|
@@ -549,12 +621,12 @@ function lintContent(
|
|
|
549
621
|
}
|
|
550
622
|
if (errorInfo.range &&
|
|
551
623
|
(!Array.isArray(errorInfo.range) ||
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
624
|
+
(errorInfo.range.length !== 2) ||
|
|
625
|
+
!helpers.isNumber(errorInfo.range[0]) ||
|
|
626
|
+
(errorInfo.range[0] < 1) ||
|
|
627
|
+
!helpers.isNumber(errorInfo.range[1]) ||
|
|
628
|
+
(errorInfo.range[1] < 1) ||
|
|
629
|
+
((errorInfo.range[0] + errorInfo.range[1] - 1) >
|
|
558
630
|
lines[errorInfo.lineNumber - 1].length))) {
|
|
559
631
|
throwError("range");
|
|
560
632
|
}
|
|
@@ -599,78 +671,114 @@ function lintContent(
|
|
|
599
671
|
cleanFixInfo.insertText = fixInfo.insertText;
|
|
600
672
|
}
|
|
601
673
|
}
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
"
|
|
605
|
-
"
|
|
606
|
-
"
|
|
674
|
+
results.push({
|
|
675
|
+
lineNumber,
|
|
676
|
+
"ruleName": rule.names[0],
|
|
677
|
+
"ruleNames": rule.names,
|
|
678
|
+
"ruleDescription": rule.description,
|
|
679
|
+
"ruleInformation": rule.information ? rule.information.href : null,
|
|
680
|
+
"errorDetail": errorInfo.detail || null,
|
|
681
|
+
"errorContext": errorInfo.context || null,
|
|
682
|
+
"errorRange": errorInfo.range ? [ ...errorInfo.range ] : null,
|
|
607
683
|
"fixInfo": fixInfo ? cleanFixInfo : null
|
|
608
684
|
});
|
|
609
685
|
}
|
|
610
|
-
// Call (possibly external) rule function
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
686
|
+
// Call (possibly external) rule function to report errors
|
|
687
|
+
const catchCallsOnError = (error) => onError({
|
|
688
|
+
"lineNumber": 1,
|
|
689
|
+
"detail": `This rule threw an exception: ${error.message || error}`
|
|
690
|
+
});
|
|
691
|
+
const invokeRuleFunction = () => rule.function(params, onError);
|
|
692
|
+
if (rule.asynchronous) {
|
|
693
|
+
// Asynchronous rule, ensure it returns a Promise
|
|
694
|
+
const ruleFunctionPromise =
|
|
695
|
+
Promise.resolve().then(invokeRuleFunction);
|
|
696
|
+
return handleRuleFailures ?
|
|
697
|
+
ruleFunctionPromise.catch(catchCallsOnError) :
|
|
698
|
+
ruleFunctionPromise;
|
|
699
|
+
}
|
|
700
|
+
// Synchronous rule
|
|
701
|
+
try {
|
|
702
|
+
invokeRuleFunction();
|
|
703
|
+
} catch (error) {
|
|
704
|
+
if (handleRuleFailures) {
|
|
705
|
+
catchCallsOnError(error);
|
|
706
|
+
} else {
|
|
707
|
+
throw error;
|
|
619
708
|
}
|
|
620
|
-
} else {
|
|
621
|
-
rule.function(params, onError);
|
|
622
709
|
}
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
710
|
+
return null;
|
|
711
|
+
}
|
|
712
|
+
// eslint-disable-next-line jsdoc/require-jsdoc
|
|
713
|
+
function formatResults() {
|
|
714
|
+
// Sort results by rule name by line number
|
|
715
|
+
results.sort((a, b) => (
|
|
716
|
+
a.ruleName.localeCompare(b.ruleName) ||
|
|
717
|
+
a.lineNumber - b.lineNumber
|
|
718
|
+
));
|
|
719
|
+
if (resultVersion < 3) {
|
|
720
|
+
// Remove fixInfo and multiple errors for the same rule and line number
|
|
721
|
+
const noPrevious = {
|
|
722
|
+
"ruleName": null,
|
|
723
|
+
"lineNumber": -1
|
|
724
|
+
};
|
|
725
|
+
results = results.filter((error, index, array) => {
|
|
726
|
+
delete error.fixInfo;
|
|
727
|
+
const previous = array[index - 1] || noPrevious;
|
|
728
|
+
return (
|
|
729
|
+
(error.ruleName !== previous.ruleName) ||
|
|
730
|
+
(error.lineNumber !== previous.lineNumber)
|
|
731
|
+
);
|
|
732
|
+
});
|
|
733
|
+
}
|
|
734
|
+
if (resultVersion === 0) {
|
|
735
|
+
// Return a dictionary of rule->[line numbers]
|
|
736
|
+
const dictionary = {};
|
|
737
|
+
for (const error of results) {
|
|
738
|
+
const ruleLines = dictionary[error.ruleName] || [];
|
|
739
|
+
ruleLines.push(error.lineNumber);
|
|
740
|
+
dictionary[error.ruleName] = ruleLines;
|
|
741
|
+
}
|
|
742
|
+
// @ts-ignore
|
|
743
|
+
results = dictionary;
|
|
744
|
+
} else if (resultVersion === 1) {
|
|
745
|
+
// Use ruleAlias instead of ruleNames
|
|
746
|
+
for (const error of results) {
|
|
747
|
+
error.ruleAlias = error.ruleNames[1] || error.ruleName;
|
|
748
|
+
delete error.ruleNames;
|
|
749
|
+
}
|
|
750
|
+
} else {
|
|
751
|
+
// resultVersion 2 or 3: Remove unwanted ruleName
|
|
752
|
+
for (const error of results) {
|
|
753
|
+
delete error.ruleName;
|
|
662
754
|
}
|
|
663
755
|
}
|
|
756
|
+
return results;
|
|
664
757
|
}
|
|
665
758
|
// Run all rules
|
|
759
|
+
const ruleListAsync = ruleList.filter((rule) => rule.asynchronous);
|
|
760
|
+
const ruleListSync = ruleList.filter((rule) => !rule.asynchronous);
|
|
761
|
+
const ruleListAsyncFirst = [
|
|
762
|
+
...ruleListAsync,
|
|
763
|
+
...ruleListSync
|
|
764
|
+
];
|
|
765
|
+
const callbackSuccess = () => callback(null, formatResults());
|
|
766
|
+
const callbackError =
|
|
767
|
+
(error) => callback(error instanceof Error ? error : new Error(error));
|
|
666
768
|
try {
|
|
667
|
-
|
|
769
|
+
const ruleResults = ruleListAsyncFirst.map(forRule);
|
|
770
|
+
if (ruleListAsync.length > 0) {
|
|
771
|
+
Promise.all(ruleResults.slice(0, ruleListAsync.length))
|
|
772
|
+
.then(callbackSuccess)
|
|
773
|
+
.catch(callbackError);
|
|
774
|
+
} else {
|
|
775
|
+
callbackSuccess();
|
|
776
|
+
}
|
|
668
777
|
} catch (error) {
|
|
778
|
+
callbackError(error);
|
|
779
|
+
} finally {
|
|
669
780
|
cache.clear();
|
|
670
|
-
return callback(error);
|
|
671
781
|
}
|
|
672
|
-
cache.clear();
|
|
673
|
-
return callback(null, result);
|
|
674
782
|
}
|
|
675
783
|
|
|
676
784
|
/**
|
|
@@ -680,6 +788,7 @@ function lintContent(
|
|
|
680
788
|
* @param {string} file Path of file to lint.
|
|
681
789
|
* @param {Object} md Instance of markdown-it.
|
|
682
790
|
* @param {Configuration} config Configuration object.
|
|
791
|
+
* @param {ConfigurationParser[] | null} configParsers Configuration parsers.
|
|
683
792
|
* @param {RegExp} frontMatter Regular expression for front matter.
|
|
684
793
|
* @param {boolean} handleRuleFailures Whether to handle exceptions in rules.
|
|
685
794
|
* @param {boolean} noInlineConfig Whether to allow inline configuration.
|
|
@@ -694,6 +803,7 @@ function lintFile(
|
|
|
694
803
|
file,
|
|
695
804
|
md,
|
|
696
805
|
config,
|
|
806
|
+
configParsers,
|
|
697
807
|
frontMatter,
|
|
698
808
|
handleRuleFailures,
|
|
699
809
|
noInlineConfig,
|
|
@@ -706,8 +816,19 @@ function lintFile(
|
|
|
706
816
|
if (err) {
|
|
707
817
|
return callback(err);
|
|
708
818
|
}
|
|
709
|
-
return lintContent(
|
|
710
|
-
|
|
819
|
+
return lintContent(
|
|
820
|
+
ruleList,
|
|
821
|
+
file,
|
|
822
|
+
content,
|
|
823
|
+
md,
|
|
824
|
+
config,
|
|
825
|
+
configParsers,
|
|
826
|
+
frontMatter,
|
|
827
|
+
handleRuleFailures,
|
|
828
|
+
noInlineConfig,
|
|
829
|
+
resultVersion,
|
|
830
|
+
callback
|
|
831
|
+
);
|
|
711
832
|
}
|
|
712
833
|
// Make a/synchronous call to read file
|
|
713
834
|
if (synchronous) {
|
|
@@ -731,9 +852,10 @@ function lintInput(options, synchronous, callback) {
|
|
|
731
852
|
callback = callback || function noop() {};
|
|
732
853
|
// eslint-disable-next-line unicorn/prefer-spread
|
|
733
854
|
const ruleList = rules.concat(options.customRules || []);
|
|
734
|
-
const ruleErr = validateRuleList(ruleList);
|
|
855
|
+
const ruleErr = validateRuleList(ruleList, synchronous);
|
|
735
856
|
if (ruleErr) {
|
|
736
|
-
|
|
857
|
+
callback(ruleErr);
|
|
858
|
+
return;
|
|
737
859
|
}
|
|
738
860
|
let files = [];
|
|
739
861
|
if (Array.isArray(options.files)) {
|
|
@@ -744,114 +866,99 @@ function lintInput(options, synchronous, callback) {
|
|
|
744
866
|
const strings = options.strings || {};
|
|
745
867
|
const stringsKeys = Object.keys(strings);
|
|
746
868
|
const config = options.config || { "default": true };
|
|
869
|
+
const configParsers = options.configParsers || null;
|
|
747
870
|
const frontMatter = (options.frontMatter === undefined) ?
|
|
748
871
|
helpers.frontMatterRe : options.frontMatter;
|
|
749
872
|
const handleRuleFailures = !!options.handleRuleFailures;
|
|
750
873
|
const noInlineConfig = !!options.noInlineConfig;
|
|
751
874
|
const resultVersion = (options.resultVersion === undefined) ?
|
|
752
|
-
|
|
875
|
+
3 : options.resultVersion;
|
|
753
876
|
const md = markdownIt({ "html": true });
|
|
754
877
|
const markdownItPlugins = options.markdownItPlugins || [];
|
|
755
|
-
|
|
878
|
+
for (const plugin of markdownItPlugins) {
|
|
756
879
|
// @ts-ignore
|
|
757
880
|
md.use(...plugin);
|
|
758
|
-
}
|
|
881
|
+
}
|
|
759
882
|
const fs = options.fs || require("fs");
|
|
760
883
|
const results = newResults(ruleList);
|
|
761
884
|
let done = false;
|
|
762
|
-
|
|
763
|
-
let syncItem = null;
|
|
885
|
+
let concurrency = 0;
|
|
764
886
|
// eslint-disable-next-line jsdoc/require-jsdoc
|
|
765
|
-
function
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
887
|
+
function lintWorker() {
|
|
888
|
+
let currentItem = null;
|
|
889
|
+
// eslint-disable-next-line jsdoc/require-jsdoc
|
|
890
|
+
function lintWorkerCallback(err, result) {
|
|
891
|
+
concurrency--;
|
|
892
|
+
if (err) {
|
|
893
|
+
done = true;
|
|
894
|
+
return callback(err);
|
|
895
|
+
}
|
|
896
|
+
results[currentItem] = result;
|
|
897
|
+
if (!synchronous) {
|
|
898
|
+
lintWorker();
|
|
899
|
+
}
|
|
900
|
+
return null;
|
|
769
901
|
}
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
syncItem,
|
|
777
|
-
strings[syncItem] || "",
|
|
778
|
-
md,
|
|
779
|
-
config,
|
|
780
|
-
frontMatter,
|
|
781
|
-
handleRuleFailures,
|
|
782
|
-
noInlineConfig,
|
|
783
|
-
resultVersion,
|
|
784
|
-
syncCallback
|
|
785
|
-
);
|
|
786
|
-
}
|
|
787
|
-
if (synchronous) {
|
|
788
|
-
// Lint files synchronously
|
|
789
|
-
while (!done && (syncItem = files.shift())) {
|
|
902
|
+
if (done) {
|
|
903
|
+
// Abort for error or nothing left to do
|
|
904
|
+
} else if (files.length > 0) {
|
|
905
|
+
// Lint next file
|
|
906
|
+
concurrency++;
|
|
907
|
+
currentItem = files.shift();
|
|
790
908
|
lintFile(
|
|
791
909
|
ruleList,
|
|
792
|
-
|
|
910
|
+
currentItem,
|
|
793
911
|
md,
|
|
794
912
|
config,
|
|
913
|
+
configParsers,
|
|
795
914
|
frontMatter,
|
|
796
915
|
handleRuleFailures,
|
|
797
916
|
noInlineConfig,
|
|
798
917
|
resultVersion,
|
|
799
918
|
fs,
|
|
800
919
|
synchronous,
|
|
801
|
-
|
|
920
|
+
lintWorkerCallback
|
|
802
921
|
);
|
|
803
|
-
}
|
|
804
|
-
|
|
805
|
-
}
|
|
806
|
-
// Lint files asynchronously
|
|
807
|
-
let concurrency = 0;
|
|
808
|
-
// eslint-disable-next-line jsdoc/require-jsdoc
|
|
809
|
-
function lintConcurrently() {
|
|
810
|
-
const asyncItem = files.shift();
|
|
811
|
-
if (done) {
|
|
812
|
-
// Nothing to do
|
|
813
|
-
} else if (asyncItem) {
|
|
922
|
+
} else if ((currentItem = stringsKeys.shift())) {
|
|
923
|
+
// Lint next string
|
|
814
924
|
concurrency++;
|
|
815
|
-
|
|
925
|
+
lintContent(
|
|
816
926
|
ruleList,
|
|
817
|
-
|
|
927
|
+
currentItem,
|
|
928
|
+
strings[currentItem] || "",
|
|
818
929
|
md,
|
|
819
930
|
config,
|
|
931
|
+
configParsers,
|
|
820
932
|
frontMatter,
|
|
821
933
|
handleRuleFailures,
|
|
822
934
|
noInlineConfig,
|
|
823
935
|
resultVersion,
|
|
824
|
-
|
|
825
|
-
synchronous,
|
|
826
|
-
(err, result) => {
|
|
827
|
-
concurrency--;
|
|
828
|
-
if (err) {
|
|
829
|
-
done = true;
|
|
830
|
-
return callback(err);
|
|
831
|
-
}
|
|
832
|
-
results[asyncItem] = result;
|
|
833
|
-
lintConcurrently();
|
|
834
|
-
return null;
|
|
835
|
-
}
|
|
936
|
+
lintWorkerCallback
|
|
836
937
|
);
|
|
837
938
|
} else if (concurrency === 0) {
|
|
939
|
+
// Finish
|
|
838
940
|
done = true;
|
|
839
941
|
return callback(null, results);
|
|
840
942
|
}
|
|
841
943
|
return null;
|
|
842
944
|
}
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
945
|
+
if (synchronous) {
|
|
946
|
+
while (!done) {
|
|
947
|
+
lintWorker();
|
|
948
|
+
}
|
|
949
|
+
} else {
|
|
950
|
+
// Testing on a Raspberry Pi 4 Model B with an artificial 5ms file access
|
|
951
|
+
// delay suggests that a concurrency factor of 8 can eliminate the impact
|
|
952
|
+
// of that delay (i.e., total time is the same as with no delay).
|
|
953
|
+
lintWorker();
|
|
954
|
+
lintWorker();
|
|
955
|
+
lintWorker();
|
|
956
|
+
lintWorker();
|
|
957
|
+
lintWorker();
|
|
958
|
+
lintWorker();
|
|
959
|
+
lintWorker();
|
|
960
|
+
lintWorker();
|
|
961
|
+
}
|
|
855
962
|
}
|
|
856
963
|
|
|
857
964
|
/**
|
|
@@ -874,6 +981,7 @@ const markdownlintPromisify = promisify && promisify(markdownlint);
|
|
|
874
981
|
* @returns {Promise<LintResults>} Results object.
|
|
875
982
|
*/
|
|
876
983
|
function markdownlintPromise(options) {
|
|
984
|
+
// @ts-ignore
|
|
877
985
|
return markdownlintPromisify(options);
|
|
878
986
|
}
|
|
879
987
|
|
|
@@ -884,48 +992,17 @@ function markdownlintPromise(options) {
|
|
|
884
992
|
* @returns {LintResults} Results object.
|
|
885
993
|
*/
|
|
886
994
|
function markdownlintSync(options) {
|
|
887
|
-
let results =
|
|
995
|
+
let results = {};
|
|
888
996
|
lintInput(options, true, function callback(error, res) {
|
|
889
997
|
if (error) {
|
|
890
998
|
throw error;
|
|
891
999
|
}
|
|
892
1000
|
results = res;
|
|
893
1001
|
});
|
|
1002
|
+
// @ts-ignore
|
|
894
1003
|
return results;
|
|
895
1004
|
}
|
|
896
1005
|
|
|
897
|
-
/**
|
|
898
|
-
* Parse the content of a configuration file.
|
|
899
|
-
*
|
|
900
|
-
* @param {string} name Name of the configuration file.
|
|
901
|
-
* @param {string} content Configuration content.
|
|
902
|
-
* @param {ConfigurationParser[]} parsers Parsing function(s).
|
|
903
|
-
* @returns {Object} Configuration object and error message.
|
|
904
|
-
*/
|
|
905
|
-
function parseConfiguration(name, content, parsers) {
|
|
906
|
-
let config = null;
|
|
907
|
-
let message = "";
|
|
908
|
-
const errors = [];
|
|
909
|
-
// Try each parser
|
|
910
|
-
(parsers || [ JSON.parse ]).every((parser) => {
|
|
911
|
-
try {
|
|
912
|
-
config = parser(content);
|
|
913
|
-
} catch (error) {
|
|
914
|
-
errors.push(error.message);
|
|
915
|
-
}
|
|
916
|
-
return !config;
|
|
917
|
-
});
|
|
918
|
-
// Message if unable to parse
|
|
919
|
-
if (!config) {
|
|
920
|
-
errors.unshift(`Unable to parse '${name}'`);
|
|
921
|
-
message = errors.join("; ");
|
|
922
|
-
}
|
|
923
|
-
return {
|
|
924
|
-
config,
|
|
925
|
-
message
|
|
926
|
-
};
|
|
927
|
-
}
|
|
928
|
-
|
|
929
1006
|
/**
|
|
930
1007
|
* Resolve referenced "extends" path in a configuration file
|
|
931
1008
|
* using path.resolve() with require.resolve() as a fallback.
|
|
@@ -933,7 +1010,7 @@ function parseConfiguration(name, content, parsers) {
|
|
|
933
1010
|
* @param {string} configFile Configuration file name.
|
|
934
1011
|
* @param {string} referenceId Referenced identifier to resolve.
|
|
935
1012
|
* @param {Object} fs File system implementation.
|
|
936
|
-
* @param {ResolveConfigExtendsCallback}
|
|
1013
|
+
* @param {ResolveConfigExtendsCallback} callback Callback (err, result)
|
|
937
1014
|
* function.
|
|
938
1015
|
* @returns {void}
|
|
939
1016
|
*/
|
|
@@ -1003,6 +1080,7 @@ function readConfig(file, parsers, fs, callback) {
|
|
|
1003
1080
|
} else {
|
|
1004
1081
|
// @ts-ignore
|
|
1005
1082
|
callback = parsers;
|
|
1083
|
+
// @ts-ignore
|
|
1006
1084
|
parsers = null;
|
|
1007
1085
|
}
|
|
1008
1086
|
}
|
|
@@ -1010,14 +1088,18 @@ function readConfig(file, parsers, fs, callback) {
|
|
|
1010
1088
|
fs = require("fs");
|
|
1011
1089
|
}
|
|
1012
1090
|
// Read file
|
|
1091
|
+
const os = require("os");
|
|
1092
|
+
file = helpers.expandTildePath(file, os);
|
|
1013
1093
|
fs.readFile(file, "utf8", (err, content) => {
|
|
1014
1094
|
if (err) {
|
|
1095
|
+
// @ts-ignore
|
|
1015
1096
|
return callback(err);
|
|
1016
1097
|
}
|
|
1017
1098
|
// Try to parse file
|
|
1018
1099
|
// @ts-ignore
|
|
1019
1100
|
const { config, message } = parseConfiguration(file, content, parsers);
|
|
1020
1101
|
if (!config) {
|
|
1102
|
+
// @ts-ignore
|
|
1021
1103
|
return callback(new Error(message));
|
|
1022
1104
|
}
|
|
1023
1105
|
// Extend configuration
|
|
@@ -1026,16 +1108,19 @@ function readConfig(file, parsers, fs, callback) {
|
|
|
1026
1108
|
delete config.extends;
|
|
1027
1109
|
return resolveConfigExtends(
|
|
1028
1110
|
file,
|
|
1029
|
-
configExtends,
|
|
1111
|
+
helpers.expandTildePath(configExtends, os),
|
|
1030
1112
|
fs,
|
|
1031
1113
|
(_, resolvedExtends) => readConfig(
|
|
1114
|
+
// @ts-ignore
|
|
1032
1115
|
resolvedExtends,
|
|
1033
1116
|
parsers,
|
|
1034
1117
|
fs,
|
|
1035
1118
|
(errr, extendsConfig) => {
|
|
1036
1119
|
if (errr) {
|
|
1120
|
+
// @ts-ignore
|
|
1037
1121
|
return callback(errr);
|
|
1038
1122
|
}
|
|
1123
|
+
// @ts-ignore
|
|
1039
1124
|
return callback(null, {
|
|
1040
1125
|
...extendsConfig,
|
|
1041
1126
|
...config
|
|
@@ -1044,6 +1129,7 @@ function readConfig(file, parsers, fs, callback) {
|
|
|
1044
1129
|
)
|
|
1045
1130
|
);
|
|
1046
1131
|
}
|
|
1132
|
+
// @ts-ignore
|
|
1047
1133
|
return callback(null, config);
|
|
1048
1134
|
});
|
|
1049
1135
|
}
|
|
@@ -1077,6 +1163,8 @@ function readConfigSync(file, parsers, fs) {
|
|
|
1077
1163
|
fs = require("fs");
|
|
1078
1164
|
}
|
|
1079
1165
|
// Read file
|
|
1166
|
+
const os = require("os");
|
|
1167
|
+
file = helpers.expandTildePath(file, os);
|
|
1080
1168
|
const content = fs.readFileSync(file, "utf8");
|
|
1081
1169
|
// Try to parse file
|
|
1082
1170
|
const { config, message } = parseConfiguration(file, content, parsers);
|
|
@@ -1087,7 +1175,11 @@ function readConfigSync(file, parsers, fs) {
|
|
|
1087
1175
|
const configExtends = config.extends;
|
|
1088
1176
|
if (configExtends) {
|
|
1089
1177
|
delete config.extends;
|
|
1090
|
-
const resolvedExtends = resolveConfigExtendsSync(
|
|
1178
|
+
const resolvedExtends = resolveConfigExtendsSync(
|
|
1179
|
+
file,
|
|
1180
|
+
helpers.expandTildePath(configExtends, os),
|
|
1181
|
+
fs
|
|
1182
|
+
);
|
|
1091
1183
|
return {
|
|
1092
1184
|
...readConfigSync(resolvedExtends, parsers, fs),
|
|
1093
1185
|
...config
|
|
@@ -1102,7 +1194,7 @@ function readConfigSync(file, parsers, fs) {
|
|
|
1102
1194
|
* @returns {string} SemVer string.
|
|
1103
1195
|
*/
|
|
1104
1196
|
function getVersion() {
|
|
1105
|
-
return require("
|
|
1197
|
+
return require("./constants").version;
|
|
1106
1198
|
}
|
|
1107
1199
|
|
|
1108
1200
|
// Export a/synchronous/Promise APIs
|
|
@@ -1172,7 +1264,7 @@ module.exports = markdownlint;
|
|
|
1172
1264
|
*
|
|
1173
1265
|
* @typedef {Object} RuleOnErrorInfo
|
|
1174
1266
|
* @property {number} lineNumber Line number (1-based).
|
|
1175
|
-
* @property {string} [
|
|
1267
|
+
* @property {string} [detail] Detail about the error.
|
|
1176
1268
|
* @property {string} [context] Context for the error.
|
|
1177
1269
|
* @property {number[]} [range] Column number (1-based) and length.
|
|
1178
1270
|
* @property {RuleOnErrorFixInfo} [fixInfo] Fix information.
|
|
@@ -1196,6 +1288,7 @@ module.exports = markdownlint;
|
|
|
1196
1288
|
* @property {string} description Rule description.
|
|
1197
1289
|
* @property {URL} [information] Link to more information.
|
|
1198
1290
|
* @property {string[]} tags Rule tag(s).
|
|
1291
|
+
* @property {boolean} [asynchronous] True if asynchronous.
|
|
1199
1292
|
* @property {RuleFunction} function Rule implementation.
|
|
1200
1293
|
*/
|
|
1201
1294
|
|
|
@@ -1203,16 +1296,17 @@ module.exports = markdownlint;
|
|
|
1203
1296
|
* Configuration options.
|
|
1204
1297
|
*
|
|
1205
1298
|
* @typedef {Object} Options
|
|
1206
|
-
* @property {string[] | string} [files] Files to lint.
|
|
1207
|
-
* @property {Object.<string, string>} [strings] Strings to lint.
|
|
1208
1299
|
* @property {Configuration} [config] Configuration object.
|
|
1300
|
+
* @property {ConfigurationParser[]} [configParsers] Configuration parsers.
|
|
1209
1301
|
* @property {Rule[] | Rule} [customRules] Custom rules.
|
|
1302
|
+
* @property {string[] | string} [files] Files to lint.
|
|
1210
1303
|
* @property {RegExp} [frontMatter] Front matter pattern.
|
|
1304
|
+
* @property {Object} [fs] File system implementation.
|
|
1211
1305
|
* @property {boolean} [handleRuleFailures] True to catch exceptions.
|
|
1306
|
+
* @property {Plugin[]} [markdownItPlugins] Additional plugins.
|
|
1212
1307
|
* @property {boolean} [noInlineConfig] True to ignore HTML directives.
|
|
1213
1308
|
* @property {number} [resultVersion] Results object version.
|
|
1214
|
-
* @property {
|
|
1215
|
-
* @property {Object} [fs] File system implementation.
|
|
1309
|
+
* @property {Object.<string, string>} [strings] Strings to lint.
|
|
1216
1310
|
*/
|
|
1217
1311
|
|
|
1218
1312
|
/**
|