markdownlint 0.22.0 → 0.25.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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
- const deprecatedRuleNames = [ "MD002", "MD006" ];
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
@@ -51,10 +55,25 @@ function validateRuleList(ruleList) {
51
55
  result = newError(property);
52
56
  }
53
57
  });
54
- if (!result && rule.information) {
55
- if (Object.getPrototypeOf(rule.information) !== URL.prototype) {
56
- result = newError("information");
57
- }
58
+ if (
59
+ !result &&
60
+ rule.information &&
61
+ (Object.getPrototypeOf(rule.information) !== URL.prototype)
62
+ ) {
63
+ result = newError("information");
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
+ );
58
77
  }
59
78
  if (!result) {
60
79
  rule.names.forEach(function forName(name) {
@@ -176,27 +195,21 @@ function removeFrontMatter(content, frontMatter) {
176
195
  * @returns {void}
177
196
  */
178
197
  function annotateTokens(tokens, lines) {
179
- let tableMap = null;
198
+ let trMap = null;
180
199
  tokens.forEach(function forToken(token) {
181
- // Handle missing maps for table head/body
182
- if (
183
- (token.type === "thead_open") ||
184
- (token.type === "tbody_open")
185
- ) {
186
- tableMap = token.map.slice();
187
- } else if (
188
- (token.type === "tr_close") &&
189
- tableMap
190
- ) {
191
- tableMap[0]++;
192
- } else if (
193
- (token.type === "thead_close") ||
194
- (token.type === "tbody_close")
195
- ) {
196
- 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 ];
197
208
  }
198
- if (tableMap && !token.map) {
199
- token.map = tableMap.slice();
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]++;
200
213
  }
201
214
  // Update token metadata
202
215
  if (token.map) {
@@ -329,8 +342,7 @@ function getEnabledRulesPerLineNumber(
329
342
  const enabledRulesPerLineNumber = new Array(1 + frontMatterLines.length);
330
343
  // Helper functions
331
344
  // eslint-disable-next-line jsdoc/require-jsdoc
332
- function handleInlineConfig(perLine, forEachMatch, forEachLine) {
333
- const input = perLine ? lines : [ lines.join("\n") ];
345
+ function handleInlineConfig(input, forEachMatch, forEachLine) {
334
346
  input.forEach((line, lineIndex) => {
335
347
  if (!noInlineConfig) {
336
348
  let match = null;
@@ -361,6 +373,7 @@ function getEnabledRulesPerLineNumber(
361
373
  }
362
374
  // eslint-disable-next-line jsdoc/require-jsdoc
363
375
  function applyEnableDisable(action, parameter, state) {
376
+ state = { ...state };
364
377
  const enabled = (action.startsWith("ENABLE"));
365
378
  const items = parameter ?
366
379
  parameter.trim().toUpperCase().split(/\s+/) :
@@ -370,40 +383,42 @@ function getEnabledRulesPerLineNumber(
370
383
  state[ruleName] = enabled;
371
384
  });
372
385
  });
386
+ return state;
373
387
  }
374
388
  // eslint-disable-next-line jsdoc/require-jsdoc
375
389
  function enableDisableFile(action, parameter) {
376
390
  if ((action === "ENABLE-FILE") || (action === "DISABLE-FILE")) {
377
- applyEnableDisable(action, parameter, enabledRules);
391
+ enabledRules = applyEnableDisable(action, parameter, enabledRules);
378
392
  }
379
393
  }
380
394
  // eslint-disable-next-line jsdoc/require-jsdoc
381
395
  function captureRestoreEnableDisable(action, parameter) {
382
396
  if (action === "CAPTURE") {
383
- capturedRules = { ...enabledRules };
397
+ capturedRules = enabledRules;
384
398
  } else if (action === "RESTORE") {
385
- enabledRules = { ...capturedRules };
399
+ enabledRules = capturedRules;
386
400
  } else if ((action === "ENABLE") || (action === "DISABLE")) {
387
- enabledRules = { ...enabledRules };
388
- applyEnableDisable(action, parameter, enabledRules);
401
+ enabledRules = applyEnableDisable(action, parameter, enabledRules);
389
402
  }
390
403
  }
391
404
  // eslint-disable-next-line jsdoc/require-jsdoc
392
405
  function updateLineState() {
393
- enabledRulesPerLineNumber.push({ ...enabledRules });
406
+ enabledRulesPerLineNumber.push(enabledRules);
394
407
  }
395
408
  // eslint-disable-next-line jsdoc/require-jsdoc
396
409
  function disableNextLine(action, parameter, lineNumber) {
397
410
  if (action === "DISABLE-NEXT-LINE") {
398
- applyEnableDisable(
399
- action,
400
- parameter,
401
- enabledRulesPerLineNumber[lineNumber + 1] || {}
402
- );
411
+ const nextLineNumber = frontMatterLines.length + lineNumber + 1;
412
+ enabledRulesPerLineNumber[nextLineNumber] =
413
+ applyEnableDisable(
414
+ action,
415
+ parameter,
416
+ enabledRulesPerLineNumber[nextLineNumber] || {}
417
+ );
403
418
  }
404
419
  }
405
420
  // Handle inline comments
406
- handleInlineConfig(false, configureFile);
421
+ handleInlineConfig([ lines.join("\n") ], configureFile);
407
422
  const effectiveConfig = getEffectiveConfig(
408
423
  ruleList, config, aliasToRuleNames);
409
424
  ruleList.forEach((rule) => {
@@ -412,9 +427,9 @@ function getEnabledRulesPerLineNumber(
412
427
  enabledRules[ruleName] = !!effectiveConfig[ruleName];
413
428
  });
414
429
  capturedRules = enabledRules;
415
- handleInlineConfig(true, enableDisableFile);
416
- handleInlineConfig(true, captureRestoreEnableDisable, updateLineState);
417
- handleInlineConfig(true, disableNextLine);
430
+ handleInlineConfig(lines, enableDisableFile);
431
+ handleInlineConfig(lines, captureRestoreEnableDisable, updateLineState);
432
+ handleInlineConfig(lines, disableNextLine);
418
433
  // Return results
419
434
  return {
420
435
  effectiveConfig,
@@ -422,38 +437,6 @@ function getEnabledRulesPerLineNumber(
422
437
  };
423
438
  }
424
439
 
425
- /**
426
- * Compare function for Array.prototype.sort for ascending order of errors.
427
- *
428
- * @param {LintError} a First error.
429
- * @param {LintError} b Second error.
430
- * @returns {number} Positive value if a>b, negative value if b<a, 0 otherwise.
431
- */
432
- function lineNumberComparison(a, b) {
433
- return a.lineNumber - b.lineNumber;
434
- }
435
-
436
- /**
437
- * Filter function to include everything.
438
- *
439
- * @returns {boolean} True.
440
- */
441
- function filterAllValues() {
442
- return true;
443
- }
444
-
445
- /**
446
- * Function to return unique values from a sorted errors array.
447
- *
448
- * @param {LintError} value Error instance.
449
- * @param {number} index Index in array.
450
- * @param {LintError[]} array Array of errors.
451
- * @returns {boolean} Filter value.
452
- */
453
- function uniqueFilterForSortedErrors(value, index, array) {
454
- return (index === 0) || (value.lineNumber > array[index - 1].lineNumber);
455
- }
456
-
457
440
  /**
458
441
  * Lints a string containing Markdown content.
459
442
  *
@@ -503,27 +486,28 @@ function lintContent(
503
486
  );
504
487
  // Create parameters for rules
505
488
  const params = {
506
- name,
507
- tokens,
508
- lines,
509
- frontMatterLines
489
+ "name": helpers.deepFreeze(name),
490
+ "tokens": helpers.deepFreeze(tokens),
491
+ "lines": helpers.deepFreeze(lines),
492
+ "frontMatterLines": helpers.deepFreeze(frontMatterLines)
510
493
  };
511
494
  cache.lineMetadata(helpers.getLineMetadata(params));
512
- cache.flattenedLists(helpers.flattenLists(params));
495
+ cache.flattenedLists(helpers.flattenLists(params.tokens));
496
+ cache.codeBlockAndSpanRanges(
497
+ helpers.codeBlockAndSpanRanges(params, cache.lineMetadata())
498
+ );
513
499
  // Function to run for each rule
514
- const result = (resultVersion === 0) ? {} : [];
500
+ let results = [];
515
501
  // eslint-disable-next-line jsdoc/require-jsdoc
516
502
  function forRule(rule) {
517
503
  // Configure rule
518
- const ruleNameFriendly = rule.names[0];
519
- const ruleName = ruleNameFriendly.toUpperCase();
504
+ const ruleName = rule.names[0].toUpperCase();
520
505
  params.config = effectiveConfig[ruleName];
521
506
  // eslint-disable-next-line jsdoc/require-jsdoc
522
507
  function throwError(property) {
523
508
  throw new Error(
524
509
  "Property '" + property + "' of onError parameter is incorrect.");
525
510
  }
526
- const errors = [];
527
511
  // eslint-disable-next-line jsdoc/require-jsdoc
528
512
  function onError(errorInfo) {
529
513
  if (!errorInfo ||
@@ -532,6 +516,10 @@ function lintContent(
532
516
  (errorInfo.lineNumber > lines.length)) {
533
517
  throwError("lineNumber");
534
518
  }
519
+ const lineNumber = errorInfo.lineNumber + frontMatterLines.length;
520
+ if (!enabledRulesPerLineNumber[lineNumber][ruleName]) {
521
+ return;
522
+ }
535
523
  if (errorInfo.detail &&
536
524
  !helpers.isString(errorInfo.detail)) {
537
525
  throwError("detail");
@@ -542,12 +530,12 @@ function lintContent(
542
530
  }
543
531
  if (errorInfo.range &&
544
532
  (!Array.isArray(errorInfo.range) ||
545
- (errorInfo.range.length !== 2) ||
546
- !helpers.isNumber(errorInfo.range[0]) ||
547
- (errorInfo.range[0] < 1) ||
548
- !helpers.isNumber(errorInfo.range[1]) ||
549
- (errorInfo.range[1] < 1) ||
550
- ((errorInfo.range[0] + errorInfo.range[1] - 1) >
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) >
551
539
  lines[errorInfo.lineNumber - 1].length))) {
552
540
  throwError("range");
553
541
  }
@@ -592,78 +580,114 @@ function lintContent(
592
580
  cleanFixInfo.insertText = fixInfo.insertText;
593
581
  }
594
582
  }
595
- errors.push({
596
- "lineNumber": errorInfo.lineNumber + frontMatterLines.length,
597
- "detail": errorInfo.detail || null,
598
- "context": errorInfo.context || null,
599
- "range": errorInfo.range ? [ ...errorInfo.range ] : null,
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,
600
592
  "fixInfo": fixInfo ? cleanFixInfo : null
601
593
  });
602
594
  }
603
- // Call (possibly external) rule function
604
- if (handleRuleFailures) {
605
- try {
606
- rule.function(params, onError);
607
- } catch (error) {
608
- onError({
609
- "lineNumber": 1,
610
- "detail": `This rule threw an exception: ${error.message}`
611
- });
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;
612
617
  }
613
- } else {
614
- rule.function(params, onError);
615
618
  }
616
- // Record any errors (significant performance benefit from length check)
617
- if (errors.length > 0) {
618
- errors.sort(lineNumberComparison);
619
- const filteredErrors = errors
620
- .filter((resultVersion === 3) ?
621
- filterAllValues :
622
- uniqueFilterForSortedErrors)
623
- .filter(function removeDisabledRules(error) {
624
- return enabledRulesPerLineNumber[error.lineNumber][ruleName];
625
- })
626
- .map(function formatResults(error) {
627
- if (resultVersion === 0) {
628
- return error.lineNumber;
629
- }
630
- const errorObject = {};
631
- errorObject.lineNumber = error.lineNumber;
632
- if (resultVersion === 1) {
633
- errorObject.ruleName = ruleNameFriendly;
634
- errorObject.ruleAlias = rule.names[1] || rule.names[0];
635
- } else {
636
- errorObject.ruleNames = rule.names;
637
- }
638
- errorObject.ruleDescription = rule.description;
639
- errorObject.ruleInformation =
640
- rule.information ? rule.information.href : null;
641
- errorObject.errorDetail = error.detail;
642
- errorObject.errorContext = error.context;
643
- errorObject.errorRange = error.range;
644
- if (resultVersion === 3) {
645
- errorObject.fixInfo = error.fixInfo;
646
- }
647
- return errorObject;
648
- });
649
- if (filteredErrors.length > 0) {
650
- if (resultVersion === 0) {
651
- result[ruleNameFriendly] = filteredErrors;
652
- } else {
653
- Array.prototype.push.apply(result, filteredErrors);
654
- }
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;
655
663
  }
656
664
  }
665
+ return results;
657
666
  }
658
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));
659
677
  try {
660
- ruleList.forEach(forRule);
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
+ }
661
686
  } catch (error) {
687
+ callbackError(error);
688
+ } finally {
662
689
  cache.clear();
663
- return callback(error);
664
690
  }
665
- cache.clear();
666
- return callback(null, result);
667
691
  }
668
692
 
669
693
  /**
@@ -677,6 +701,7 @@ function lintContent(
677
701
  * @param {boolean} handleRuleFailures Whether to handle exceptions in rules.
678
702
  * @param {boolean} noInlineConfig Whether to allow inline configuration.
679
703
  * @param {number} resultVersion Version of the LintResults object to return.
704
+ * @param {Object} fs File system implementation.
680
705
  * @param {boolean} synchronous Whether to execute synchronously.
681
706
  * @param {Function} callback Callback (err, result) function.
682
707
  * @returns {void}
@@ -690,6 +715,7 @@ function lintFile(
690
715
  handleRuleFailures,
691
716
  noInlineConfig,
692
717
  resultVersion,
718
+ fs,
693
719
  synchronous,
694
720
  callback) {
695
721
  // eslint-disable-next-line jsdoc/require-jsdoc
@@ -702,10 +728,9 @@ function lintFile(
702
728
  }
703
729
  // Make a/synchronous call to read file
704
730
  if (synchronous) {
705
- // @ts-ignore
706
- lintContentWrapper(null, fs.readFileSync(file, helpers.utf8Encoding));
731
+ lintContentWrapper(null, fs.readFileSync(file, "utf8"));
707
732
  } else {
708
- fs.readFile(file, helpers.utf8Encoding, lintContentWrapper);
733
+ fs.readFile(file, "utf8", lintContentWrapper);
709
734
  }
710
735
  }
711
736
 
@@ -721,14 +746,15 @@ function lintInput(options, synchronous, callback) {
721
746
  // Normalize inputs
722
747
  options = options || {};
723
748
  callback = callback || function noop() {};
749
+ // eslint-disable-next-line unicorn/prefer-spread
724
750
  const ruleList = rules.concat(options.customRules || []);
725
- const ruleErr = validateRuleList(ruleList);
751
+ const ruleErr = validateRuleList(ruleList, synchronous);
726
752
  if (ruleErr) {
727
753
  return callback(ruleErr);
728
754
  }
729
755
  let files = [];
730
756
  if (Array.isArray(options.files)) {
731
- files = options.files.slice();
757
+ files = [ ...options.files ];
732
758
  } else if (options.files) {
733
759
  files = [ String(options.files) ];
734
760
  }
@@ -747,98 +773,85 @@ function lintInput(options, synchronous, callback) {
747
773
  // @ts-ignore
748
774
  md.use(...plugin);
749
775
  });
776
+ const fs = options.fs || require("fs");
750
777
  const results = newResults(ruleList);
751
778
  let done = false;
752
- // Linting of strings is always synchronous
753
- let syncItem = null;
779
+ let concurrency = 0;
754
780
  // eslint-disable-next-line jsdoc/require-jsdoc
755
- function syncCallback(err, result) {
756
- if (err) {
757
- done = true;
758
- return callback(err);
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;
759
795
  }
760
- results[syncItem] = result;
761
- return null;
762
- }
763
- while (!done && (syncItem = stringsKeys.shift())) {
764
- lintContent(
765
- ruleList,
766
- syncItem,
767
- strings[syncItem] || "",
768
- md,
769
- config,
770
- frontMatter,
771
- handleRuleFailures,
772
- noInlineConfig,
773
- resultVersion,
774
- syncCallback
775
- );
776
- }
777
- if (synchronous) {
778
- // Lint files synchronously
779
- 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();
780
802
  lintFile(
781
803
  ruleList,
782
- syncItem,
804
+ currentItem,
783
805
  md,
784
806
  config,
785
807
  frontMatter,
786
808
  handleRuleFailures,
787
809
  noInlineConfig,
788
810
  resultVersion,
811
+ fs,
789
812
  synchronous,
790
- syncCallback
813
+ lintWorkerCallback
791
814
  );
792
- }
793
- return done || callback(null, results);
794
- }
795
- // Lint files asynchronously
796
- let concurrency = 0;
797
- // eslint-disable-next-line jsdoc/require-jsdoc
798
- function lintConcurrently() {
799
- const asyncItem = files.shift();
800
- if (done) {
801
- // Nothing to do
802
- } else if (asyncItem) {
815
+ } else if (stringsKeys.length > 0) {
816
+ // Lint next string
803
817
  concurrency++;
804
- lintFile(
818
+ currentItem = stringsKeys.shift();
819
+ lintContent(
805
820
  ruleList,
806
- asyncItem,
821
+ currentItem,
822
+ strings[currentItem] || "",
807
823
  md,
808
824
  config,
809
825
  frontMatter,
810
826
  handleRuleFailures,
811
827
  noInlineConfig,
812
828
  resultVersion,
813
- synchronous,
814
- (err, result) => {
815
- concurrency--;
816
- if (err) {
817
- done = true;
818
- return callback(err);
819
- }
820
- results[asyncItem] = result;
821
- lintConcurrently();
822
- return null;
823
- }
829
+ lintWorkerCallback
824
830
  );
825
831
  } else if (concurrency === 0) {
832
+ // Finish
826
833
  done = true;
827
834
  return callback(null, results);
828
835
  }
829
836
  return null;
830
837
  }
831
- // Testing on a Raspberry Pi 4 Model B with an artificial 5ms file access
832
- // delay suggests that a concurrency factor of 8 can eliminate the impact
833
- // of that delay (i.e., total time is the same as with no delay).
834
- lintConcurrently();
835
- lintConcurrently();
836
- lintConcurrently();
837
- lintConcurrently();
838
- lintConcurrently();
839
- lintConcurrently();
840
- lintConcurrently();
841
- lintConcurrently();
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
+ }
842
855
  return null;
843
856
  }
844
857
 
@@ -853,7 +866,7 @@ function markdownlint(options, callback) {
853
866
  return lintInput(options, false, callback);
854
867
  }
855
868
 
856
- const markdownlintPromisify = promisify(markdownlint);
869
+ const markdownlintPromisify = promisify && promisify(markdownlint);
857
870
 
858
871
  /**
859
872
  * Lint specified Markdown files.
@@ -894,12 +907,13 @@ function parseConfiguration(name, content, parsers) {
894
907
  let config = null;
895
908
  let message = "";
896
909
  const errors = [];
910
+ let index = 0;
897
911
  // Try each parser
898
912
  (parsers || [ JSON.parse ]).every((parser) => {
899
913
  try {
900
914
  config = parser(content);
901
915
  } catch (error) {
902
- errors.push(error.message);
916
+ errors.push(`Parser ${index++}: ${error.message}`);
903
917
  }
904
918
  return !config;
905
919
  });
@@ -920,22 +934,55 @@ function parseConfiguration(name, content, parsers) {
920
934
  *
921
935
  * @param {string} configFile Configuration file name.
922
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.
923
968
  * @returns {string} Resolved path to file.
924
969
  */
925
- function resolveConfigExtends(configFile, referenceId) {
970
+ function resolveConfigExtendsSync(configFile, referenceId, fs) {
926
971
  const configFileDirname = path.dirname(configFile);
927
972
  const resolvedExtendsFile = path.resolve(configFileDirname, referenceId);
928
973
  try {
929
- if (fs.statSync(resolvedExtendsFile).isFile()) {
930
- return resolvedExtendsFile;
931
- }
974
+ fs.accessSync(resolvedExtendsFile);
975
+ return resolvedExtendsFile;
932
976
  } catch {
933
- // If not a file or fs.statSync throws, try require.resolve
977
+ // Not a file, try require.resolve
934
978
  }
935
979
  try {
936
- return require.resolve(referenceId, { "paths": [ configFileDirname ] });
980
+ return dynamicRequire.resolve(
981
+ referenceId,
982
+ { "paths": [ configFileDirname ] }
983
+ );
937
984
  } catch {
938
- // If require.resolve throws, return resolvedExtendsFile
985
+ // Unable to resolve, return resolvedExtendsFile
939
986
  }
940
987
  return resolvedExtendsFile;
941
988
  }
@@ -946,17 +993,26 @@ function resolveConfigExtends(configFile, referenceId) {
946
993
  * @param {string} file Configuration file name.
947
994
  * @param {ConfigurationParser[] | ReadConfigCallback} parsers Parsing
948
995
  * function(s).
996
+ * @param {Object} [fs] File system implementation.
949
997
  * @param {ReadConfigCallback} [callback] Callback (err, result) function.
950
998
  * @returns {void}
951
999
  */
952
- function readConfig(file, parsers, callback) {
1000
+ function readConfig(file, parsers, fs, callback) {
953
1001
  if (!callback) {
954
- // @ts-ignore
955
- callback = parsers;
956
- parsers = null;
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");
957
1013
  }
958
1014
  // Read file
959
- fs.readFile(file, helpers.utf8Encoding, (err, content) => {
1015
+ fs.readFile(file, "utf8", (err, content) => {
960
1016
  if (err) {
961
1017
  return callback(err);
962
1018
  }
@@ -970,33 +1026,43 @@ function readConfig(file, parsers, callback) {
970
1026
  const configExtends = config.extends;
971
1027
  if (configExtends) {
972
1028
  delete config.extends;
973
- const resolvedExtends = resolveConfigExtends(file, configExtends);
974
- return readConfig(resolvedExtends, parsers, (errr, extendsConfig) => {
975
- if (errr) {
976
- return callback(errr);
977
- }
978
- return callback(null, {
979
- ...extendsConfig,
980
- ...config
981
- });
982
- });
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
+ );
983
1048
  }
984
1049
  return callback(null, config);
985
1050
  });
986
1051
  }
987
1052
 
988
- const readConfigPromisify = promisify(readConfig);
1053
+ const readConfigPromisify = promisify && promisify(readConfig);
989
1054
 
990
1055
  /**
991
1056
  * Read specified configuration file.
992
1057
  *
993
1058
  * @param {string} file Configuration file name.
994
1059
  * @param {ConfigurationParser[]} [parsers] Parsing function(s).
1060
+ * @param {Object} [fs] File system implementation.
995
1061
  * @returns {Promise<Configuration>} Configuration object.
996
1062
  */
997
- function readConfigPromise(file, parsers) {
1063
+ function readConfigPromise(file, parsers, fs) {
998
1064
  // @ts-ignore
999
- return readConfigPromisify(file, parsers);
1065
+ return readConfigPromisify(file, parsers, fs);
1000
1066
  }
1001
1067
 
1002
1068
  /**
@@ -1004,12 +1070,16 @@ function readConfigPromise(file, parsers) {
1004
1070
  *
1005
1071
  * @param {string} file Configuration file name.
1006
1072
  * @param {ConfigurationParser[]} [parsers] Parsing function(s).
1073
+ * @param {Object} [fs] File system implementation.
1007
1074
  * @returns {Configuration} Configuration object.
1075
+ * @throws An Error if processing fails.
1008
1076
  */
1009
- function readConfigSync(file, parsers) {
1077
+ function readConfigSync(file, parsers, fs) {
1078
+ if (!fs) {
1079
+ fs = require("fs");
1080
+ }
1010
1081
  // Read file
1011
- // @ts-ignore
1012
- const content = fs.readFileSync(file, helpers.utf8Encoding);
1082
+ const content = fs.readFileSync(file, "utf8");
1013
1083
  // Try to parse file
1014
1084
  const { config, message } = parseConfiguration(file, content, parsers);
1015
1085
  if (!config) {
@@ -1019,9 +1089,9 @@ function readConfigSync(file, parsers) {
1019
1089
  const configExtends = config.extends;
1020
1090
  if (configExtends) {
1021
1091
  delete config.extends;
1022
- const resolvedExtends = resolveConfigExtends(file, configExtends);
1092
+ const resolvedExtends = resolveConfigExtendsSync(file, configExtends, fs);
1023
1093
  return {
1024
- ...readConfigSync(resolvedExtends, parsers),
1094
+ ...readConfigSync(resolvedExtends, parsers, fs),
1025
1095
  ...config
1026
1096
  };
1027
1097
  }
@@ -1034,7 +1104,7 @@ function readConfigSync(file, parsers) {
1034
1104
  * @returns {string} SemVer string.
1035
1105
  */
1036
1106
  function getVersion() {
1037
- return require("../package.json").version;
1107
+ return require("./constants").version;
1038
1108
  }
1039
1109
 
1040
1110
  // Export a/synchronous/Promise APIs
@@ -1104,7 +1174,7 @@ module.exports = markdownlint;
1104
1174
  *
1105
1175
  * @typedef {Object} RuleOnErrorInfo
1106
1176
  * @property {number} lineNumber Line number (1-based).
1107
- * @property {string} [details] Details about the error.
1177
+ * @property {string} [detail] Detail about the error.
1108
1178
  * @property {string} [context] Context for the error.
1109
1179
  * @property {number[]} [range] Column number (1-based) and length.
1110
1180
  * @property {RuleOnErrorFixInfo} [fixInfo] Fix information.
@@ -1128,6 +1198,7 @@ module.exports = markdownlint;
1128
1198
  * @property {string} description Rule description.
1129
1199
  * @property {URL} [information] Link to more information.
1130
1200
  * @property {string[]} tags Rule tag(s).
1201
+ * @property {boolean} [asynchronous] True if asynchronous.
1131
1202
  * @property {RuleFunction} function Rule implementation.
1132
1203
  */
1133
1204
 
@@ -1144,6 +1215,7 @@ module.exports = markdownlint;
1144
1215
  * @property {boolean} [noInlineConfig] True to ignore HTML directives.
1145
1216
  * @property {number} [resultVersion] Results object version.
1146
1217
  * @property {Plugin[]} [markdownItPlugins] Additional plugins.
1218
+ * @property {Object} [fs] File system implementation.
1147
1219
  */
1148
1220
 
1149
1221
  /**
@@ -1178,20 +1250,21 @@ module.exports = markdownlint;
1178
1250
  * @property {string} errorDetail Detail about the error.
1179
1251
  * @property {string} errorContext Context for the error.
1180
1252
  * @property {number[]} errorRange Column number (1-based) and length.
1181
- * @property {FixInfo} fixInfo Fix information.
1253
+ * @property {FixInfo} [fixInfo] Fix information.
1182
1254
  */
1183
1255
 
1184
1256
  /**
1185
1257
  * Fix information.
1186
1258
  *
1187
1259
  * @typedef {Object} FixInfo
1260
+ * @property {number} [lineNumber] Line number (1-based).
1188
1261
  * @property {number} [editColumn] Column of the fix (1-based).
1189
1262
  * @property {number} [deleteCount] Count of characters to delete.
1190
1263
  * @property {string} [insertText] Text to insert (after deleting).
1191
1264
  */
1192
1265
 
1193
1266
  /**
1194
- * Called with the result of the lint operation.
1267
+ * Called with the result of the lint function.
1195
1268
  *
1196
1269
  * @callback LintCallback
1197
1270
  * @param {Error | null} err Error object or null.
@@ -1221,10 +1294,19 @@ module.exports = markdownlint;
1221
1294
  */
1222
1295
 
1223
1296
  /**
1224
- * Called with the result of the readConfig operation.
1297
+ * Called with the result of the readConfig function.
1225
1298
  *
1226
1299
  * @callback ReadConfigCallback
1227
1300
  * @param {Error | null} err Error object or null.
1228
1301
  * @param {Configuration} [config] Configuration object.
1229
1302
  * @returns {void}
1230
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
+ */