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.
- package/CONTRIBUTING.md +2 -0
- package/LICENSE +1 -1
- package/README.md +125 -83
- package/demo/markdownlint-browser.js +1719 -1269
- package/doc/Rules.md +133 -15
- package/helpers/LICENSE +1 -1
- package/helpers/helpers.js +427 -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 -146
- 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 +3 -2
- package/lib/md033.js +2 -3
- package/lib/md034.js +2 -2
- package/lib/md035.js +7 -2
- package/lib/md036.js +2 -2
- 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 +52 -0
- package/lib/rules.js +6 -4
- package/package.json +36 -29
- package/schema/.markdownlint.jsonc +15 -2
- package/schema/.markdownlint.yaml +14 -1
- package/schema/markdownlint-config-schema.json +119 -737
- 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,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
|
|
30
|
-
const
|
|
31
|
-
|
|
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
|
|
34
|
-
|
|
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
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
|
|
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
|
-
}
|
|
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
|
|
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)
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
}
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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.
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
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
|
-
|
|
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);
|
|
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
|
-
*
|
|
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
|
|
612
|
+
* @returns {boolean} True iff the specified range is within.
|
|
594
613
|
*/
|
|
595
|
-
|
|
614
|
+
const withinAnyRange = (ranges, lineIndex, index, length) => (
|
|
596
615
|
!ranges.every((span) => (
|
|
597
616
|
(lineIndex !== span[0]) ||
|
|
598
|
-
(index
|
|
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.
|
|
735
|
+
for (const [ tokenLineIndex, tokenLine ] of lines.entries()) {
|
|
640
736
|
const inLine = [];
|
|
641
|
-
|
|
642
|
-
while ((linkMatch = linkRe.exec(tokenLine))) {
|
|
737
|
+
forEachLink(tokenLine, (index, match) => {
|
|
643
738
|
let markerMatch = null;
|
|
644
|
-
while ((markerMatch = emphasisMarkersRe.exec(
|
|
645
|
-
inLine.push(
|
|
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.
|
|
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 {
|
|
911
|
+
* @param {Object} [os] Node.js "os" module.
|
|
682
912
|
* @returns {string} Preferred line ending.
|
|
683
913
|
*/
|
|
684
|
-
function getPreferredLineEnding(input,
|
|
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
|
-
|
|
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
|
-
|
|
754
|
-
|
|
755
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
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
|
-
*
|
|
1122
|
+
* Expands a path with a tilde to an absolute path.
|
|
879
1123
|
*
|
|
880
|
-
* @param {
|
|
881
|
-
* @
|
|
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
|
|
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;
|
|
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.
|
|
1132
|
+
module.exports.expandTildePath = expandTildePath;
|