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