markdownlint 0.23.0 → 0.25.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CONTRIBUTING.md +13 -3
- package/LICENSE +1 -1
- package/README.md +53 -16
- package/demo/markdownlint-browser.js +769 -461
- package/doc/CustomRules.md +15 -4
- package/doc/Prettier.md +23 -0
- package/doc/Rules.md +100 -12
- package/helpers/LICENSE +1 -1
- package/helpers/helpers.js +199 -39
- package/helpers/package.json +1 -1
- package/lib/cache.js +14 -5
- package/lib/constants.js +7 -0
- package/lib/markdownlint.d.ts +30 -10
- package/lib/markdownlint.js +326 -247
- package/lib/md007.js +4 -2
- package/lib/md010.js +25 -16
- package/lib/md011.js +31 -27
- package/lib/md033.js +12 -8
- package/lib/md035.js +4 -4
- package/lib/md037.js +19 -5
- package/lib/md039.js +2 -1
- package/lib/md043.js +8 -4
- package/lib/md044.js +54 -71
- package/lib/md049.js +45 -0
- package/lib/md050.js +45 -0
- package/lib/rules.js +4 -4
- package/package.json +44 -34
- package/schema/.markdownlint.jsonc +19 -3
- package/schema/.markdownlint.yaml +15 -1
- package/schema/markdownlint-config-schema.json +108 -4
- package/schema/build-config-example.js +0 -53
- package/schema/build-config-schema.js +0 -420
package/lib/markdownlint.js
CHANGED
|
@@ -2,23 +2,27 @@
|
|
|
2
2
|
|
|
3
3
|
"use strict";
|
|
4
4
|
|
|
5
|
-
const fs = require("fs");
|
|
6
5
|
const path = require("path");
|
|
7
6
|
const { promisify } = require("util");
|
|
8
7
|
const markdownIt = require("markdown-it");
|
|
8
|
+
const { deprecatedRuleNames } = require("./constants");
|
|
9
9
|
const rules = require("./rules");
|
|
10
10
|
const helpers = require("../helpers");
|
|
11
11
|
const cache = require("./cache");
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
// @ts-ignore
|
|
14
|
+
// eslint-disable-next-line camelcase, max-len, no-inline-comments, no-undef
|
|
15
|
+
const dynamicRequire = (typeof __non_webpack_require__ === "undefined") ? require : /* c8 ignore next */ __non_webpack_require__;
|
|
16
|
+
// Capture native require implementation for dynamic loading of modules
|
|
14
17
|
|
|
15
18
|
/**
|
|
16
19
|
* Validate the list of rules for structure and reuse.
|
|
17
20
|
*
|
|
18
21
|
* @param {Rule[]} ruleList List of rules.
|
|
22
|
+
* @param {boolean} synchronous Whether to execute synchronously.
|
|
19
23
|
* @returns {string} Error message if validation fails.
|
|
20
24
|
*/
|
|
21
|
-
function validateRuleList(ruleList) {
|
|
25
|
+
function validateRuleList(ruleList, synchronous) {
|
|
22
26
|
let result = null;
|
|
23
27
|
if (ruleList.length === rules.length) {
|
|
24
28
|
// No need to validate if only using built-in rules
|
|
@@ -58,6 +62,19 @@ function validateRuleList(ruleList) {
|
|
|
58
62
|
) {
|
|
59
63
|
result = newError("information");
|
|
60
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
|
+
}
|
|
61
78
|
if (!result) {
|
|
62
79
|
rule.names.forEach(function forName(name) {
|
|
63
80
|
const nameUpper = name.toUpperCase();
|
|
@@ -178,27 +195,21 @@ function removeFrontMatter(content, frontMatter) {
|
|
|
178
195
|
* @returns {void}
|
|
179
196
|
*/
|
|
180
197
|
function annotateTokens(tokens, lines) {
|
|
181
|
-
let
|
|
198
|
+
let trMap = null;
|
|
182
199
|
tokens.forEach(function forToken(token) {
|
|
183
|
-
//
|
|
184
|
-
if (
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
tableMap
|
|
192
|
-
) {
|
|
193
|
-
tableMap[0]++;
|
|
194
|
-
} else if (
|
|
195
|
-
(token.type === "thead_close") ||
|
|
196
|
-
(token.type === "tbody_close")
|
|
197
|
-
) {
|
|
198
|
-
tableMap = null;
|
|
200
|
+
// Provide missing maps for table content
|
|
201
|
+
if (token.type === "tr_open") {
|
|
202
|
+
trMap = token.map;
|
|
203
|
+
} else if (token.type === "tr_close") {
|
|
204
|
+
trMap = null;
|
|
205
|
+
}
|
|
206
|
+
if (!token.map && trMap) {
|
|
207
|
+
token.map = [ ...trMap ];
|
|
199
208
|
}
|
|
200
|
-
|
|
201
|
-
|
|
209
|
+
// Adjust maps for math blocks
|
|
210
|
+
if (helpers.isMathBlock(token) && token.map[1]) {
|
|
211
|
+
// markdown-it-texmath plugin does not account for math_block_end
|
|
212
|
+
token.map[1]++;
|
|
202
213
|
}
|
|
203
214
|
// Update token metadata
|
|
204
215
|
if (token.map) {
|
|
@@ -331,8 +342,7 @@ function getEnabledRulesPerLineNumber(
|
|
|
331
342
|
const enabledRulesPerLineNumber = new Array(1 + frontMatterLines.length);
|
|
332
343
|
// Helper functions
|
|
333
344
|
// eslint-disable-next-line jsdoc/require-jsdoc
|
|
334
|
-
function handleInlineConfig(
|
|
335
|
-
const input = perLine ? lines : [ lines.join("\n") ];
|
|
345
|
+
function handleInlineConfig(input, forEachMatch, forEachLine) {
|
|
336
346
|
input.forEach((line, lineIndex) => {
|
|
337
347
|
if (!noInlineConfig) {
|
|
338
348
|
let match = null;
|
|
@@ -363,6 +373,7 @@ function getEnabledRulesPerLineNumber(
|
|
|
363
373
|
}
|
|
364
374
|
// eslint-disable-next-line jsdoc/require-jsdoc
|
|
365
375
|
function applyEnableDisable(action, parameter, state) {
|
|
376
|
+
state = { ...state };
|
|
366
377
|
const enabled = (action.startsWith("ENABLE"));
|
|
367
378
|
const items = parameter ?
|
|
368
379
|
parameter.trim().toUpperCase().split(/\s+/) :
|
|
@@ -372,40 +383,42 @@ function getEnabledRulesPerLineNumber(
|
|
|
372
383
|
state[ruleName] = enabled;
|
|
373
384
|
});
|
|
374
385
|
});
|
|
386
|
+
return state;
|
|
375
387
|
}
|
|
376
388
|
// eslint-disable-next-line jsdoc/require-jsdoc
|
|
377
389
|
function enableDisableFile(action, parameter) {
|
|
378
390
|
if ((action === "ENABLE-FILE") || (action === "DISABLE-FILE")) {
|
|
379
|
-
applyEnableDisable(action, parameter, enabledRules);
|
|
391
|
+
enabledRules = applyEnableDisable(action, parameter, enabledRules);
|
|
380
392
|
}
|
|
381
393
|
}
|
|
382
394
|
// eslint-disable-next-line jsdoc/require-jsdoc
|
|
383
395
|
function captureRestoreEnableDisable(action, parameter) {
|
|
384
396
|
if (action === "CAPTURE") {
|
|
385
|
-
capturedRules =
|
|
397
|
+
capturedRules = enabledRules;
|
|
386
398
|
} else if (action === "RESTORE") {
|
|
387
|
-
enabledRules =
|
|
399
|
+
enabledRules = capturedRules;
|
|
388
400
|
} else if ((action === "ENABLE") || (action === "DISABLE")) {
|
|
389
|
-
enabledRules =
|
|
390
|
-
applyEnableDisable(action, parameter, enabledRules);
|
|
401
|
+
enabledRules = applyEnableDisable(action, parameter, enabledRules);
|
|
391
402
|
}
|
|
392
403
|
}
|
|
393
404
|
// eslint-disable-next-line jsdoc/require-jsdoc
|
|
394
405
|
function updateLineState() {
|
|
395
|
-
enabledRulesPerLineNumber.push(
|
|
406
|
+
enabledRulesPerLineNumber.push(enabledRules);
|
|
396
407
|
}
|
|
397
408
|
// eslint-disable-next-line jsdoc/require-jsdoc
|
|
398
409
|
function disableNextLine(action, parameter, lineNumber) {
|
|
399
410
|
if (action === "DISABLE-NEXT-LINE") {
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
411
|
+
const nextLineNumber = frontMatterLines.length + lineNumber + 1;
|
|
412
|
+
enabledRulesPerLineNumber[nextLineNumber] =
|
|
413
|
+
applyEnableDisable(
|
|
414
|
+
action,
|
|
415
|
+
parameter,
|
|
416
|
+
enabledRulesPerLineNumber[nextLineNumber] || {}
|
|
417
|
+
);
|
|
405
418
|
}
|
|
406
419
|
}
|
|
407
420
|
// Handle inline comments
|
|
408
|
-
handleInlineConfig(
|
|
421
|
+
handleInlineConfig([ lines.join("\n") ], configureFile);
|
|
409
422
|
const effectiveConfig = getEffectiveConfig(
|
|
410
423
|
ruleList, config, aliasToRuleNames);
|
|
411
424
|
ruleList.forEach((rule) => {
|
|
@@ -414,9 +427,9 @@ function getEnabledRulesPerLineNumber(
|
|
|
414
427
|
enabledRules[ruleName] = !!effectiveConfig[ruleName];
|
|
415
428
|
});
|
|
416
429
|
capturedRules = enabledRules;
|
|
417
|
-
handleInlineConfig(
|
|
418
|
-
handleInlineConfig(
|
|
419
|
-
handleInlineConfig(
|
|
430
|
+
handleInlineConfig(lines, enableDisableFile);
|
|
431
|
+
handleInlineConfig(lines, captureRestoreEnableDisable, updateLineState);
|
|
432
|
+
handleInlineConfig(lines, disableNextLine);
|
|
420
433
|
// Return results
|
|
421
434
|
return {
|
|
422
435
|
effectiveConfig,
|
|
@@ -424,38 +437,6 @@ function getEnabledRulesPerLineNumber(
|
|
|
424
437
|
};
|
|
425
438
|
}
|
|
426
439
|
|
|
427
|
-
/**
|
|
428
|
-
* Compare function for Array.prototype.sort for ascending order of errors.
|
|
429
|
-
*
|
|
430
|
-
* @param {LintError} a First error.
|
|
431
|
-
* @param {LintError} b Second error.
|
|
432
|
-
* @returns {number} Positive value if a>b, negative value if b<a, 0 otherwise.
|
|
433
|
-
*/
|
|
434
|
-
function lineNumberComparison(a, b) {
|
|
435
|
-
return a.lineNumber - b.lineNumber;
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
/**
|
|
439
|
-
* Filter function to include everything.
|
|
440
|
-
*
|
|
441
|
-
* @returns {boolean} True.
|
|
442
|
-
*/
|
|
443
|
-
function filterAllValues() {
|
|
444
|
-
return true;
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
/**
|
|
448
|
-
* Function to return unique values from a sorted errors array.
|
|
449
|
-
*
|
|
450
|
-
* @param {LintError} value Error instance.
|
|
451
|
-
* @param {number} index Index in array.
|
|
452
|
-
* @param {LintError[]} array Array of errors.
|
|
453
|
-
* @returns {boolean} Filter value.
|
|
454
|
-
*/
|
|
455
|
-
function uniqueFilterForSortedErrors(value, index, array) {
|
|
456
|
-
return (index === 0) || (value.lineNumber > array[index - 1].lineNumber);
|
|
457
|
-
}
|
|
458
|
-
|
|
459
440
|
/**
|
|
460
441
|
* Lints a string containing Markdown content.
|
|
461
442
|
*
|
|
@@ -505,27 +486,28 @@ function lintContent(
|
|
|
505
486
|
);
|
|
506
487
|
// Create parameters for rules
|
|
507
488
|
const params = {
|
|
508
|
-
name,
|
|
509
|
-
tokens,
|
|
510
|
-
lines,
|
|
511
|
-
frontMatterLines
|
|
489
|
+
"name": helpers.deepFreeze(name),
|
|
490
|
+
"tokens": helpers.deepFreeze(tokens),
|
|
491
|
+
"lines": helpers.deepFreeze(lines),
|
|
492
|
+
"frontMatterLines": helpers.deepFreeze(frontMatterLines)
|
|
512
493
|
};
|
|
513
494
|
cache.lineMetadata(helpers.getLineMetadata(params));
|
|
514
|
-
cache.flattenedLists(helpers.flattenLists(params));
|
|
495
|
+
cache.flattenedLists(helpers.flattenLists(params.tokens));
|
|
496
|
+
cache.codeBlockAndSpanRanges(
|
|
497
|
+
helpers.codeBlockAndSpanRanges(params, cache.lineMetadata())
|
|
498
|
+
);
|
|
515
499
|
// Function to run for each rule
|
|
516
|
-
|
|
500
|
+
let results = [];
|
|
517
501
|
// eslint-disable-next-line jsdoc/require-jsdoc
|
|
518
502
|
function forRule(rule) {
|
|
519
503
|
// Configure rule
|
|
520
|
-
const
|
|
521
|
-
const ruleName = ruleNameFriendly.toUpperCase();
|
|
504
|
+
const ruleName = rule.names[0].toUpperCase();
|
|
522
505
|
params.config = effectiveConfig[ruleName];
|
|
523
506
|
// eslint-disable-next-line jsdoc/require-jsdoc
|
|
524
507
|
function throwError(property) {
|
|
525
508
|
throw new Error(
|
|
526
509
|
"Property '" + property + "' of onError parameter is incorrect.");
|
|
527
510
|
}
|
|
528
|
-
const errors = [];
|
|
529
511
|
// eslint-disable-next-line jsdoc/require-jsdoc
|
|
530
512
|
function onError(errorInfo) {
|
|
531
513
|
if (!errorInfo ||
|
|
@@ -534,6 +516,10 @@ function lintContent(
|
|
|
534
516
|
(errorInfo.lineNumber > lines.length)) {
|
|
535
517
|
throwError("lineNumber");
|
|
536
518
|
}
|
|
519
|
+
const lineNumber = errorInfo.lineNumber + frontMatterLines.length;
|
|
520
|
+
if (!enabledRulesPerLineNumber[lineNumber][ruleName]) {
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
537
523
|
if (errorInfo.detail &&
|
|
538
524
|
!helpers.isString(errorInfo.detail)) {
|
|
539
525
|
throwError("detail");
|
|
@@ -544,12 +530,12 @@ function lintContent(
|
|
|
544
530
|
}
|
|
545
531
|
if (errorInfo.range &&
|
|
546
532
|
(!Array.isArray(errorInfo.range) ||
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
533
|
+
(errorInfo.range.length !== 2) ||
|
|
534
|
+
!helpers.isNumber(errorInfo.range[0]) ||
|
|
535
|
+
(errorInfo.range[0] < 1) ||
|
|
536
|
+
!helpers.isNumber(errorInfo.range[1]) ||
|
|
537
|
+
(errorInfo.range[1] < 1) ||
|
|
538
|
+
((errorInfo.range[0] + errorInfo.range[1] - 1) >
|
|
553
539
|
lines[errorInfo.lineNumber - 1].length))) {
|
|
554
540
|
throwError("range");
|
|
555
541
|
}
|
|
@@ -594,78 +580,114 @@ function lintContent(
|
|
|
594
580
|
cleanFixInfo.insertText = fixInfo.insertText;
|
|
595
581
|
}
|
|
596
582
|
}
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
"
|
|
600
|
-
"
|
|
601
|
-
"
|
|
583
|
+
results.push({
|
|
584
|
+
lineNumber,
|
|
585
|
+
"ruleName": rule.names[0],
|
|
586
|
+
"ruleNames": rule.names,
|
|
587
|
+
"ruleDescription": rule.description,
|
|
588
|
+
"ruleInformation": rule.information ? rule.information.href : null,
|
|
589
|
+
"errorDetail": errorInfo.detail || null,
|
|
590
|
+
"errorContext": errorInfo.context || null,
|
|
591
|
+
"errorRange": errorInfo.range ? [ ...errorInfo.range ] : null,
|
|
602
592
|
"fixInfo": fixInfo ? cleanFixInfo : null
|
|
603
593
|
});
|
|
604
594
|
}
|
|
605
|
-
// Call (possibly external) rule function
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
595
|
+
// Call (possibly external) rule function to report errors
|
|
596
|
+
const catchCallsOnError = (error) => onError({
|
|
597
|
+
"lineNumber": 1,
|
|
598
|
+
"detail": `This rule threw an exception: ${error.message || error}`
|
|
599
|
+
});
|
|
600
|
+
const invokeRuleFunction = () => rule.function(params, onError);
|
|
601
|
+
if (rule.asynchronous) {
|
|
602
|
+
// Asynchronous rule, ensure it returns a Promise
|
|
603
|
+
const ruleFunctionPromise =
|
|
604
|
+
Promise.resolve().then(invokeRuleFunction);
|
|
605
|
+
return handleRuleFailures ?
|
|
606
|
+
ruleFunctionPromise.catch(catchCallsOnError) :
|
|
607
|
+
ruleFunctionPromise;
|
|
608
|
+
}
|
|
609
|
+
// Synchronous rule
|
|
610
|
+
try {
|
|
611
|
+
invokeRuleFunction();
|
|
612
|
+
} catch (error) {
|
|
613
|
+
if (handleRuleFailures) {
|
|
614
|
+
catchCallsOnError(error);
|
|
615
|
+
} else {
|
|
616
|
+
throw error;
|
|
614
617
|
}
|
|
615
|
-
} else {
|
|
616
|
-
rule.function(params, onError);
|
|
617
618
|
}
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
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
|
-
|
|
619
|
+
return null;
|
|
620
|
+
}
|
|
621
|
+
// eslint-disable-next-line jsdoc/require-jsdoc
|
|
622
|
+
function formatResults() {
|
|
623
|
+
// Sort results by rule name by line number
|
|
624
|
+
results.sort((a, b) => (
|
|
625
|
+
a.ruleName.localeCompare(b.ruleName) ||
|
|
626
|
+
a.lineNumber - b.lineNumber
|
|
627
|
+
));
|
|
628
|
+
if (resultVersion < 3) {
|
|
629
|
+
// Remove fixInfo and multiple errors for the same rule and line number
|
|
630
|
+
const noPrevious = {
|
|
631
|
+
"ruleName": null,
|
|
632
|
+
"lineNumber": -1
|
|
633
|
+
};
|
|
634
|
+
results = results.filter((error, index, array) => {
|
|
635
|
+
delete error.fixInfo;
|
|
636
|
+
const previous = array[index - 1] || noPrevious;
|
|
637
|
+
return (
|
|
638
|
+
(error.ruleName !== previous.ruleName) ||
|
|
639
|
+
(error.lineNumber !== previous.lineNumber)
|
|
640
|
+
);
|
|
641
|
+
});
|
|
642
|
+
}
|
|
643
|
+
if (resultVersion === 0) {
|
|
644
|
+
// Return a dictionary of rule->[line numbers]
|
|
645
|
+
const dictionary = {};
|
|
646
|
+
for (const error of results) {
|
|
647
|
+
const ruleLines = dictionary[error.ruleName] || [];
|
|
648
|
+
ruleLines.push(error.lineNumber);
|
|
649
|
+
dictionary[error.ruleName] = ruleLines;
|
|
650
|
+
}
|
|
651
|
+
// @ts-ignore
|
|
652
|
+
results = dictionary;
|
|
653
|
+
} else if (resultVersion === 1) {
|
|
654
|
+
// Use ruleAlias instead of ruleNames
|
|
655
|
+
for (const error of results) {
|
|
656
|
+
error.ruleAlias = error.ruleNames[1] || error.ruleName;
|
|
657
|
+
delete error.ruleNames;
|
|
658
|
+
}
|
|
659
|
+
} else {
|
|
660
|
+
// resultVersion 2 or 3: Remove unwanted ruleName
|
|
661
|
+
for (const error of results) {
|
|
662
|
+
delete error.ruleName;
|
|
657
663
|
}
|
|
658
664
|
}
|
|
665
|
+
return results;
|
|
659
666
|
}
|
|
660
667
|
// Run all rules
|
|
668
|
+
const ruleListAsync = ruleList.filter((rule) => rule.asynchronous);
|
|
669
|
+
const ruleListSync = ruleList.filter((rule) => !rule.asynchronous);
|
|
670
|
+
const ruleListAsyncFirst = [
|
|
671
|
+
...ruleListAsync,
|
|
672
|
+
...ruleListSync
|
|
673
|
+
];
|
|
674
|
+
const callbackSuccess = () => callback(null, formatResults());
|
|
675
|
+
const callbackError =
|
|
676
|
+
(error) => callback(error instanceof Error ? error : new Error(error));
|
|
661
677
|
try {
|
|
662
|
-
|
|
678
|
+
const ruleResults = ruleListAsyncFirst.map(forRule);
|
|
679
|
+
if (ruleListAsync.length > 0) {
|
|
680
|
+
Promise.all(ruleResults.slice(0, ruleListAsync.length))
|
|
681
|
+
.then(callbackSuccess)
|
|
682
|
+
.catch(callbackError);
|
|
683
|
+
} else {
|
|
684
|
+
callbackSuccess();
|
|
685
|
+
}
|
|
663
686
|
} catch (error) {
|
|
687
|
+
callbackError(error);
|
|
688
|
+
} finally {
|
|
664
689
|
cache.clear();
|
|
665
|
-
return callback(error);
|
|
666
690
|
}
|
|
667
|
-
cache.clear();
|
|
668
|
-
return callback(null, result);
|
|
669
691
|
}
|
|
670
692
|
|
|
671
693
|
/**
|
|
@@ -679,6 +701,7 @@ function lintContent(
|
|
|
679
701
|
* @param {boolean} handleRuleFailures Whether to handle exceptions in rules.
|
|
680
702
|
* @param {boolean} noInlineConfig Whether to allow inline configuration.
|
|
681
703
|
* @param {number} resultVersion Version of the LintResults object to return.
|
|
704
|
+
* @param {Object} fs File system implementation.
|
|
682
705
|
* @param {boolean} synchronous Whether to execute synchronously.
|
|
683
706
|
* @param {Function} callback Callback (err, result) function.
|
|
684
707
|
* @returns {void}
|
|
@@ -692,6 +715,7 @@ function lintFile(
|
|
|
692
715
|
handleRuleFailures,
|
|
693
716
|
noInlineConfig,
|
|
694
717
|
resultVersion,
|
|
718
|
+
fs,
|
|
695
719
|
synchronous,
|
|
696
720
|
callback) {
|
|
697
721
|
// eslint-disable-next-line jsdoc/require-jsdoc
|
|
@@ -704,10 +728,9 @@ function lintFile(
|
|
|
704
728
|
}
|
|
705
729
|
// Make a/synchronous call to read file
|
|
706
730
|
if (synchronous) {
|
|
707
|
-
|
|
708
|
-
lintContentWrapper(null, fs.readFileSync(file, helpers.utf8Encoding));
|
|
731
|
+
lintContentWrapper(null, fs.readFileSync(file, "utf8"));
|
|
709
732
|
} else {
|
|
710
|
-
fs.readFile(file,
|
|
733
|
+
fs.readFile(file, "utf8", lintContentWrapper);
|
|
711
734
|
}
|
|
712
735
|
}
|
|
713
736
|
|
|
@@ -725,13 +748,13 @@ function lintInput(options, synchronous, callback) {
|
|
|
725
748
|
callback = callback || function noop() {};
|
|
726
749
|
// eslint-disable-next-line unicorn/prefer-spread
|
|
727
750
|
const ruleList = rules.concat(options.customRules || []);
|
|
728
|
-
const ruleErr = validateRuleList(ruleList);
|
|
751
|
+
const ruleErr = validateRuleList(ruleList, synchronous);
|
|
729
752
|
if (ruleErr) {
|
|
730
753
|
return callback(ruleErr);
|
|
731
754
|
}
|
|
732
755
|
let files = [];
|
|
733
756
|
if (Array.isArray(options.files)) {
|
|
734
|
-
files = options.files
|
|
757
|
+
files = [ ...options.files ];
|
|
735
758
|
} else if (options.files) {
|
|
736
759
|
files = [ String(options.files) ];
|
|
737
760
|
}
|
|
@@ -750,98 +773,85 @@ function lintInput(options, synchronous, callback) {
|
|
|
750
773
|
// @ts-ignore
|
|
751
774
|
md.use(...plugin);
|
|
752
775
|
});
|
|
776
|
+
const fs = options.fs || require("fs");
|
|
753
777
|
const results = newResults(ruleList);
|
|
754
778
|
let done = false;
|
|
755
|
-
|
|
756
|
-
let syncItem = null;
|
|
779
|
+
let concurrency = 0;
|
|
757
780
|
// eslint-disable-next-line jsdoc/require-jsdoc
|
|
758
|
-
function
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
781
|
+
function lintWorker() {
|
|
782
|
+
let currentItem = null;
|
|
783
|
+
// eslint-disable-next-line jsdoc/require-jsdoc
|
|
784
|
+
function lintWorkerCallback(err, result) {
|
|
785
|
+
concurrency--;
|
|
786
|
+
if (err) {
|
|
787
|
+
done = true;
|
|
788
|
+
return callback(err);
|
|
789
|
+
}
|
|
790
|
+
results[currentItem] = result;
|
|
791
|
+
if (!synchronous) {
|
|
792
|
+
lintWorker();
|
|
793
|
+
}
|
|
794
|
+
return null;
|
|
762
795
|
}
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
syncItem,
|
|
770
|
-
strings[syncItem] || "",
|
|
771
|
-
md,
|
|
772
|
-
config,
|
|
773
|
-
frontMatter,
|
|
774
|
-
handleRuleFailures,
|
|
775
|
-
noInlineConfig,
|
|
776
|
-
resultVersion,
|
|
777
|
-
syncCallback
|
|
778
|
-
);
|
|
779
|
-
}
|
|
780
|
-
if (synchronous) {
|
|
781
|
-
// Lint files synchronously
|
|
782
|
-
while (!done && (syncItem = files.shift())) {
|
|
796
|
+
if (done) {
|
|
797
|
+
// Abort for error or nothing left to do
|
|
798
|
+
} else if (files.length > 0) {
|
|
799
|
+
// Lint next file
|
|
800
|
+
concurrency++;
|
|
801
|
+
currentItem = files.shift();
|
|
783
802
|
lintFile(
|
|
784
803
|
ruleList,
|
|
785
|
-
|
|
804
|
+
currentItem,
|
|
786
805
|
md,
|
|
787
806
|
config,
|
|
788
807
|
frontMatter,
|
|
789
808
|
handleRuleFailures,
|
|
790
809
|
noInlineConfig,
|
|
791
810
|
resultVersion,
|
|
811
|
+
fs,
|
|
792
812
|
synchronous,
|
|
793
|
-
|
|
813
|
+
lintWorkerCallback
|
|
794
814
|
);
|
|
795
|
-
}
|
|
796
|
-
|
|
797
|
-
}
|
|
798
|
-
// Lint files asynchronously
|
|
799
|
-
let concurrency = 0;
|
|
800
|
-
// eslint-disable-next-line jsdoc/require-jsdoc
|
|
801
|
-
function lintConcurrently() {
|
|
802
|
-
const asyncItem = files.shift();
|
|
803
|
-
if (done) {
|
|
804
|
-
// Nothing to do
|
|
805
|
-
} else if (asyncItem) {
|
|
815
|
+
} else if (stringsKeys.length > 0) {
|
|
816
|
+
// Lint next string
|
|
806
817
|
concurrency++;
|
|
807
|
-
|
|
818
|
+
currentItem = stringsKeys.shift();
|
|
819
|
+
lintContent(
|
|
808
820
|
ruleList,
|
|
809
|
-
|
|
821
|
+
currentItem,
|
|
822
|
+
strings[currentItem] || "",
|
|
810
823
|
md,
|
|
811
824
|
config,
|
|
812
825
|
frontMatter,
|
|
813
826
|
handleRuleFailures,
|
|
814
827
|
noInlineConfig,
|
|
815
828
|
resultVersion,
|
|
816
|
-
|
|
817
|
-
(err, result) => {
|
|
818
|
-
concurrency--;
|
|
819
|
-
if (err) {
|
|
820
|
-
done = true;
|
|
821
|
-
return callback(err);
|
|
822
|
-
}
|
|
823
|
-
results[asyncItem] = result;
|
|
824
|
-
lintConcurrently();
|
|
825
|
-
return null;
|
|
826
|
-
}
|
|
829
|
+
lintWorkerCallback
|
|
827
830
|
);
|
|
828
831
|
} else if (concurrency === 0) {
|
|
832
|
+
// Finish
|
|
829
833
|
done = true;
|
|
830
834
|
return callback(null, results);
|
|
831
835
|
}
|
|
832
836
|
return null;
|
|
833
837
|
}
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
838
|
+
if (synchronous) {
|
|
839
|
+
while (!done) {
|
|
840
|
+
lintWorker();
|
|
841
|
+
}
|
|
842
|
+
} else {
|
|
843
|
+
// Testing on a Raspberry Pi 4 Model B with an artificial 5ms file access
|
|
844
|
+
// delay suggests that a concurrency factor of 8 can eliminate the impact
|
|
845
|
+
// of that delay (i.e., total time is the same as with no delay).
|
|
846
|
+
lintWorker();
|
|
847
|
+
lintWorker();
|
|
848
|
+
lintWorker();
|
|
849
|
+
lintWorker();
|
|
850
|
+
lintWorker();
|
|
851
|
+
lintWorker();
|
|
852
|
+
lintWorker();
|
|
853
|
+
lintWorker();
|
|
854
|
+
}
|
|
845
855
|
return null;
|
|
846
856
|
}
|
|
847
857
|
|
|
@@ -897,12 +907,13 @@ function parseConfiguration(name, content, parsers) {
|
|
|
897
907
|
let config = null;
|
|
898
908
|
let message = "";
|
|
899
909
|
const errors = [];
|
|
910
|
+
let index = 0;
|
|
900
911
|
// Try each parser
|
|
901
912
|
(parsers || [ JSON.parse ]).every((parser) => {
|
|
902
913
|
try {
|
|
903
914
|
config = parser(content);
|
|
904
915
|
} catch (error) {
|
|
905
|
-
errors.push(error.message);
|
|
916
|
+
errors.push(`Parser ${index++}: ${error.message}`);
|
|
906
917
|
}
|
|
907
918
|
return !config;
|
|
908
919
|
});
|
|
@@ -923,22 +934,55 @@ function parseConfiguration(name, content, parsers) {
|
|
|
923
934
|
*
|
|
924
935
|
* @param {string} configFile Configuration file name.
|
|
925
936
|
* @param {string} referenceId Referenced identifier to resolve.
|
|
937
|
+
* @param {Object} fs File system implementation.
|
|
938
|
+
* @param {ResolveConfigExtendsCallback} [callback] Callback (err, result)
|
|
939
|
+
* function.
|
|
940
|
+
* @returns {void}
|
|
941
|
+
*/
|
|
942
|
+
function resolveConfigExtends(configFile, referenceId, fs, callback) {
|
|
943
|
+
const configFileDirname = path.dirname(configFile);
|
|
944
|
+
const resolvedExtendsFile = path.resolve(configFileDirname, referenceId);
|
|
945
|
+
fs.access(resolvedExtendsFile, (err) => {
|
|
946
|
+
if (err) {
|
|
947
|
+
// Not a file, try require.resolve
|
|
948
|
+
try {
|
|
949
|
+
return callback(null, dynamicRequire.resolve(
|
|
950
|
+
referenceId,
|
|
951
|
+
{ "paths": [ configFileDirname ] }
|
|
952
|
+
));
|
|
953
|
+
} catch {
|
|
954
|
+
// Unable to resolve, use resolvedExtendsFile
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
return callback(null, resolvedExtendsFile);
|
|
958
|
+
});
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
/**
|
|
962
|
+
* Resolve referenced "extends" path in a configuration file
|
|
963
|
+
* using path.resolve() with require.resolve() as a fallback.
|
|
964
|
+
*
|
|
965
|
+
* @param {string} configFile Configuration file name.
|
|
966
|
+
* @param {string} referenceId Referenced identifier to resolve.
|
|
967
|
+
* @param {Object} fs File system implementation.
|
|
926
968
|
* @returns {string} Resolved path to file.
|
|
927
969
|
*/
|
|
928
|
-
function
|
|
970
|
+
function resolveConfigExtendsSync(configFile, referenceId, fs) {
|
|
929
971
|
const configFileDirname = path.dirname(configFile);
|
|
930
972
|
const resolvedExtendsFile = path.resolve(configFileDirname, referenceId);
|
|
931
973
|
try {
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
}
|
|
974
|
+
fs.accessSync(resolvedExtendsFile);
|
|
975
|
+
return resolvedExtendsFile;
|
|
935
976
|
} catch {
|
|
936
|
-
//
|
|
977
|
+
// Not a file, try require.resolve
|
|
937
978
|
}
|
|
938
979
|
try {
|
|
939
|
-
return
|
|
980
|
+
return dynamicRequire.resolve(
|
|
981
|
+
referenceId,
|
|
982
|
+
{ "paths": [ configFileDirname ] }
|
|
983
|
+
);
|
|
940
984
|
} catch {
|
|
941
|
-
//
|
|
985
|
+
// Unable to resolve, return resolvedExtendsFile
|
|
942
986
|
}
|
|
943
987
|
return resolvedExtendsFile;
|
|
944
988
|
}
|
|
@@ -949,17 +993,26 @@ function resolveConfigExtends(configFile, referenceId) {
|
|
|
949
993
|
* @param {string} file Configuration file name.
|
|
950
994
|
* @param {ConfigurationParser[] | ReadConfigCallback} parsers Parsing
|
|
951
995
|
* function(s).
|
|
996
|
+
* @param {Object} [fs] File system implementation.
|
|
952
997
|
* @param {ReadConfigCallback} [callback] Callback (err, result) function.
|
|
953
998
|
* @returns {void}
|
|
954
999
|
*/
|
|
955
|
-
function readConfig(file, parsers, callback) {
|
|
1000
|
+
function readConfig(file, parsers, fs, callback) {
|
|
956
1001
|
if (!callback) {
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
1002
|
+
if (fs) {
|
|
1003
|
+
callback = fs;
|
|
1004
|
+
fs = null;
|
|
1005
|
+
} else {
|
|
1006
|
+
// @ts-ignore
|
|
1007
|
+
callback = parsers;
|
|
1008
|
+
parsers = null;
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
if (!fs) {
|
|
1012
|
+
fs = require("fs");
|
|
960
1013
|
}
|
|
961
1014
|
// Read file
|
|
962
|
-
fs.readFile(file,
|
|
1015
|
+
fs.readFile(file, "utf8", (err, content) => {
|
|
963
1016
|
if (err) {
|
|
964
1017
|
return callback(err);
|
|
965
1018
|
}
|
|
@@ -973,16 +1026,25 @@ function readConfig(file, parsers, callback) {
|
|
|
973
1026
|
const configExtends = config.extends;
|
|
974
1027
|
if (configExtends) {
|
|
975
1028
|
delete config.extends;
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
1029
|
+
return resolveConfigExtends(
|
|
1030
|
+
file,
|
|
1031
|
+
configExtends,
|
|
1032
|
+
fs,
|
|
1033
|
+
(_, resolvedExtends) => readConfig(
|
|
1034
|
+
resolvedExtends,
|
|
1035
|
+
parsers,
|
|
1036
|
+
fs,
|
|
1037
|
+
(errr, extendsConfig) => {
|
|
1038
|
+
if (errr) {
|
|
1039
|
+
return callback(errr);
|
|
1040
|
+
}
|
|
1041
|
+
return callback(null, {
|
|
1042
|
+
...extendsConfig,
|
|
1043
|
+
...config
|
|
1044
|
+
});
|
|
1045
|
+
}
|
|
1046
|
+
)
|
|
1047
|
+
);
|
|
986
1048
|
}
|
|
987
1049
|
return callback(null, config);
|
|
988
1050
|
});
|
|
@@ -995,11 +1057,12 @@ const readConfigPromisify = promisify && promisify(readConfig);
|
|
|
995
1057
|
*
|
|
996
1058
|
* @param {string} file Configuration file name.
|
|
997
1059
|
* @param {ConfigurationParser[]} [parsers] Parsing function(s).
|
|
1060
|
+
* @param {Object} [fs] File system implementation.
|
|
998
1061
|
* @returns {Promise<Configuration>} Configuration object.
|
|
999
1062
|
*/
|
|
1000
|
-
function readConfigPromise(file, parsers) {
|
|
1063
|
+
function readConfigPromise(file, parsers, fs) {
|
|
1001
1064
|
// @ts-ignore
|
|
1002
|
-
return readConfigPromisify(file, parsers);
|
|
1065
|
+
return readConfigPromisify(file, parsers, fs);
|
|
1003
1066
|
}
|
|
1004
1067
|
|
|
1005
1068
|
/**
|
|
@@ -1007,12 +1070,16 @@ function readConfigPromise(file, parsers) {
|
|
|
1007
1070
|
*
|
|
1008
1071
|
* @param {string} file Configuration file name.
|
|
1009
1072
|
* @param {ConfigurationParser[]} [parsers] Parsing function(s).
|
|
1073
|
+
* @param {Object} [fs] File system implementation.
|
|
1010
1074
|
* @returns {Configuration} Configuration object.
|
|
1075
|
+
* @throws An Error if processing fails.
|
|
1011
1076
|
*/
|
|
1012
|
-
function readConfigSync(file, parsers) {
|
|
1077
|
+
function readConfigSync(file, parsers, fs) {
|
|
1078
|
+
if (!fs) {
|
|
1079
|
+
fs = require("fs");
|
|
1080
|
+
}
|
|
1013
1081
|
// Read file
|
|
1014
|
-
|
|
1015
|
-
const content = fs.readFileSync(file, helpers.utf8Encoding);
|
|
1082
|
+
const content = fs.readFileSync(file, "utf8");
|
|
1016
1083
|
// Try to parse file
|
|
1017
1084
|
const { config, message } = parseConfiguration(file, content, parsers);
|
|
1018
1085
|
if (!config) {
|
|
@@ -1022,9 +1089,9 @@ function readConfigSync(file, parsers) {
|
|
|
1022
1089
|
const configExtends = config.extends;
|
|
1023
1090
|
if (configExtends) {
|
|
1024
1091
|
delete config.extends;
|
|
1025
|
-
const resolvedExtends =
|
|
1092
|
+
const resolvedExtends = resolveConfigExtendsSync(file, configExtends, fs);
|
|
1026
1093
|
return {
|
|
1027
|
-
...readConfigSync(resolvedExtends, parsers),
|
|
1094
|
+
...readConfigSync(resolvedExtends, parsers, fs),
|
|
1028
1095
|
...config
|
|
1029
1096
|
};
|
|
1030
1097
|
}
|
|
@@ -1037,7 +1104,7 @@ function readConfigSync(file, parsers) {
|
|
|
1037
1104
|
* @returns {string} SemVer string.
|
|
1038
1105
|
*/
|
|
1039
1106
|
function getVersion() {
|
|
1040
|
-
return require("
|
|
1107
|
+
return require("./constants").version;
|
|
1041
1108
|
}
|
|
1042
1109
|
|
|
1043
1110
|
// Export a/synchronous/Promise APIs
|
|
@@ -1107,7 +1174,7 @@ module.exports = markdownlint;
|
|
|
1107
1174
|
*
|
|
1108
1175
|
* @typedef {Object} RuleOnErrorInfo
|
|
1109
1176
|
* @property {number} lineNumber Line number (1-based).
|
|
1110
|
-
* @property {string} [
|
|
1177
|
+
* @property {string} [detail] Detail about the error.
|
|
1111
1178
|
* @property {string} [context] Context for the error.
|
|
1112
1179
|
* @property {number[]} [range] Column number (1-based) and length.
|
|
1113
1180
|
* @property {RuleOnErrorFixInfo} [fixInfo] Fix information.
|
|
@@ -1131,6 +1198,7 @@ module.exports = markdownlint;
|
|
|
1131
1198
|
* @property {string} description Rule description.
|
|
1132
1199
|
* @property {URL} [information] Link to more information.
|
|
1133
1200
|
* @property {string[]} tags Rule tag(s).
|
|
1201
|
+
* @property {boolean} [asynchronous] True if asynchronous.
|
|
1134
1202
|
* @property {RuleFunction} function Rule implementation.
|
|
1135
1203
|
*/
|
|
1136
1204
|
|
|
@@ -1147,6 +1215,7 @@ module.exports = markdownlint;
|
|
|
1147
1215
|
* @property {boolean} [noInlineConfig] True to ignore HTML directives.
|
|
1148
1216
|
* @property {number} [resultVersion] Results object version.
|
|
1149
1217
|
* @property {Plugin[]} [markdownItPlugins] Additional plugins.
|
|
1218
|
+
* @property {Object} [fs] File system implementation.
|
|
1150
1219
|
*/
|
|
1151
1220
|
|
|
1152
1221
|
/**
|
|
@@ -1188,13 +1257,14 @@ module.exports = markdownlint;
|
|
|
1188
1257
|
* Fix information.
|
|
1189
1258
|
*
|
|
1190
1259
|
* @typedef {Object} FixInfo
|
|
1260
|
+
* @property {number} [lineNumber] Line number (1-based).
|
|
1191
1261
|
* @property {number} [editColumn] Column of the fix (1-based).
|
|
1192
1262
|
* @property {number} [deleteCount] Count of characters to delete.
|
|
1193
1263
|
* @property {string} [insertText] Text to insert (after deleting).
|
|
1194
1264
|
*/
|
|
1195
1265
|
|
|
1196
1266
|
/**
|
|
1197
|
-
* Called with the result of the lint
|
|
1267
|
+
* Called with the result of the lint function.
|
|
1198
1268
|
*
|
|
1199
1269
|
* @callback LintCallback
|
|
1200
1270
|
* @param {Error | null} err Error object or null.
|
|
@@ -1224,10 +1294,19 @@ module.exports = markdownlint;
|
|
|
1224
1294
|
*/
|
|
1225
1295
|
|
|
1226
1296
|
/**
|
|
1227
|
-
* Called with the result of the readConfig
|
|
1297
|
+
* Called with the result of the readConfig function.
|
|
1228
1298
|
*
|
|
1229
1299
|
* @callback ReadConfigCallback
|
|
1230
1300
|
* @param {Error | null} err Error object or null.
|
|
1231
1301
|
* @param {Configuration} [config] Configuration object.
|
|
1232
1302
|
* @returns {void}
|
|
1233
1303
|
*/
|
|
1304
|
+
|
|
1305
|
+
/**
|
|
1306
|
+
* Called with the result of the resolveConfigExtends function.
|
|
1307
|
+
*
|
|
1308
|
+
* @callback ResolveConfigExtendsCallback
|
|
1309
|
+
* @param {Error | null} err Error object or null.
|
|
1310
|
+
* @param {string} [path] Resolved path to file.
|
|
1311
|
+
* @returns {void}
|
|
1312
|
+
*/
|