markdownlint 0.25.1 → 0.26.2

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 (50) hide show
  1. package/CONTRIBUTING.md +2 -0
  2. package/README.md +126 -83
  3. package/demo/markdownlint-browser.js +1743 -1288
  4. package/doc/Rules.md +145 -16
  5. package/helpers/helpers.js +429 -192
  6. package/helpers/package.json +6 -2
  7. package/lib/cache.js +15 -25
  8. package/lib/constants.js +1 -1
  9. package/lib/markdownlint.d.ts +21 -17
  10. package/lib/markdownlint.js +237 -151
  11. package/lib/md004.js +4 -4
  12. package/lib/md005.js +4 -4
  13. package/lib/md006.js +4 -4
  14. package/lib/md007.js +4 -4
  15. package/lib/md009.js +20 -13
  16. package/lib/md010.js +15 -2
  17. package/lib/md011.js +2 -2
  18. package/lib/md013.js +4 -5
  19. package/lib/md014.js +4 -4
  20. package/lib/md021.js +3 -1
  21. package/lib/md026.js +1 -1
  22. package/lib/md027.js +2 -2
  23. package/lib/md028.js +2 -2
  24. package/lib/md029.js +5 -4
  25. package/lib/md030.js +4 -4
  26. package/lib/md031.js +2 -2
  27. package/lib/md032.js +7 -7
  28. package/lib/md033.js +3 -4
  29. package/lib/md034.js +2 -2
  30. package/lib/md035.js +7 -2
  31. package/lib/md036.js +2 -2
  32. package/lib/md037.js +23 -20
  33. package/lib/md038.js +7 -4
  34. package/lib/md039.js +6 -4
  35. package/lib/md042.js +17 -10
  36. package/lib/md043.js +2 -2
  37. package/lib/md044.js +20 -9
  38. package/lib/md046.js +14 -13
  39. package/lib/md048.js +13 -14
  40. package/lib/md049-md050.js +73 -0
  41. package/lib/md051.js +87 -0
  42. package/lib/md052.js +35 -0
  43. package/lib/md053.js +59 -0
  44. package/lib/rules.js +6 -4
  45. package/package.json +25 -18
  46. package/schema/.markdownlint.jsonc +19 -1
  47. package/schema/.markdownlint.yaml +18 -1
  48. package/schema/markdownlint-config-schema.json +126 -728
  49. package/lib/md049.js +0 -45
  50. package/lib/md050.js +0 -45
@@ -12,11 +12,15 @@ module.exports.frontMatterRe =
12
12
  // eslint-disable-next-line max-len
13
13
  /((^---\s*$[^]*?^---\s*$)|(^\+\+\+\s*$[^]*?^(\+\+\+|\.\.\.)\s*$)|(^\{\s*$[^]*?^\}\s*$))(\r\n|\r|\n|$)/m;
14
14
 
15
- // Regular expression for matching inline disable/enable comments
16
- const inlineCommentRe =
15
+ // Regular expression for matching the start of inline disable/enable comments
16
+ const inlineCommentStartRe =
17
17
  // eslint-disable-next-line max-len
