markdownlint 0.20.0 → 0.20.4
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 +47 -47
- package/LICENSE +21 -21
- package/README.md +881 -868
- package/demo/markdownlint-browser.js +5438 -5306
- package/doc/CustomRules.md +313 -313
- package/doc/Rules.md +1847 -1795
- package/helpers/LICENSE +21 -21
- package/helpers/README.md +50 -50
- package/helpers/helpers.js +687 -627
- package/helpers/package.json +18 -18
- package/lib/cache.js +24 -24
- package/lib/markdownlint.d.ts +352 -352
- package/lib/markdownlint.js +1101 -1101
- package/lib/md001.js +22 -22
- package/lib/md002.js +22 -22
- package/lib/md003.js +42 -42
- package/lib/md004.js +43 -43
- package/lib/md005.js +69 -69
- package/lib/md006.js +34 -34
- package/lib/md007.js +47 -47
- package/lib/md009.js +77 -77
- package/lib/md010.js +38 -38
- package/lib/md011.js +38 -38
- package/lib/md012.js +32 -32
- package/lib/md013.js +99 -99
- package/lib/md014.js +53 -53
- package/lib/md018.js +34 -34
- package/lib/md019.js +39 -39
- package/lib/md020.js +62 -62
- package/lib/md021.js +60 -60
- package/lib/md022.js +56 -56
- package/lib/md023.js +38 -38
- package/lib/md024.js +38 -38
- package/lib/md025.js +32 -32
- package/lib/md026.js +40 -40
- package/lib/md027.js +56 -56
- package/lib/md028.js +38 -38
- package/lib/md029.js +66 -66
- package/lib/md030.js +50 -50
- package/lib/md031.js +39 -39
- package/lib/md032.js +50 -50
- package/lib/md033.js +45 -45
- package/lib/md034.js +61 -61
- package/lib/md035.js +21 -21
- package/lib/md036.js +56 -56
- package/lib/md037.js +150 -89
- package/lib/md038.js +68 -68
- package/lib/md039.js +64 -64
- package/lib/md040.js +18 -18
- package/lib/md041.js +31 -31
- package/lib/md042.js +41 -41
- package/lib/md043.js +45 -45
- package/lib/md044.js +77 -71
- package/lib/md045.js +18 -18
- package/lib/md046.js +32 -32
- package/lib/md047.js +28 -28
- package/lib/md048.js +29 -29
- package/lib/rules.js +61 -61
- package/package.json +73 -67
- package/schema/build-config-schema.js +405 -405
- package/schema/markdownlint-config-schema.json +1550 -1550
- package/style/all.json +5 -5
- package/style/cirosantilli.json +22 -22
- package/style/relaxed.json +13 -13
package/lib/markdownlint.js
CHANGED
|
@@ -1,1101 +1,1101 @@
|
|
|
1
|
-
// @ts-check
|
|
2
|
-
|
|
3
|
-
"use strict";
|
|
4
|
-
|
|
5
|
-
const fs = require("fs");
|
|
6
|
-
const path = require("path");
|
|
7
|
-
const { URL } = require("url");
|
|
8
|
-
const markdownIt = require("markdown-it");
|
|
9
|
-
const rules = require("./rules");
|
|
10
|
-
const helpers = require("../helpers");
|
|
11
|
-
const cache = require("./cache");
|
|
12
|
-
|
|
13
|
-
const deprecatedRuleNames = [ "MD002", "MD006" ];
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Validate the list of rules for structure and reuse.
|
|
18
|
-
*
|
|
19
|
-
* @param {Rule[]} ruleList List of rules.
|
|
20
|
-
* @returns {string} Error message if validation fails.
|
|
21
|
-
*/
|
|
22
|
-
function validateRuleList(ruleList) {
|
|
23
|
-
let result = null;
|
|
24
|
-
if (ruleList.length === rules.length) {
|
|
25
|
-
// No need to validate if only using built-in rules
|
|
26
|
-
return result;
|
|
27
|
-
}
|
|
28
|
-
const allIds = {};
|
|
29
|
-
ruleList.forEach(function forRule(rule, index) {
|
|
30
|
-
const customIndex = index - rules.length;
|
|
31
|
-
// eslint-disable-next-line jsdoc/require-jsdoc
|
|
32
|
-
function newError(property) {
|
|
33
|
-
return new Error(
|
|
34
|
-
"Property '" + property + "' of custom rule at index " +
|
|
35
|
-
customIndex + " is incorrect.");
|
|
36
|
-
}
|
|
37
|
-
[ "names", "tags" ].forEach(function forProperty(property) {
|
|
38
|
-
const value = rule[property];
|
|
39
|
-
if (!result &&
|
|
40
|
-
(!value || !Array.isArray(value) || (value.length === 0) ||
|
|
41
|
-
!value.every(helpers.isString) || value.some(helpers.isEmptyString))) {
|
|
42
|
-
result = newError(property);
|
|
43
|
-
}
|
|
44
|
-
});
|
|
45
|
-
[
|
|
46
|
-
[ "description", "string" ],
|
|
47
|
-
[ "function", "function" ]
|
|
48
|
-
].forEach(function forProperty(propertyInfo) {
|
|
49
|
-
const property = propertyInfo[0];
|
|
50
|
-
const value = rule[property];
|
|
51
|
-
if (!result && (!value || (typeof value !== propertyInfo[1]))) {
|
|
52
|
-
result = newError(property);
|
|
53
|
-
}
|
|
54
|
-
});
|
|
55
|
-
if (!result && rule.information) {
|
|
56
|
-
if (Object.getPrototypeOf(rule.information) !== URL.prototype) {
|
|
57
|
-
result = newError("information");
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
if (!result) {
|
|
61
|
-
rule.names.forEach(function forName(name) {
|
|
62
|
-
const nameUpper = name.toUpperCase();
|
|
63
|
-
if (!result && (allIds[nameUpper] !== undefined)) {
|
|
64
|
-
result = new Error("Name '" + name + "' of custom rule at index " +
|
|
65
|
-
customIndex + " is already used as a name or tag.");
|
|
66
|
-
}
|
|
67
|
-
allIds[nameUpper] = true;
|
|
68
|
-
});
|
|
69
|
-
rule.tags.forEach(function forTag(tag) {
|
|
70
|
-
const tagUpper = tag.toUpperCase();
|
|
71
|
-
if (!result && allIds[tagUpper]) {
|
|
72
|
-
result = new Error("Tag '" + tag + "' of custom rule at index " +
|
|
73
|
-
customIndex + " is already used as a name.");
|
|
74
|
-
}
|
|
75
|
-
allIds[tagUpper] = false;
|
|
76
|
-
});
|
|
77
|
-
}
|
|
78
|
-
});
|
|
79
|
-
return result;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* Creates a LintResults instance with toString for pretty display.
|
|
84
|
-
*
|
|
85
|
-
* @param {Rule[]} ruleList List of rules.
|
|
86
|
-
* @returns {LintResults} New LintResults instance.
|
|
87
|
-
*/
|
|
88
|
-
function newResults(ruleList) {
|
|
89
|
-
// eslint-disable-next-line jsdoc/require-jsdoc
|
|
90
|
-
function Results() {}
|
|
91
|
-
Results.prototype.toString = function toString(useAlias) {
|
|
92
|
-
const that = this;
|
|
93
|
-
let ruleNameToRule = null;
|
|
94
|
-
const results = [];
|
|
95
|
-
Object.keys(that).forEach(function forFile(file) {
|
|
96
|
-
const fileResults = that[file];
|
|
97
|
-
if (Array.isArray(fileResults)) {
|
|
98
|
-
fileResults.forEach(function forResult(result) {
|
|
99
|
-
const ruleMoniker = result.ruleNames ?
|
|
100
|
-
result.ruleNames.join("/") :
|
|
101
|
-
(result.ruleName + "/" + result.ruleAlias);
|
|
102
|
-
results.push(
|
|
103
|
-
file + ": " +
|
|
104
|
-
result.lineNumber + ": " +
|
|
105
|
-
ruleMoniker + " " +
|
|
106
|
-
result.ruleDescription +
|
|
107
|
-
(result.errorDetail ?
|
|
108
|
-
" [" + result.errorDetail + "]" :
|
|
109
|
-
"") +
|
|
110
|
-
(result.errorContext ?
|
|
111
|
-
" [Context: \"" + result.errorContext + "\"]" :
|
|
112
|
-
""));
|
|
113
|
-
});
|
|
114
|
-
} else {
|
|
115
|
-
if (!ruleNameToRule) {
|
|
116
|
-
ruleNameToRule = {};
|
|
117
|
-
ruleList.forEach(function forRule(rule) {
|
|
118
|
-
const ruleName = rule.names[0].toUpperCase();
|
|
119
|
-
ruleNameToRule[ruleName] = rule;
|
|
120
|
-
});
|
|
121
|
-
}
|
|
122
|
-
Object.keys(fileResults).forEach(function forRule(ruleName) {
|
|
123
|
-
const rule = ruleNameToRule[ruleName.toUpperCase()];
|
|
124
|
-
const ruleResults = fileResults[ruleName];
|
|
125
|
-
ruleResults.forEach(function forLine(lineNumber) {
|
|
126
|
-
const nameIndex = Math.min(useAlias ? 1 : 0, rule.names.length - 1);
|
|
127
|
-
const result =
|
|
128
|
-
file + ": " +
|
|
129
|
-
lineNumber + ": " +
|
|
130
|
-
rule.names[nameIndex] + " " +
|
|
131
|
-
rule.description;
|
|
132
|
-
results.push(result);
|
|
133
|
-
});
|
|
134
|
-
});
|
|
135
|
-
}
|
|
136
|
-
});
|
|
137
|
-
return results.join("\n");
|
|
138
|
-
};
|
|
139
|
-
// @ts-ignore
|
|
140
|
-
return new Results();
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
/**
|
|
144
|
-
* Remove front matter (if present at beginning of content).
|
|
145
|
-
*
|
|
146
|
-
* @param {string} content Markdown content.
|
|
147
|
-
* @param {RegExp} frontMatter Regular expression to match front matter.
|
|
148
|
-
* @returns {Object} Trimmed content and front matter lines.
|
|
149
|
-
*/
|
|
150
|
-
function removeFrontMatter(content, frontMatter) {
|
|
151
|
-
let frontMatterLines = [];
|
|
152
|
-
if (frontMatter) {
|
|
153
|
-
const frontMatterMatch = content.match(frontMatter);
|
|
154
|
-
if (frontMatterMatch && !frontMatterMatch.index) {
|
|
155
|
-
const contentMatched = frontMatterMatch[0];
|
|
156
|
-
content = content.slice(contentMatched.length);
|
|
157
|
-
frontMatterLines = contentMatched.split(helpers.newLineRe);
|
|
158
|
-
if (frontMatterLines.length &&
|
|
159
|
-
(frontMatterLines[frontMatterLines.length - 1] === "")) {
|
|
160
|
-
frontMatterLines.length--;
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
return {
|
|
165
|
-
"content": content,
|
|
166
|
-
"frontMatterLines": frontMatterLines
|
|
167
|
-
};
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
/**
|
|
171
|
-
* Annotate tokens with line/lineNumber.
|
|
172
|
-
*
|
|
173
|
-
* @param {MarkdownItToken[]} tokens Array of markdown-it tokens.
|
|
174
|
-
* @param {string[]} lines Lines of Markdown content.
|
|
175
|
-
* @returns {void}
|
|
176
|
-
*/
|
|
177
|
-
function annotateTokens(tokens, lines) {
|
|
178
|
-
let tbodyMap = null;
|
|
179
|
-
tokens.forEach(function forToken(token) {
|
|
180
|
-
// Handle missing maps for table body
|
|
181
|
-
if (token.type === "tbody_open") {
|
|
182
|
-
tbodyMap = token.map.slice();
|
|
183
|
-
} else if ((token.type === "tr_close") && tbodyMap) {
|
|
184
|
-
tbodyMap[0]++;
|
|
185
|
-
} else if (token.type === "tbody_close") {
|
|
186
|
-
tbodyMap = null;
|
|
187
|
-
}
|
|
188
|
-
if (tbodyMap && !token.map) {
|
|
189
|
-
token.map = tbodyMap.slice();
|
|
190
|
-
}
|
|
191
|
-
// Update token metadata
|
|
192
|
-
if (token.map) {
|
|
193
|
-
token.line = lines[token.map[0]];
|
|
194
|
-
token.lineNumber = token.map[0] + 1;
|
|
195
|
-
// Trim bottom of token to exclude whitespace lines
|
|
196
|
-
while (token.map[1] && !((lines[token.map[1] - 1] || "").trim())) {
|
|
197
|
-
token.map[1]--;
|
|
198
|
-
}
|
|
199
|
-
// Annotate children with lineNumber
|
|
200
|
-
let lineNumber = token.lineNumber;
|
|
201
|
-
const codeSpanExtraLines = [];
|
|
202
|
-
helpers.forEachInlineCodeSpan(
|
|
203
|
-
token.content,
|
|
204
|
-
function handleInlineCodeSpan(code) {
|
|
205
|
-
codeSpanExtraLines.push(code.split(helpers.newLineRe).length - 1);
|
|
206
|
-
}
|
|
207
|
-
);
|
|
208
|
-
(token.children || []).forEach(function forChild(child) {
|
|
209
|
-
child.lineNumber = lineNumber;
|
|
210
|
-
child.line = lines[lineNumber - 1];
|
|
211
|
-
if ((child.type === "softbreak") || (child.type === "hardbreak")) {
|
|
212
|
-
lineNumber++;
|
|
213
|
-
} else if (child.type === "code_inline") {
|
|
214
|
-
lineNumber += codeSpanExtraLines.shift();
|
|
215
|
-
}
|
|
216
|
-
});
|
|
217
|
-
}
|
|
218
|
-
});
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
/**
|
|
222
|
-
* Map rule names/tags to canonical rule name.
|
|
223
|
-
*
|
|
224
|
-
* @param {Rule[]} ruleList List of rules.
|
|
225
|
-
* @returns {Object.<string, string[]>} Map of alias to rule name.
|
|
226
|
-
*/
|
|
227
|
-
function mapAliasToRuleNames(ruleList) {
|
|
228
|
-
const aliasToRuleNames = {};
|
|
229
|
-
// const tagToRuleNames = {};
|
|
230
|
-
ruleList.forEach(function forRule(rule) {
|
|
231
|
-
const ruleName = rule.names[0].toUpperCase();
|
|
232
|
-
// The following is useful for updating README.md:
|
|
233
|
-
// console.log(
|
|
234
|
-
// "* **[" + ruleName + "](doc/Rules.md#" + ruleName.toLowerCase() +
|
|
235
|
-
// ")** *" + rule.names.slice(1).join("/") + "* - " + rule.description);
|
|
236
|
-
rule.names.forEach(function forName(name) {
|
|
237
|
-
const nameUpper = name.toUpperCase();
|
|
238
|
-
aliasToRuleNames[nameUpper] = [ ruleName ];
|
|
239
|
-
});
|
|
240
|
-
rule.tags.forEach(function forTag(tag) {
|
|
241
|
-
const tagUpper = tag.toUpperCase();
|
|
242
|
-
const ruleNames = aliasToRuleNames[tagUpper] || [];
|
|
243
|
-
ruleNames.push(ruleName);
|
|
244
|
-
aliasToRuleNames[tagUpper] = ruleNames;
|
|
245
|
-
// tagToRuleNames[tag] = ruleName;
|
|
246
|
-
});
|
|
247
|
-
});
|
|
248
|
-
// The following is useful for updating README.md:
|
|
249
|
-
// Object.keys(tagToRuleNames).sort().forEach(function forTag(tag) {
|
|
250
|
-
// console.log("* **" + tag + "** - " +
|
|
251
|
-
// aliasToRuleNames[tag.toUpperCase()].join(", "));
|
|
252
|
-
// });
|
|
253
|
-
// @ts-ignore
|
|
254
|
-
return aliasToRuleNames;
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
/**
|
|
258
|
-
* Apply (and normalize) configuration object.
|
|
259
|
-
*
|
|
260
|
-
* @param {Rule[]} ruleList List of rules.
|
|
261
|
-
* @param {Configuration} config Configuration object.
|
|
262
|
-
* @param {Object.<string, string[]>} aliasToRuleNames Map of alias to rule
|
|
263
|
-
* names.
|
|
264
|
-
* @returns {Configuration} Effective configuration.
|
|
265
|
-
*/
|
|
266
|
-
function getEffectiveConfig(ruleList, config, aliasToRuleNames) {
|
|
267
|
-
const defaultKey = Object.keys(config).filter(
|
|
268
|
-
(key) => key.toUpperCase() === "DEFAULT"
|
|
269
|
-
);
|
|
270
|
-
const ruleDefault = (defaultKey.length === 0) || !!config[defaultKey[0]];
|
|
271
|
-
const effectiveConfig = {};
|
|
272
|
-
ruleList.forEach((rule) => {
|
|
273
|
-
const ruleName = rule.names[0].toUpperCase();
|
|
274
|
-
effectiveConfig[ruleName] = ruleDefault;
|
|
275
|
-
});
|
|
276
|
-
deprecatedRuleNames.forEach((ruleName) => {
|
|
277
|
-
effectiveConfig[ruleName] = false;
|
|
278
|
-
});
|
|
279
|
-
Object.keys(config).forEach((key) => {
|
|
280
|
-
let value = config[key];
|
|
281
|
-
if (value) {
|
|
282
|
-
if (!(value instanceof Object)) {
|
|
283
|
-
value = {};
|
|
284
|
-
}
|
|
285
|
-
} else {
|
|
286
|
-
value = false;
|
|
287
|
-
}
|
|
288
|
-
const keyUpper = key.toUpperCase();
|
|
289
|
-
(aliasToRuleNames[keyUpper] || []).forEach((ruleName) => {
|
|
290
|
-
effectiveConfig[ruleName] = value;
|
|
291
|
-
});
|
|
292
|
-
});
|
|
293
|
-
return effectiveConfig;
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
/**
|
|
297
|
-
* Create a mapping of enabled rules per line.
|
|
298
|
-
*
|
|
299
|
-
* @param {Rule[]} ruleList List of rules.
|
|
300
|
-
* @param {string[]} lines List of content lines.
|
|
301
|
-
* @param {string[]} frontMatterLines List of front matter lines.
|
|
302
|
-
* @param {boolean} noInlineConfig Whether to allow inline configuration.
|
|
303
|
-
* @param {Configuration} config Configuration object.
|
|
304
|
-
* @param {Object.<string, string[]>} aliasToRuleNames Map of alias to rule
|
|
305
|
-
* names.
|
|
306
|
-
* @returns {Object} Effective configuration and enabled rules per line number.
|
|
307
|
-
*/
|
|
308
|
-
function getEnabledRulesPerLineNumber(
|
|
309
|
-
ruleList,
|
|
310
|
-
lines,
|
|
311
|
-
frontMatterLines,
|
|
312
|
-
noInlineConfig,
|
|
313
|
-
config,
|
|
314
|
-
aliasToRuleNames) {
|
|
315
|
-
// Shared variables
|
|
316
|
-
let enabledRules = {};
|
|
317
|
-
let capturedRules = {};
|
|
318
|
-
const allRuleNames = [];
|
|
319
|
-
const enabledRulesPerLineNumber = new Array(1 + frontMatterLines.length);
|
|
320
|
-
// Helper functions
|
|
321
|
-
// eslint-disable-next-line jsdoc/require-jsdoc
|
|
322
|
-
function handleInlineConfig(perLine, forEachMatch, forEachLine) {
|
|
323
|
-
const input = perLine ? lines : [ lines.join("\n") ];
|
|
324
|
-
input.forEach((line) => {
|
|
325
|
-
if (!noInlineConfig) {
|
|
326
|
-
let match = null;
|
|
327
|
-
while ((match = helpers.inlineCommentRe.exec(line))) {
|
|
328
|
-
const action = (match[1] || match[3]).toUpperCase();
|
|
329
|
-
const parameter = match[2] || match[4];
|
|
330
|
-
forEachMatch(action, parameter);
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
|
-
if (forEachLine) {
|
|
334
|
-
forEachLine();
|
|
335
|
-
}
|
|
336
|
-
});
|
|
337
|
-
}
|
|
338
|
-
// eslint-disable-next-line jsdoc/require-jsdoc
|
|
339
|
-
function configureFile(action, parameter) {
|
|
340
|
-
if (action === "CONFIGURE-FILE") {
|
|
341
|
-
try {
|
|
342
|
-
const json = JSON.parse(parameter);
|
|
343
|
-
config = {
|
|
344
|
-
...config,
|
|
345
|
-
...json
|
|
346
|
-
};
|
|
347
|
-
} catch (ex) {
|
|
348
|
-
// Ignore parse errors for inline configuration
|
|
349
|
-
}
|
|
350
|
-
}
|
|
351
|
-
}
|
|
352
|
-
// eslint-disable-next-line jsdoc/require-jsdoc
|
|
353
|
-
function applyEnableDisable(action, parameter) {
|
|
354
|
-
const enabled = (action.startsWith("ENABLE"));
|
|
355
|
-
const items = parameter ?
|
|
356
|
-
parameter.trim().toUpperCase().split(/\s+/) :
|
|
357
|
-
allRuleNames;
|
|
358
|
-
items.forEach((nameUpper) => {
|
|
359
|
-
(aliasToRuleNames[nameUpper] || []).forEach((ruleName) => {
|
|
360
|
-
enabledRules[ruleName] = enabled;
|
|
361
|
-
});
|
|
362
|
-
});
|
|
363
|
-
}
|
|
364
|
-
// eslint-disable-next-line jsdoc/require-jsdoc
|
|
365
|
-
function enableDisableFile(action, parameter) {
|
|
366
|
-
if ((action === "ENABLE-FILE") || (action === "DISABLE-FILE")) {
|
|
367
|
-
applyEnableDisable(action, parameter);
|
|
368
|
-
}
|
|
369
|
-
}
|
|
370
|
-
// eslint-disable-next-line jsdoc/require-jsdoc
|
|
371
|
-
function captureRestoreEnableDisable(action, parameter) {
|
|
372
|
-
if (action === "CAPTURE") {
|
|
373
|
-
capturedRules = { ...enabledRules };
|
|
374
|
-
} else if (action === "RESTORE") {
|
|
375
|
-
enabledRules = { ...capturedRules };
|
|
376
|
-
} else if ((action === "ENABLE") || (action === "DISABLE")) {
|
|
377
|
-
enabledRules = { ...enabledRules };
|
|
378
|
-
applyEnableDisable(action, parameter);
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
|
-
// eslint-disable-next-line jsdoc/require-jsdoc
|
|
382
|
-
function updateLineState() {
|
|
383
|
-
enabledRulesPerLineNumber.push(enabledRules);
|
|
384
|
-
}
|
|
385
|
-
// Handle inline comments
|
|
386
|
-
handleInlineConfig(false, configureFile);
|
|
387
|
-
const effectiveConfig = getEffectiveConfig(
|
|
388
|
-
ruleList, config, aliasToRuleNames);
|
|
389
|
-
ruleList.forEach((rule) => {
|
|
390
|
-
const ruleName = rule.names[0].toUpperCase();
|
|
391
|
-
allRuleNames.push(ruleName);
|
|
392
|
-
enabledRules[ruleName] = !!effectiveConfig[ruleName];
|
|
393
|
-
});
|
|
394
|
-
capturedRules = enabledRules;
|
|
395
|
-
handleInlineConfig(true, enableDisableFile);
|
|
396
|
-
handleInlineConfig(true, captureRestoreEnableDisable, updateLineState);
|
|
397
|
-
// Return results
|
|
398
|
-
return {
|
|
399
|
-
effectiveConfig,
|
|
400
|
-
enabledRulesPerLineNumber
|
|
401
|
-
};
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
/**
|
|
405
|
-
* Compare function for Array.prototype.sort for ascending order of errors.
|
|
406
|
-
*
|
|
407
|
-
* @param {LintError} a First error.
|
|
408
|
-
* @param {LintError} b Second error.
|
|
409
|
-
* @returns {number} Positive value if a>b, negative value if b<a, 0 otherwise.
|
|
410
|
-
*/
|
|
411
|
-
function lineNumberComparison(a, b) {
|
|
412
|
-
return a.lineNumber - b.lineNumber;
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
/**
|
|
416
|
-
* Filter function to include everything.
|
|
417
|
-
*
|
|
418
|
-
* @returns {boolean} True.
|
|
419
|
-
*/
|
|
420
|
-
function filterAllValues() {
|
|
421
|
-
return true;
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
/**
|
|
425
|
-
* Function to return unique values from a sorted errors array.
|
|
426
|
-
*
|
|
427
|
-
* @param {LintError} value Error instance.
|
|
428
|
-
* @param {number} index Index in array.
|
|
429
|
-
* @param {LintError[]} array Array of errors.
|
|
430
|
-
* @returns {boolean} Filter value.
|
|
431
|
-
*/
|
|
432
|
-
function uniqueFilterForSortedErrors(value, index, array) {
|
|
433
|
-
return (index === 0) || (value.lineNumber > array[index - 1].lineNumber);
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
/**
|
|
437
|
-
* Lints a string containing Markdown content.
|
|
438
|
-
*
|
|
439
|
-
* @param {Rule[]} ruleList List of rules.
|
|
440
|
-
* @param {string} name Identifier for the content.
|
|
441
|
-
* @param {string} content Markdown content
|
|
442
|
-
* @param {Object} md markdown-it instance.
|
|
443
|
-
* @param {Configuration} config Configuration object.
|
|
444
|
-
* @param {RegExp} frontMatter Regular expression for front matter.
|
|
445
|
-
* @param {boolean} handleRuleFailures Whether to handle exceptions in rules.
|
|
446
|
-
* @param {boolean} noInlineConfig Whether to allow inline configuration.
|
|
447
|
-
* @param {number} resultVersion Version of the LintResults object to return.
|
|
448
|
-
* @param {Function} callback Callback (err, result) function.
|
|
449
|
-
* @returns {void}
|
|
450
|
-
*/
|
|
451
|
-
function lintContent(
|
|
452
|
-
ruleList,
|
|
453
|
-
name,
|
|
454
|
-
content,
|
|
455
|
-
md,
|
|
456
|
-
config,
|
|
457
|
-
frontMatter,
|
|
458
|
-
handleRuleFailures,
|
|
459
|
-
noInlineConfig,
|
|
460
|
-
resultVersion,
|
|
461
|
-
callback) {
|
|
462
|
-
// Remove UTF-8 byte order marker (if present)
|
|
463
|
-
content = content.replace(/^\ufeff/, "");
|
|
464
|
-
// Remove front matter
|
|
465
|
-
const removeFrontMatterResult = removeFrontMatter(content, frontMatter);
|
|
466
|
-
const frontMatterLines = removeFrontMatterResult.frontMatterLines;
|
|
467
|
-
// Ignore the content of HTML comments
|
|
468
|
-
content = helpers.clearHtmlCommentText(removeFrontMatterResult.content);
|
|
469
|
-
// Parse content into tokens and lines
|
|
470
|
-
const tokens = md.parse(content, {});
|
|
471
|
-
const lines = content.split(helpers.newLineRe);
|
|
472
|
-
annotateTokens(tokens, lines);
|
|
473
|
-
const aliasToRuleNames = mapAliasToRuleNames(ruleList);
|
|
474
|
-
const { effectiveConfig, enabledRulesPerLineNumber } =
|
|
475
|
-
getEnabledRulesPerLineNumber(
|
|
476
|
-
ruleList,
|
|
477
|
-
lines,
|
|
478
|
-
frontMatterLines,
|
|
479
|
-
noInlineConfig,
|
|
480
|
-
config,
|
|
481
|
-
aliasToRuleNames
|
|
482
|
-
);
|
|
483
|
-
// Create parameters for rules
|
|
484
|
-
const params = {
|
|
485
|
-
name,
|
|
486
|
-
tokens,
|
|
487
|
-
lines,
|
|
488
|
-
frontMatterLines
|
|
489
|
-
};
|
|
490
|
-
cache.lineMetadata(helpers.getLineMetadata(params));
|
|
491
|
-
cache.flattenedLists(helpers.flattenLists(params));
|
|
492
|
-
// Function to run for each rule
|
|
493
|
-
const result = (resultVersion === 0) ? {} : [];
|
|
494
|
-
// eslint-disable-next-line jsdoc/require-jsdoc
|
|
495
|
-
function forRule(rule) {
|
|
496
|
-
// Configure rule
|
|
497
|
-
const ruleNameFriendly = rule.names[0];
|
|
498
|
-
const ruleName = ruleNameFriendly.toUpperCase();
|
|
499
|
-
params.config = effectiveConfig[ruleName];
|
|
500
|
-
// eslint-disable-next-line jsdoc/require-jsdoc
|
|
501
|
-
function throwError(property) {
|
|
502
|
-
throw new Error(
|
|
503
|
-
"Property '" + property + "' of onError parameter is incorrect.");
|
|
504
|
-
}
|
|
505
|
-
const errors = [];
|
|
506
|
-
// eslint-disable-next-line jsdoc/require-jsdoc
|
|
507
|
-
function onError(errorInfo) {
|
|
508
|
-
if (!errorInfo ||
|
|
509
|
-
!helpers.isNumber(errorInfo.lineNumber) ||
|
|
510
|
-
(errorInfo.lineNumber < 1) ||
|
|
511
|
-
(errorInfo.lineNumber > lines.length)) {
|
|
512
|
-
throwError("lineNumber");
|
|
513
|
-
}
|
|
514
|
-
if (errorInfo.detail &&
|
|
515
|
-
!helpers.isString(errorInfo.detail)) {
|
|
516
|
-
throwError("detail");
|
|
517
|
-
}
|
|
518
|
-
if (errorInfo.context &&
|
|
519
|
-
!helpers.isString(errorInfo.context)) {
|
|
520
|
-
throwError("context");
|
|
521
|
-
}
|
|
522
|
-
if (errorInfo.range &&
|
|
523
|
-
(!Array.isArray(errorInfo.range) ||
|
|
524
|
-
(errorInfo.range.length !== 2) ||
|
|
525
|
-
!helpers.isNumber(errorInfo.range[0]) ||
|
|
526
|
-
(errorInfo.range[0] < 1) ||
|
|
527
|
-
!helpers.isNumber(errorInfo.range[1]) ||
|
|
528
|
-
(errorInfo.range[1] < 1) ||
|
|
529
|
-
((errorInfo.range[0] + errorInfo.range[1] - 1) >
|
|
530
|
-
lines[errorInfo.lineNumber - 1].length))) {
|
|
531
|
-
throwError("range");
|
|
532
|
-
}
|
|
533
|
-
const fixInfo = errorInfo.fixInfo;
|
|
534
|
-
const cleanFixInfo = {};
|
|
535
|
-
if (fixInfo) {
|
|
536
|
-
if (!helpers.isObject(fixInfo)) {
|
|
537
|
-
throwError("fixInfo");
|
|
538
|
-
}
|
|
539
|
-
if (fixInfo.lineNumber !== undefined) {
|
|
540
|
-
if ((!helpers.isNumber(fixInfo.lineNumber) ||
|
|
541
|
-
(fixInfo.lineNumber < 1) ||
|
|
542
|
-
(fixInfo.lineNumber > lines.length))) {
|
|
543
|
-
throwError("fixInfo.lineNumber");
|
|
544
|
-
}
|
|
545
|
-
cleanFixInfo.lineNumber =
|
|
546
|
-
fixInfo.lineNumber + frontMatterLines.length;
|
|
547
|
-
}
|
|
548
|
-
const effectiveLineNumber = fixInfo.lineNumber || errorInfo.lineNumber;
|
|
549
|
-
if (fixInfo.editColumn !== undefined) {
|
|
550
|
-
if ((!helpers.isNumber(fixInfo.editColumn) ||
|
|
551
|
-
(fixInfo.editColumn < 1) ||
|
|
552
|
-
(fixInfo.editColumn >
|
|
553
|
-
lines[effectiveLineNumber - 1].length + 1))) {
|
|
554
|
-
throwError("fixInfo.editColumn");
|
|
555
|
-
}
|
|
556
|
-
cleanFixInfo.editColumn = fixInfo.editColumn;
|
|
557
|
-
}
|
|
558
|
-
if (fixInfo.deleteCount !== undefined) {
|
|
559
|
-
if ((!helpers.isNumber(fixInfo.deleteCount) ||
|
|
560
|
-
(fixInfo.deleteCount < -1) ||
|
|
561
|
-
(fixInfo.deleteCount >
|
|
562
|
-
lines[effectiveLineNumber - 1].length))) {
|
|
563
|
-
throwError("fixInfo.deleteCount");
|
|
564
|
-
}
|
|
565
|
-
cleanFixInfo.deleteCount = fixInfo.deleteCount;
|
|
566
|
-
}
|
|
567
|
-
if (fixInfo.insertText !== undefined) {
|
|
568
|
-
if (!helpers.isString(fixInfo.insertText)) {
|
|
569
|
-
throwError("fixInfo.insertText");
|
|
570
|
-
}
|
|
571
|
-
cleanFixInfo.insertText = fixInfo.insertText;
|
|
572
|
-
}
|
|
573
|
-
}
|
|
574
|
-
errors.push({
|
|
575
|
-
"lineNumber": errorInfo.lineNumber + frontMatterLines.length,
|
|
576
|
-
"detail": errorInfo.detail || null,
|
|
577
|
-
"context": errorInfo.context || null,
|
|
578
|
-
"range": errorInfo.range ? [ ...errorInfo.range ] : null,
|
|
579
|
-
"fixInfo": fixInfo ? cleanFixInfo : null
|
|
580
|
-
});
|
|
581
|
-
}
|
|
582
|
-
// Call (possibly external) rule function
|
|
583
|
-
if (handleRuleFailures) {
|
|
584
|
-
try {
|
|
585
|
-
rule.function(params, onError);
|
|
586
|
-
} catch (ex) {
|
|
587
|
-
onError({
|
|
588
|
-
"lineNumber": 1,
|
|
589
|
-
"detail": `This rule threw an exception: ${ex.message}`
|
|
590
|
-
});
|
|
591
|
-
}
|
|
592
|
-
} else {
|
|
593
|
-
rule.function(params, onError);
|
|
594
|
-
}
|
|
595
|
-
// Record any errors (significant performance benefit from length check)
|
|
596
|
-
if (errors.length) {
|
|
597
|
-
errors.sort(lineNumberComparison);
|
|
598
|
-
const filteredErrors = errors
|
|
599
|
-
.filter((resultVersion === 3) ?
|
|
600
|
-
filterAllValues :
|
|
601
|
-
uniqueFilterForSortedErrors)
|
|
602
|
-
.filter(function removeDisabledRules(error) {
|
|
603
|
-
return enabledRulesPerLineNumber[error.lineNumber][ruleName];
|
|
604
|
-
})
|
|
605
|
-
.map(function formatResults(error) {
|
|
606
|
-
if (resultVersion === 0) {
|
|
607
|
-
return error.lineNumber;
|
|
608
|
-
}
|
|
609
|
-
const errorObject = {};
|
|
610
|
-
errorObject.lineNumber = error.lineNumber;
|
|
611
|
-
if (resultVersion === 1) {
|
|
612
|
-
errorObject.ruleName = ruleNameFriendly;
|
|
613
|
-
errorObject.ruleAlias = rule.names[1] || rule.names[0];
|
|
614
|
-
} else {
|
|
615
|
-
errorObject.ruleNames = rule.names;
|
|
616
|
-
}
|
|
617
|
-
errorObject.ruleDescription = rule.description;
|
|
618
|
-
errorObject.ruleInformation =
|
|
619
|
-
rule.information ? rule.information.href : null;
|
|
620
|
-
errorObject.errorDetail = error.detail;
|
|
621
|
-
errorObject.errorContext = error.context;
|
|
622
|
-
errorObject.errorRange = error.range;
|
|
623
|
-
if (resultVersion === 3) {
|
|
624
|
-
errorObject.fixInfo = error.fixInfo;
|
|
625
|
-
}
|
|
626
|
-
return errorObject;
|
|
627
|
-
});
|
|
628
|
-
if (filteredErrors.length) {
|
|
629
|
-
if (resultVersion === 0) {
|
|
630
|
-
result[ruleNameFriendly] = filteredErrors;
|
|
631
|
-
} else {
|
|
632
|
-
Array.prototype.push.apply(result, filteredErrors);
|
|
633
|
-
}
|
|
634
|
-
}
|
|
635
|
-
}
|
|
636
|
-
}
|
|
637
|
-
// Run all rules
|
|
638
|
-
try {
|
|
639
|
-
ruleList.forEach(forRule);
|
|
640
|
-
} catch (ex) {
|
|
641
|
-
cache.clear();
|
|
642
|
-
return callback(ex);
|
|
643
|
-
}
|
|
644
|
-
cache.clear();
|
|
645
|
-
return callback(null, result);
|
|
646
|
-
}
|
|
647
|
-
|
|
648
|
-
/**
|
|
649
|
-
* Lints a file containing Markdown content.
|
|
650
|
-
*
|
|
651
|
-
* @param {Rule[]} ruleList List of rules.
|
|
652
|
-
* @param {string} file Path of file to lint.
|
|
653
|
-
* @param {Object} md markdown-it instance.
|
|
654
|
-
* @param {Configuration} config Configuration object.
|
|
655
|
-
* @param {RegExp} frontMatter Regular expression for front matter.
|
|
656
|
-
* @param {boolean} handleRuleFailures Whether to handle exceptions in rules.
|
|
657
|
-
* @param {boolean} noInlineConfig Whether to allow inline configuration.
|
|
658
|
-
* @param {number} resultVersion Version of the LintResults object to return.
|
|
659
|
-
* @param {boolean} synchronous Whether to execute synchronously.
|
|
660
|
-
* @param {Function} callback Callback (err, result) function.
|
|
661
|
-
* @returns {void}
|
|
662
|
-
*/
|
|
663
|
-
function lintFile(
|
|
664
|
-
ruleList,
|
|
665
|
-
file,
|
|
666
|
-
md,
|
|
667
|
-
config,
|
|
668
|
-
frontMatter,
|
|
669
|
-
handleRuleFailures,
|
|
670
|
-
noInlineConfig,
|
|
671
|
-
resultVersion,
|
|
672
|
-
synchronous,
|
|
673
|
-
callback) {
|
|
674
|
-
// eslint-disable-next-line jsdoc/require-jsdoc
|
|
675
|
-
function lintContentWrapper(err, content) {
|
|
676
|
-
if (err) {
|
|
677
|
-
return callback(err);
|
|
678
|
-
}
|
|
679
|
-
return lintContent(ruleList, file, content, md, config, frontMatter,
|
|
680
|
-
handleRuleFailures, noInlineConfig, resultVersion, callback);
|
|
681
|
-
}
|
|
682
|
-
// Make a/synchronous call to read file
|
|
683
|
-
if (synchronous) {
|
|
684
|
-
lintContentWrapper(null, fs.readFileSync(file, helpers.utf8Encoding));
|
|
685
|
-
} else {
|
|
686
|
-
fs.readFile(file, helpers.utf8Encoding, lintContentWrapper);
|
|
687
|
-
}
|
|
688
|
-
}
|
|
689
|
-
|
|
690
|
-
/**
|
|
691
|
-
* Lint files and strings specified in the Options object.
|
|
692
|
-
*
|
|
693
|
-
* @param {Options} options Options object.
|
|
694
|
-
* @param {boolean} synchronous Whether to execute synchronously.
|
|
695
|
-
* @param {Function} callback Callback (err, result) function.
|
|
696
|
-
* @returns {void}
|
|
697
|
-
*/
|
|
698
|
-
function lintInput(options, synchronous, callback) {
|
|
699
|
-
// Normalize inputs
|
|
700
|
-
options = options || {};
|
|
701
|
-
callback = callback || function noop() {};
|
|
702
|
-
const ruleList = rules.concat(options.customRules || []);
|
|
703
|
-
const ruleErr = validateRuleList(ruleList);
|
|
704
|
-
if (ruleErr) {
|
|
705
|
-
return callback(ruleErr);
|
|
706
|
-
}
|
|
707
|
-
let files = [];
|
|
708
|
-
if (Array.isArray(options.files)) {
|
|
709
|
-
files = options.files.slice();
|
|
710
|
-
} else if (options.files) {
|
|
711
|
-
files = [ String(options.files) ];
|
|
712
|
-
}
|
|
713
|
-
const strings = options.strings || {};
|
|
714
|
-
const stringsKeys = Object.keys(strings);
|
|
715
|
-
const config = options.config || { "default": true };
|
|
716
|
-
const frontMatter = (options.frontMatter === undefined) ?
|
|
717
|
-
helpers.frontMatterRe : options.frontMatter;
|
|
718
|
-
const handleRuleFailures = !!options.handleRuleFailures;
|
|
719
|
-
const noInlineConfig = !!options.noInlineConfig;
|
|
720
|
-
const resultVersion = (options.resultVersion === undefined) ?
|
|
721
|
-
2 : options.resultVersion;
|
|
722
|
-
const md = markdownIt({ "html": true });
|
|
723
|
-
const markdownItPlugins = options.markdownItPlugins || [];
|
|
724
|
-
markdownItPlugins.forEach(function forPlugin(plugin) {
|
|
725
|
-
// @ts-ignore
|
|
726
|
-
md.use(...plugin);
|
|
727
|
-
});
|
|
728
|
-
const results = newResults(ruleList);
|
|
729
|
-
// Helper to lint the next string or file
|
|
730
|
-
/* eslint-disable consistent-return */
|
|
731
|
-
// eslint-disable-next-line jsdoc/require-jsdoc
|
|
732
|
-
function lintNextItem() {
|
|
733
|
-
let iterating = true;
|
|
734
|
-
let item = null;
|
|
735
|
-
// eslint-disable-next-line jsdoc/require-jsdoc
|
|
736
|
-
function lintNextItemCallback(err, result) {
|
|
737
|
-
if (err) {
|
|
738
|
-
iterating = false;
|
|
739
|
-
return callback(err);
|
|
740
|
-
}
|
|
741
|
-
results[item] = result;
|
|
742
|
-
return iterating || lintNextItem();
|
|
743
|
-
}
|
|
744
|
-
while (iterating) {
|
|
745
|
-
if ((item = stringsKeys.shift())) {
|
|
746
|
-
lintContent(
|
|
747
|
-
ruleList,
|
|
748
|
-
item,
|
|
749
|
-
strings[item] || "",
|
|
750
|
-
md,
|
|
751
|
-
config,
|
|
752
|
-
frontMatter,
|
|
753
|
-
handleRuleFailures,
|
|
754
|
-
noInlineConfig,
|
|
755
|
-
resultVersion,
|
|
756
|
-
lintNextItemCallback);
|
|
757
|
-
} else if ((item = files.shift())) {
|
|
758
|
-
iterating = synchronous;
|
|
759
|
-
lintFile(
|
|
760
|
-
ruleList,
|
|
761
|
-
item,
|
|
762
|
-
md,
|
|
763
|
-
config,
|
|
764
|
-
frontMatter,
|
|
765
|
-
handleRuleFailures,
|
|
766
|
-
noInlineConfig,
|
|
767
|
-
resultVersion,
|
|
768
|
-
synchronous,
|
|
769
|
-
lintNextItemCallback);
|
|
770
|
-
} else {
|
|
771
|
-
return callback(null, results);
|
|
772
|
-
}
|
|
773
|
-
}
|
|
774
|
-
}
|
|
775
|
-
return lintNextItem();
|
|
776
|
-
}
|
|
777
|
-
|
|
778
|
-
/**
|
|
779
|
-
* Lint specified Markdown files.
|
|
780
|
-
*
|
|
781
|
-
* @param {Options} options Configuration options.
|
|
782
|
-
* @param {LintCallback} callback Callback (err, result) function.
|
|
783
|
-
* @returns {void}
|
|
784
|
-
*/
|
|
785
|
-
function markdownlint(options, callback) {
|
|
786
|
-
return lintInput(options, false, callback);
|
|
787
|
-
}
|
|
788
|
-
|
|
789
|
-
/**
|
|
790
|
-
* Lint specified Markdown files synchronously.
|
|
791
|
-
*
|
|
792
|
-
* @param {Options} options Configuration options.
|
|
793
|
-
* @returns {LintResults} Results object.
|
|
794
|
-
*/
|
|
795
|
-
function markdownlintSync(options) {
|
|
796
|
-
let results = null;
|
|
797
|
-
lintInput(options, true, function callback(error, res) {
|
|
798
|
-
if (error) {
|
|
799
|
-
throw error;
|
|
800
|
-
}
|
|
801
|
-
results = res;
|
|
802
|
-
});
|
|
803
|
-
return results;
|
|
804
|
-
}
|
|
805
|
-
|
|
806
|
-
/**
|
|
807
|
-
* Parse the content of a configuration file.
|
|
808
|
-
*
|
|
809
|
-
* @param {string} name Name of the configuration file.
|
|
810
|
-
* @param {string} content Configuration content.
|
|
811
|
-
* @param {ConfigurationParser[]} parsers Parsing function(s).
|
|
812
|
-
* @returns {Object} Configuration object and error message.
|
|
813
|
-
*/
|
|
814
|
-
function parseConfiguration(name, content, parsers) {
|
|
815
|
-
let config = null;
|
|
816
|
-
let message = "";
|
|
817
|
-
const errors = [];
|
|
818
|
-
// Try each parser
|
|
819
|
-
(parsers || [ JSON.parse ]).every((parser) => {
|
|
820
|
-
try {
|
|
821
|
-
config = parser(content);
|
|
822
|
-
} catch (ex) {
|
|
823
|
-
errors.push(ex.message);
|
|
824
|
-
}
|
|
825
|
-
return !config;
|
|
826
|
-
});
|
|
827
|
-
// Message if unable to parse
|
|
828
|
-
if (!config) {
|
|
829
|
-
errors.unshift(`Unable to parse '${name}'`);
|
|
830
|
-
message = errors.join("; ");
|
|
831
|
-
}
|
|
832
|
-
return {
|
|
833
|
-
config,
|
|
834
|
-
message
|
|
835
|
-
};
|
|
836
|
-
}
|
|
837
|
-
|
|
838
|
-
/**
|
|
839
|
-
* Read specified configuration file.
|
|
840
|
-
*
|
|
841
|
-
* @param {string} file Configuration file name.
|
|
842
|
-
* @param {ConfigurationParser[] | ReadConfigCallback} parsers Parsing
|
|
843
|
-
* function(s).
|
|
844
|
-
* @param {ReadConfigCallback} [callback] Callback (err, result) function.
|
|
845
|
-
* @returns {void}
|
|
846
|
-
*/
|
|
847
|
-
function readConfig(file, parsers, callback) {
|
|
848
|
-
if (!callback) {
|
|
849
|
-
// @ts-ignore
|
|
850
|
-
callback = parsers;
|
|
851
|
-
parsers = null;
|
|
852
|
-
}
|
|
853
|
-
// Read file
|
|
854
|
-
fs.readFile(file, helpers.utf8Encoding, (err, content) => {
|
|
855
|
-
if (err) {
|
|
856
|
-
return callback(err);
|
|
857
|
-
}
|
|
858
|
-
// Try to parse file
|
|
859
|
-
// @ts-ignore
|
|
860
|
-
const { config, message } = parseConfiguration(file, content, parsers);
|
|
861
|
-
if (!config) {
|
|
862
|
-
return callback(new Error(message));
|
|
863
|
-
}
|
|
864
|
-
// Extend configuration
|
|
865
|
-
const configExtends = config.extends;
|
|
866
|
-
if (configExtends) {
|
|
867
|
-
delete config.extends;
|
|
868
|
-
const extendsFile = path.resolve(path.dirname(file), configExtends);
|
|
869
|
-
return readConfig(extendsFile, parsers, (errr, extendsConfig) => {
|
|
870
|
-
if (errr) {
|
|
871
|
-
return callback(errr);
|
|
872
|
-
}
|
|
873
|
-
return callback(null, {
|
|
874
|
-
...extendsConfig,
|
|
875
|
-
...config
|
|
876
|
-
});
|
|
877
|
-
});
|
|
878
|
-
}
|
|
879
|
-
return callback(null, config);
|
|
880
|
-
});
|
|
881
|
-
}
|
|
882
|
-
|
|
883
|
-
/**
|
|
884
|
-
* Read specified configuration file synchronously.
|
|
885
|
-
*
|
|
886
|
-
* @param {string} file Configuration file name.
|
|
887
|
-
* @param {ConfigurationParser[]} [parsers] Parsing function(s).
|
|
888
|
-
* @returns {Configuration} Configuration object.
|
|
889
|
-
*/
|
|
890
|
-
function readConfigSync(file, parsers) {
|
|
891
|
-
// Read file
|
|
892
|
-
const content = fs.readFileSync(file, helpers.utf8Encoding);
|
|
893
|
-
// Try to parse file
|
|
894
|
-
const { config, message } = parseConfiguration(file, content, parsers);
|
|
895
|
-
if (!config) {
|
|
896
|
-
throw new Error(message);
|
|
897
|
-
}
|
|
898
|
-
// Extend configuration
|
|
899
|
-
const configExtends = config.extends;
|
|
900
|
-
if (configExtends) {
|
|
901
|
-
delete config.extends;
|
|
902
|
-
return {
|
|
903
|
-
...readConfigSync(
|
|
904
|
-
path.resolve(path.dirname(file), configExtends),
|
|
905
|
-
parsers
|
|
906
|
-
),
|
|
907
|
-
...config
|
|
908
|
-
};
|
|
909
|
-
}
|
|
910
|
-
return config;
|
|
911
|
-
}
|
|
912
|
-
|
|
913
|
-
// Export a/synchronous APIs
|
|
914
|
-
markdownlint.sync = markdownlintSync;
|
|
915
|
-
markdownlint.readConfig = readConfig;
|
|
916
|
-
markdownlint.readConfigSync = readConfigSync;
|
|
917
|
-
module.exports = markdownlint;
|
|
918
|
-
|
|
919
|
-
// Type declarations
|
|
920
|
-
|
|
921
|
-
/**
|
|
922
|
-
* Function to implement rule logic.
|
|
923
|
-
*
|
|
924
|
-
* @callback RuleFunction
|
|
925
|
-
* @param {RuleParams} params Rule parameters.
|
|
926
|
-
* @param {RuleOnError} onError Error-reporting callback.
|
|
927
|
-
* @returns {void}
|
|
928
|
-
*/
|
|
929
|
-
|
|
930
|
-
/**
|
|
931
|
-
* Rule parameters.
|
|
932
|
-
*
|
|
933
|
-
* @typedef {Object} RuleParams
|
|
934
|
-
* @property {string} name File/string name.
|
|
935
|
-
* @property {MarkdownItToken[]} tokens markdown-it token objects.
|
|
936
|
-
* @property {string[]} lines File/string lines.
|
|
937
|
-
* @property {string[]} frontMatterLines Front matter lines.
|
|
938
|
-
* @property {RuleConfiguration} config Rule configuration.
|
|
939
|
-
*/
|
|
940
|
-
|
|
941
|
-
/**
|
|
942
|
-
* Markdown-It token.
|
|
943
|
-
*
|
|
944
|
-
* @typedef {Object} MarkdownItToken
|
|
945
|
-
* @property {string[][]} attrs HTML attributes.
|
|
946
|
-
* @property {boolean} block Block-level token.
|
|
947
|
-
* @property {MarkdownItToken[]} children Child nodes.
|
|
948
|
-
* @property {string} content Tag contents.
|
|
949
|
-
* @property {boolean} hidden Ignore element.
|
|
950
|
-
* @property {string} info Fence info.
|
|
951
|
-
* @property {number} level Nesting level.
|
|
952
|
-
* @property {number[]} map Beginning/ending line numbers.
|
|
953
|
-
* @property {string} markup Markup text.
|
|
954
|
-
* @property {Object} meta Arbitrary data.
|
|
955
|
-
* @property {number} nesting Level change.
|
|
956
|
-
* @property {string} tag HTML tag name.
|
|
957
|
-
* @property {string} type Token type.
|
|
958
|
-
* @property {number} lineNumber Line number (1-based).
|
|
959
|
-
* @property {string} line Line content.
|
|
960
|
-
*/
|
|
961
|
-
|
|
962
|
-
/**
|
|
963
|
-
* Error-reporting callback.
|
|
964
|
-
*
|
|
965
|
-
* @callback RuleOnError
|
|
966
|
-
* @param {RuleOnErrorInfo} onErrorInfo Error information.
|
|
967
|
-
* @returns {void}
|
|
968
|
-
*/
|
|
969
|
-
|
|
970
|
-
/**
|
|
971
|
-
* Fix information for RuleOnError callback.
|
|
972
|
-
*
|
|
973
|
-
* @typedef {Object} RuleOnErrorInfo
|
|
974
|
-
* @property {number} lineNumber Line number (1-based).
|
|
975
|
-
* @property {string} [details] Details about the error.
|
|
976
|
-
* @property {string} [context] Context for the error.
|
|
977
|
-
* @property {number[]} [range] Column number (1-based) and length.
|
|
978
|
-
* @property {RuleOnErrorFixInfo} [fixInfo] Fix information.
|
|
979
|
-
*/
|
|
980
|
-
|
|
981
|
-
/**
|
|
982
|
-
* Fix information for RuleOnErrorInfo.
|
|
983
|
-
*
|
|
984
|
-
* @typedef {Object} RuleOnErrorFixInfo
|
|
985
|
-
* @property {number} [lineNumber] Line number (1-based).
|
|
986
|
-
* @property {number} [editColumn] Column of the fix (1-based).
|
|
987
|
-
* @property {number} [deleteCount] Count of characters to delete.
|
|
988
|
-
* @property {string} [insertText] Text to insert (after deleting).
|
|
989
|
-
*/
|
|
990
|
-
|
|
991
|
-
/**
|
|
992
|
-
* Rule definition.
|
|
993
|
-
*
|
|
994
|
-
* @typedef {Object} Rule
|
|
995
|
-
* @property {string[]} names Rule name(s).
|
|
996
|
-
* @property {string} description Rule description.
|
|
997
|
-
* @property {URL} [information] Link to more information.
|
|
998
|
-
* @property {string[]} tags Rule tag(s).
|
|
999
|
-
* @property {RuleFunction} function Rule implementation.
|
|
1000
|
-
*/
|
|
1001
|
-
|
|
1002
|
-
/**
|
|
1003
|
-
* Configuration options.
|
|
1004
|
-
*
|
|
1005
|
-
* @typedef {Object} Options
|
|
1006
|
-
* @property {string[] | string} [files] Files to lint.
|
|
1007
|
-
* @property {Object.<string, string>} [strings] Strings to lint.
|
|
1008
|
-
* @property {Configuration} [config] Configuration object.
|
|
1009
|
-
* @property {Rule[] | Rule} [customRules] Custom rules.
|
|
1010
|
-
* @property {RegExp} [frontMatter] Front matter pattern.
|
|
1011
|
-
* @property {boolean} [handleRuleFailures] True to catch exceptions.
|
|
1012
|
-
* @property {boolean} [noInlineConfig] True to ignore HTML directives.
|
|
1013
|
-
* @property {number} [resultVersion] Results object version.
|
|
1014
|
-
* @property {Plugin[]} [markdownItPlugins] Additional plugins.
|
|
1015
|
-
*/
|
|
1016
|
-
|
|
1017
|
-
/**
|
|
1018
|
-
* markdown-it plugin.
|
|
1019
|
-
*
|
|
1020
|
-
* @typedef {Array} Plugin
|
|
1021
|
-
*/
|
|
1022
|
-
|
|
1023
|
-
/**
|
|
1024
|
-
* Function to pretty-print lint results.
|
|
1025
|
-
*
|
|
1026
|
-
* @callback ToStringCallback
|
|
1027
|
-
* @param {boolean} [ruleAliases] True to use rule aliases.
|
|
1028
|
-
* @returns {string}
|
|
1029
|
-
*/
|
|
1030
|
-
|
|
1031
|
-
/**
|
|
1032
|
-
* Lint results (for resultVersion 3).
|
|
1033
|
-
*
|
|
1034
|
-
* @typedef {Object.<string, LintError[]>} LintResults
|
|
1035
|
-
*/
|
|
1036
|
-
// The following should be part of the LintResults typedef, but that causes
|
|
1037
|
-
// TypeScript to "forget" about the string map.
|
|
1038
|
-
// * @property {ToStringCallback} toString String representation.
|
|
1039
|
-
// https://github.com/microsoft/TypeScript/issues/34911
|
|
1040
|
-
|
|
1041
|
-
/**
|
|
1042
|
-
* Lint error.
|
|
1043
|
-
*
|
|
1044
|
-
* @typedef {Object} LintError
|
|
1045
|
-
* @property {number} lineNumber Line number (1-based).
|
|
1046
|
-
* @property {string[]} ruleNames Rule name(s).
|
|
1047
|
-
* @property {string} ruleDescription Rule description.
|
|
1048
|
-
* @property {string} ruleInformation Link to more information.
|
|
1049
|
-
* @property {string} errorDetail Detail about the error.
|
|
1050
|
-
* @property {string} errorContext Context for the error.
|
|
1051
|
-
* @property {number[]} errorRange Column number (1-based) and length.
|
|
1052
|
-
* @property {FixInfo} fixInfo Fix information.
|
|
1053
|
-
*/
|
|
1054
|
-
|
|
1055
|
-
/**
|
|
1056
|
-
* Fix information.
|
|
1057
|
-
*
|
|
1058
|
-
* @typedef {Object} FixInfo
|
|
1059
|
-
* @property {number} [editColumn] Column of the fix (1-based).
|
|
1060
|
-
* @property {number} [deleteCount] Count of characters to delete.
|
|
1061
|
-
* @property {string} [insertText] Text to insert (after deleting).
|
|
1062
|
-
*/
|
|
1063
|
-
|
|
1064
|
-
/**
|
|
1065
|
-
* Called with the result of the lint operation.
|
|
1066
|
-
*
|
|
1067
|
-
* @callback LintCallback
|
|
1068
|
-
* @param {Error | null} err Error object or null.
|
|
1069
|
-
* @param {LintResults} [results] Lint results.
|
|
1070
|
-
* @returns {void}
|
|
1071
|
-
*/
|
|
1072
|
-
|
|
1073
|
-
/**
|
|
1074
|
-
* Configuration object for linting rules. For a detailed schema, see
|
|
1075
|
-
* {@link ../schema/markdownlint-config-schema.json}.
|
|
1076
|
-
*
|
|
1077
|
-
* @typedef {Object.<string, RuleConfiguration>} Configuration
|
|
1078
|
-
*/
|
|
1079
|
-
|
|
1080
|
-
/**
|
|
1081
|
-
* Rule configuration object.
|
|
1082
|
-
*
|
|
1083
|
-
* @typedef {boolean | Object} RuleConfiguration Rule configuration.
|
|
1084
|
-
*/
|
|
1085
|
-
|
|
1086
|
-
/**
|
|
1087
|
-
* Parses a configuration string and returns a configuration object.
|
|
1088
|
-
*
|
|
1089
|
-
* @callback ConfigurationParser
|
|
1090
|
-
* @param {string} text Configuration string.
|
|
1091
|
-
* @returns {Configuration}
|
|
1092
|
-
*/
|
|
1093
|
-
|
|
1094
|
-
/**
|
|
1095
|
-
* Called with the result of the readConfig operation.
|
|
1096
|
-
*
|
|
1097
|
-
* @callback ReadConfigCallback
|
|
1098
|
-
* @param {Error | null} err Error object or null.
|
|
1099
|
-
* @param {Configuration} [config] Configuration object.
|
|
1100
|
-
* @returns {void}
|
|
1101
|
-
*/
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
"use strict";
|
|
4
|
+
|
|
5
|
+
const fs = require("fs");
|
|
6
|
+
const path = require("path");
|
|
7
|
+
const { URL } = require("url");
|
|
8
|
+
const markdownIt = require("markdown-it");
|
|
9
|
+
const rules = require("./rules");
|
|
10
|
+
const helpers = require("../helpers");
|
|
11
|
+
const cache = require("./cache");
|
|
12
|
+
|
|
13
|
+
const deprecatedRuleNames = [ "MD002", "MD006" ];
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Validate the list of rules for structure and reuse.
|
|
18
|
+
*
|
|
19
|
+
* @param {Rule[]} ruleList List of rules.
|
|
20
|
+
* @returns {string} Error message if validation fails.
|
|
21
|
+
*/
|
|
22
|
+
function validateRuleList(ruleList) {
|
|
23
|
+
let result = null;
|
|
24
|
+
if (ruleList.length === rules.length) {
|
|
25
|
+
// No need to validate if only using built-in rules
|
|
26
|
+
return result;
|
|
27
|
+
}
|
|
28
|
+
const allIds = {};
|
|
29
|
+
ruleList.forEach(function forRule(rule, index) {
|
|
30
|
+
const customIndex = index - rules.length;
|
|
31
|
+
// eslint-disable-next-line jsdoc/require-jsdoc
|
|
32
|
+
function newError(property) {
|
|
33
|
+
return new Error(
|
|
34
|
+
"Property '" + property + "' of custom rule at index " +
|
|
35
|
+
customIndex + " is incorrect.");
|
|
36
|
+
}
|
|
37
|
+
[ "names", "tags" ].forEach(function forProperty(property) {
|
|
38
|
+
const value = rule[property];
|
|
39
|
+
if (!result &&
|
|
40
|
+
(!value || !Array.isArray(value) || (value.length === 0) ||
|
|
41
|
+
!value.every(helpers.isString) || value.some(helpers.isEmptyString))) {
|
|
42
|
+
result = newError(property);
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
[
|
|
46
|
+
[ "description", "string" ],
|
|
47
|
+
[ "function", "function" ]
|
|
48
|
+
].forEach(function forProperty(propertyInfo) {
|
|
49
|
+
const property = propertyInfo[0];
|
|
50
|
+
const value = rule[property];
|
|
51
|
+
if (!result && (!value || (typeof value !== propertyInfo[1]))) {
|
|
52
|
+
result = newError(property);
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
if (!result && rule.information) {
|
|
56
|
+
if (Object.getPrototypeOf(rule.information) !== URL.prototype) {
|
|
57
|
+
result = newError("information");
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
if (!result) {
|
|
61
|
+
rule.names.forEach(function forName(name) {
|
|
62
|
+
const nameUpper = name.toUpperCase();
|
|
63
|
+
if (!result && (allIds[nameUpper] !== undefined)) {
|
|
64
|
+
result = new Error("Name '" + name + "' of custom rule at index " +
|
|
65
|
+
customIndex + " is already used as a name or tag.");
|
|
66
|
+
}
|
|
67
|
+
allIds[nameUpper] = true;
|
|
68
|
+
});
|
|
69
|
+
rule.tags.forEach(function forTag(tag) {
|
|
70
|
+
const tagUpper = tag.toUpperCase();
|
|
71
|
+
if (!result && allIds[tagUpper]) {
|
|
72
|
+
result = new Error("Tag '" + tag + "' of custom rule at index " +
|
|
73
|
+
customIndex + " is already used as a name.");
|
|
74
|
+
}
|
|
75
|
+
allIds[tagUpper] = false;
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
return result;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Creates a LintResults instance with toString for pretty display.
|
|
84
|
+
*
|
|
85
|
+
* @param {Rule[]} ruleList List of rules.
|
|
86
|
+
* @returns {LintResults} New LintResults instance.
|
|
87
|
+
*/
|
|
88
|
+
function newResults(ruleList) {
|
|
89
|
+
// eslint-disable-next-line jsdoc/require-jsdoc
|
|
90
|
+
function Results() {}
|
|
91
|
+
Results.prototype.toString = function toString(useAlias) {
|
|
92
|
+
const that = this;
|
|
93
|
+
let ruleNameToRule = null;
|
|
94
|
+
const results = [];
|
|
95
|
+
Object.keys(that).forEach(function forFile(file) {
|
|
96
|
+
const fileResults = that[file];
|
|
97
|
+
if (Array.isArray(fileResults)) {
|
|
98
|
+
fileResults.forEach(function forResult(result) {
|
|
99
|
+
const ruleMoniker = result.ruleNames ?
|
|
100
|
+
result.ruleNames.join("/") :
|
|
101
|
+
(result.ruleName + "/" + result.ruleAlias);
|
|
102
|
+
results.push(
|
|
103
|
+
file + ": " +
|
|
104
|
+
result.lineNumber + ": " +
|
|
105
|
+
ruleMoniker + " " +
|
|
106
|
+
result.ruleDescription +
|
|
107
|
+
(result.errorDetail ?
|
|
108
|
+
" [" + result.errorDetail + "]" :
|
|
109
|
+
"") +
|
|
110
|
+
(result.errorContext ?
|
|
111
|
+
" [Context: \"" + result.errorContext + "\"]" :
|
|
112
|
+
""));
|
|
113
|
+
});
|
|
114
|
+
} else {
|
|
115
|
+
if (!ruleNameToRule) {
|
|
116
|
+
ruleNameToRule = {};
|
|
117
|
+
ruleList.forEach(function forRule(rule) {
|
|
118
|
+
const ruleName = rule.names[0].toUpperCase();
|
|
119
|
+
ruleNameToRule[ruleName] = rule;
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
Object.keys(fileResults).forEach(function forRule(ruleName) {
|
|
123
|
+
const rule = ruleNameToRule[ruleName.toUpperCase()];
|
|
124
|
+
const ruleResults = fileResults[ruleName];
|
|
125
|
+
ruleResults.forEach(function forLine(lineNumber) {
|
|
126
|
+
const nameIndex = Math.min(useAlias ? 1 : 0, rule.names.length - 1);
|
|
127
|
+
const result =
|
|
128
|
+
file + ": " +
|
|
129
|
+
lineNumber + ": " +
|
|
130
|
+
rule.names[nameIndex] + " " +
|
|
131
|
+
rule.description;
|
|
132
|
+
results.push(result);
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
return results.join("\n");
|
|
138
|
+
};
|
|
139
|
+
// @ts-ignore
|
|
140
|
+
return new Results();
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Remove front matter (if present at beginning of content).
|
|
145
|
+
*
|
|
146
|
+
* @param {string} content Markdown content.
|
|
147
|
+
* @param {RegExp} frontMatter Regular expression to match front matter.
|
|
148
|
+
* @returns {Object} Trimmed content and front matter lines.
|
|
149
|
+
*/
|
|
150
|
+
function removeFrontMatter(content, frontMatter) {
|
|
151
|
+
let frontMatterLines = [];
|
|
152
|
+
if (frontMatter) {
|
|
153
|
+
const frontMatterMatch = content.match(frontMatter);
|
|
154
|
+
if (frontMatterMatch && !frontMatterMatch.index) {
|
|
155
|
+
const contentMatched = frontMatterMatch[0];
|
|
156
|
+
content = content.slice(contentMatched.length);
|
|
157
|
+
frontMatterLines = contentMatched.split(helpers.newLineRe);
|
|
158
|
+
if (frontMatterLines.length &&
|
|
159
|
+
(frontMatterLines[frontMatterLines.length - 1] === "")) {
|
|
160
|
+
frontMatterLines.length--;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return {
|
|
165
|
+
"content": content,
|
|
166
|
+
"frontMatterLines": frontMatterLines
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Annotate tokens with line/lineNumber.
|
|
172
|
+
*
|
|
173
|
+
* @param {MarkdownItToken[]} tokens Array of markdown-it tokens.
|
|
174
|
+
* @param {string[]} lines Lines of Markdown content.
|
|
175
|
+
* @returns {void}
|
|
176
|
+
*/
|
|
177
|
+
function annotateTokens(tokens, lines) {
|
|
178
|
+
let tbodyMap = null;
|
|
179
|
+
tokens.forEach(function forToken(token) {
|
|
180
|
+
// Handle missing maps for table body
|
|
181
|
+
if (token.type === "tbody_open") {
|
|
182
|
+
tbodyMap = token.map.slice();
|
|
183
|
+
} else if ((token.type === "tr_close") && tbodyMap) {
|
|
184
|
+
tbodyMap[0]++;
|
|
185
|
+
} else if (token.type === "tbody_close") {
|
|
186
|
+
tbodyMap = null;
|
|
187
|
+
}
|
|
188
|
+
if (tbodyMap && !token.map) {
|
|
189
|
+
token.map = tbodyMap.slice();
|
|
190
|
+
}
|
|
191
|
+
// Update token metadata
|
|
192
|
+
if (token.map) {
|
|
193
|
+
token.line = lines[token.map[0]];
|
|
194
|
+
token.lineNumber = token.map[0] + 1;
|
|
195
|
+
// Trim bottom of token to exclude whitespace lines
|
|
196
|
+
while (token.map[1] && !((lines[token.map[1] - 1] || "").trim())) {
|
|
197
|
+
token.map[1]--;
|
|
198
|
+
}
|
|
199
|
+
// Annotate children with lineNumber
|
|
200
|
+
let lineNumber = token.lineNumber;
|
|
201
|
+
const codeSpanExtraLines = [];
|
|
202
|
+
helpers.forEachInlineCodeSpan(
|
|
203
|
+
token.content,
|
|
204
|
+
function handleInlineCodeSpan(code) {
|
|
205
|
+
codeSpanExtraLines.push(code.split(helpers.newLineRe).length - 1);
|
|
206
|
+
}
|
|
207
|
+
);
|
|
208
|
+
(token.children || []).forEach(function forChild(child) {
|
|
209
|
+
child.lineNumber = lineNumber;
|
|
210
|
+
child.line = lines[lineNumber - 1];
|
|
211
|
+
if ((child.type === "softbreak") || (child.type === "hardbreak")) {
|
|
212
|
+
lineNumber++;
|
|
213
|
+
} else if (child.type === "code_inline") {
|
|
214
|
+
lineNumber += codeSpanExtraLines.shift();
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Map rule names/tags to canonical rule name.
|
|
223
|
+
*
|
|
224
|
+
* @param {Rule[]} ruleList List of rules.
|
|
225
|
+
* @returns {Object.<string, string[]>} Map of alias to rule name.
|
|
226
|
+
*/
|
|
227
|
+
function mapAliasToRuleNames(ruleList) {
|
|
228
|
+
const aliasToRuleNames = {};
|
|
229
|
+
// const tagToRuleNames = {};
|
|
230
|
+
ruleList.forEach(function forRule(rule) {
|
|
231
|
+
const ruleName = rule.names[0].toUpperCase();
|
|
232
|
+
// The following is useful for updating README.md:
|
|
233
|
+
// console.log(
|
|
234
|
+
// "* **[" + ruleName + "](doc/Rules.md#" + ruleName.toLowerCase() +
|
|
235
|
+
// ")** *" + rule.names.slice(1).join("/") + "* - " + rule.description);
|
|
236
|
+
rule.names.forEach(function forName(name) {
|
|
237
|
+
const nameUpper = name.toUpperCase();
|
|
238
|
+
aliasToRuleNames[nameUpper] = [ ruleName ];
|
|
239
|
+
});
|
|
240
|
+
rule.tags.forEach(function forTag(tag) {
|
|
241
|
+
const tagUpper = tag.toUpperCase();
|
|
242
|
+
const ruleNames = aliasToRuleNames[tagUpper] || [];
|
|
243
|
+
ruleNames.push(ruleName);
|
|
244
|
+
aliasToRuleNames[tagUpper] = ruleNames;
|
|
245
|
+
// tagToRuleNames[tag] = ruleName;
|
|
246
|
+
});
|
|
247
|
+
});
|
|
248
|
+
// The following is useful for updating README.md:
|
|
249
|
+
// Object.keys(tagToRuleNames).sort().forEach(function forTag(tag) {
|
|
250
|
+
// console.log("* **" + tag + "** - " +
|
|
251
|
+
// aliasToRuleNames[tag.toUpperCase()].join(", "));
|
|
252
|
+
// });
|
|
253
|
+
// @ts-ignore
|
|
254
|
+
return aliasToRuleNames;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Apply (and normalize) configuration object.
|
|
259
|
+
*
|
|
260
|
+
* @param {Rule[]} ruleList List of rules.
|
|
261
|
+
* @param {Configuration} config Configuration object.
|
|
262
|
+
* @param {Object.<string, string[]>} aliasToRuleNames Map of alias to rule
|
|
263
|
+
* names.
|
|
264
|
+
* @returns {Configuration} Effective configuration.
|
|
265
|
+
*/
|
|
266
|
+
function getEffectiveConfig(ruleList, config, aliasToRuleNames) {
|
|
267
|
+
const defaultKey = Object.keys(config).filter(
|
|
268
|
+
(key) => key.toUpperCase() === "DEFAULT"
|
|
269
|
+
);
|
|
270
|
+
const ruleDefault = (defaultKey.length === 0) || !!config[defaultKey[0]];
|
|
271
|
+
const effectiveConfig = {};
|
|
272
|
+
ruleList.forEach((rule) => {
|
|
273
|
+
const ruleName = rule.names[0].toUpperCase();
|
|
274
|
+
effectiveConfig[ruleName] = ruleDefault;
|
|
275
|
+
});
|
|
276
|
+
deprecatedRuleNames.forEach((ruleName) => {
|
|
277
|
+
effectiveConfig[ruleName] = false;
|
|
278
|
+
});
|
|
279
|
+
Object.keys(config).forEach((key) => {
|
|
280
|
+
let value = config[key];
|
|
281
|
+
if (value) {
|
|
282
|
+
if (!(value instanceof Object)) {
|
|
283
|
+
value = {};
|
|
284
|
+
}
|
|
285
|
+
} else {
|
|
286
|
+
value = false;
|
|
287
|
+
}
|
|
288
|
+
const keyUpper = key.toUpperCase();
|
|
289
|
+
(aliasToRuleNames[keyUpper] || []).forEach((ruleName) => {
|
|
290
|
+
effectiveConfig[ruleName] = value;
|
|
291
|
+
});
|
|
292
|
+
});
|
|
293
|
+
return effectiveConfig;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Create a mapping of enabled rules per line.
|
|
298
|
+
*
|
|
299
|
+
* @param {Rule[]} ruleList List of rules.
|
|
300
|
+
* @param {string[]} lines List of content lines.
|
|
301
|
+
* @param {string[]} frontMatterLines List of front matter lines.
|
|
302
|
+
* @param {boolean} noInlineConfig Whether to allow inline configuration.
|
|
303
|
+
* @param {Configuration} config Configuration object.
|
|
304
|
+
* @param {Object.<string, string[]>} aliasToRuleNames Map of alias to rule
|
|
305
|
+
* names.
|
|
306
|
+
* @returns {Object} Effective configuration and enabled rules per line number.
|
|
307
|
+
*/
|
|
308
|
+
function getEnabledRulesPerLineNumber(
|
|
309
|
+
ruleList,
|
|
310
|
+
lines,
|
|
311
|
+
frontMatterLines,
|
|
312
|
+
noInlineConfig,
|
|
313
|
+
config,
|
|
314
|
+
aliasToRuleNames) {
|
|
315
|
+
// Shared variables
|
|
316
|
+
let enabledRules = {};
|
|
317
|
+
let capturedRules = {};
|
|
318
|
+
const allRuleNames = [];
|
|
319
|
+
const enabledRulesPerLineNumber = new Array(1 + frontMatterLines.length);
|
|
320
|
+
// Helper functions
|
|
321
|
+
// eslint-disable-next-line jsdoc/require-jsdoc
|
|
322
|
+
function handleInlineConfig(perLine, forEachMatch, forEachLine) {
|
|
323
|
+
const input = perLine ? lines : [ lines.join("\n") ];
|
|
324
|
+
input.forEach((line) => {
|
|
325
|
+
if (!noInlineConfig) {
|
|
326
|
+
let match = null;
|
|
327
|
+
while ((match = helpers.inlineCommentRe.exec(line))) {
|
|
328
|
+
const action = (match[1] || match[3]).toUpperCase();
|
|
329
|
+
const parameter = match[2] || match[4];
|
|
330
|
+
forEachMatch(action, parameter);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
if (forEachLine) {
|
|
334
|
+
forEachLine();
|
|
335
|
+
}
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
// eslint-disable-next-line jsdoc/require-jsdoc
|
|
339
|
+
function configureFile(action, parameter) {
|
|
340
|
+
if (action === "CONFIGURE-FILE") {
|
|
341
|
+
try {
|
|
342
|
+
const json = JSON.parse(parameter);
|
|
343
|
+
config = {
|
|
344
|
+
...config,
|
|
345
|
+
...json
|
|
346
|
+
};
|
|
347
|
+
} catch (ex) {
|
|
348
|
+
// Ignore parse errors for inline configuration
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
// eslint-disable-next-line jsdoc/require-jsdoc
|
|
353
|
+
function applyEnableDisable(action, parameter) {
|
|
354
|
+
const enabled = (action.startsWith("ENABLE"));
|
|
355
|
+
const items = parameter ?
|
|
356
|
+
parameter.trim().toUpperCase().split(/\s+/) :
|
|
357
|
+
allRuleNames;
|
|
358
|
+
items.forEach((nameUpper) => {
|
|
359
|
+
(aliasToRuleNames[nameUpper] || []).forEach((ruleName) => {
|
|
360
|
+
enabledRules[ruleName] = enabled;
|
|
361
|
+
});
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
// eslint-disable-next-line jsdoc/require-jsdoc
|
|
365
|
+
function enableDisableFile(action, parameter) {
|
|
366
|
+
if ((action === "ENABLE-FILE") || (action === "DISABLE-FILE")) {
|
|
367
|
+
applyEnableDisable(action, parameter);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
// eslint-disable-next-line jsdoc/require-jsdoc
|
|
371
|
+
function captureRestoreEnableDisable(action, parameter) {
|
|
372
|
+
if (action === "CAPTURE") {
|
|
373
|
+
capturedRules = { ...enabledRules };
|
|
374
|
+
} else if (action === "RESTORE") {
|
|
375
|
+
enabledRules = { ...capturedRules };
|
|
376
|
+
} else if ((action === "ENABLE") || (action === "DISABLE")) {
|
|
377
|
+
enabledRules = { ...enabledRules };
|
|
378
|
+
applyEnableDisable(action, parameter);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
// eslint-disable-next-line jsdoc/require-jsdoc
|
|
382
|
+
function updateLineState() {
|
|
383
|
+
enabledRulesPerLineNumber.push(enabledRules);
|
|
384
|
+
}
|
|
385
|
+
// Handle inline comments
|
|
386
|
+
handleInlineConfig(false, configureFile);
|
|
387
|
+
const effectiveConfig = getEffectiveConfig(
|
|
388
|
+
ruleList, config, aliasToRuleNames);
|
|
389
|
+
ruleList.forEach((rule) => {
|
|
390
|
+
const ruleName = rule.names[0].toUpperCase();
|
|
391
|
+
allRuleNames.push(ruleName);
|
|
392
|
+
enabledRules[ruleName] = !!effectiveConfig[ruleName];
|
|
393
|
+
});
|
|
394
|
+
capturedRules = enabledRules;
|
|
395
|
+
handleInlineConfig(true, enableDisableFile);
|
|
396
|
+
handleInlineConfig(true, captureRestoreEnableDisable, updateLineState);
|
|
397
|
+
// Return results
|
|
398
|
+
return {
|
|
399
|
+
effectiveConfig,
|
|
400
|
+
enabledRulesPerLineNumber
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* Compare function for Array.prototype.sort for ascending order of errors.
|
|
406
|
+
*
|
|
407
|
+
* @param {LintError} a First error.
|
|
408
|
+
* @param {LintError} b Second error.
|
|
409
|
+
* @returns {number} Positive value if a>b, negative value if b<a, 0 otherwise.
|
|
410
|
+
*/
|
|
411
|
+
function lineNumberComparison(a, b) {
|
|
412
|
+
return a.lineNumber - b.lineNumber;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* Filter function to include everything.
|
|
417
|
+
*
|
|
418
|
+
* @returns {boolean} True.
|
|
419
|
+
*/
|
|
420
|
+
function filterAllValues() {
|
|
421
|
+
return true;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* Function to return unique values from a sorted errors array.
|
|
426
|
+
*
|
|
427
|
+
* @param {LintError} value Error instance.
|
|
428
|
+
* @param {number} index Index in array.
|
|
429
|
+
* @param {LintError[]} array Array of errors.
|
|
430
|
+
* @returns {boolean} Filter value.
|
|
431
|
+
*/
|
|
432
|
+
function uniqueFilterForSortedErrors(value, index, array) {
|
|
433
|
+
return (index === 0) || (value.lineNumber > array[index - 1].lineNumber);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* Lints a string containing Markdown content.
|
|
438
|
+
*
|
|
439
|
+
* @param {Rule[]} ruleList List of rules.
|
|
440
|
+
* @param {string} name Identifier for the content.
|
|
441
|
+
* @param {string} content Markdown content
|
|
442
|
+
* @param {Object} md markdown-it instance.
|
|
443
|
+
* @param {Configuration} config Configuration object.
|
|
444
|
+
* @param {RegExp} frontMatter Regular expression for front matter.
|
|
445
|
+
* @param {boolean} handleRuleFailures Whether to handle exceptions in rules.
|
|
446
|
+
* @param {boolean} noInlineConfig Whether to allow inline configuration.
|
|
447
|
+
* @param {number} resultVersion Version of the LintResults object to return.
|
|
448
|
+
* @param {Function} callback Callback (err, result) function.
|
|
449
|
+
* @returns {void}
|
|
450
|
+
*/
|
|
451
|
+
function lintContent(
|
|
452
|
+
ruleList,
|
|
453
|
+
name,
|
|
454
|
+
content,
|
|
455
|
+
md,
|
|
456
|
+
config,
|
|
457
|
+
frontMatter,
|
|
458
|
+
handleRuleFailures,
|
|
459
|
+
noInlineConfig,
|
|
460
|
+
resultVersion,
|
|
461
|
+
callback) {
|
|
462
|
+
// Remove UTF-8 byte order marker (if present)
|
|
463
|
+
content = content.replace(/^\ufeff/, "");
|
|
464
|
+
// Remove front matter
|
|
465
|
+
const removeFrontMatterResult = removeFrontMatter(content, frontMatter);
|
|
466
|
+
const frontMatterLines = removeFrontMatterResult.frontMatterLines;
|
|
467
|
+
// Ignore the content of HTML comments
|
|
468
|
+
content = helpers.clearHtmlCommentText(removeFrontMatterResult.content);
|
|
469
|
+
// Parse content into tokens and lines
|
|
470
|
+
const tokens = md.parse(content, {});
|
|
471
|
+
const lines = content.split(helpers.newLineRe);
|
|
472
|
+
annotateTokens(tokens, lines);
|
|
473
|
+
const aliasToRuleNames = mapAliasToRuleNames(ruleList);
|
|
474
|
+
const { effectiveConfig, enabledRulesPerLineNumber } =
|
|
475
|
+
getEnabledRulesPerLineNumber(
|
|
476
|
+
ruleList,
|
|
477
|
+
lines,
|
|
478
|
+
frontMatterLines,
|
|
479
|
+
noInlineConfig,
|
|
480
|
+
config,
|
|
481
|
+
aliasToRuleNames
|
|
482
|
+
);
|
|
483
|
+
// Create parameters for rules
|
|
484
|
+
const params = {
|
|
485
|
+
name,
|
|
486
|
+
tokens,
|
|
487
|
+
lines,
|
|
488
|
+
frontMatterLines
|
|
489
|
+
};
|
|
490
|
+
cache.lineMetadata(helpers.getLineMetadata(params));
|
|
491
|
+
cache.flattenedLists(helpers.flattenLists(params));
|
|
492
|
+
// Function to run for each rule
|
|
493
|
+
const result = (resultVersion === 0) ? {} : [];
|
|
494
|
+
// eslint-disable-next-line jsdoc/require-jsdoc
|
|
495
|
+
function forRule(rule) {
|
|
496
|
+
// Configure rule
|
|
497
|
+
const ruleNameFriendly = rule.names[0];
|
|
498
|
+
const ruleName = ruleNameFriendly.toUpperCase();
|
|
499
|
+
params.config = effectiveConfig[ruleName];
|
|
500
|
+
// eslint-disable-next-line jsdoc/require-jsdoc
|
|
501
|
+
function throwError(property) {
|
|
502
|
+
throw new Error(
|
|
503
|
+
"Property '" + property + "' of onError parameter is incorrect.");
|
|
504
|
+
}
|
|
505
|
+
const errors = [];
|
|
506
|
+
// eslint-disable-next-line jsdoc/require-jsdoc
|
|
507
|
+
function onError(errorInfo) {
|
|
508
|
+
if (!errorInfo ||
|
|
509
|
+
!helpers.isNumber(errorInfo.lineNumber) ||
|
|
510
|
+
(errorInfo.lineNumber < 1) ||
|
|
511
|
+
(errorInfo.lineNumber > lines.length)) {
|
|
512
|
+
throwError("lineNumber");
|
|
513
|
+
}
|
|
514
|
+
if (errorInfo.detail &&
|
|
515
|
+
!helpers.isString(errorInfo.detail)) {
|
|
516
|
+
throwError("detail");
|
|
517
|
+
}
|
|
518
|
+
if (errorInfo.context &&
|
|
519
|
+
!helpers.isString(errorInfo.context)) {
|
|
520
|
+
throwError("context");
|
|
521
|
+
}
|
|
522
|
+
if (errorInfo.range &&
|
|
523
|
+
(!Array.isArray(errorInfo.range) ||
|
|
524
|
+
(errorInfo.range.length !== 2) ||
|
|
525
|
+
!helpers.isNumber(errorInfo.range[0]) ||
|
|
526
|
+
(errorInfo.range[0] < 1) ||
|
|
527
|
+
!helpers.isNumber(errorInfo.range[1]) ||
|
|
528
|
+
(errorInfo.range[1] < 1) ||
|
|
529
|
+
((errorInfo.range[0] + errorInfo.range[1] - 1) >
|
|
530
|
+
lines[errorInfo.lineNumber - 1].length))) {
|
|
531
|
+
throwError("range");
|
|
532
|
+
}
|
|
533
|
+
const fixInfo = errorInfo.fixInfo;
|
|
534
|
+
const cleanFixInfo = {};
|
|
535
|
+
if (fixInfo) {
|
|
536
|
+
if (!helpers.isObject(fixInfo)) {
|
|
537
|
+
throwError("fixInfo");
|
|
538
|
+
}
|
|
539
|
+
if (fixInfo.lineNumber !== undefined) {
|
|
540
|
+
if ((!helpers.isNumber(fixInfo.lineNumber) ||
|
|
541
|
+
(fixInfo.lineNumber < 1) ||
|
|
542
|
+
(fixInfo.lineNumber > lines.length))) {
|
|
543
|
+
throwError("fixInfo.lineNumber");
|
|
544
|
+
}
|
|
545
|
+
cleanFixInfo.lineNumber =
|
|
546
|
+
fixInfo.lineNumber + frontMatterLines.length;
|
|
547
|
+
}
|
|
548
|
+
const effectiveLineNumber = fixInfo.lineNumber || errorInfo.lineNumber;
|
|
549
|
+
if (fixInfo.editColumn !== undefined) {
|
|
550
|
+
if ((!helpers.isNumber(fixInfo.editColumn) ||
|
|
551
|
+
(fixInfo.editColumn < 1) ||
|
|
552
|
+
(fixInfo.editColumn >
|
|
553
|
+
lines[effectiveLineNumber - 1].length + 1))) {
|
|
554
|
+
throwError("fixInfo.editColumn");
|
|
555
|
+
}
|
|
556
|
+
cleanFixInfo.editColumn = fixInfo.editColumn;
|
|
557
|
+
}
|
|
558
|
+
if (fixInfo.deleteCount !== undefined) {
|
|
559
|
+
if ((!helpers.isNumber(fixInfo.deleteCount) ||
|
|
560
|
+
(fixInfo.deleteCount < -1) ||
|
|
561
|
+
(fixInfo.deleteCount >
|
|
562
|
+
lines[effectiveLineNumber - 1].length))) {
|
|
563
|
+
throwError("fixInfo.deleteCount");
|
|
564
|
+
}
|
|
565
|
+
cleanFixInfo.deleteCount = fixInfo.deleteCount;
|
|
566
|
+
}
|
|
567
|
+
if (fixInfo.insertText !== undefined) {
|
|
568
|
+
if (!helpers.isString(fixInfo.insertText)) {
|
|
569
|
+
throwError("fixInfo.insertText");
|
|
570
|
+
}
|
|
571
|
+
cleanFixInfo.insertText = fixInfo.insertText;
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
errors.push({
|
|
575
|
+
"lineNumber": errorInfo.lineNumber + frontMatterLines.length,
|
|
576
|
+
"detail": errorInfo.detail || null,
|
|
577
|
+
"context": errorInfo.context || null,
|
|
578
|
+
"range": errorInfo.range ? [ ...errorInfo.range ] : null,
|
|
579
|
+
"fixInfo": fixInfo ? cleanFixInfo : null
|
|
580
|
+
});
|
|
581
|
+
}
|
|
582
|
+
// Call (possibly external) rule function
|
|
583
|
+
if (handleRuleFailures) {
|
|
584
|
+
try {
|
|
585
|
+
rule.function(params, onError);
|
|
586
|
+
} catch (ex) {
|
|
587
|
+
onError({
|
|
588
|
+
"lineNumber": 1,
|
|
589
|
+
"detail": `This rule threw an exception: ${ex.message}`
|
|
590
|
+
});
|
|
591
|
+
}
|
|
592
|
+
} else {
|
|
593
|
+
rule.function(params, onError);
|
|
594
|
+
}
|
|
595
|
+
// Record any errors (significant performance benefit from length check)
|
|
596
|
+
if (errors.length) {
|
|
597
|
+
errors.sort(lineNumberComparison);
|
|
598
|
+
const filteredErrors = errors
|
|
599
|
+
.filter((resultVersion === 3) ?
|
|
600
|
+
filterAllValues :
|
|
601
|
+
uniqueFilterForSortedErrors)
|
|
602
|
+
.filter(function removeDisabledRules(error) {
|
|
603
|
+
return enabledRulesPerLineNumber[error.lineNumber][ruleName];
|
|
604
|
+
})
|
|
605
|
+
.map(function formatResults(error) {
|
|
606
|
+
if (resultVersion === 0) {
|
|
607
|
+
return error.lineNumber;
|
|
608
|
+
}
|
|
609
|
+
const errorObject = {};
|
|
610
|
+
errorObject.lineNumber = error.lineNumber;
|
|
611
|
+
if (resultVersion === 1) {
|
|
612
|
+
errorObject.ruleName = ruleNameFriendly;
|
|
613
|
+
errorObject.ruleAlias = rule.names[1] || rule.names[0];
|
|
614
|
+
} else {
|
|
615
|
+
errorObject.ruleNames = rule.names;
|
|
616
|
+
}
|
|
617
|
+
errorObject.ruleDescription = rule.description;
|
|
618
|
+
errorObject.ruleInformation =
|
|
619
|
+
rule.information ? rule.information.href : null;
|
|
620
|
+
errorObject.errorDetail = error.detail;
|
|
621
|
+
errorObject.errorContext = error.context;
|
|
622
|
+
errorObject.errorRange = error.range;
|
|
623
|
+
if (resultVersion === 3) {
|
|
624
|
+
errorObject.fixInfo = error.fixInfo;
|
|
625
|
+
}
|
|
626
|
+
return errorObject;
|
|
627
|
+
});
|
|
628
|
+
if (filteredErrors.length) {
|
|
629
|
+
if (resultVersion === 0) {
|
|
630
|
+
result[ruleNameFriendly] = filteredErrors;
|
|
631
|
+
} else {
|
|
632
|
+
Array.prototype.push.apply(result, filteredErrors);
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
// Run all rules
|
|
638
|
+
try {
|
|
639
|
+
ruleList.forEach(forRule);
|
|
640
|
+
} catch (ex) {
|
|
641
|
+
cache.clear();
|
|
642
|
+
return callback(ex);
|
|
643
|
+
}
|
|
644
|
+
cache.clear();
|
|
645
|
+
return callback(null, result);
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
/**
|
|
649
|
+
* Lints a file containing Markdown content.
|
|
650
|
+
*
|
|
651
|
+
* @param {Rule[]} ruleList List of rules.
|
|
652
|
+
* @param {string} file Path of file to lint.
|
|
653
|
+
* @param {Object} md markdown-it instance.
|
|
654
|
+
* @param {Configuration} config Configuration object.
|
|
655
|
+
* @param {RegExp} frontMatter Regular expression for front matter.
|
|
656
|
+
* @param {boolean} handleRuleFailures Whether to handle exceptions in rules.
|
|
657
|
+
* @param {boolean} noInlineConfig Whether to allow inline configuration.
|
|
658
|
+
* @param {number} resultVersion Version of the LintResults object to return.
|
|
659
|
+
* @param {boolean} synchronous Whether to execute synchronously.
|
|
660
|
+
* @param {Function} callback Callback (err, result) function.
|
|
661
|
+
* @returns {void}
|
|
662
|
+
*/
|
|
663
|
+
function lintFile(
|
|
664
|
+
ruleList,
|
|
665
|
+
file,
|
|
666
|
+
md,
|
|
667
|
+
config,
|
|
668
|
+
frontMatter,
|
|
669
|
+
handleRuleFailures,
|
|
670
|
+
noInlineConfig,
|
|
671
|
+
resultVersion,
|
|
672
|
+
synchronous,
|
|
673
|
+
callback) {
|
|
674
|
+
// eslint-disable-next-line jsdoc/require-jsdoc
|
|
675
|
+
function lintContentWrapper(err, content) {
|
|
676
|
+
if (err) {
|
|
677
|
+
return callback(err);
|
|
678
|
+
}
|
|
679
|
+
return lintContent(ruleList, file, content, md, config, frontMatter,
|
|
680
|
+
handleRuleFailures, noInlineConfig, resultVersion, callback);
|
|
681
|
+
}
|
|
682
|
+
// Make a/synchronous call to read file
|
|
683
|
+
if (synchronous) {
|
|
684
|
+
lintContentWrapper(null, fs.readFileSync(file, helpers.utf8Encoding));
|
|
685
|
+
} else {
|
|
686
|
+
fs.readFile(file, helpers.utf8Encoding, lintContentWrapper);
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
/**
|
|
691
|
+
* Lint files and strings specified in the Options object.
|
|
692
|
+
*
|
|
693
|
+
* @param {Options} options Options object.
|
|
694
|
+
* @param {boolean} synchronous Whether to execute synchronously.
|
|
695
|
+
* @param {Function} callback Callback (err, result) function.
|
|
696
|
+
* @returns {void}
|
|
697
|
+
*/
|
|
698
|
+
function lintInput(options, synchronous, callback) {
|
|
699
|
+
// Normalize inputs
|
|
700
|
+
options = options || {};
|
|
701
|
+
callback = callback || function noop() {};
|
|
702
|
+
const ruleList = rules.concat(options.customRules || []);
|
|
703
|
+
const ruleErr = validateRuleList(ruleList);
|
|
704
|
+
if (ruleErr) {
|
|
705
|
+
return callback(ruleErr);
|
|
706
|
+
}
|
|
707
|
+
let files = [];
|
|
708
|
+
if (Array.isArray(options.files)) {
|
|
709
|
+
files = options.files.slice();
|
|
710
|
+
} else if (options.files) {
|
|
711
|
+
files = [ String(options.files) ];
|
|
712
|
+
}
|
|
713
|
+
const strings = options.strings || {};
|
|
714
|
+
const stringsKeys = Object.keys(strings);
|
|
715
|
+
const config = options.config || { "default": true };
|
|
716
|
+
const frontMatter = (options.frontMatter === undefined) ?
|
|
717
|
+
helpers.frontMatterRe : options.frontMatter;
|
|
718
|
+
const handleRuleFailures = !!options.handleRuleFailures;
|
|
719
|
+
const noInlineConfig = !!options.noInlineConfig;
|
|
720
|
+
const resultVersion = (options.resultVersion === undefined) ?
|
|
721
|
+
2 : options.resultVersion;
|
|
722
|
+
const md = markdownIt({ "html": true });
|
|
723
|
+
const markdownItPlugins = options.markdownItPlugins || [];
|
|
724
|
+
markdownItPlugins.forEach(function forPlugin(plugin) {
|
|
725
|
+
// @ts-ignore
|
|
726
|
+
md.use(...plugin);
|
|
727
|
+
});
|
|
728
|
+
const results = newResults(ruleList);
|
|
729
|
+
// Helper to lint the next string or file
|
|
730
|
+
/* eslint-disable consistent-return */
|
|
731
|
+
// eslint-disable-next-line jsdoc/require-jsdoc
|
|
732
|
+
function lintNextItem() {
|
|
733
|
+
let iterating = true;
|
|
734
|
+
let item = null;
|
|
735
|
+
// eslint-disable-next-line jsdoc/require-jsdoc
|
|
736
|
+
function lintNextItemCallback(err, result) {
|
|
737
|
+
if (err) {
|
|
738
|
+
iterating = false;
|
|
739
|
+
return callback(err);
|
|
740
|
+
}
|
|
741
|
+
results[item] = result;
|
|
742
|
+
return iterating || lintNextItem();
|
|
743
|
+
}
|
|
744
|
+
while (iterating) {
|
|
745
|
+
if ((item = stringsKeys.shift())) {
|
|
746
|
+
lintContent(
|
|
747
|
+
ruleList,
|
|
748
|
+
item,
|
|
749
|
+
strings[item] || "",
|
|
750
|
+
md,
|
|
751
|
+
config,
|
|
752
|
+
frontMatter,
|
|
753
|
+
handleRuleFailures,
|
|
754
|
+
noInlineConfig,
|
|
755
|
+
resultVersion,
|
|
756
|
+
lintNextItemCallback);
|
|
757
|
+
} else if ((item = files.shift())) {
|
|
758
|
+
iterating = synchronous;
|
|
759
|
+
lintFile(
|
|
760
|
+
ruleList,
|
|
761
|
+
item,
|
|
762
|
+
md,
|
|
763
|
+
config,
|
|
764
|
+
frontMatter,
|
|
765
|
+
handleRuleFailures,
|
|
766
|
+
noInlineConfig,
|
|
767
|
+
resultVersion,
|
|
768
|
+
synchronous,
|
|
769
|
+
lintNextItemCallback);
|
|
770
|
+
} else {
|
|
771
|
+
return callback(null, results);
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
return lintNextItem();
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
/**
|
|
779
|
+
* Lint specified Markdown files.
|
|
780
|
+
*
|
|
781
|
+
* @param {Options} options Configuration options.
|
|
782
|
+
* @param {LintCallback} callback Callback (err, result) function.
|
|
783
|
+
* @returns {void}
|
|
784
|
+
*/
|
|
785
|
+
function markdownlint(options, callback) {
|
|
786
|
+
return lintInput(options, false, callback);
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
/**
|
|
790
|
+
* Lint specified Markdown files synchronously.
|
|
791
|
+
*
|
|
792
|
+
* @param {Options} options Configuration options.
|
|
793
|
+
* @returns {LintResults} Results object.
|
|
794
|
+
*/
|
|
795
|
+
function markdownlintSync(options) {
|
|
796
|
+
let results = null;
|
|
797
|
+
lintInput(options, true, function callback(error, res) {
|
|
798
|
+
if (error) {
|
|
799
|
+
throw error;
|
|
800
|
+
}
|
|
801
|
+
results = res;
|
|
802
|
+
});
|
|
803
|
+
return results;
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
/**
|
|
807
|
+
* Parse the content of a configuration file.
|
|
808
|
+
*
|
|
809
|
+
* @param {string} name Name of the configuration file.
|
|
810
|
+
* @param {string} content Configuration content.
|
|
811
|
+
* @param {ConfigurationParser[]} parsers Parsing function(s).
|
|
812
|
+
* @returns {Object} Configuration object and error message.
|
|
813
|
+
*/
|
|
814
|
+
function parseConfiguration(name, content, parsers) {
|
|
815
|
+
let config = null;
|
|
816
|
+
let message = "";
|
|
817
|
+
const errors = [];
|
|
818
|
+
// Try each parser
|
|
819
|
+
(parsers || [ JSON.parse ]).every((parser) => {
|
|
820
|
+
try {
|
|
821
|
+
config = parser(content);
|
|
822
|
+
} catch (ex) {
|
|
823
|
+
errors.push(ex.message);
|
|
824
|
+
}
|
|
825
|
+
return !config;
|
|
826
|
+
});
|
|
827
|
+
// Message if unable to parse
|
|
828
|
+
if (!config) {
|
|
829
|
+
errors.unshift(`Unable to parse '${name}'`);
|
|
830
|
+
message = errors.join("; ");
|
|
831
|
+
}
|
|
832
|
+
return {
|
|
833
|
+
config,
|
|
834
|
+
message
|
|
835
|
+
};
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
/**
|
|
839
|
+
* Read specified configuration file.
|
|
840
|
+
*
|
|
841
|
+
* @param {string} file Configuration file name.
|
|
842
|
+
* @param {ConfigurationParser[] | ReadConfigCallback} parsers Parsing
|
|
843
|
+
* function(s).
|
|
844
|
+
* @param {ReadConfigCallback} [callback] Callback (err, result) function.
|
|
845
|
+
* @returns {void}
|
|
846
|
+
*/
|
|
847
|
+
function readConfig(file, parsers, callback) {
|
|
848
|
+
if (!callback) {
|
|
849
|
+
// @ts-ignore
|
|
850
|
+
callback = parsers;
|
|
851
|
+
parsers = null;
|
|
852
|
+
}
|
|
853
|
+
// Read file
|
|
854
|
+
fs.readFile(file, helpers.utf8Encoding, (err, content) => {
|
|
855
|
+
if (err) {
|
|
856
|
+
return callback(err);
|
|
857
|
+
}
|
|
858
|
+
// Try to parse file
|
|
859
|
+
// @ts-ignore
|
|
860
|
+
const { config, message } = parseConfiguration(file, content, parsers);
|
|
861
|
+
if (!config) {
|
|
862
|
+
return callback(new Error(message));
|
|
863
|
+
}
|
|
864
|
+
// Extend configuration
|
|
865
|
+
const configExtends = config.extends;
|
|
866
|
+
if (configExtends) {
|
|
867
|
+
delete config.extends;
|
|
868
|
+
const extendsFile = path.resolve(path.dirname(file), configExtends);
|
|
869
|
+
return readConfig(extendsFile, parsers, (errr, extendsConfig) => {
|
|
870
|
+
if (errr) {
|
|
871
|
+
return callback(errr);
|
|
872
|
+
}
|
|
873
|
+
return callback(null, {
|
|
874
|
+
...extendsConfig,
|
|
875
|
+
...config
|
|
876
|
+
});
|
|
877
|
+
});
|
|
878
|
+
}
|
|
879
|
+
return callback(null, config);
|
|
880
|
+
});
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
/**
|
|
884
|
+
* Read specified configuration file synchronously.
|
|
885
|
+
*
|
|
886
|
+
* @param {string} file Configuration file name.
|
|
887
|
+
* @param {ConfigurationParser[]} [parsers] Parsing function(s).
|
|
888
|
+
* @returns {Configuration} Configuration object.
|
|
889
|
+
*/
|
|
890
|
+
function readConfigSync(file, parsers) {
|
|
891
|
+
// Read file
|
|
892
|
+
const content = fs.readFileSync(file, helpers.utf8Encoding);
|
|
893
|
+
// Try to parse file
|
|
894
|
+
const { config, message } = parseConfiguration(file, content, parsers);
|
|
895
|
+
if (!config) {
|
|
896
|
+
throw new Error(message);
|
|
897
|
+
}
|
|
898
|
+
// Extend configuration
|
|
899
|
+
const configExtends = config.extends;
|
|
900
|
+
if (configExtends) {
|
|
901
|
+
delete config.extends;
|
|
902
|
+
return {
|
|
903
|
+
...readConfigSync(
|
|
904
|
+
path.resolve(path.dirname(file), configExtends),
|
|
905
|
+
parsers
|
|
906
|
+
),
|
|
907
|
+
...config
|
|
908
|
+
};
|
|
909
|
+
}
|
|
910
|
+
return config;
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
// Export a/synchronous APIs
|
|
914
|
+
markdownlint.sync = markdownlintSync;
|
|
915
|
+
markdownlint.readConfig = readConfig;
|
|
916
|
+
markdownlint.readConfigSync = readConfigSync;
|
|
917
|
+
module.exports = markdownlint;
|
|
918
|
+
|
|
919
|
+
// Type declarations
|
|
920
|
+
|
|
921
|
+
/**
|
|
922
|
+
* Function to implement rule logic.
|
|
923
|
+
*
|
|
924
|
+
* @callback RuleFunction
|
|
925
|
+
* @param {RuleParams} params Rule parameters.
|
|
926
|
+
* @param {RuleOnError} onError Error-reporting callback.
|
|
927
|
+
* @returns {void}
|
|
928
|
+
*/
|
|
929
|
+
|
|
930
|
+
/**
|
|
931
|
+
* Rule parameters.
|
|
932
|
+
*
|
|
933
|
+
* @typedef {Object} RuleParams
|
|
934
|
+
* @property {string} name File/string name.
|
|
935
|
+
* @property {MarkdownItToken[]} tokens markdown-it token objects.
|
|
936
|
+
* @property {string[]} lines File/string lines.
|
|
937
|
+
* @property {string[]} frontMatterLines Front matter lines.
|
|
938
|
+
* @property {RuleConfiguration} config Rule configuration.
|
|
939
|
+
*/
|
|
940
|
+
|
|
941
|
+
/**
|
|
942
|
+
* Markdown-It token.
|
|
943
|
+
*
|
|
944
|
+
* @typedef {Object} MarkdownItToken
|
|
945
|
+
* @property {string[][]} attrs HTML attributes.
|
|
946
|
+
* @property {boolean} block Block-level token.
|
|
947
|
+
* @property {MarkdownItToken[]} children Child nodes.
|
|
948
|
+
* @property {string} content Tag contents.
|
|
949
|
+
* @property {boolean} hidden Ignore element.
|
|
950
|
+
* @property {string} info Fence info.
|
|
951
|
+
* @property {number} level Nesting level.
|
|
952
|
+
* @property {number[]} map Beginning/ending line numbers.
|
|
953
|
+
* @property {string} markup Markup text.
|
|
954
|
+
* @property {Object} meta Arbitrary data.
|
|
955
|
+
* @property {number} nesting Level change.
|
|
956
|
+
* @property {string} tag HTML tag name.
|
|
957
|
+
* @property {string} type Token type.
|
|
958
|
+
* @property {number} lineNumber Line number (1-based).
|
|
959
|
+
* @property {string} line Line content.
|
|
960
|
+
*/
|
|
961
|
+
|
|
962
|
+
/**
|
|
963
|
+
* Error-reporting callback.
|
|
964
|
+
*
|
|
965
|
+
* @callback RuleOnError
|
|
966
|
+
* @param {RuleOnErrorInfo} onErrorInfo Error information.
|
|
967
|
+
* @returns {void}
|
|
968
|
+
*/
|
|
969
|
+
|
|
970
|
+
/**
|
|
971
|
+
* Fix information for RuleOnError callback.
|
|
972
|
+
*
|
|
973
|
+
* @typedef {Object} RuleOnErrorInfo
|
|
974
|
+
* @property {number} lineNumber Line number (1-based).
|
|
975
|
+
* @property {string} [details] Details about the error.
|
|
976
|
+
* @property {string} [context] Context for the error.
|
|
977
|
+
* @property {number[]} [range] Column number (1-based) and length.
|
|
978
|
+
* @property {RuleOnErrorFixInfo} [fixInfo] Fix information.
|
|
979
|
+
*/
|
|
980
|
+
|
|
981
|
+
/**
|
|
982
|
+
* Fix information for RuleOnErrorInfo.
|
|
983
|
+
*
|
|
984
|
+
* @typedef {Object} RuleOnErrorFixInfo
|
|
985
|
+
* @property {number} [lineNumber] Line number (1-based).
|
|
986
|
+
* @property {number} [editColumn] Column of the fix (1-based).
|
|
987
|
+
* @property {number} [deleteCount] Count of characters to delete.
|
|
988
|
+
* @property {string} [insertText] Text to insert (after deleting).
|
|
989
|
+
*/
|
|
990
|
+
|
|
991
|
+
/**
|
|
992
|
+
* Rule definition.
|
|
993
|
+
*
|
|
994
|
+
* @typedef {Object} Rule
|
|
995
|
+
* @property {string[]} names Rule name(s).
|
|
996
|
+
* @property {string} description Rule description.
|
|
997
|
+
* @property {URL} [information] Link to more information.
|
|
998
|
+
* @property {string[]} tags Rule tag(s).
|
|
999
|
+
* @property {RuleFunction} function Rule implementation.
|
|
1000
|
+
*/
|
|
1001
|
+
|
|
1002
|
+
/**
|
|
1003
|
+
* Configuration options.
|
|
1004
|
+
*
|
|
1005
|
+
* @typedef {Object} Options
|
|
1006
|
+
* @property {string[] | string} [files] Files to lint.
|
|
1007
|
+
* @property {Object.<string, string>} [strings] Strings to lint.
|
|
1008
|
+
* @property {Configuration} [config] Configuration object.
|
|
1009
|
+
* @property {Rule[] | Rule} [customRules] Custom rules.
|
|
1010
|
+
* @property {RegExp} [frontMatter] Front matter pattern.
|
|
1011
|
+
* @property {boolean} [handleRuleFailures] True to catch exceptions.
|
|
1012
|
+
* @property {boolean} [noInlineConfig] True to ignore HTML directives.
|
|
1013
|
+
* @property {number} [resultVersion] Results object version.
|
|
1014
|
+
* @property {Plugin[]} [markdownItPlugins] Additional plugins.
|
|
1015
|
+
*/
|
|
1016
|
+
|
|
1017
|
+
/**
|
|
1018
|
+
* markdown-it plugin.
|
|
1019
|
+
*
|
|
1020
|
+
* @typedef {Array} Plugin
|
|
1021
|
+
*/
|
|
1022
|
+
|
|
1023
|
+
/**
|
|
1024
|
+
* Function to pretty-print lint results.
|
|
1025
|
+
*
|
|
1026
|
+
* @callback ToStringCallback
|
|
1027
|
+
* @param {boolean} [ruleAliases] True to use rule aliases.
|
|
1028
|
+
* @returns {string}
|
|
1029
|
+
*/
|
|
1030
|
+
|
|
1031
|
+
/**
|
|
1032
|
+
* Lint results (for resultVersion 3).
|
|
1033
|
+
*
|
|
1034
|
+
* @typedef {Object.<string, LintError[]>} LintResults
|
|
1035
|
+
*/
|
|
1036
|
+
// The following should be part of the LintResults typedef, but that causes
|
|
1037
|
+
// TypeScript to "forget" about the string map.
|
|
1038
|
+
// * @property {ToStringCallback} toString String representation.
|
|
1039
|
+
// https://github.com/microsoft/TypeScript/issues/34911
|
|
1040
|
+
|
|
1041
|
+
/**
|
|
1042
|
+
* Lint error.
|
|
1043
|
+
*
|
|
1044
|
+
* @typedef {Object} LintError
|
|
1045
|
+
* @property {number} lineNumber Line number (1-based).
|
|
1046
|
+
* @property {string[]} ruleNames Rule name(s).
|
|
1047
|
+
* @property {string} ruleDescription Rule description.
|
|
1048
|
+
* @property {string} ruleInformation Link to more information.
|
|
1049
|
+
* @property {string} errorDetail Detail about the error.
|
|
1050
|
+
* @property {string} errorContext Context for the error.
|
|
1051
|
+
* @property {number[]} errorRange Column number (1-based) and length.
|
|
1052
|
+
* @property {FixInfo} fixInfo Fix information.
|
|
1053
|
+
*/
|
|
1054
|
+
|
|
1055
|
+
/**
|
|
1056
|
+
* Fix information.
|
|
1057
|
+
*
|
|
1058
|
+
* @typedef {Object} FixInfo
|
|
1059
|
+
* @property {number} [editColumn] Column of the fix (1-based).
|
|
1060
|
+
* @property {number} [deleteCount] Count of characters to delete.
|
|
1061
|
+
* @property {string} [insertText] Text to insert (after deleting).
|
|
1062
|
+
*/
|
|
1063
|
+
|
|
1064
|
+
/**
|
|
1065
|
+
* Called with the result of the lint operation.
|
|
1066
|
+
*
|
|
1067
|
+
* @callback LintCallback
|
|
1068
|
+
* @param {Error | null} err Error object or null.
|
|
1069
|
+
* @param {LintResults} [results] Lint results.
|
|
1070
|
+
* @returns {void}
|
|
1071
|
+
*/
|
|
1072
|
+
|
|
1073
|
+
/**
|
|
1074
|
+
* Configuration object for linting rules. For a detailed schema, see
|
|
1075
|
+
* {@link ../schema/markdownlint-config-schema.json}.
|
|
1076
|
+
*
|
|
1077
|
+
* @typedef {Object.<string, RuleConfiguration>} Configuration
|
|
1078
|
+
*/
|
|
1079
|
+
|
|
1080
|
+
/**
|
|
1081
|
+
* Rule configuration object.
|
|
1082
|
+
*
|
|
1083
|
+
* @typedef {boolean | Object} RuleConfiguration Rule configuration.
|
|
1084
|
+
*/
|
|
1085
|
+
|
|
1086
|
+
/**
|
|
1087
|
+
* Parses a configuration string and returns a configuration object.
|
|
1088
|
+
*
|
|
1089
|
+
* @callback ConfigurationParser
|
|
1090
|
+
* @param {string} text Configuration string.
|
|
1091
|
+
* @returns {Configuration}
|
|
1092
|
+
*/
|
|
1093
|
+
|
|
1094
|
+
/**
|
|
1095
|
+
* Called with the result of the readConfig operation.
|
|
1096
|
+
*
|
|
1097
|
+
* @callback ReadConfigCallback
|
|
1098
|
+
* @param {Error | null} err Error object or null.
|
|
1099
|
+
* @param {Configuration} [config] Configuration object.
|
|
1100
|
+
* @returns {void}
|
|
1101
|
+
*/
|