markdownlint 0.34.0 → 0.36.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/README.md +97 -45
  3. package/demo/markdownlint-browser.js +2186 -2164
  4. package/doc/CustomRules.md +66 -255
  5. package/doc/Rules.md +102 -29
  6. package/doc/md003.md +2 -2
  7. package/doc/md004.md +4 -3
  8. package/doc/md034.md +5 -0
  9. package/doc/md035.md +3 -8
  10. package/doc/md044.md +10 -0
  11. package/doc/md046.md +2 -2
  12. package/doc/md048.md +3 -2
  13. package/doc/md049.md +4 -3
  14. package/doc/md050.md +4 -4
  15. package/doc/md051.md +11 -3
  16. package/doc/md055.md +3 -2
  17. package/doc/md058.md +48 -0
  18. package/helpers/README.md +2 -45
  19. package/helpers/helpers.js +115 -607
  20. package/helpers/micromark-helpers.cjs +296 -0
  21. package/helpers/micromark-parse.cjs +313 -0
  22. package/helpers/package.json +5 -2
  23. package/helpers/shared.js +6 -0
  24. package/helpers/test.cjs +28 -0
  25. package/lib/cache.js +62 -13
  26. package/lib/configuration-strict.d.ts +1199 -0
  27. package/lib/configuration.d.ts +3 -1173
  28. package/lib/constants.js +3 -2
  29. package/lib/markdownit.cjs +172 -0
  30. package/lib/markdownlint.d.ts +103 -56
  31. package/lib/markdownlint.js +180 -120
  32. package/lib/md001.js +15 -9
  33. package/lib/md003.js +14 -9
  34. package/lib/md004.js +55 -48
  35. package/lib/md005.js +4 -13
  36. package/lib/md007.js +7 -11
  37. package/lib/md009.js +50 -44
  38. package/lib/md010.js +52 -33
  39. package/lib/md011.js +32 -23
  40. package/lib/md012.js +17 -9
  41. package/lib/md013.js +44 -38
  42. package/lib/md014.js +31 -38
  43. package/lib/md018.js +17 -9
  44. package/lib/md019-md021.js +80 -0
  45. package/lib/md020.js +14 -8
  46. package/lib/md022.js +13 -29
  47. package/lib/md023.js +22 -23
  48. package/lib/md024.js +12 -9
  49. package/lib/md025.js +15 -10
  50. package/lib/md026.js +2 -7
  51. package/lib/md027.js +7 -12
  52. package/lib/md028.js +3 -8
  53. package/lib/md029.js +40 -34
  54. package/lib/md030.js +4 -10
  55. package/lib/md031.js +45 -23
  56. package/lib/md032.js +33 -31
  57. package/lib/md033.js +3 -8
  58. package/lib/md034.js +21 -34
  59. package/lib/md035.js +2 -7
  60. package/lib/md036.js +19 -24
  61. package/lib/md037.js +3 -8
  62. package/lib/md038.js +11 -22
  63. package/lib/md039.js +64 -54
  64. package/lib/md040.js +13 -20
  65. package/lib/md041.js +5 -5
  66. package/lib/md042.js +44 -38
  67. package/lib/md043.js +20 -14
  68. package/lib/md044.js +34 -36
  69. package/lib/md045.js +5 -10
  70. package/lib/md046.js +2 -8
  71. package/lib/md048.js +30 -23
  72. package/lib/md049-md050.js +22 -12
  73. package/lib/md051.js +20 -28
  74. package/lib/md052.js +2 -2
  75. package/lib/md053.js +7 -6
  76. package/lib/md054.js +16 -23
  77. package/lib/md055.js +41 -49
  78. package/lib/md056.js +32 -33
  79. package/lib/md058.js +58 -0
  80. package/lib/rules.js +11 -4
  81. package/package.json +23 -20
  82. package/schema/.markdownlint.jsonc +58 -52
  83. package/schema/.markdownlint.yaml +56 -51
  84. package/schema/ValidatingConfiguration.md +6 -8
  85. package/schema/markdownlint-config-schema-strict.json +1841 -0
  86. package/schema/markdownlint-config-schema.json +141 -109
  87. package/style/prettier.json +1 -0
  88. package/helpers/micromark.cjs +0 -445
  89. package/lib/md019.js +0 -42
  90. package/lib/md021.js +0 -65
@@ -2,34 +2,25 @@
2
2
 
3
3
  "use strict";
4
4
 
5
- const micromark = require("./micromark.cjs");
5
+ const micromark = require("./micromark-helpers.cjs");
6
6
 
7
7
  const { newLineRe, nextLinesRe } = require("./shared.js");
8
8
 
9
9
  module.exports.newLineRe = newLineRe;
10
10
  module.exports.nextLinesRe = nextLinesRe;
11
11
 
12
+ /** @typedef {import("../lib/markdownlint.js").RuleOnError} RuleOnError */
13
+ /** @typedef {import("../lib/markdownlint.js").RuleOnErrorFixInfo} RuleOnErrorFixInfo */
14
+
12
15
  // Regular expression for matching common front matter (YAML and TOML)
13
16
  module.exports.frontMatterRe =