18
- /<!--\s*markdownlint-(?:(?:(disable|enable|capture|restore|disable-file|enable-file|disable-next-line)((?:\s+[a-z0-9_-]+)*))|(?:(configure-file)\s+([\s\S]*?)))\s*-->/ig;
19
- module.exports.inlineCommentRe = inlineCommentRe;
18
+ /(<!--\s*markdownlint-(disable|enable|capture|restore|disable-file|enable-file|disable-line|disable-next-line|configure-file))(?:\s|-->)/ig;
19
+ module.exports.inlineCommentStartRe = inlineCommentStartRe;
20
+
21
+ // Regular expression for matching HTML elements
22
+ const htmlElementRe = /<(([A-Za-z][A-Za-z0-9-]*)(?:\s[^`>]*)?)\/?>/g;
23
+ module.exports.htmlElementRe = htmlElementRe;
20
24
 
21
25
  // Regular expressions for range matching
22
26
  module.exports.bareUrlRe = /(?:http|ftp)s?:\/\/[^\s\]"']*(?:\/|[^\s\]"'\W])/ig;
@@ -26,12 +30,17 @@ module.exports.orderedListItemMarkerRe = /^[\s>]*0*(\d+)[.)]/;
26
30
  // Regular expression for all instances of emphasis markers
27
31
  const emphasisMarkersRe = /[_*]/g;
28
32
 
29
- // Regular expression for inline links and shortcut reference links
30
- const linkRe = /(\[(?:[^[\]]|\[[^\]]*\])*\])(\(\S*\)|\[\S*\])?/g;
31
- module.exports.linkRe = linkRe;
33
+ // Regular expression for blockquote prefixes
34
+ const blockquotePrefixRe = /^[>\s]*/;
35
+ module.exports.blockquotePrefixRe = blockquotePrefixRe;
36
+
37
+ // Regular expression for reference links (full, collapsed, and shortcut)
38
+ const referenceLinkRe =
39
+ /!?\\?\[((?:\[[^\]\0]*]|[^\]\0])*)](?:(?:\[([^\]\0]*)\])|([^(])|$)/g;
32
40
 
33
- // Regular expression for link reference definition lines
34
- module.exports.linkReferenceRe = /^ {0,3}\[[^\]]+]:\s.*$/;
41
+ // Regular expression for link reference definitions
42
+ const linkReferenceDefinitionRe = /^ {0,3}\[([^\]]*[^\\])]:/;
43
+ module.exports.linkReferenceDefinitionRe = linkReferenceDefinitionRe;
35
44
 
36
45
  // All punctuation characters (normal and full-width)
37
46
  const allPunctuation = ".,;:!?。,;:!?";
@@ -60,22 +69,43 @@ module.exports.isObject = function isObject(obj) {
60
69
  return (obj !== null) && (typeof obj === "object") && !Array.isArray(obj);
61
70
  };
62
71
 
63
- // Returns true iff the input line is blank (no content)
64
- // Example: Contains nothing, whitespace, or comment (unclosed start/end okay)
65
- module.exports.isBlankLine = function isBlankLine(line) {
66
- // Call to String.replace follows best practices and is not a security check
67
- // False-positive for js/incomplete-multi-character-sanitization
72
+ /**
73
+ * Returns true iff the input line is blank (contains nothing, whitespace, or
74
+ * comments (unclosed start/end comments allowed)).
75
+ *
76
+ * @param {string} line Input line.
77
+ * @returns {boolean} True iff line is blank.
78
+ */
79
+ function isBlankLine(line) {
80
+ const startComment = "<!--";
81
+ const endComment = "-->";
82
+ const removeComments = (s) => {
83
+ // eslint-disable-next-line no-constant-condition
84
+ while (true) {
85
+ const start = s.indexOf(startComment);
86
+ const end = s.indexOf(endComment);
87
+ if ((end !== -1) && ((start === -1) || (end < start))) {
88
+ // Unmatched end comment is first
89
+ s = s.slice(end + endComment.length);
90
+ } else if ((start !== -1) && (end !== -1)) {
91
+ // Start comment is before end comment
92
+ s = s.slice(0, start) + s.slice(end + endComment.length);
93
+ } else if ((start !== -1) && (end === -1)) {
94
+ // Unmatched start comment is last
95
+ s = s.slice(0, start);
96
+ } else {
97
+ // No more comments to remove
98
+ return s;
99
+ }
100
+ }
101
+ };
68
102
  return (
69
103
  !line ||
70
104
  !line.trim() ||
71
- !line
72
- .replace(/<!--.*?-->/g, "")
73
- .replace(/<!--.*$/g, "")
74
- .replace(/^.*-->/g, "")
75
- .replace(/>/g, "")
76
- .trim()
105
+ !removeComments(line).replace(/>/g, "").trim()
77
106
  );
78
- };
107
+ }
108
+ module.exports.isBlankLine = isBlankLine;
79
109
 
80
110
  /**
81
111
  * Compare function for Array.prototype.sort for ascending order of numbers.
@@ -135,16 +165,10 @@ module.exports.clearHtmlCommentText = function clearHtmlCommentText(text) {
135
165
  !content.endsWith("-") && !content.includes("--"));
136
166
  // If a valid block/inline comment...
137
167
  if (isValid) {
138
- const inlineCommentIndex = text
139
- .slice(i, j + htmlCommentEnd.length)
140
- .search(inlineCommentRe);
141
- // If not a markdownlint inline directive...
142
- if (inlineCommentIndex === -1) {
143
- text =
144
- text.slice(0, i + htmlCommentBegin.length) +
145
- content.replace(/[^\r\n]/g, ".") +
146
- text.slice(j);
147
- }
168
+ text =
169
+ text.slice(0, i + htmlCommentBegin.length) +
170
+ content.replace(/[^\r\n]/g, ".") +
171
+ text.slice(j);
148
172
  }
149
173
  }
150
174
  }
@@ -253,11 +277,11 @@ module.exports.unorderedListStyleFor = function unorderedListStyleFor(token) {
253
277
  * @returns {void}
254
278
  */
255
279
  function filterTokens(params, type, handler) {
256
- params.tokens.forEach(function forToken(token) {
280
+ for (const token of params.tokens) {
257
281
  if (token.type === type) {
258
282
  handler(token);
259
283
  }
260
- });
284
+ }
261
285
  }
262
286
  module.exports.filterTokens = filterTokens;
263
287
 
@@ -308,11 +332,11 @@ module.exports.getLineMetadata = function getLineMetadata(params) {
308
332
  filterTokens(params, "hr", (token) => {
309
333
  lineMetadata[token.map[0]][6] = true;
310
334
  });
311
- params.tokens.filter(isMathBlock).forEach((token) => {
335
+ for (const token of params.tokens.filter(isMathBlock)) {
312
336
  for (let i = token.map[0]; i < token.map[1]; i++) {
313
337
  lineMetadata[i][7] = true;
314
338
  }
315
- });
339
+ }
316
340
  return lineMetadata;
317
341
  };
318
342
 
@@ -325,9 +349,9 @@ module.exports.getLineMetadata = function getLineMetadata(params) {
325
349
  * @returns {void}
326
350
  */
327
351
  function forEachLine(lineMetadata, handler) {
328
- lineMetadata.forEach(function forMetadata(metadata) {
352
+ for (const metadata of lineMetadata) {
329
353
  handler(...metadata);
330
- });
354
+ }
331
355
  }
332
356
  module.exports.forEachLine = forEachLine;
333
357
 
@@ -339,7 +363,7 @@ module.exports.flattenLists = function flattenLists(tokens) {
339
363
  let nesting = 0;
340
364
  const nestingStack = [];
341
365
  let lastWithMap = { "map": [ 0, 1 ] };
342
- tokens.forEach((token) => {
366
+ for (const token of tokens) {
343
367
  if ((token.type === "bullet_list_open") ||
344
368
  (token.type === "ordered_list_open")) {
345
369
  // Save current context and start a new one
@@ -372,12 +396,13 @@ module.exports.flattenLists = function flattenLists(tokens) {
372
396
  nestingStack.push(nesting);
373
397
  nesting = 0;
374
398
  } else if (token.type === "blockquote_close") {
375
- nesting = nestingStack.pop();
376
- } else if (token.map) {
399
+ nesting = nestingStack.pop() || 0;
400
+ }
401
+ if (token.map) {
377
402
  // Track last token with map
378
403
  lastWithMap = token;
379
404
  }
380
- });
405
+ }
381
406
  return flattenedLists;
382
407
  };
383
408
 
@@ -385,26 +410,26 @@ module.exports.flattenLists = function flattenLists(tokens) {
385
410
  module.exports.forEachInlineChild =
386
411
  function forEachInlineChild(params, type, handler) {
387
412
  filterTokens(params, "inline", function forToken(token) {
388
- token.children.forEach(function forChild(child) {
413
+ for (const child of token.children) {
389
414
  if (child.type === type) {
390
415
  handler(child, token);
391
416
  }
392
- });
417
+ }
393
418
  });
394
419
  };
395
420
 
396
421
  // Calls the provided function for each heading's content
397
422
  module.exports.forEachHeading = function forEachHeading(params, handler) {
398
423
  let heading = null;
399
- params.tokens.forEach(function forToken(token) {
424
+ for (const token of params.tokens) {
400
425
  if (token.type === "heading_open") {
401
426
  heading = token;
402
427
  } else if (token.type === "heading_close") {
403
428
  heading = null;
404
429
  } else if ((token.type === "inline") && heading) {
405
- handler(heading, token.content);
430
+ handler(heading, token.content, token);
406
431
  }
407
- });
432
+ }
408
433
  };
409
434
 
410
435
  /**
@@ -416,84 +441,71 @@ module.exports.forEachHeading = function forEachHeading(params, handler) {
416
441
  * @returns {void}
417
442
  */
418
443
  function forEachInlineCodeSpan(input, handler) {
419
- let currentLine = 0;
420
- let currentColumn = 0;
421
- let index = 0;
422
- while (index < input.length) {
423
- let startIndex = -1;
424
- let startLine = -1;
425
- let startColumn = -1;
426
- let tickCount = 0;
427
- let currentTicks = 0;
428
- let state = "normal";
429
- // Deliberate <= so trailing 0 completes the last span (ex: "text `code`")
430
- // False-positive for js/index-out-of-bounds
431
- for (; index <= input.length; index++) {
432
- const char = input[index];
433
- // Ignore backticks in link destination
434
- if ((char === "[") && (state === "normal")) {
435
- state = "linkTextOpen";
436
- } else if ((char === "]") && (state === "linkTextOpen")) {
437
- state = "linkTextClosed";
438
- } else if ((char === "(") && (state === "linkTextClosed")) {
439
- state = "linkDestinationOpen";
440
- } else if (
441
- ((char === "(") && (state === "linkDestinationOpen")) ||
442
- ((char === ")") && (state === "linkDestinationOpen")) ||
443
- (state === "linkTextClosed")) {
444
- state = "normal";
445
- }
446
- // Parse backtick open/close
447
- if ((char === "`") && (state !== "linkDestinationOpen")) {
448
- // Count backticks at start or end of code span
449
- currentTicks++;
450
- if ((startIndex === -1) || (startColumn === -1)) {
451
- startIndex = index + 1;
452
- }
453
- } else {
454
- if ((startIndex >= 0) &&
455
- (startColumn >= 0) &&
456
- (tickCount === currentTicks)) {
457
- // Found end backticks; invoke callback for code span
444
+ const backtickRe = /`+/g;
445
+ let match = null;
446
+ const backticksLengthAndIndex = [];
447
+ while ((match = backtickRe.exec(input)) !== null) {
448
+ backticksLengthAndIndex.push([ match[0].length, match.index ]);
449
+ }
450
+ const newLinesIndex = [];
451
+ while ((match = newLineRe.exec(input)) !== null) {
452
+ newLinesIndex.push(match.index);
453
+ }
454
+ let lineIndex = 0;
455
+ let lineStartIndex = 0;
456
+ let k = 0;
457
+ for (let i = 0; i < backticksLengthAndIndex.length - 1; i++) {
458
+ const [ startLength, startIndex ] = backticksLengthAndIndex[i];
459
+ if ((startIndex === 0) || (input[startIndex - 1] !== "\\")) {
460
+ for (let j = i + 1; j < backticksLengthAndIndex.length; j++) {
461
+ const [ endLength, endIndex ] = backticksLengthAndIndex[j];
462
+ if (startLength === endLength) {
463
+ for (; k < newLinesIndex.length; k++) {
464
+ const newLineIndex = newLinesIndex[k];
465
+ if (startIndex < newLineIndex) {
466
+ break;
467
+ }
468
+ lineIndex++;
469
+ lineStartIndex = newLineIndex + 1;
470
+ }
471
+ const columnIndex = startIndex - lineStartIndex + startLength;
458
472
  handler(
459
- input.substring(startIndex, index - currentTicks),
460
- startLine, startColumn, tickCount);
461
- startIndex = -1;
462
- startColumn = -1;
463
- } else if ((startIndex >= 0) && (startColumn === -1)) {
464
- // Found start backticks
465
- tickCount = currentTicks;
466
- startLine = currentLine;
467
- startColumn = currentColumn;
473
+ input.slice(startIndex + startLength, endIndex),
474
+ lineIndex,
475
+ columnIndex,
476
+ startLength
477
+ );
478
+ i = j;
479
+ break;
468
480
  }
469
- // Not in backticks
470
- currentTicks = 0;
471
- }
472
- if (char === "\n") {
473
- // On next line
474
- currentLine++;
475
- currentColumn = 0;
476
- } else if ((char === "\\") &&
477
- ((startIndex === -1) || (startColumn === -1)) &&
478
- (input[index + 1] !== "\n")) {
479
- // Escape character outside code, skip next
480
- index++;
481
- currentColumn += 2;
482
- } else {
483
- // On next column
484
- currentColumn++;
485
481
  }
486
482
  }
487
- if (startIndex >= 0) {
488
- // Restart loop after unmatched start backticks (ex: "`text``code``")
489
- index = startIndex;
490
- currentLine = startLine;
491
- currentColumn = startColumn;
492
- }
493
483
  }
494
484
  }
495
485
  module.exports.forEachInlineCodeSpan = forEachInlineCodeSpan;
496
486
 
487
+ /**
488
+ * Adds ellipsis to the left/right/middle of the specified text.
489
+ *
490
+ * @param {string} text Text to ellipsify.
491
+ * @param {boolean} [start] True iff the start of the text is important.
492
+ * @param {boolean} [end] True iff the end of the text is important.
493
+ * @returns {string} Ellipsified text.
494
+ */
495
+ function ellipsify(text, start, end) {
496
+ if (text.length <= 30) {
497
+ // Nothing to do
498
+ } else if (start && end) {
499
+ text = text.slice(0, 15) + "..." + text.slice(-15);
500
+ } else if (end) {
501
+ text = "..." + text.slice(-30);
502
+ } else {
503
+ text = text.slice(0, 30) + "...";
504
+ }
505
+ return text;
506
+ }
507
+ module.exports.ellipsify = ellipsify;
508
+
497
509
  /**
498
510
  * Adds a generic error object via the onError callback.
499
511
  *
@@ -534,16 +546,8 @@ module.exports.addErrorDetailIf = function addErrorDetailIf(
534
546
  // Adds an error object with context via the onError callback
535
547
  module.exports.addErrorContext = function addErrorContext(
536
548
  onError, lineNumber, context, left, right, range, fixInfo) {
537
- if (context.length <= 30) {
538
- // Nothing to do
539
- } else if (left && right) {
540
- context = context.substr(0, 15) + "..." + context.substr(-15);
541
- } else if (right) {
542
- context = "..." + context.substr(-30);
543
- } else {
544
- context = context.substr(0, 30) + "...";
545
- }
546
- addError(onError, lineNumber, null, context, range, fixInfo);
549
+ context = ellipsify(context, left, right);
550
+ addError(onError, lineNumber, undefined, context, range, fixInfo);
547
551
  };
548
552
 
549
553
  /**
@@ -584,21 +588,41 @@ module.exports.codeBlockAndSpanRanges = (params, lineMetadata) => {
584
588
  };
585
589
 
586
590
  /**
587
- * Determines whether the specified range overlaps another range.
591
+ * Returns an array of HTML element ranges.
592
+ *
593
+ * @param {Object} params RuleParams instance.
594
+ * @param {Object} lineMetadata Line metadata object.
595
+ * @returns {number[][]} Array of ranges (lineIndex, columnIndex, length).
596
+ */
597
+ module.exports.htmlElementRanges = (params, lineMetadata) => {
598
+ const exclusions = [];
599
+ forEachLine(lineMetadata, (line, lineIndex, inCode) => {
600
+ let match = null;
601
+ // eslint-disable-next-line no-unmodified-loop-condition
602
+ while (!inCode && ((match = htmlElementRe.exec(line)) !== null)) {
603
+ exclusions.push([ lineIndex, match.index, match[0].length ]);
604
+ }
605
+ });
606
+ return exclusions;
607
+ };
608
+
609
+ /**
610
+ * Determines whether the specified range is within another range.
588
611
  *
589
612
  * @param {number[][]} ranges Array of ranges (line, index, length).
590
613
  * @param {number} lineIndex Line index to check.
591
614
  * @param {number} index Index to check.
592
615
  * @param {number} length Length to check.
593
- * @returns {boolean} True iff the specified range overlaps.
616
+ * @returns {boolean} True iff the specified range is within.
594
617
  */
595
- module.exports.overlapsAnyRange = (ranges, lineIndex, index, length) => (
618
+ const withinAnyRange = (ranges, lineIndex, index, length) => (
596
619
  !ranges.every((span) => (
597
620
  (lineIndex !== span[0]) ||
598
- (index + length < span[1]) ||
599
- (index > span[1] + span[2])
621
+ (index < span[1]) ||
622
+ (index + length > span[1] + span[2])
600
623
  ))
601
624
  );
625
+ module.exports.withinAnyRange = withinAnyRange;
602
626
 
603
627
  // Returns a range object for a line by applying a RegExp
604
628
  module.exports.rangeFromRegExp = function rangeFromRegExp(line, regexp) {
@@ -626,6 +650,82 @@ module.exports.frontMatterHasTitle =
626
650
  frontMatterLines.some((line) => frontMatterTitleRe.test(line));
627
651
  };
628
652
 
653
+ /**
654
+ * Calls the provided function for each link.
655
+ *
656
+ * @param {string} line Line of Markdown input.
657
+ * @param {Function} handler Function taking (index, link, text, destination).
658
+ * @returns {void}
659
+ */
660
+ function forEachLink(line, handler) {
661
+ // Helper to find matching close symbol for link text/destination
662
+ const findClosingSymbol = (index) => {
663
+ const begin = line[index];
664
+ const end = (begin === "[") ? "]" : ")";
665
+ let nesting = 0;
666
+ let escaping = false;
667
+ let pointy = false;
668
+ for (let i = index + 1; i < line.length; i++) {
669
+ const current = line[i];
670
+ if (current === "\\") {
671
+ escaping = !escaping;
672
+ } else if (!escaping && (current === begin)) {
673
+ nesting++;
674
+ } else if (!escaping && (current === end)) {
675
+ if (nesting > 0) {
676
+ nesting--;
677
+ } else if (!pointy) {
678
+ // Return index after matching close symbol
679
+ return i + 1;
680
+ }
681
+ } else if ((i === index + 1) && (begin === "(") && (current === "<")) {
682
+ pointy = true;
683
+ } else if (!escaping && pointy && current === ">") {
684
+ pointy = false;
685
+ nesting = 0;
686
+ } else {
687
+ escaping = false;
688
+ }
689
+ }
690
+ // No match found
691
+ return -1;
692
+ };
693
+ // Scan line for unescaped "[" character
694
+ let escaping = false;
695
+ for (let i = 0; i < line.length; i++) {
696
+ const current = line[i];
697
+ if (current === "\\") {
698
+ escaping = !escaping;
699
+ } else if (!escaping && (current === "[")) {
700
+ // Scan for matching close "]" of link text
701
+ const textEnd = findClosingSymbol(i);
702
+ if (textEnd !== -1) {
703
+ if ((line[textEnd] === "(") || (line[textEnd] === "[")) {
704
+ // Scan for matching close ")" or "]" of link destination
705
+ const destEnd = findClosingSymbol(textEnd);
706
+ if (destEnd !== -1) {
707
+ // Call handler with link text and destination
708
+ const link = line.slice(i, destEnd);
709
+ const text = line.slice(i, textEnd);
710
+ const dest = line.slice(textEnd, destEnd);
711
+ handler(i, link, text, dest);
712
+ i = destEnd;
713
+ }
714
+ }
715
+ if (i < textEnd) {
716
+ // Call handler with link text only
717
+ const text = line.slice(i, textEnd);
718
+ handler(i, text, text);
719
+ i = textEnd;
720
+ }
721
+ }
722
+ } else {
723
+ escaping = false;
724
+ }
725
+ }
726
+ }
727
+ module.exports.forEachLink = forEachLink;
728
+
629
729
  /**
630
730
  * Returns a list of emphasis markers in code spans and links.
631
731
  *
@@ -636,17 +736,16 @@ function emphasisMarkersInContent(params) {
636
736
  const { lines } = params;
637
737
  const byLine = new Array(lines.length);
638
738
  // Search links
639
- lines.forEach((tokenLine, tokenLineIndex) => {
739
+ for (const [ tokenLineIndex, tokenLine ] of lines.entries()) {
640
740
  const inLine = [];
641
- let linkMatch = null;
642
- while ((linkMatch = linkRe.exec(tokenLine))) {
741
+ forEachLink(tokenLine, (index, match) => {
643
742
  let markerMatch = null;
644
- while ((markerMatch = emphasisMarkersRe.exec(linkMatch[0]))) {
645
- inLine.push(linkMatch.index + markerMatch.index);
743
+ while ((markerMatch = emphasisMarkersRe.exec(match))) {
744
+ inLine.push(index + markerMatch.index);
646
745
  }
647
- }
746
+ });
648
747
  byLine[tokenLineIndex] = inLine;
649
- });
748
+ }
650
749
  // Search code spans
651
750
  filterTokens(params, "inline", (token) => {
652
751
  const { children, lineNumber, map } = token;
@@ -656,7 +755,7 @@ function emphasisMarkersInContent(params) {
656
755
  tokenLines.join("\n"),
657
756
  (code, lineIndex, column, tickCount) => {
658
757
  const codeLines = code.split(newLineRe);
659
- codeLines.forEach((codeLine, codeLineIndex) => {
758
+ for (const [ codeLineIndex, codeLine ] of codeLines.entries()) {
660
759
  const byLineIndex = lineNumber - 1 + lineIndex + codeLineIndex;
661
760
  const inLine = byLine[byLineIndex];
662
761
  const codeLineOffset = codeLineIndex ? 0 : column - 1 + tickCount;
@@ -665,7 +764,7 @@ function emphasisMarkersInContent(params) {
665
764
  inLine.push(codeLineOffset + match.index);
666
765
  }
667
766
  byLine[byLineIndex] = inLine;
668
- });
767
+ }
669
768
  }
670
769
  );
671
770
  }
@@ -674,19 +773,152 @@ function emphasisMarkersInContent(params) {
674
773
  }
675
774
  module.exports.emphasisMarkersInContent = emphasisMarkersInContent;
676
775
 
776
+ /**
777
+ * Returns an object with information about reference links and images.
778
+ *
779
+ * @param {Object} lineMetadata Line metadata object.
780
+ * @returns {Object} Reference link/image data.
781
+ */
782
+ function getReferenceLinkImageData(lineMetadata) {
783
+ // Initialize return values
784
+ const references = new Map();
785
+ const shortcuts = new Set();
786
+ const definitions = new Map();
787
+ const duplicateDefinitions = [];
788
+ // Define helper functions
789
+ const normalizeLabel = (s) => s.toLowerCase().trim().replace(/\s+/g, " ");
790
+ const exclusions = [];
791
+ const excluded = (match) => withinAnyRange(
792
+ exclusions, 0, match.index, match[0].length - (match[3] || "").length
793
+ );
794
+ // Convert input to single-line so multi-line links/images are easier
795
+ const lineOffsets = [];
796
+ let currentOffset = 0;
797
+ const contentLines = [];
798
+ forEachLine(lineMetadata, (line, lineIndex, inCode) => {
799
+ lineOffsets[lineIndex] = currentOffset;
800
+ if (!inCode) {
801
+ line = line.replace(blockquotePrefixRe, "");
802
+ if (line.trim().length === 0) {
803
+ // Allow RegExp to detect the end of a block
804
+ line = "\0";
805
+ }
806
+ contentLines.push(line);
807
+ currentOffset += line.length + 1;
808
+ }
809
+ });
810
+ lineOffsets.push(currentOffset);
811
+ const contentLine = contentLines.join(" ");
812
+ // Determine single-line exclusions for inline code spans
813
+ forEachInlineCodeSpan(contentLine, (code, lineIndex, columnIndex) => {
814
+ exclusions.push([ 0, columnIndex, code.length ]);
815
+ });
816
+ // Identify all link/image reference definitions
817
+ forEachLine(lineMetadata, (line, lineIndex, inCode) => {
818
+ if (!inCode) {
819
+ const linkReferenceDefinitionMatch = linkReferenceDefinitionRe.exec(line);
820
+ if (linkReferenceDefinitionMatch) {
821
+ const label = normalizeLabel(linkReferenceDefinitionMatch[1]);
822
+ if (definitions.has(label)) {
823
+ duplicateDefinitions.push([ label, lineIndex ]);
824
+ } else {
825
+ definitions.set(label, lineIndex);
826
+ }
827
+ exclusions.push([ 0, lineOffsets[lineIndex], line.length ]);
828
+ }
829
+ }
830
+ });
831
+ // Identify all link and image references
832
+ let lineIndex = 0;
833
+ const pendingContents = [
834
+ {
835
+ "content": contentLine,
836
+ "contentLineIndex": 0,
837
+ "contentIndex": 0,
838
+ "topLevel": true
839
+ }
840
+ ];
841
+ let pendingContent = null;
842
+ while ((pendingContent = pendingContents.shift())) {
843
+ const { content, contentLineIndex, contentIndex, topLevel } =
844
+ pendingContent;
845
+ let referenceLinkMatch = null;
846
+ while ((referenceLinkMatch = referenceLinkRe.exec(content)) !== null) {
847
+ const [ matchString, matchText, matchLabel ] = referenceLinkMatch;
848
+ if (
849
+ !matchString.startsWith("\\") &&
850
+ !matchString.startsWith("!\\") &&
851
+ !matchText.endsWith("\\") &&
852
+ !(matchLabel || "").endsWith("\\") &&
853
+ !(topLevel && excluded(referenceLinkMatch))
854
+ ) {
855
+ const shortcutLink = (matchLabel === undefined);
856
+ const collapsedLink =
857
+ (!shortcutLink && (matchLabel.length === 0));
858
+ const label = normalizeLabel(
859
+ (shortcutLink || collapsedLink) ? matchText : matchLabel
860
+ );
861
+ if (label.length > 0) {
862
+ const referenceindex = referenceLinkMatch.index;
863
+ if (topLevel) {
864
+ // Calculate line index
865
+ while (lineOffsets[lineIndex + 1] <= referenceindex) {
866
+ lineIndex++;
867
+ }
868
+ } else {
869
+ // Use provided line index
870
+ lineIndex = contentLineIndex;
871
+ }
872
+ const referenceIndex = referenceindex +
873
+ (topLevel ? -lineOffsets[lineIndex] : contentIndex);
874
+ if (shortcutLink) {
875
+ // Track separately due to ambiguity in "text [text] text"
876
+ shortcuts.add(label);
877
+ } else {
878
+ // Track reference and location
879
+ const referenceData = references.get(label) || [];
880
+ referenceData.push([
881
+ lineIndex,
882
+ referenceIndex,
883
+ matchString.length
884
+ ]);
885
+ references.set(label, referenceData);
886
+ }
887
+ // Check for links embedded in brackets
888
+ if (!matchString.startsWith("!")) {
889
+ pendingContents.push({
890
+ "content": matchText,
891
+ "contentLineIndex": lineIndex,
892
+ "contentIndex": referenceIndex + 1,
893
+ "topLevel": false
894
+ });
895
+ }
896
+ }
897
+ }
898
+ }
899
+ }
900
+ return {
901
+ references,
902
+ shortcuts,
903
+ definitions,
904
+ duplicateDefinitions
905
+ };
906
+ }
907
+ module.exports.getReferenceLinkImageData = getReferenceLinkImageData;
908
+
677
909
  /**
678
910
  * Gets the most common line ending, falling back to the platform default.
679
911
  *
680
912
  * @param {string} input Markdown content to analyze.
681
- * @param {string} [platform] Platform identifier (process.platform).
913
+ * @param {Object} [os] Node.js "os" module.
682
914
  * @returns {string} Preferred line ending.
683
915
  */
684
- function getPreferredLineEnding(input, platform) {
916
+ function getPreferredLineEnding(input, os) {
685
917
  let cr = 0;
686
918
  let lf = 0;
687
919
  let crlf = 0;
688
920
  const endings = input.match(newLineRe) || [];
689
- endings.forEach((ending) => {
921
+ for (const ending of endings) {
690
922
  // eslint-disable-next-line default-case
691
923
  switch (ending) {
692
924
  case "\r":
@@ -699,11 +931,10 @@ function getPreferredLineEnding(input, platform) {
699
931
  crlf++;
700
932
  break;
701
933
  }
702
- });
934
+ }
703
935
  let preferredLineEnding = null;
704
936
  if (!cr && !lf && !crlf) {
705
- preferredLineEnding =
706
- ((platform || process.platform) === "win32") ? "\r\n" : "\n";
937
+ preferredLineEnding = (os && os.EOL) || "\n";
707
938
  } else if ((lf >= crlf) && (lf >= cr)) {
708
939
  preferredLineEnding = "\n";
709
940
  } else if (crlf >= cr) {
@@ -736,8 +967,8 @@ function normalizeFixInfo(fixInfo, lineNumber) {
736
967
  *
737
968
  * @param {string} line Line of Markdown content.
738
969
  * @param {Object} fixInfo RuleOnErrorFixInfo instance.
739
- * @param {string} lineEnding Line ending to use.
740
- * @returns {string} Fixed content.
970
+ * @param {string} [lineEnding] Line ending to use.
971
+ * @returns {string | null} Fixed content.
741
972
  */
742
973
  function applyFix(line, fixInfo, lineEnding) {
743
974
  const { editColumn, deleteCount, insertText } = normalizeFixInfo(fixInfo);
@@ -750,9 +981,15 @@ function applyFix(line, fixInfo, lineEnding) {
750
981
  }
751
982
  module.exports.applyFix = applyFix;
752
983
 
753
- // Applies as many fixes as possible to the input lines
754
- module.exports.applyFixes = function applyFixes(input, errors) {
755
- const lineEnding = getPreferredLineEnding(input);
984
+ /**
985
+ * Applies as many fixes as possible to Markdown content.
986
+ *
987
+ * @param {string} input Lines of Markdown content.
988
+ * @param {Object[]} errors RuleOnErrorInfo instances.
989
+ * @returns {string} Corrected content.
990
+ */
991
+ function applyFixes(input, errors) {
992
+ const lineEnding = getPreferredLineEnding(input, require("os"));
756
993
  const lines = input.split(newLineRe);
757
994
  // Normalize fixInfo objects
758
995
  let fixInfos = errors
@@ -782,8 +1019,10 @@ module.exports.applyFixes = function applyFixes(input, errors) {
782
1019
  return unique;
783
1020
  });
784
1021
  // Collapse insert/no-delete and no-insert/delete for same line/column
785
- lastFixInfo = {};
786
- fixInfos.forEach((fixInfo) => {
1022
+ lastFixInfo = {
1023
+ "lineNumber": -1
1024
+ };
1025
+ for (const fixInfo of fixInfos) {
787
1026
  if (
788
1027
  (fixInfo.lineNumber === lastFixInfo.lineNumber) &&
789
1028
  (fixInfo.editColumn === lastFixInfo.editColumn) &&
@@ -795,12 +1034,12 @@ module.exports.applyFixes = function applyFixes(input, errors) {
795
1034
  lastFixInfo.lineNumber = 0;
796
1035
  }
797
1036
  lastFixInfo = fixInfo;
798
- });
1037
+ }
799
1038
  fixInfos = fixInfos.filter((fixInfo) => fixInfo.lineNumber);
800
1039
  // Apply all (remaining/updated) fixes
801
1040
  let lastLineIndex = -1;
802
1041
  let lastEditIndex = -1;
803
- fixInfos.forEach((fixInfo) => {
1042
+ for (const fixInfo of fixInfos) {
804
1043
  const { lineNumber, editColumn, deleteCount } = fixInfo;
805
1044
  const lineIndex = lineNumber - 1;
806
1045
  const editIndex = editColumn - 1;
@@ -810,14 +1049,16 @@ module.exports.applyFixes = function applyFixes(input, errors) {
810
1049
  ((editIndex + deleteCount) <=
811
1050
  (lastEditIndex - ((deleteCount > 0) ? 0 : 1)))
812
1051
  ) {
1052
+ // @ts-ignore
813
1053
  lines[lineIndex] = applyFix(lines[lineIndex], fixInfo, lineEnding);
814
1054
  }
815
1055
  lastLineIndex = lineIndex;
816
1056
  lastEditIndex = editIndex;
817
- });
1057
+ }
818
1058
  // Return corrected input
819
1059
  return lines.filter((line) => line !== null).join(lineEnding);
820
- };
1060
+ }
1061
+ module.exports.applyFixes = applyFixes;
821
1062
 
822
1063
  /**
823
1064
  * Gets the range and fixInfo values for reporting an error if the expected
@@ -827,28 +1068,33 @@ module.exports.applyFixes = function applyFixes(input, errors) {
827
1068
  * @param {number} lineIndex Line index to check.
828
1069
  * @param {string} search Text to search for.
829
1070
  * @param {string} replace Text to replace with.
1071
+ * @param {number} [instance] Instance on the line (1-based).
830
1072
  * @returns {Object} Range and fixInfo wrapper.
831
1073
  */
832
- function getRangeAndFixInfoIfFound(lines, lineIndex, search, replace) {
833
- let range = null;
834
- let fixInfo = null;
835
- const searchIndex = lines[lineIndex].indexOf(search);
836
- if (searchIndex !== -1) {
837
- const column = searchIndex + 1;
838
- const length = search.length;
839
- range = [ column, length ];
840
- fixInfo = {
841
- "editColumn": column,
842
- "deleteCount": length,
843
- "insertText": replace
1074
+ module.exports.getRangeAndFixInfoIfFound =
1075
+ (lines, lineIndex, search, replace, instance = 1) => {
1076
+ let range = null;
1077
+ let fixInfo = null;
1078
+ let searchIndex = -1;
1079
+ while (instance > 0) {
1080
+ searchIndex = lines[lineIndex].indexOf(search, searchIndex + 1);
1081
+ instance--;
1082
+ }
1083
+ if (searchIndex !== -1) {
1084
+ const column = searchIndex + 1;
1085
+ const length = search.length;
1086
+ range = [ column, length ];
1087
+ fixInfo = {
1088
+ "editColumn": column,
1089
+ "deleteCount": length,
1090
+ "insertText": replace
1091
+ };
1092
+ }
1093
+ return {
1094
+ range,
1095
+ fixInfo
844
1096
  };
845
- }
846
- return {
847
- range,
848
- fixInfo
849
1097
  };
850
- }
851
- module.exports.getRangeAndFixInfoIfFound = getRangeAndFixInfoIfFound;
852
1098
 
853
1099
  /**
854
1100
  * Gets the next (subsequent) child token if it is of the expected type.
@@ -875,23 +1121,14 @@ function getNextChildToken(parentToken, childToken, nextType, nextNextType) {
875
1121
  module.exports.getNextChildToken = getNextChildToken;
876
1122
 
877
1123
  /**
878
- * Calls Object.freeze() on an object and its children.
1124
+ * Expands a path with a tilde to an absolute path.
879
1125
  *
880
- * @param {Object} obj Object to deep freeze.
881
- * @returns {Object} Object passed to the function.
1126
+ * @param {string} file Path that may begin with a tilde.
1127
+ * @param {Object} os Node.js "os" module.
1128
+ * @returns {string} Absolute path (or original path).
882
1129
  */
883
- function deepFreeze(obj) {
884
- const pending = [ obj ];
885
- let current = null;
886
- while ((current = pending.shift())) {
887
- Object.freeze(current);
888
- for (const name of Object.getOwnPropertyNames(current)) {
889
- const value = current[name];
890
- if (value && (typeof value === "object")) {
891
- pending.push(value);
892
- }
893
- }
894
- }
895
- return obj;
1130
+ function expandTildePath(file, os) {
1131
+ const homedir = os && os.homedir && os.homedir();
1132
+ return homedir ? file.replace(/^~($|\/|\\)/, `${homedir}$1`) : file;
896
1133
  }
897
- module.exports.deepFreeze = deepFreeze;
1134
+ module.exports.expandTildePath = expandTildePath;