markdownlint 0.25.0 → 0.26.1

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