markdownlint 0.24.0 → 0.26.0

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