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.
- package/CONTRIBUTING.md +2 -0
- package/README.md +126 -83
- package/demo/markdownlint-browser.js +1743 -1288
- package/doc/Rules.md +145 -16
- package/helpers/helpers.js +429 -192
- package/helpers/package.json +6 -2
- package/lib/cache.js +15 -25
- package/lib/constants.js +1 -1
- package/lib/markdownlint.d.ts +21 -17
- package/lib/markdownlint.js +237 -151
- package/lib/md004.js +4 -4
- package/lib/md005.js +4 -4
- package/lib/md006.js +4 -4
- package/lib/md007.js +4 -4
- package/lib/md009.js +20 -13
- package/lib/md010.js +15 -2
- package/lib/md011.js +2 -2
- package/lib/md013.js +4 -5
- package/lib/md014.js +4 -4
- package/lib/md021.js +3 -1
- package/lib/md026.js +1 -1
- package/lib/md027.js +2 -2
- package/lib/md028.js +2 -2
- package/lib/md029.js +5 -4
- package/lib/md030.js +4 -4
- package/lib/md031.js +2 -2
- package/lib/md032.js +7 -7
- package/lib/md033.js +3 -4
- package/lib/md034.js +2 -2
- package/lib/md035.js +7 -2
- package/lib/md036.js +2 -2
- package/lib/md037.js +23 -20
- package/lib/md038.js +7 -4
- package/lib/md039.js +6 -4
- package/lib/md042.js +17 -10
- package/lib/md043.js +2 -2
- package/lib/md044.js +20 -9
- package/lib/md046.js +14 -13
- package/lib/md048.js +13 -14
- package/lib/md049-md050.js +73 -0
- package/lib/md051.js +87 -0
- package/lib/md052.js +35 -0
- package/lib/md053.js +59 -0
- package/lib/rules.js +6 -4
- package/package.json +25 -18
- package/schema/.markdownlint.jsonc +19 -1
- package/schema/.markdownlint.yaml +18 -1
- package/schema/markdownlint-config-schema.json +126 -728
- package/lib/md049.js +0 -45
- package/lib/md050.js +0 -45
package/helpers/helpers.js
CHANGED
|
@@ -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
|
|
15
|
+
// Regular expression for matching the start of inline disable/enable comments
|
|
16
|
+
const inlineCommentStartRe =
|
|
17
17
|
// eslint-disable-next-line max-len
|
|
18
|
-
|
|
19
|
-
module.exports.
|
|
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
|
|
30
|
-
const
|
|
31
|
-
module.exports.
|
|
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
|
|
34
|
-
|
|
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
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
|
|
139
|
-
.slice(
|
|
140
|
-
.
|
|
141
|
-
|
|
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
|
|
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)
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
}
|
|
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
|
|
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
|
|
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
|
-
|
|
420
|
-
let
|
|
421
|
-
|
|
422
|
-
while (
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
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.
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
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
|
-
|
|
538
|
-
|
|
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
|
-
*
|
|
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
|
|
616
|
+
* @returns {boolean} True iff the specified range is within.
|
|
594
617
|
*/
|
|
595
|
-
|
|
618
|
+
const withinAnyRange = (ranges, lineIndex, index, length) => (
|
|
596
619
|
!ranges.every((span) => (
|
|
597
620
|
(lineIndex !== span[0]) ||
|
|
598
|
-
(index
|
|
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.
|
|
739
|
+
for (const [ tokenLineIndex, tokenLine ] of lines.entries()) {
|
|
640
740
|
const inLine = [];
|
|
641
|
-
|
|
642
|
-
while ((linkMatch = linkRe.exec(tokenLine))) {
|
|
741
|
+
forEachLink(tokenLine, (index, match) => {
|
|
643
742
|
let markerMatch = null;
|
|
644
|
-
while ((markerMatch = emphasisMarkersRe.exec(
|
|
645
|
-
inLine.push(
|
|
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.
|
|
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 {
|
|
913
|
+
* @param {Object} [os] Node.js "os" module.
|
|
682
914
|
* @returns {string} Preferred line ending.
|
|
683
915
|
*/
|
|
684
|
-
function getPreferredLineEnding(input,
|
|
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
|
-
|
|
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
|
-
|
|
754
|
-
|
|
755
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
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
|
-
*
|
|
1124
|
+
* Expands a path with a tilde to an absolute path.
|
|
879
1125
|
*
|
|
880
|
-
* @param {
|
|
881
|
-
* @
|
|
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
|
|
884
|
-
const
|
|
885
|
-
|
|
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.
|
|
1134
|
+
module.exports.expandTildePath = expandTildePath;
|