14
- /((^---\s*$[\s\S]+?^---\s*)|(^\+\+\+\s*$[\s\S]+?^(\+\+\+|\.\.\.)\s*)|(^\{\s*$[\s\S]+?^\}\s*))(\r\n|\r|\n|$)/m;
17
+ /((^---[^\S\r\n\u2028\u2029]*$[\s\S]+?^---\s*)|(^\+\+\+[^\S\r\n\u2028\u2029]*$[\s\S]+?^(\+\+\+|\.\.\.)\s*)|(^\{[^\S\r\n\u2028\u2029]*$[\s\S]+?^\}\s*))(\r\n|\r|\n|$)/m;
15
18
 
16
19
  // Regular expression for matching the start of inline disable/enable comments
17
20
  const inlineCommentStartRe =
18
21
  /(<!--\s*markdownlint-(disable|enable|capture|restore|disable-file|enable-file|disable-line|disable-next-line|configure-file))(?:\s|-->)/gi;
19
22
  module.exports.inlineCommentStartRe = inlineCommentStartRe;
20
23
 
21
- // Regular expressions for range matching
22
- module.exports.listItemMarkerRe = /^([\s>]*)(?:[*+-]|\d+[.)])\s+/;
23
- module.exports.orderedListItemMarkerRe = /^[\s>]*0*(\d+)[.)]/;
24
-
25
- // Regular expression for blockquote prefixes
26
- const blockquotePrefixRe = /^[>\s]*/;
27
- module.exports.blockquotePrefixRe = blockquotePrefixRe;
28
-
29
- // Regular expression for link reference definitions
30
- const linkReferenceDefinitionRe = /^ {0,3}\[([^\]]*[^\\])\]:/;
31
- module.exports.linkReferenceDefinitionRe = linkReferenceDefinitionRe;
32
-
33
24
  // Regular expression for identifying an HTML entity at the end of a line
34
25
  module.exports.endOfLineHtmlEntityRe =
