markdownlint 0.20.0 → 0.20.4

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