35
26
  /&(?:#\d+|#[xX][\da-fA-F]+|[a-zA-Z]{2,31}|blk\d{2}|emsp1[34]|frac\d{2}|sup\d|there4);$/;
@@ -143,7 +134,6 @@ function isBlankLine(line) {
143
134
  const startComment = "<!--";
144
135
  const endComment = "-->";
145
136
  const removeComments = (s) => {
146
- // eslint-disable-next-line no-constant-condition
147
137
  while (true) {
148
138
  const start = s.indexOf(startComment);
149
139
  const end = s.indexOf(endComment);
@@ -170,35 +160,6 @@ function isBlankLine(line) {
170
160
  }
171
161
  module.exports.isBlankLine = isBlankLine;
172
162
 
173
- /**
174
- * Compare function for Array.prototype.sort for ascending order of numbers.
175
- *
176
- * @param {number} a First number.
177
- * @param {number} b Second number.
178
- * @returns {number} Positive value if a>b, negative value if b<a, 0 otherwise.
179
- */
180
- module.exports.numericSortAscending = function numericSortAscending(a, b) {
181
- return a - b;
182
- };
183
-
184
- // Returns true iff the sorted array contains the specified element
185
- module.exports.includesSorted = function includesSorted(array, element) {
186
- let left = 0;
187
- let right = array.length - 1;
188
- while (left <= right) {
189
- // eslint-disable-next-line no-bitwise
190
- const mid = (left + right) >> 1;
191
- if (array[mid] < element) {
192
- left = mid + 1;
193
- } else if (array[mid] > element) {
194
- right = mid - 1;
195
- } else {
196
- return true;
197
- }
198
- }
199
- return false;
200
- };
201
-
202
163
  // Replaces the content of properly-formatted CommonMark comments with "."
203
164
  // This preserves the line/column information for the rest of the document
204
165
  // https://spec.commonmark.org/0.29/#html-blocks
@@ -257,301 +218,6 @@ module.exports.escapeForRegExp = function escapeForRegExp(str) {
257
218
  return str.replace(/[-/\\^$*+?.()|[\]{}]/g, "\\$&");
258
219
  };
259
220
 
260
- /**
261
- * Return the string representation of a fence markup character.
262
- *
263
- * @param {string} markup Fence string.
264
- * @returns {string} String representation.
265
- */
266
- module.exports.fencedCodeBlockStyleFor =
267
- function fencedCodeBlockStyleFor(markup) {
268
- switch (markup[0]) {
269
- case "~":
270
- return "tilde";
271
- default:
272
- return "backtick";
273
- }
274
- };
275
-
276
- /**
277
- * Return the string representation of a emphasis or strong markup character.
278
- *
279
- * @param {string} markup Emphasis or strong string.
280
- * @returns {"asterisk" | "underscore"} String representation.
281
- */
282
- module.exports.emphasisOrStrongStyleFor =
283
- function emphasisOrStrongStyleFor(markup) {
284
- switch (markup[0]) {
285
- case "*":
286
- return "asterisk";
287
- default:
288
- return "underscore";
289
- }
290
- };
291
-
292
- /**
293
- * Return the number of characters of indent for a token.
294
- *
295
- * @param {Object} token MarkdownItToken instance.
296
- * @returns {number} Characters of indent.
297
- */
298
- function indentFor(token) {
299
- const line = token.line.replace(/^[\s>]*(> |>)/, "");
300
- return line.length - line.trimStart().length;
301
- }
302
- module.exports.indentFor = indentFor;
303
-
304
- // Returns the heading style for a heading token
305
- module.exports.headingStyleFor = function headingStyleFor(token) {
306
- if ((token.map[1] - token.map[0]) === 1) {
307
- if (/[^\\]#\s*$/.test(token.line)) {
308
- return "atx_closed";
309
- }
310
- return "atx";
311
- }
312
- return "setext";
313
- };
314
-
315
- /**
316
- * Return the string representation of an unordered list marker.
317
- *
318
- * @param {Object} token MarkdownItToken instance.
319
- * @returns {"asterisk" | "dash" | "plus"} String representation.
320
- */
321
- module.exports.unorderedListStyleFor = function unorderedListStyleFor(token) {
322
- switch (token.markup) {
323
- case "-":
324
- return "dash";
325
- case "+":
326
- return "plus";
327
- // case "*":
328
- default:
329
- return "asterisk";
330
- }
331
- };
332
-
333
- /**
334
- * @callback TokenCallback
335
- * @param {MarkdownItToken} token Current token.
336
- * @returns {void}
337
- */
338
-
339
- /**
340
- * Calls the provided function for each matching token.
341
- *
342
- * @param {Object} params RuleParams instance.
343
- * @param {string} type Token type identifier.
344
- * @param {TokenCallback} handler Callback function.
345
- * @returns {void}
346
- */
347
- function filterTokens(params, type, handler) {
348
- for (const token of params.parsers.markdownit.tokens) {
349
- if (token.type === type) {
350
- handler(token);
351
- }
352
- }
353
- }
354
- module.exports.filterTokens = filterTokens;
355
-
356
- /**
357
- * @typedef {Array} LineMetadata
358
- */
359
-
360
- /**
361
- * Gets a line metadata array.
362
- *
363
- * @param {Object} params RuleParams instance.
364
- * @returns {LineMetadata} Line metadata.
365
- */
366
- function getLineMetadata(params) {
367
- const lineMetadata = params.lines.map(
368
- (line, index) => [ line, index, false, 0, false, false, false ]
369
- );
370
- filterTokens(params, "fence", (token) => {
371
- lineMetadata[token.map[0]][3] = 1;
372
- lineMetadata[token.map[1] - 1][3] = -1;
373
- for (let i = token.map[0] + 1; i < token.map[1] - 1; i++) {
374
- lineMetadata[i][2] = true;
375
- }
376
- });
377
- filterTokens(params, "code_block", (token) => {
378
- for (let i = token.map[0]; i < token.map[1]; i++) {
379
- lineMetadata[i][2] = true;
380
- }
381
- });
382
- filterTokens(params, "table_open", (token) => {
383
- for (let i = token.map[0]; i < token.map[1]; i++) {
384
- lineMetadata[i][4] = true;
385
- }
386
- });
387
- filterTokens(params, "list_item_open", (token) => {
388
- let count = 1;
389
- for (let i = token.map[0]; i < token.map[1]; i++) {
390
- lineMetadata[i][5] = count;
391
- count++;
392
- }
393
- });
394
- filterTokens(params, "hr", (token) => {
395
- lineMetadata[token.map[0]][6] = true;
396
- });
397
- return lineMetadata;
398
- }
399
- module.exports.getLineMetadata = getLineMetadata;
400
-
401
- /**
402
- * @callback EachLineCallback
403
- * @param {string} line Line content.
404
- * @param {number} lineIndex Line index (0-based).
405
- * @param {boolean} inCode Iff in a code block.
406
- * @param {number} onFence + if open, - if closed, 0 otherwise.
407
- * @param {boolean} inTable Iff in a table.
408
- * @param {boolean} inItem Iff in a list item.
409
- * @param {boolean} inBreak Iff in semantic break.
410
- * @returns {void}
411
- */
412
-
413
- /**
414
- * Calls the provided function for each line.
415
- *
416
- * @param {LineMetadata} lineMetadata Line metadata object.
417
- * @param {EachLineCallback} handler Function taking (line, lineIndex, inCode,
418
- * onFence, inTable, inItem, inBreak).
419
- * @returns {void}
420
- */
421
- function forEachLine(lineMetadata, handler) {
422
- for (const metadata of lineMetadata) {
423
- // @ts-ignore
424
- handler(...metadata);
425
- }
426
- }
427
- module.exports.forEachLine = forEachLine;
428
-
429
- // Returns (nested) lists as a flat array (in order)
430
- module.exports.flattenLists = function flattenLists(tokens) {
431
- const flattenedLists = [];
432
- const stack = [];
433
- let current = null;
434
- let nesting = 0;
435
- const nestingStack = [];
436
- let lastWithMap = { "map": [ 0, 1 ] };
437
- for (const token of tokens) {
438
- if ((token.type === "bullet_list_open") ||
439
- (token.type === "ordered_list_open")) {
440
- // Save current context and start a new one
441
- stack.push(current);
442
- current = {
443
- "unordered": (token.type === "bullet_list_open"),
444
- "parentsUnordered": !current ||
445
- (current.unordered && current.parentsUnordered),
446
- "open": token,
447
- "indent": indentFor(token),
448
- "parentIndent": (current && current.indent) || 0,
449
- "items": [],
450
- "nesting": nesting,
451
- "lastLineIndex": -1,
452
- "insert": flattenedLists.length
453
- };
454
- nesting++;
455
- } else if ((token.type === "bullet_list_close") ||
456
- (token.type === "ordered_list_close")) {
457
- // Finalize current context and restore previous
458
- current.lastLineIndex = lastWithMap.map[1];
459
- flattenedLists.splice(current.insert, 0, current);
460
- delete current.insert;
461
- current = stack.pop();
462
- nesting--;
463
- } else if (token.type === "list_item_open") {
464
- // Add list item
465
- current.items.push(token);
466
- } else if (token.type === "blockquote_open") {
467
- nestingStack.push(nesting);
468
- nesting = 0;
469
- } else if (token.type === "blockquote_close") {
470
- nesting = nestingStack.pop() || 0;
471
- }
472
- if (token.map) {
473
- // Track last token with map
474
- lastWithMap = token;
475
- }
476
- }
477
- return flattenedLists;
478
- };
479
-
480
- // Calls the provided function for each heading's content
481
- module.exports.forEachHeading = function forEachHeading(params, handler) {
482
- let heading = null;
483
- for (const token of params.parsers.markdownit.tokens) {
484
- if (token.type === "heading_open") {
485
- heading = token;
486
- } else if (token.type === "heading_close") {
487
- heading = null;
488
- } else if ((token.type === "inline") && heading) {
489
- handler(heading, token.content, token);
490
- }
491
- }
492
- };
493
-
494
- /**
495
- * @callback InlineCodeSpanCallback
496
- * @param {string} code Code content.
497
- * @param {number} lineIndex Line index (0-based).
498
- * @param {number} columnIndex Column index (0-based).
499
- * @param {number} ticks Count of backticks.
500
- * @returns {void}
501
- */
502
-
503
- /**
504
- * Calls the provided function for each inline code span's content.
505
- *
506
- * @param {string} input Markdown content.
507
- * @param {InlineCodeSpanCallback} handler Callback function taking (code,
508
- * lineIndex, columnIndex, ticks).
509
- * @returns {void}
510
- */
511
- function forEachInlineCodeSpan(input, handler) {
512
- const backtickRe = /`+/g;
513
- let match = null;
514
- const backticksLengthAndIndex = [];
515
- while ((match = backtickRe.exec(input)) !== null) {
516
- backticksLengthAndIndex.push([ match[0].length, match.index ]);
517
- }
518
- const newLinesIndex = [];
519
- while ((match = newLineRe.exec(input)) !== null) {
520
- newLinesIndex.push(match.index);
521
- }
522
- let lineIndex = 0;
523
- let lineStartIndex = 0;
524
- let k = 0;
525
- for (let i = 0; i < backticksLengthAndIndex.length - 1; i++) {
526
- const [ startLength, startIndex ] = backticksLengthAndIndex[i];
527
- if ((startIndex === 0) || (input[startIndex - 1] !== "\\")) {
528
- for (let j = i + 1; j < backticksLengthAndIndex.length; j++) {
529
- const [ endLength, endIndex ] = backticksLengthAndIndex[j];
530
- if (startLength === endLength) {
531
- for (; k < newLinesIndex.length; k++) {
532
- const newLineIndex = newLinesIndex[k];
533
- if (startIndex < newLineIndex) {
534
- break;
535
- }
536
- lineIndex++;
537
- lineStartIndex = newLineIndex + 1;
538
- }
539
- const columnIndex = startIndex - lineStartIndex + startLength;
540
- handler(
541
- input.slice(startIndex + startLength, endIndex),
542
- lineIndex,
543
- columnIndex,
544
- startLength
545
- );
546
- i = j;
547
- break;
548
- }
549
- }
550
- }
551
- }
552
- }
553
- module.exports.forEachInlineCodeSpan = forEachInlineCodeSpan;
554
-
555
221
  /**
556
222
  * Adds ellipsis to the left/right/middle of the specified text.
557
223
  *
@@ -577,12 +243,12 @@ module.exports.ellipsify = ellipsify;
577
243
  /**
578
244
  * Adds a generic error object via the onError callback.
579
245
  *
580
- * @param {Object} onError RuleOnError instance.
246
+ * @param {RuleOnError} onError RuleOnError instance.
581
247
  * @param {number} lineNumber Line number.
582
248
  * @param {string} [detail] Error details.
583
249
  * @param {string} [context] Error context.
584
250
  * @param {number[]} [range] Column and length of error.
585
- * @param {Object} [fixInfo] RuleOnErrorFixInfo instance.
251
+ * @param {RuleOnErrorFixInfo} [fixInfo] RuleOnErrorFixInfo instance.
586
252
  * @returns {void}
587
253
  */
588
254
  function addError(onError, lineNumber, detail, context, range, fixInfo) {
@@ -596,8 +262,20 @@ function addError(onError, lineNumber, detail, context, range, fixInfo) {
596
262
  }
597
263
  module.exports.addError = addError;
598
264
 
599
- // Adds an error object with details conditionally via the onError callback
600
- module.exports.addErrorDetailIf = function addErrorDetailIf(
265
+ /**
266
+ * Adds an error object with details conditionally via the onError callback.
267
+ *
268
+ * @param {RuleOnError} onError RuleOnError instance.
269
+ * @param {number} lineNumber Line number.
270
+ * @param {Object} expected Expected value.
271
+ * @param {Object} actual Actual value.
272
+ * @param {string} [detail] Error details.
273
+ * @param {string} [context] Error context.
274
+ * @param {number[]} [range] Column and length of error.
275
+ * @param {RuleOnErrorFixInfo} [fixInfo] RuleOnErrorFixInfo instance.
276
+ * @returns {void}
277
+ */
278
+ function addErrorDetailIf(
601
279
  onError, lineNumber, expected, actual, detail, context, range, fixInfo) {
602
280
  if (expected !== actual) {
603
281
  addError(
@@ -609,80 +287,64 @@ module.exports.addErrorDetailIf = function addErrorDetailIf(
609
287
  range,
610
288
  fixInfo);
611
289
  }
612
- };
290
+ }
291
+ module.exports.addErrorDetailIf = addErrorDetailIf;
613
292
 
614
- // Adds an error object with context via the onError callback
615
- module.exports.addErrorContext = function addErrorContext(
616
- onError, lineNumber, context, left, right, range, fixInfo) {
617
- context = ellipsify(context, left, right);
293
+ /**
294
+ * Adds an error object with context via the onError callback.
295
+ *
296
+ * @param {RuleOnError} onError RuleOnError instance.
297
+ * @param {number} lineNumber Line number.
298
+ * @param {string} context Error context.
299
+ * @param {boolean} [start] True iff the start of the text is important.
300
+ * @param {boolean} [end] True iff the end of the text is important.
301
+ * @param {number[]} [range] Column and length of error.
302
+ * @param {RuleOnErrorFixInfo} [fixInfo] RuleOnErrorFixInfo instance.
303
+ * @returns {void}
304
+ */
305
+ function addErrorContext(
306
+ onError, lineNumber, context, start, end, range, fixInfo) {
307
+ context = ellipsify(context, start, end);
618
308
  addError(onError, lineNumber, undefined, context, range, fixInfo);
619
- };
309
+ }
310
+ module.exports.addErrorContext = addErrorContext;
620
311
 
621
312
  /**
622
- * Returns an array of code block and span content ranges.
313
+ * Defines a range within a file (start line/column to end line/column, subset of MicromarkToken).
623
314
  *
624
- * @param {Object} params RuleParams instance.
625
- * @param {Object} lineMetadata Line metadata object.
626
- * @returns {number[][]} Array of ranges (lineIndex, columnIndex, length).
315
+ * @typedef {Object} FileRange
316
+ * @property {number} startLine Start line (1-based).
317
+ * @property {number} startColumn Start column (1-based).
318
+ * @property {number} endLine End line (1-based).
319
+ * @property {number} endColumn End column (1-based).
627
320
  */
628
- module.exports.codeBlockAndSpanRanges = (params, lineMetadata) => {
629
- const exclusions = [];
630
- // Add code block ranges (excludes fences)
631
- forEachLine(lineMetadata, (line, lineIndex, inCode, onFence) => {
632
- if (inCode && !onFence) {
633
- exclusions.push([ lineIndex, 0, line.length ]);
634
- }
635
- });
636
- // Add code span ranges (excludes ticks)
637
- filterTokens(params, "inline", (token) => {
638
- if (token.children.some((child) => child.type === "code_inline")) {
639
- const tokenLines = params.lines.slice(token.map[0], token.map[1]);
640
- forEachInlineCodeSpan(
641
- tokenLines.join("\n"),
642
- (code, lineIndex, columnIndex) => {
643
- const codeLines = code.split(newLineRe);
644
- for (const [ i, line ] of codeLines.entries()) {
645
- exclusions.push([
646
- token.lineNumber - 1 + lineIndex + i,
647
- i ? 0 : columnIndex,
648
- line.length
649
- ]);
650
- }
651
- }
652
- );
653
- }
654
- });
655
- return exclusions;
656
- };
657
321
 
658
322
  /**
659
- * Determines whether the specified range is within another range.
323
+ * Returns whether line/column A is less than or equal to line/column B.
660
324
  *
661
- * @param {number[][]} ranges Array of ranges (line, index, length).
662
- * @param {number} lineIndex Line index to check.
663
- * @param {number} index Index to check.
664
- * @param {number} length Length to check.
665
- * @returns {boolean} True iff the specified range is within.
666
- */
667
- const withinAnyRange = (ranges, lineIndex, index, length) => (
668
- !ranges.every((span) => (
669
- (lineIndex !== span[0]) ||
670
- (index < span[1]) ||
671
- (index + length > span[1] + span[2])
672
- ))
325
+ * @param {number} lineA Line A.
326
+ * @param {number} columnA Column A.
327
+ * @param {number} lineB Line B.
328
+ * @param {number} columnB Column B.
329
+ * @returns {boolean} True iff A is less than or equal to B.
330
+ */
331
+ const positionLessThanOrEqual = (lineA, columnA, lineB, columnB) => (
332
+ (lineA < lineB) ||
333
+ ((lineA === lineB) && (columnA <= columnB))
673
334
  );
674
- module.exports.withinAnyRange = withinAnyRange;
675
335
 
676
- // Returns a range object for a line by applying a RegExp
677
- module.exports.rangeFromRegExp = function rangeFromRegExp(line, regexp) {
678
- let range = null;
679
- const match = line.match(regexp);
680
- if (match) {
681
- const column = match.index + 1;
682
- const length = match[0].length;
683
- range = [ column, length ];
684
- }
685
- return range;
336
+ /**
337
+ * Returns whether two ranges (or MicromarkTokens) overlap anywhere.
338
+ *
339
+ * @param {FileRange|import("../lib/markdownlint.js").MicromarkToken} rangeA Range A.
340
+ * @param {FileRange|import("../lib/markdownlint.js").MicromarkToken} rangeB Range B.
341
+ * @returns {boolean} True iff the two ranges overlap.
342
+ */
343
+ module.exports.hasOverlap = function hasOverlap(rangeA, rangeB) {
344
+ const lte = positionLessThanOrEqual(rangeA.startLine, rangeA.startColumn, rangeB.startLine, rangeB.startColumn);
345
+ const first = lte ? rangeA : rangeB;
346
+ const second = lte ? rangeB : rangeA;
347
+ return positionLessThanOrEqual(second.startLine, second.startColumn, first.endLine, first.endColumn);
686
348
  };
687
349
 
688
350
  // Determines if the front matter includes a title
@@ -702,16 +364,28 @@ module.exports.frontMatterHasTitle =
702
364
  /**
703
365
  * Returns an object with information about reference links and images.
704
366
  *
705
- * @param {import("../helpers/micromark.cjs").Token[]} tokens Micromark tokens.
367
+ * @param {import("../helpers/micromark-helpers.cjs").Token[]} tokens Micromark tokens.
706
368
  * @returns {Object} Reference link/image data.
707
369
  */
708
370
  function getReferenceLinkImageData(tokens) {
709
371
  const normalizeReference = (s) => s.toLowerCase().trim().replace(/\s+/g, " ");
372
+ const references = new Map();
373
+ const shortcuts = new Map();
374
+ const addReferenceToDictionary = (token, label, isShortcut) => {
375
+ const referenceDatum = [
376
+ token.startLine - 1,
377
+ token.startColumn - 1,
378
+ token.text.length
379
+ ];
380
+ const reference = normalizeReference(label);
381
+ const dictionary = isShortcut ? shortcuts : references;
382
+ const referenceData = dictionary.get(reference) || [];
383
+ referenceData.push(referenceDatum);
384
+ dictionary.set(reference, referenceData);
385
+ };
710
386
  const definitions = new Map();
711
387
  const definitionLineIndices = [];
712
388
  const duplicateDefinitions = [];
713
- const references = new Map();
714
- const shortcuts = new Map();
715
389
  const filteredTokens =
716
390
  micromark.filterByTypes(
717
391
  tokens,
@@ -721,7 +395,9 @@ function getReferenceLinkImageData(tokens) {
721
395
  // definitions and definitionLineIndices
722
396
  "definitionLabelString", "gfmFootnoteDefinitionLabelString",
723
397
  // references and shortcuts
724
- "gfmFootnoteCall", "image", "link"
398
+ "gfmFootnoteCall", "image", "link",
399
+ // undefined link labels
400
+ "undefinedReferenceCollapsed", "undefinedReferenceFull", "undefinedReferenceShortcut"
725
401
  ]
726
402
  );
727
403
  for (const token of filteredTokens) {
@@ -744,15 +420,10 @@ function getReferenceLinkImageData(tokens) {
744
420
  if (definitions.has(reference)) {
745
421
  duplicateDefinitions.push([ reference, token.startLine - 1 ]);
746
422
  } else {
747
- let destinationString = null;
748
423
  const parent =
749
- micromark.getTokenParentOfType(token, [ "definition" ]);
750
- if (parent) {
751
- destinationString = micromark.getTokenTextByType(
752
- micromark.filterByPredicate(parent.children),
753
- "definitionDestinationString"
754
- );
755
- }
424
+ micromark.getParentOfType(token, [ "definition" ]);
425
+ const destinationString = parent &&
426
+ micromark.getDescendantsByType(parent, [ "definitionDestination", "definitionDestinationRaw", "definitionDestinationString" ])[0]?.text;
756
427
  definitions.set(
757
428
  reference,
758
429
  [ token.startLine - 1, destinationString ]
@@ -764,67 +435,38 @@ function getReferenceLinkImageData(tokens) {
764
435
  case "image":
765
436
  case "link":
766
437
  {
767
- let isShortcut = false;
768
- let isFullOrCollapsed = false;
769
- let labelText = null;
770
- let referenceStringText = null;
771
- const shortcutCandidate =
772
- micromark.matchAndGetTokensByType(token.children, [ "label" ]);
773
- if (shortcutCandidate) {
774
- labelText =
775
- micromark.getTokenTextByType(
776
- shortcutCandidate[0].children, "labelText"
777
- );
778
- isShortcut = (labelText !== null);
779
- }
780
- const fullAndCollapsedCandidate =
781
- micromark.matchAndGetTokensByType(
782
- token.children, [ "label", "reference" ]
438
+ // Identify if shortcut or full/collapsed
439
+ let isShortcut = (token.children.length === 1);
440
+ const isFullOrCollapsed = (token.children.length === 2) && !token.children.some((t) => t.type === "resource");
441
+ const [ labelText ] = micromark.getDescendantsByType(token, [ "label", "labelText" ]);
442
+ const [ referenceString ] = micromark.getDescendantsByType(token, [ "reference", "referenceString" ]);
443
+ let label = labelText?.text;
444
+ // Identify if footnote
445
+ if (!isShortcut && !isFullOrCollapsed) {
446
+ const [ footnoteCallMarker, footnoteCallString ] = token.children.filter(
447
+ (t) => [ "gfmFootnoteCallMarker", "gfmFootnoteCallString" ].includes(t.type)
783
448
  );
784
- if (fullAndCollapsedCandidate) {
785
- labelText =
786
- micromark.getTokenTextByType(
787
- fullAndCollapsedCandidate[0].children, "labelText"
788
- );
789
- referenceStringText =
790
- micromark.getTokenTextByType(
791
- fullAndCollapsedCandidate[1].children, "referenceString"
792
- );
793
- isFullOrCollapsed = (labelText !== null);
794
- }
795
- const footnote = micromark.matchAndGetTokensByType(
796
- token.children,
797
- [
798
- "gfmFootnoteCallLabelMarker", "gfmFootnoteCallMarker",
799
- "gfmFootnoteCallString", "gfmFootnoteCallLabelMarker"
800
- ],
801
- [ "gfmFootnoteCallMarker", "gfmFootnoteCallString" ]
802
- );
803
- if (footnote) {
804
- const callMarkerText = footnote[0].text;
805
- const callString = footnote[1].text;
806
- labelText = `${callMarkerText}${callString}`;
807
- isShortcut = true;
449
+ if (footnoteCallMarker && footnoteCallString) {
450
+ label = `${footnoteCallMarker.text}${footnoteCallString.text}`;
451
+ isShortcut = true;
452
+ }
808
453
  }
809
- // Track shortcuts separately due to ambiguity in "text [text] text"
454
+ // Track link (handle shortcuts separately due to ambiguity in "text [text] text")
810
455
  if (isShortcut || isFullOrCollapsed) {
811
- const referenceDatum = [
812
- token.startLine - 1,
813
- token.startColumn - 1,
814
- token.text.length,
815
- // @ts-ignore
816
- labelText.length,
817
- (referenceStringText || "").length
818
- ];
819
- const reference =
820
- normalizeReference(referenceStringText || labelText);
821
- const dictionary = isShortcut ? shortcuts : references;
822
- const referenceData = dictionary.get(reference) || [];
823
- referenceData.push(referenceDatum);
824
- dictionary.set(reference, referenceData);
456
+ addReferenceToDictionary(token, referenceString?.text || label, isShortcut);
825
457
  }
826
458
  }
827
459
  break;
460
+ case "undefinedReferenceCollapsed":
461
+ case "undefinedReferenceFull":
462
+ case "undefinedReferenceShortcut":
463
+ {
464
+ const undefinedReference = micromark.getDescendantsByType(token, [ "undefinedReference" ])[0];
465
+ const label = undefinedReference.children.map((t) => t.text).join("");
466
+ const isShortcut = (token.type === "undefinedReferenceShortcut");
467
+ addReferenceToDictionary(token, label, isShortcut);
468
+ }
469
+ break;
828
470
  }
829
471
  }
830
472
  return {
@@ -877,120 +519,6 @@ function getPreferredLineEnding(input, os) {
877
519
  }
878
520
  module.exports.getPreferredLineEnding = getPreferredLineEnding;
879
521
 
880
- /**
881
- * Normalizes the fields of a RuleOnErrorFixInfo instance.
882
- *
883
- * @param {Object} fixInfo RuleOnErrorFixInfo instance.
884
- * @param {number} [lineNumber] Line number.
885
- * @returns {Object} Normalized RuleOnErrorFixInfo instance.
886
- */
887
- function normalizeFixInfo(fixInfo, lineNumber) {
888
- return {
889
- "lineNumber": fixInfo.lineNumber || lineNumber,
890
- "editColumn": fixInfo.editColumn || 1,
891
- "deleteCount": fixInfo.deleteCount || 0,
892
- "insertText": fixInfo.insertText || ""
893
- };
894
- }
895
-
896
- /**
897
- * Fixes the specified error on a line of Markdown content.
898
- *
899
- * @param {string} line Line of Markdown content.
900
- * @param {Object} fixInfo RuleOnErrorFixInfo instance.
901
- * @param {string} [lineEnding] Line ending to use.
902
- * @returns {string | null} Fixed content.
903
- */
904
- function applyFix(line, fixInfo, lineEnding) {
905
- const { editColumn, deleteCount, insertText } = normalizeFixInfo(fixInfo);
906
- const editIndex = editColumn - 1;
907
- return (deleteCount === -1) ?
908
- null :
909
- line.slice(0, editIndex) +
910
- insertText.replace(/\n/g, lineEnding || "\n") +
911
- line.slice(editIndex + deleteCount);
912
- }
913
- module.exports.applyFix = applyFix;
914
-
915
- /**
916
- * Applies as many fixes as possible to Markdown content.
917
- *
918
- * @param {string} input Lines of Markdown content.
919
- * @param {Object[]} errors RuleOnErrorInfo instances.
920
- * @returns {string} Corrected content.
921
- */
922
- function applyFixes(input, errors) {
923
- const lineEnding = getPreferredLineEnding(input, require("node:os"));
924
- const lines = input.split(newLineRe);
925
- // Normalize fixInfo objects
926
- let fixInfos = errors
927
- .filter((error) => error.fixInfo)
928
- .map((error) => normalizeFixInfo(error.fixInfo, error.lineNumber));
929
- // Sort bottom-to-top, line-deletes last, right-to-left, long-to-short
930
- fixInfos.sort((a, b) => {
931
- const aDeletingLine = (a.deleteCount === -1);
932
- const bDeletingLine = (b.deleteCount === -1);
933
- return (
934
- (b.lineNumber - a.lineNumber) ||
935
- (aDeletingLine ? 1 : (bDeletingLine ? -1 : 0)) ||
936
- (b.editColumn - a.editColumn) ||
937
- (b.insertText.length - a.insertText.length)
938
- );
939
- });
940
- // Remove duplicate entries (needed for following collapse step)
941
- let lastFixInfo = {};
942
- fixInfos = fixInfos.filter((fixInfo) => {
943
- const unique = (
944
- (fixInfo.lineNumber !== lastFixInfo.lineNumber) ||
945
- (fixInfo.editColumn !== lastFixInfo.editColumn) ||
946
- (fixInfo.deleteCount !== lastFixInfo.deleteCount) ||
947
- (fixInfo.insertText !== lastFixInfo.insertText)
948
- );
949
- lastFixInfo = fixInfo;
950
- return unique;
951
- });
952
- // Collapse insert/no-delete and no-insert/delete for same line/column
953
- lastFixInfo = {
954
- "lineNumber": -1
955
- };
956
- for (const fixInfo of fixInfos) {
957
- if (
958
- (fixInfo.lineNumber === lastFixInfo.lineNumber) &&
959
- (fixInfo.editColumn === lastFixInfo.editColumn) &&
960
- !fixInfo.insertText &&
961
- (fixInfo.deleteCount > 0) &&
962
- lastFixInfo.insertText &&
963
- !lastFixInfo.deleteCount) {
964
- fixInfo.insertText = lastFixInfo.insertText;
965
- lastFixInfo.lineNumber = 0;
966
- }
967
- lastFixInfo = fixInfo;
968
- }
969
- fixInfos = fixInfos.filter((fixInfo) => fixInfo.lineNumber);
970
- // Apply all (remaining/updated) fixes
971
- let lastLineIndex = -1;
972
- let lastEditIndex = -1;
973
- for (const fixInfo of fixInfos) {
974
- const { lineNumber, editColumn, deleteCount } = fixInfo;
975
- const lineIndex = lineNumber - 1;
976
- const editIndex = editColumn - 1;
977
- if (
978
- (lineIndex !== lastLineIndex) ||
979
- (deleteCount === -1) ||
980
- ((editIndex + deleteCount) <=
981
- (lastEditIndex - ((deleteCount > 0) ? 0 : 1)))
982
- ) {
983
- // @ts-ignore
984
- lines[lineIndex] = applyFix(lines[lineIndex], fixInfo, lineEnding);
985
- }
986
- lastLineIndex = lineIndex;
987
- lastEditIndex = editIndex;
988
- }
989
- // Return corrected input
990
- return lines.filter((line) => line !== null).join(lineEnding);
991
- }
992
- module.exports.applyFixes = applyFixes;
993
-
994
522
  /**
995
523
  * Expands a path with a tilde to an absolute path.
996
524
  *
@@ -1003,23 +531,3 @@ function expandTildePath(file, os) {
1003
531
  return homedir ? file.replace(/^~($|\/|\\)/, `${homedir}$1`) : file;
1004
532
  }
1005
533
  module.exports.expandTildePath = expandTildePath;
1006
-
1007
- // Copied from markdownlint.js to avoid TypeScript compiler import() issue.
1008
- /**
1009
- * @typedef {Object} MarkdownItToken
1010
- * @property {string[][]} attrs HTML attributes.
1011
- * @property {boolean} block Block-level token.
1012
- * @property {MarkdownItToken[]} children Child nodes.
1013
- * @property {string} content Tag contents.
1014
- * @property {boolean} hidden Ignore element.
1015
- * @property {string} info Fence info.
1016
- * @property {number} level Nesting level.
1017
- * @property {number[]} map Beginning/ending line numbers.
1018
- * @property {string} markup Markup text.
1019
- * @property {Object} meta Arbitrary data.
1020
- * @property {number} nesting Level change.
1021
- * @property {string} tag HTML tag name.
1022
- * @property {string} type Token type.
1023
- * @property {number} lineNumber Line number (1-based).
1024
- * @property {string} line Line content.
1025
- */