markdownlint 0.20.2 → 0.21.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 (64) hide show
  1. package/CONTRIBUTING.md +50 -47
  2. package/LICENSE +21 -21
  3. package/README.md +915 -871
  4. package/demo/markdownlint-browser.js +4693 -5400
  5. package/doc/CustomRules.md +315 -313
  6. package/doc/Rules.md +1868 -1795
  7. package/helpers/LICENSE +21 -21
  8. package/helpers/README.md +50 -50
  9. package/helpers/helpers.js +690 -671
  10. package/helpers/package.json +18 -18
  11. package/lib/cache.js +24 -24
  12. package/lib/markdownlint.d.ts +365 -352
  13. package/lib/markdownlint.js +1186 -1101
  14. package/lib/md001.js +22 -22
  15. package/lib/md002.js +22 -22
  16. package/lib/md003.js +42 -42
  17. package/lib/md004.js +43 -43
  18. package/lib/md005.js +69 -69
  19. package/lib/md006.js +34 -34
  20. package/lib/md007.js +47 -47
  21. package/lib/md009.js +77 -77
  22. package/lib/md010.js +38 -38
  23. package/lib/md011.js +42 -38
  24. package/lib/md012.js +32 -32
  25. package/lib/md013.js +99 -99
  26. package/lib/md014.js +53 -53
  27. package/lib/md018.js +34 -34
  28. package/lib/md019.js +39 -39
  29. package/lib/md020.js +62 -62
  30. package/lib/md021.js +60 -60
  31. package/lib/md022.js +56 -56
  32. package/lib/md023.js +38 -38
  33. package/lib/md024.js +38 -38
  34. package/lib/md025.js +32 -32
  35. package/lib/md026.js +40 -40
  36. package/lib/md027.js +56 -56
  37. package/lib/md028.js +38 -38
  38. package/lib/md029.js +66 -66
  39. package/lib/md030.js +50 -50
  40. package/lib/md031.js +40 -39
  41. package/lib/md032.js +50 -50
  42. package/lib/md033.js +45 -45
  43. package/lib/md034.js +61 -61
  44. package/lib/md035.js +21 -21
  45. package/lib/md036.js +56 -56
  46. package/lib/md037.js +150 -141
  47. package/lib/md038.js +68 -68
  48. package/lib/md039.js +64 -64
  49. package/lib/md040.js +18 -18
  50. package/lib/md041.js +31 -31
  51. package/lib/md042.js +42 -41
  52. package/lib/md043.js +45 -45
  53. package/lib/md044.js +94 -71
  54. package/lib/md045.js +18 -18
  55. package/lib/md046.js +32 -32
  56. package/lib/md047.js +28 -28
  57. package/lib/md048.js +29 -29
  58. package/lib/rules.js +60 -61
  59. package/package.json +74 -67
  60. package/schema/build-config-schema.js +409 -405
  61. package/schema/markdownlint-config-schema.json +7 -2
  62. package/style/all.json +5 -5
  63. package/style/cirosantilli.json +22 -22
  64. package/style/relaxed.json +13 -13
@@ -1,671 +1,690 @@
1
- // @ts-check
2
-
3
- "use strict";
4
-
5
- const os = require("os");
6
-
7
- // Regular expression for matching common newline characters
8
- // See NEWLINES_RE in markdown-it/lib/rules_core/normalize.js
9
- const newLineRe = /\r\n?|\n/g;
10
- module.exports.newLineRe = newLineRe;
11
-
12
- // Regular expression for matching common front matter (YAML and TOML)
13
- module.exports.frontMatterRe =
14
- // eslint-disable-next-line max-len
15
- /((^---\s*$[^]*?^---\s*$)|(^\+\+\+\s*$[^]*?^(\+\+\+|\.\.\.)\s*$))(\r\n|\r|\n|$)/m;
16
-
17
- // Regular expression for matching inline disable/enable comments
18
- const inlineCommentRe =
19
- // eslint-disable-next-line max-len
20
- /<!--\s*markdownlint-(?:(?:(disable|enable|capture|restore|disable-file|enable-file)((?:\s+[a-z0-9_-]+)*))|(?:(configure-file)\s+([\s\S]*?)))\s*-->/ig;
21
- module.exports.inlineCommentRe = inlineCommentRe;
22
-
23
- // Regular expressions for range matching
24
- module.exports.bareUrlRe = /(?:http|ftp)s?:\/\/[^\s\]"']*/ig;
25
- module.exports.listItemMarkerRe = /^([\s>]*)(?:[*+-]|\d+[.)])\s+/;
26
- module.exports.orderedListItemMarkerRe = /^[\s>]*0*(\d+)[.)]/;
27
-
28
- // Regular expression for emphasis markers
29
- const emphasisMarkersRe = /[_*]+/g;
30
-
31
- // readFile options for reading with the UTF-8 encoding
32
- module.exports.utf8Encoding = { "encoding": "utf8" };
33
-
34
- // All punctuation characters (normal and full-width)
35
- module.exports.allPunctuation = ".,;:!?。,;:!?";
36
-
37
- // Returns true iff the input is a number
38
- module.exports.isNumber = function isNumber(obj) {
39
- return typeof obj === "number";
40
- };
41
-
42
- // Returns true iff the input is a string
43
- module.exports.isString = function isString(obj) {
44
- return typeof obj === "string";
45
- };
46
-
47
- // Returns true iff the input string is empty
48
- module.exports.isEmptyString = function isEmptyString(str) {
49
- return str.length === 0;
50
- };
51
-
52
- // Returns true iff the input is an object
53
- module.exports.isObject = function isObject(obj) {
54
- return (obj !== null) && (typeof obj === "object") && !Array.isArray(obj);
55
- };
56
-
57
- // Returns true iff the input line is blank (no content)
58
- // Example: Contains nothing, whitespace, or comments
59
- const blankLineRe = />|(?:<!--.*?-->)/g;
60
- module.exports.isBlankLine = function isBlankLine(line) {
61
- return !line || !line.trim() || !line.replace(blankLineRe, "").trim();
62
- };
63
-
64
- /**
65
- * Compare function for Array.prototype.sort for ascending order of numbers.
66
- *
67
- * @param {number} a First number.
68
- * @param {number} b Second number.
69
- * @returns {number} Positive value if a>b, negative value if b<a, 0 otherwise.
70
- */
71
- module.exports.numericSortAscending = function numericSortAscending(a, b) {
72
- return a - b;
73
- };
74
-
75
- // Returns true iff the sorted array contains the specified element
76
- module.exports.includesSorted = function includesSorted(array, element) {
77
- let left = 0;
78
- let right = array.length - 1;
79
- while (left <= right) {
80
- /* eslint-disable no-bitwise */
81
- const mid = (left + right) >> 1;
82
- if (array[mid] < element) {
83
- left = mid + 1;
84
- } else if (array[mid] > element) {
85
- right = mid - 1;
86
- } else {
87
- return true;
88
- }
89
- }
90
- return false;
91
- };
92
-
93
- // Replaces the text of all properly-formatted HTML comments with whitespace
94
- // This preserves the line/column information for the rest of the document
95
- // Trailing whitespace is avoided with a '\' character in the last column
96
- // See https://www.w3.org/TR/html5/syntax.html#comments for details
97
- const htmlCommentBegin = "<!--";
98
- const htmlCommentEnd = "-->";
99
- module.exports.clearHtmlCommentText = function clearHtmlCommentText(text) {
100
- let i = 0;
101
- while ((i = text.indexOf(htmlCommentBegin, i)) !== -1) {
102
- const j = text.indexOf(htmlCommentEnd, i);
103
- if (j === -1) {
104
- // Un-terminated comments are treated as text
105
- break;
106
- }
107
- const comment = text.slice(i + htmlCommentBegin.length, j);
108
- if ((comment.length > 0) &&
109
- (comment[0] !== ">") &&
110
- (comment[comment.length - 1] !== "-") &&
111
- !comment.includes("--") &&
112
- (text.slice(i, j + htmlCommentEnd.length)
113
- .search(inlineCommentRe) === -1)) {
114
- const blanks = comment
115
- .replace(/[^\r\n]/g, " ")
116
- .replace(/ ([\r\n])/g, "\\$1");
117
- text = text.slice(0, i + htmlCommentBegin.length) +
118
- blanks + text.slice(j);
119
- }
120
- i = j + htmlCommentEnd.length;
121
- }
122
- return text;
123
- };
124
-
125
- // Escapes a string for use in a RegExp
126
- module.exports.escapeForRegExp = function escapeForRegExp(str) {
127
- return str.replace(/[-/\\^$*+?.()|[\]{}]/g, "\\$&");
128
- };
129
-
130
- // Un-escapes Markdown content (simple algorithm; not a parser)
131
- const escapedMarkdownRe = /\\./g;
132
- module.exports.unescapeMarkdown =
133
- function unescapeMarkdown(markdown, replacement) {
134
- return markdown.replace(escapedMarkdownRe, (match) => {
135
- const char = match[1];
136
- if ("!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~".includes(char)) {
137
- return replacement || char;
138
- }
139
- return match;
140
- });
141
- };
142
-
143
- /**
144
- * Return the string representation of a fence markup character.
145
- *
146
- * @param {string} markup Fence string.
147
- * @returns {string} String representation.
148
- */
149
- module.exports.fencedCodeBlockStyleFor =
150
- function fencedCodeBlockStyleFor(markup) {
151
- switch (markup[0]) {
152
- case "~":
153
- return "tilde";
154
- default:
155
- return "backtick";
156
- }
157
- };
158
-
159
- /**
160
- * Return the number of characters of indent for a token.
161
- *
162
- * @param {Object} token MarkdownItToken instance.
163
- * @returns {number} Characters of indent.
164
- */
165
- function indentFor(token) {
166
- const line = token.line.replace(/^[\s>]*(> |>)/, "");
167
- return line.length - line.trimLeft().length;
168
- }
169
- module.exports.indentFor = indentFor;
170
-
171
- // Returns the heading style for a heading token
172
- module.exports.headingStyleFor = function headingStyleFor(token) {
173
- if ((token.map[1] - token.map[0]) === 1) {
174
- if (/[^\\]#\s*$/.test(token.line)) {
175
- return "atx_closed";
176
- }
177
- return "atx";
178
- }
179
- return "setext";
180
- };
181
-
182
- /**
183
- * Return the string representation of an unordered list marker.
184
- *
185
- * @param {Object} token MarkdownItToken instance.
186
- * @returns {string} String representation.
187
- */
188
- module.exports.unorderedListStyleFor = function unorderedListStyleFor(token) {
189
- switch (token.markup) {
190
- case "-":
191
- return "dash";
192
- case "+":
193
- return "plus";
194
- // case "*":
195
- default:
196
- return "asterisk";
197
- }
198
- };
199
-
200
- /**
201
- * Calls the provided function for each matching token.
202
- *
203
- * @param {Object} params RuleParams instance.
204
- * @param {string} type Token type identifier.
205
- * @param {Function} handler Callback function.
206
- * @returns {void}
207
- */
208
- function filterTokens(params, type, handler) {
209
- params.tokens.forEach(function forToken(token) {
210
- if (token.type === type) {
211
- handler(token);
212
- }
213
- });
214
- }
215
- module.exports.filterTokens = filterTokens;
216
-
217
- // Get line metadata array
218
- module.exports.getLineMetadata = function getLineMetadata(params) {
219
- const lineMetadata = params.lines.map(function mapLine(line, index) {
220
- return [ line, index, false, 0, false, false ];
221
- });
222
- filterTokens(params, "fence", function forToken(token) {
223
- lineMetadata[token.map[0]][3] = 1;
224
- lineMetadata[token.map[1] - 1][3] = -1;
225
- for (let i = token.map[0] + 1; i < token.map[1] - 1; i++) {
226
- lineMetadata[i][2] = true;
227
- }
228
- });
229
- filterTokens(params, "code_block", function forToken(token) {
230
- for (let i = token.map[0]; i < token.map[1]; i++) {
231
- lineMetadata[i][2] = true;
232
- }
233
- });
234
- filterTokens(params, "table_open", function forToken(token) {
235
- for (let i = token.map[0]; i < token.map[1]; i++) {
236
- lineMetadata[i][4] = true;
237
- }
238
- });
239
- filterTokens(params, "list_item_open", function forToken(token) {
240
- let count = 1;
241
- for (let i = token.map[0]; i < token.map[1]; i++) {
242
- lineMetadata[i][5] = count;
243
- count++;
244
- }
245
- });
246
- filterTokens(params, "hr", function forToken(token) {
247
- lineMetadata[token.map[0]][6] = true;
248
- });
249
- return lineMetadata;
250
- };
251
-
252
- // Calls the provided function for each line (with context)
253
- module.exports.forEachLine = function forEachLine(lineMetadata, handler) {
254
- lineMetadata.forEach(function forMetadata(metadata) {
255
- // Parameters: line, lineIndex, inCode, onFence, inTable, inBreak
256
- handler(...metadata);
257
- });
258
- };
259
-
260
- // Returns (nested) lists as a flat array (in order)
261
- module.exports.flattenLists = function flattenLists(params) {
262
- const flattenedLists = [];
263
- const stack = [];
264
- let current = null;
265
- let nesting = 0;
266
- const nestingStack = [];
267
- let lastWithMap = { "map": [ 0, 1 ] };
268
- params.tokens.forEach(function forToken(token) {
269
- if ((token.type === "bullet_list_open") ||
270
- (token.type === "ordered_list_open")) {
271
- // Save current context and start a new one
272
- stack.push(current);
273
- current = {
274
- "unordered": (token.type === "bullet_list_open"),
275
- "parentsUnordered": !current ||
276
- (current.unordered && current.parentsUnordered),
277
- "open": token,
278
- "indent": indentFor(token),
279
- "parentIndent": (current && current.indent) || 0,
280
- "items": [],
281
- "nesting": nesting,
282
- "lastLineIndex": -1,
283
- "insert": flattenedLists.length
284
- };
285
- nesting++;
286
- } else if ((token.type === "bullet_list_close") ||
287
- (token.type === "ordered_list_close")) {
288
- // Finalize current context and restore previous
289
- current.lastLineIndex = lastWithMap.map[1];
290
- flattenedLists.splice(current.insert, 0, current);
291
- delete current.insert;
292
- current = stack.pop();
293
- nesting--;
294
- } else if (token.type === "list_item_open") {
295
- // Add list item
296
- current.items.push(token);
297
- } else if (token.type === "blockquote_open") {
298
- nestingStack.push(nesting);
299
- nesting = 0;
300
- } else if (token.type === "blockquote_close") {
301
- nesting = nestingStack.pop();
302
- } else if (token.map) {
303
- // Track last token with map
304
- lastWithMap = token;
305
- }
306
- });
307
- return flattenedLists;
308
- };
309
-
310
- // Calls the provided function for each specified inline child token
311
- module.exports.forEachInlineChild =
312
- function forEachInlineChild(params, type, handler) {
313
- filterTokens(params, "inline", function forToken(token) {
314
- token.children.forEach(function forChild(child) {
315
- if (child.type === type) {
316
- handler(child, token);
317
- }
318
- });
319
- });
320
- };
321
-
322
- // Calls the provided function for each heading's content
323
- module.exports.forEachHeading = function forEachHeading(params, handler) {
324
- let heading = null;
325
- params.tokens.forEach(function forToken(token) {
326
- if (token.type === "heading_open") {
327
- heading = token;
328
- } else if (token.type === "heading_close") {
329
- heading = null;
330
- } else if ((token.type === "inline") && heading) {
331
- handler(heading, token.content);
332
- }
333
- });
334
- };
335
-
336
- /**
337
- * Calls the provided function for each inline code span's content.
338
- *
339
- * @param {string} input Markdown content.
340
- * @param {Function} handler Callback function.
341
- * @returns {void}
342
- */
343
- function forEachInlineCodeSpan(input, handler) {
344
- let currentLine = 0;
345
- let currentColumn = 0;
346
- let index = 0;
347
- while (index < input.length) {
348
- let startIndex = -1;
349
- let startLine = -1;
350
- let startColumn = -1;
351
- let tickCount = 0;
352
- let currentTicks = 0;
353
- let state = "normal";
354
- // Deliberate <= so trailing 0 completes the last span (ex: "text `code`")
355
- for (; index <= input.length; index++) {
356
- const char = input[index];
357
- // Ignore backticks in link destination
358
- if ((char === "[") && (state === "normal")) {
359
- state = "linkTextOpen";
360
- } else if ((char === "]") && (state === "linkTextOpen")) {
361
- state = "linkTextClosed";
362
- } else if ((char === "(") && (state === "linkTextClosed")) {
363
- state = "linkDestinationOpen";
364
- } else if (
365
- ((char === "(") && (state === "linkDestinationOpen")) ||
366
- ((char === ")") && (state === "linkDestinationOpen")) ||
367
- (state === "linkTextClosed")) {
368
- state = "normal";
369
- }
370
- // Parse backtick open/close
371
- if ((char === "`") && (state !== "linkDestinationOpen")) {
372
- // Count backticks at start or end of code span
373
- currentTicks++;
374
- if ((startIndex === -1) || (startColumn === -1)) {
375
- startIndex = index + 1;
376
- }
377
- } else {
378
- if ((startIndex >= 0) &&
379
- (startColumn >= 0) &&
380
- (tickCount === currentTicks)) {
381
- // Found end backticks; invoke callback for code span
382
- handler(
383
- input.substring(startIndex, index - currentTicks),
384
- startLine, startColumn, tickCount);
385
- startIndex = -1;
386
- startColumn = -1;
387
- } else if ((startIndex >= 0) && (startColumn === -1)) {
388
- // Found start backticks
389
- tickCount = currentTicks;
390
- startLine = currentLine;
391
- startColumn = currentColumn;
392
- }
393
- // Not in backticks
394
- currentTicks = 0;
395
- }
396
- if (char === "\n") {
397
- // On next line
398
- currentLine++;
399
- currentColumn = 0;
400
- } else if ((char === "\\") &&
401
- ((startIndex === -1) || (startColumn === -1)) &&
402
- (input[index + 1] !== "\n")) {
403
- // Escape character outside code, skip next
404
- index++;
405
- currentColumn += 2;
406
- } else {
407
- // On next column
408
- currentColumn++;
409
- }
410
- }
411
- if (startIndex >= 0) {
412
- // Restart loop after unmatched start backticks (ex: "`text``code``")
413
- index = startIndex;
414
- currentLine = startLine;
415
- currentColumn = startColumn;
416
- }
417
- }
418
- }
419
- module.exports.forEachInlineCodeSpan = forEachInlineCodeSpan;
420
-
421
- /**
422
- * Adds a generic error object via the onError callback.
423
- *
424
- * @param {Object} onError RuleOnError instance.
425
- * @param {number} lineNumber Line number.
426
- * @param {string} [detail] Error details.
427
- * @param {string} [context] Error context.
428
- * @param {number[]} [range] Column and length of error.
429
- * @param {Object} [fixInfo] RuleOnErrorFixInfo instance.
430
- * @returns {void}
431
- */
432
- function addError(onError, lineNumber, detail, context, range, fixInfo) {
433
- onError({
434
- lineNumber,
435
- detail,
436
- context,
437
- range,
438
- fixInfo
439
- });
440
- }
441
- module.exports.addError = addError;
442
-
443
- // Adds an error object with details conditionally via the onError callback
444
- module.exports.addErrorDetailIf = function addErrorDetailIf(
445
- onError, lineNumber, expected, actual, detail, context, range, fixInfo) {
446
- if (expected !== actual) {
447
- addError(
448
- onError,
449
- lineNumber,
450
- "Expected: " + expected + "; Actual: " + actual +
451
- (detail ? "; " + detail : ""),
452
- context,
453
- range,
454
- fixInfo);
455
- }
456
- };
457
-
458
- // Adds an error object with context via the onError callback
459
- module.exports.addErrorContext = function addErrorContext(
460
- onError, lineNumber, context, left, right, range, fixInfo) {
461
- if (context.length <= 30) {
462
- // Nothing to do
463
- } else if (left && right) {
464
- context = context.substr(0, 15) + "..." + context.substr(-15);
465
- } else if (right) {
466
- context = "..." + context.substr(-30);
467
- } else {
468
- context = context.substr(0, 30) + "...";
469
- }
470
- addError(onError, lineNumber, null, context, range, fixInfo);
471
- };
472
-
473
- // Returns a range object for a line by applying a RegExp
474
- module.exports.rangeFromRegExp = function rangeFromRegExp(line, regexp) {
475
- let range = null;
476
- const match = line.match(regexp);
477
- if (match) {
478
- const column = match.index + 1;
479
- const length = match[0].length;
480
- range = [ column, length ];
481
- }
482
- return range;
483
- };
484
-
485
- // Determines if the front matter includes a title
486
- module.exports.frontMatterHasTitle =
487
- function frontMatterHasTitle(frontMatterLines, frontMatterTitlePattern) {
488
- const ignoreFrontMatter =
489
- (frontMatterTitlePattern !== undefined) && !frontMatterTitlePattern;
490
- const frontMatterTitleRe =
491
- new RegExp(String(frontMatterTitlePattern || "^\\s*title\\s*[:=]"), "i");
492
- return !ignoreFrontMatter &&
493
- frontMatterLines.some((line) => frontMatterTitleRe.test(line));
494
- };
495
-
496
- /**
497
- * Returns a list of emphasis markers in code spans.
498
- *
499
- * @param {Object} params RuleParams instance.
500
- * @returns {number[][]} List of markers.
501
- */
502
- function emphasisMarkersInCodeSpans(params) {
503
- const { lines } = params;
504
- const byLine = new Array(lines.length);
505
- filterTokens(params, "inline", (token) => {
506
- const { children, lineNumber, map } = token;
507
- if (children.some((child) => child.type === "code_inline")) {
508
- const tokenLines = lines.slice(map[0], map[1]);
509
- forEachInlineCodeSpan(
510
- tokenLines.join("\n"),
511
- (code, lineIndex, column, tickCount) => {
512
- const codeLines = code.split(newLineRe);
513
- codeLines.forEach((codeLine, codeLineIndex) => {
514
- let match = null;
515
- while ((match = emphasisMarkersRe.exec(codeLine))) {
516
- const byLineIndex = lineNumber - 1 + lineIndex + codeLineIndex;
517
- const inLine = byLine[byLineIndex] || [];
518
- const codeLineOffset = codeLineIndex ? 0 : column - 1 + tickCount;
519
- inLine.push(codeLineOffset + match.index);
520
- byLine[byLineIndex] = inLine;
521
- }
522
- });
523
- }
524
- );
525
- }
526
- });
527
- return byLine;
528
- }
529
- module.exports.emphasisMarkersInCodeSpans = emphasisMarkersInCodeSpans;
530
-
531
- /**
532
- * Gets the most common line ending, falling back to the platform default.
533
- *
534
- * @param {string} input Markdown content to analyze.
535
- * @returns {string} Preferred line ending.
536
- */
537
- function getPreferredLineEnding(input) {
538
- let cr = 0;
539
- let lf = 0;
540
- let crlf = 0;
541
- const endings = input.match(newLineRe) || [];
542
- endings.forEach((ending) => {
543
- // eslint-disable-next-line default-case
544
- switch (ending) {
545
- case "\r":
546
- cr++;
547
- break;
548
- case "\n":
549
- lf++;
550
- break;
551
- case "\r\n":
552
- crlf++;
553
- break;
554
- }
555
- });
556
- let preferredLineEnding = null;
557
- if (!cr && !lf && !crlf) {
558
- preferredLineEnding = os.EOL;
559
- } else if ((lf >= crlf) && (lf >= cr)) {
560
- preferredLineEnding = "\n";
561
- } else if (crlf >= cr) {
562
- preferredLineEnding = "\r\n";
563
- } else {
564
- preferredLineEnding = "\r";
565
- }
566
- return preferredLineEnding;
567
- }
568
- module.exports.getPreferredLineEnding = getPreferredLineEnding;
569
-
570
- /**
571
- * Normalizes the fields of a RuleOnErrorFixInfo instance.
572
- *
573
- * @param {Object} fixInfo RuleOnErrorFixInfo instance.
574
- * @param {number} [lineNumber] Line number.
575
- * @returns {Object} Normalized RuleOnErrorFixInfo instance.
576
- */
577
- function normalizeFixInfo(fixInfo, lineNumber) {
578
- return {
579
- "lineNumber": fixInfo.lineNumber || lineNumber,
580
- "editColumn": fixInfo.editColumn || 1,
581
- "deleteCount": fixInfo.deleteCount || 0,
582
- "insertText": fixInfo.insertText || ""
583
- };
584
- }
585
-
586
- /**
587
- * Fixes the specified error on a line of Markdown content.
588
- *
589
- * @param {string} line Line of Markdown content.
590
- * @param {Object} fixInfo RuleOnErrorFixInfo instance.
591
- * @param {string} lineEnding Line ending to use.
592
- * @returns {string} Fixed content.
593
- */
594
- function applyFix(line, fixInfo, lineEnding) {
595
- const { editColumn, deleteCount, insertText } = normalizeFixInfo(fixInfo);
596
- const editIndex = editColumn - 1;
597
- return (deleteCount === -1) ?
598
- null :
599
- line.slice(0, editIndex) +
600
- insertText.replace(/\n/g, lineEnding || "\n") +
601
- line.slice(editIndex + deleteCount);
602
- }
603
- module.exports.applyFix = applyFix;
604
-
605
- // Applies as many fixes as possible to the input lines
606
- module.exports.applyFixes = function applyFixes(input, errors) {
607
- const lineEnding = getPreferredLineEnding(input);
608
- const lines = input.split(newLineRe);
609
- // Normalize fixInfo objects
610
- let fixInfos = errors
611
- .filter((error) => error.fixInfo)
612
- .map((error) => normalizeFixInfo(error.fixInfo, error.lineNumber));
613
- // Sort bottom-to-top, line-deletes last, right-to-left, long-to-short
614
- fixInfos.sort((a, b) => {
615
- const aDeletingLine = (a.deleteCount === -1);
616
- const bDeletingLine = (b.deleteCount === -1);
617
- return (
618
- (b.lineNumber - a.lineNumber) ||
619
- (aDeletingLine ? 1 : (bDeletingLine ? -1 : 0)) ||
620
- (b.editColumn - a.editColumn) ||
621
- (b.insertText.length - a.insertText.length)
622
- );
623
- });
624
- // Remove duplicate entries (needed for following collapse step)
625
- let lastFixInfo = {};
626
- fixInfos = fixInfos.filter((fixInfo) => {
627
- const unique = (
628
- (fixInfo.lineNumber !== lastFixInfo.lineNumber) ||
629
- (fixInfo.editColumn !== lastFixInfo.editColumn) ||
630
- (fixInfo.deleteCount !== lastFixInfo.deleteCount) ||
631
- (fixInfo.insertText !== lastFixInfo.insertText)
632
- );
633
- lastFixInfo = fixInfo;
634
- return unique;
635
- });
636
- // Collapse insert/no-delete and no-insert/delete for same line/column
637
- lastFixInfo = {};
638
- fixInfos.forEach((fixInfo) => {
639
- if (
640
- (fixInfo.lineNumber === lastFixInfo.lineNumber) &&
641
- (fixInfo.editColumn === lastFixInfo.editColumn) &&
642
- !fixInfo.insertText &&
643
- (fixInfo.deleteCount > 0) &&
644
- lastFixInfo.insertText &&
645
- !lastFixInfo.deleteCount) {
646
- fixInfo.insertText = lastFixInfo.insertText;
647
- lastFixInfo.lineNumber = 0;
648
- }
649
- lastFixInfo = fixInfo;
650
- });
651
- fixInfos = fixInfos.filter((fixInfo) => fixInfo.lineNumber);
652
- // Apply all (remaining/updated) fixes
653
- let lastLineIndex = -1;
654
- let lastEditIndex = -1;
655
- fixInfos.forEach((fixInfo) => {
656
- const { lineNumber, editColumn, deleteCount } = fixInfo;
657
- const lineIndex = lineNumber - 1;
658
- const editIndex = editColumn - 1;
659
- if (
660
- (lineIndex !== lastLineIndex) ||
661
- ((editIndex + deleteCount) < lastEditIndex) ||
662
- (deleteCount === -1)
663
- ) {
664
- lines[lineIndex] = applyFix(lines[lineIndex], fixInfo, lineEnding);
665
- }
666
- lastLineIndex = lineIndex;
667
- lastEditIndex = editIndex;
668
- });
669
- // Return corrected input
670
- return lines.filter((line) => line !== null).join(lineEnding);
671
- };
1
+ // @ts-check
2
+
3
+ "use strict";
4
+
5
+ const os = require("os");
6
+
7
+ // Regular expression for matching common newline characters
8
+ // See NEWLINES_RE in markdown-it/lib/rules_core/normalize.js
9
+ const newLineRe = /\r\n?|\n/g;
10
+ module.exports.newLineRe = newLineRe;
11
+
12
+ // Regular expression for matching common front matter (YAML and TOML)
13
+ module.exports.frontMatterRe =
14
+ // eslint-disable-next-line max-len
15
+ /((^---\s*$[^]*?^---\s*$)|(^\+\+\+\s*$[^]*?^(\+\+\+|\.\.\.)\s*$))(\r\n|\r|\n|$)/m;
16
+
17
+ // Regular expression for matching inline disable/enable comments
18
+ const inlineCommentRe =
19
+ // eslint-disable-next-line max-len
20
+ /<!--\s*markdownlint-(?:(?:(disable|enable|capture|restore|disable-file|enable-file)((?:\s+[a-z0-9_-]+)*))|(?:(configure-file)\s+([\s\S]*?)))\s*-->/ig;
21
+ module.exports.inlineCommentRe = inlineCommentRe;
22
+
23
+ // Regular expressions for range matching
24
+ module.exports.bareUrlRe = /(?:http|ftp)s?:\/\/[^\s\]"']*(?:\/|[^\s\]"'\W])/ig;
25
+ module.exports.listItemMarkerRe = /^([\s>]*)(?:[*+-]|\d+[.)])\s+/;
26
+ module.exports.orderedListItemMarkerRe = /^[\s>]*0*(\d+)[.)]/;
27
+
28
+ // Regular expression for all instances of emphasis markers
29
+ const emphasisMarkersRe = /[_*]/g;
30
+
31
+ // Regular expression for inline links and shortcut reference links
32
+ const linkRe = /\[(?:[^[\]]|\[[^\]]*\])*\](?:\(\S*\))?/g;
33
+
34
+ // readFile options for reading with the UTF-8 encoding
35
+ module.exports.utf8Encoding = "utf8";
36
+
37
+ // All punctuation characters (normal and full-width)
38
+ module.exports.allPunctuation = ".,;:!?。,;:!?";
39
+
40
+ // Returns true iff the input is a number
41
+ module.exports.isNumber = function isNumber(obj) {
42
+ return typeof obj === "number";
43
+ };
44
+
45
+ // Returns true iff the input is a string
46
+ module.exports.isString = function isString(obj) {
47
+ return typeof obj === "string";
48
+ };
49
+
50
+ // Returns true iff the input string is empty
51
+ module.exports.isEmptyString = function isEmptyString(str) {
52
+ return str.length === 0;
53
+ };
54
+
55
+ // Returns true iff the input is an object
56
+ module.exports.isObject = function isObject(obj) {
57
+ return (obj !== null) && (typeof obj === "object") && !Array.isArray(obj);
58
+ };
59
+
60
+ // Returns true iff the input line is blank (no content)
61
+ // Example: Contains nothing, whitespace, or comments
62
+ const blankLineRe = />|(?:<!--.*?-->)/g;
63
+ module.exports.isBlankLine = function isBlankLine(line) {
64
+ // Call to String.replace follows best practices and is not a security check
65
+ // False-positive for js/incomplete-multi-character-sanitization
66
+ return !line || !line.trim() || !line.replace(blankLineRe, "").trim();
67
+ };
68
+
69
+ /**
70
+ * Compare function for Array.prototype.sort for ascending order of numbers.
71
+ *
72
+ * @param {number} a First number.
73
+ * @param {number} b Second number.
74
+ * @returns {number} Positive value if a>b, negative value if b<a, 0 otherwise.
75
+ */
76
+ module.exports.numericSortAscending = function numericSortAscending(a, b) {
77
+ return a - b;
78
+ };
79
+
80
+ // Returns true iff the sorted array contains the specified element
81
+ module.exports.includesSorted = function includesSorted(array, element) {
82
+ let left = 0;
83
+ let right = array.length - 1;
84
+ while (left <= right) {
85
+ // eslint-disable-next-line no-bitwise
86
+ const mid = (left + right) >> 1;
87
+ if (array[mid] < element) {
88
+ left = mid + 1;
89
+ } else if (array[mid] > element) {
90
+ right = mid - 1;
91
+ } else {
92
+ return true;
93
+ }
94
+ }
95
+ return false;
96
+ };
97
+
98
+ // Replaces the text of all properly-formatted HTML comments with whitespace
99
+ // This preserves the line/column information for the rest of the document
100
+ // Trailing whitespace is avoided with a '\' character in the last column
101
+ // See https://www.w3.org/TR/html5/syntax.html#comments for details
102
+ const htmlCommentBegin = "<!--";
103
+ const htmlCommentEnd = "-->";
104
+ module.exports.clearHtmlCommentText = function clearHtmlCommentText(text) {
105
+ let i = 0;
106
+ while ((i = text.indexOf(htmlCommentBegin, i)) !== -1) {
107
+ const j = text.indexOf(htmlCommentEnd, i);
108
+ if (j === -1) {
109
+ // Un-terminated comments are treated as text
110
+ break;
111
+ }
112
+ const comment = text.slice(i + htmlCommentBegin.length, j);
113
+ if ((comment.length > 0) &&
114
+ (comment[0] !== ">") &&
115
+ (comment[comment.length - 1] !== "-") &&
116
+ !comment.includes("--") &&
117
+ (text.slice(i, j + htmlCommentEnd.length)
118
+ .search(inlineCommentRe) === -1)) {
119
+ const blanks = comment
120
+ .replace(/[^\r\n]/g, " ")
121
+ .replace(/ ([\r\n])/g, "\\$1");
122
+ text = text.slice(0, i + htmlCommentBegin.length) +
123
+ blanks + text.slice(j);
124
+ }
125
+ i = j + htmlCommentEnd.length;
126
+ }
127
+ return text;
128
+ };
129
+
130
+ // Escapes a string for use in a RegExp
131
+ module.exports.escapeForRegExp = function escapeForRegExp(str) {
132
+ return str.replace(/[-/\\^$*+?.()|[\]{}]/g, "\\$&");
133
+ };
134
+
135
+ // Un-escapes Markdown content (simple algorithm; not a parser)
136
+ const escapedMarkdownRe = /\\./g;
137
+ module.exports.unescapeMarkdown =
138
+ function unescapeMarkdown(markdown, replacement) {
139
+ return markdown.replace(escapedMarkdownRe, (match) => {
140
+ const char = match[1];
141
+ if ("!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~".includes(char)) {
142
+ return replacement || char;
143
+ }
144
+ return match;
145
+ });
146
+ };
147
+
148
+ /**
149
+ * Return the string representation of a fence markup character.
150
+ *
151
+ * @param {string} markup Fence string.
152
+ * @returns {string} String representation.
153
+ */
154
+ module.exports.fencedCodeBlockStyleFor =
155
+ function fencedCodeBlockStyleFor(markup) {
156
+ switch (markup[0]) {
157
+ case "~":
158
+ return "tilde";
159
+ default:
160
+ return "backtick";
161
+ }
162
+ };
163
+
164
+ /**
165
+ * Return the number of characters of indent for a token.
166
+ *
167
+ * @param {Object} token MarkdownItToken instance.
168
+ * @returns {number} Characters of indent.
169
+ */
170
+ function indentFor(token) {
171
+ const line = token.line.replace(/^[\s>]*(> |>)/, "");
172
+ return line.length - line.trimLeft().length;
173
+ }
174
+ module.exports.indentFor = indentFor;
175
+
176
+ // Returns the heading style for a heading token
177
+ module.exports.headingStyleFor = function headingStyleFor(token) {
178
+ if ((token.map[1] - token.map[0]) === 1) {
179
+ if (/[^\\]#\s*$/.test(token.line)) {
180
+ return "atx_closed";
181
+ }
182
+ return "atx";
183
+ }
184
+ return "setext";
185
+ };
186
+
187
+ /**
188
+ * Return the string representation of an unordered list marker.
189
+ *
190
+ * @param {Object} token MarkdownItToken instance.
191
+ * @returns {string} String representation.
192
+ */
193
+ module.exports.unorderedListStyleFor = function unorderedListStyleFor(token) {
194
+ switch (token.markup) {
195
+ case "-":
196
+ return "dash";
197
+ case "+":
198
+ return "plus";
199
+ // case "*":
200
+ default:
201
+ return "asterisk";
202
+ }
203
+ };
204
+
205
+ /**
206
+ * Calls the provided function for each matching token.
207
+ *
208
+ * @param {Object} params RuleParams instance.
209
+ * @param {string} type Token type identifier.
210
+ * @param {Function} handler Callback function.
211
+ * @returns {void}
212
+ */
213
+ function filterTokens(params, type, handler) {
214
+ params.tokens.forEach(function forToken(token) {
215
+ if (token.type === type) {
216
+ handler(token);
217
+ }
218
+ });
219
+ }
220
+ module.exports.filterTokens = filterTokens;
221
+
222
+ // Get line metadata array
223
+ module.exports.getLineMetadata = function getLineMetadata(params) {
224
+ const lineMetadata = params.lines.map(function mapLine(line, index) {
225
+ return [ line, index, false, 0, false, false ];
226
+ });
227
+ filterTokens(params, "fence", function forToken(token) {
228
+ lineMetadata[token.map[0]][3] = 1;
229
+ lineMetadata[token.map[1] - 1][3] = -1;
230
+ for (let i = token.map[0] + 1; i < token.map[1] - 1; i++) {
231
+ lineMetadata[i][2] = true;
232
+ }
233
+ });
234
+ filterTokens(params, "code_block", function forToken(token) {
235
+ for (let i = token.map[0]; i < token.map[1]; i++) {
236
+ lineMetadata[i][2] = true;
237
+ }
238
+ });
239
+ filterTokens(params, "table_open", function forToken(token) {
240
+ for (let i = token.map[0]; i < token.map[1]; i++) {
241
+ lineMetadata[i][4] = true;
242
+ }
243
+ });
244
+ filterTokens(params, "list_item_open", function forToken(token) {
245
+ let count = 1;
246
+ for (let i = token.map[0]; i < token.map[1]; i++) {
247
+ lineMetadata[i][5] = count;
248
+ count++;
249
+ }
250
+ });
251
+ filterTokens(params, "hr", function forToken(token) {
252
+ lineMetadata[token.map[0]][6] = true;
253
+ });
254
+ return lineMetadata;
255
+ };
256
+
257
+ // Calls the provided function for each line (with context)
258
+ module.exports.forEachLine = function forEachLine(lineMetadata, handler) {
259
+ lineMetadata.forEach(function forMetadata(metadata) {
260
+ // Parameters: line, lineIndex, inCode, onFence, inTable, inItem, inBreak
261
+ handler(...metadata);
262
+ });
263
+ };
264
+
265
+ // Returns (nested) lists as a flat array (in order)
266
+ module.exports.flattenLists = function flattenLists(params) {
267
+ const flattenedLists = [];
268
+ const stack = [];
269
+ let current = null;
270
+ let nesting = 0;
271
+ const nestingStack = [];
272
+ let lastWithMap = { "map": [ 0, 1 ] };
273
+ params.tokens.forEach(function forToken(token) {
274
+ if ((token.type === "bullet_list_open") ||
275
+ (token.type === "ordered_list_open")) {
276
+ // Save current context and start a new one
277
+ stack.push(current);
278
+ current = {
279
+ "unordered": (token.type === "bullet_list_open"),
280
+ "parentsUnordered": !current ||
281
+ (current.unordered && current.parentsUnordered),
282
+ "open": token,
283
+ "indent": indentFor(token),
284
+ "parentIndent": (current && current.indent) || 0,
285
+ "items": [],
286
+ "nesting": nesting,
287
+ "lastLineIndex": -1,
288
+ "insert": flattenedLists.length
289
+ };
290
+ nesting++;
291
+ } else if ((token.type === "bullet_list_close") ||
292
+ (token.type === "ordered_list_close")) {
293
+ // Finalize current context and restore previous
294
+ current.lastLineIndex = lastWithMap.map[1];
295
+ flattenedLists.splice(current.insert, 0, current);
296
+ delete current.insert;
297
+ current = stack.pop();
298
+ nesting--;
299
+ } else if (token.type === "list_item_open") {
300
+ // Add list item
301
+ current.items.push(token);
302
+ } else if (token.type === "blockquote_open") {
303
+ nestingStack.push(nesting);
304
+ nesting = 0;
305
+ } else if (token.type === "blockquote_close") {
306
+ nesting = nestingStack.pop();
307
+ } else if (token.map) {
308
+ // Track last token with map
309
+ lastWithMap = token;
310
+ }
311
+ });
312
+ return flattenedLists;
313
+ };
314
+
315
+ // Calls the provided function for each specified inline child token
316
+ module.exports.forEachInlineChild =
317
+ function forEachInlineChild(params, type, handler) {
318
+ filterTokens(params, "inline", function forToken(token) {
319
+ token.children.forEach(function forChild(child) {
320
+ if (child.type === type) {
321
+ handler(child, token);
322
+ }
323
+ });
324
+ });
325
+ };
326
+
327
+ // Calls the provided function for each heading's content
328
+ module.exports.forEachHeading = function forEachHeading(params, handler) {
329
+ let heading = null;
330
+ params.tokens.forEach(function forToken(token) {
331
+ if (token.type === "heading_open") {
332
+ heading = token;
333
+ } else if (token.type === "heading_close") {
334
+ heading = null;
335
+ } else if ((token.type === "inline") && heading) {
336
+ handler(heading, token.content);
337
+ }
338
+ });
339
+ };
340
+
341
+ /**
342
+ * Calls the provided function for each inline code span's content.
343
+ *
344
+ * @param {string} input Markdown content.
345
+ * @param {Function} handler Callback function.
346
+ * @returns {void}
347
+ */
348
+ function forEachInlineCodeSpan(input, handler) {
349
+ let currentLine = 0;
350
+ let currentColumn = 0;
351
+ let index = 0;
352
+ while (index < input.length) {
353
+ let startIndex = -1;
354
+ let startLine = -1;
355
+ let startColumn = -1;
356
+ let tickCount = 0;
357
+ let currentTicks = 0;
358
+ let state = "normal";
359
+ // Deliberate <= so trailing 0 completes the last span (ex: "text `code`")
360
+ // False-positive for js/index-out-of-bounds
361
+ for (; index <= input.length; index++) {
362
+ const char = input[index];
363
+ // Ignore backticks in link destination
364
+ if ((char === "[") && (state === "normal")) {
365
+ state = "linkTextOpen";
366
+ } else if ((char === "]") && (state === "linkTextOpen")) {
367
+ state = "linkTextClosed";
368
+ } else if ((char === "(") && (state === "linkTextClosed")) {
369
+ state = "linkDestinationOpen";
370
+ } else if (
371
+ ((char === "(") && (state === "linkDestinationOpen")) ||
372
+ ((char === ")") && (state === "linkDestinationOpen")) ||
373
+ (state === "linkTextClosed")) {
374
+ state = "normal";
375
+ }
376
+ // Parse backtick open/close
377
+ if ((char === "`") && (state !== "linkDestinationOpen")) {
378
+ // Count backticks at start or end of code span
379
+ currentTicks++;
380
+ if ((startIndex === -1) || (startColumn === -1)) {
381
+ startIndex = index + 1;
382
+ }
383
+ } else {
384
+ if ((startIndex >= 0) &&
385
+ (startColumn >= 0) &&
386
+ (tickCount === currentTicks)) {
387
+ // Found end backticks; invoke callback for code span
388
+ handler(
389
+ input.substring(startIndex, index - currentTicks),
390
+ startLine, startColumn, tickCount);
391
+ startIndex = -1;
392
+ startColumn = -1;
393
+ } else if ((startIndex >= 0) && (startColumn === -1)) {
394
+ // Found start backticks
395
+ tickCount = currentTicks;
396
+ startLine = currentLine;
397
+ startColumn = currentColumn;
398
+ }
399
+ // Not in backticks
400
+ currentTicks = 0;
401
+ }
402
+ if (char === "\n") {
403
+ // On next line
404
+ currentLine++;
405
+ currentColumn = 0;
406
+ } else if ((char === "\\") &&
407
+ ((startIndex === -1) || (startColumn === -1)) &&
408
+ (input[index + 1] !== "\n")) {
409
+ // Escape character outside code, skip next
410
+ index++;
411
+ currentColumn += 2;
412
+ } else {
413
+ // On next column
414
+ currentColumn++;
415
+ }
416
+ }
417
+ if (startIndex >= 0) {
418
+ // Restart loop after unmatched start backticks (ex: "`text``code``")
419
+ index = startIndex;
420
+ currentLine = startLine;
421
+ currentColumn = startColumn;
422
+ }
423
+ }
424
+ }
425
+ module.exports.forEachInlineCodeSpan = forEachInlineCodeSpan;
426
+
427
+ /**
428
+ * Adds a generic error object via the onError callback.
429
+ *
430
+ * @param {Object} onError RuleOnError instance.
431
+ * @param {number} lineNumber Line number.
432
+ * @param {string} [detail] Error details.
433
+ * @param {string} [context] Error context.
434
+ * @param {number[]} [range] Column and length of error.
435
+ * @param {Object} [fixInfo] RuleOnErrorFixInfo instance.
436
+ * @returns {void}
437
+ */
438
+ function addError(onError, lineNumber, detail, context, range, fixInfo) {
439
+ onError({
440
+ lineNumber,
441
+ detail,
442
+ context,
443
+ range,
444
+ fixInfo
445
+ });
446
+ }
447
+ module.exports.addError = addError;
448
+
449
+ // Adds an error object with details conditionally via the onError callback
450
+ module.exports.addErrorDetailIf = function addErrorDetailIf(
451
+ onError, lineNumber, expected, actual, detail, context, range, fixInfo) {
452
+ if (expected !== actual) {
453
+ addError(
454
+ onError,
455
+ lineNumber,
456
+ "Expected: " + expected + "; Actual: " + actual +
457
+ (detail ? "; " + detail : ""),
458
+ context,
459
+ range,
460
+ fixInfo);
461
+ }
462
+ };
463
+
464
+ // Adds an error object with context via the onError callback
465
+ module.exports.addErrorContext = function addErrorContext(
466
+ onError, lineNumber, context, left, right, range, fixInfo) {
467
+ if (context.length <= 30) {
468
+ // Nothing to do
469
+ } else if (left && right) {
470
+ context = context.substr(0, 15) + "..." + context.substr(-15);
471
+ } else if (right) {
472
+ context = "..." + context.substr(-30);
473
+ } else {
474
+ context = context.substr(0, 30) + "...";
475
+ }
476
+ addError(onError, lineNumber, null, context, range, fixInfo);
477
+ };
478
+
479
+ // Returns a range object for a line by applying a RegExp
480
+ module.exports.rangeFromRegExp = function rangeFromRegExp(line, regexp) {
481
+ let range = null;
482
+ const match = line.match(regexp);
483
+ if (match) {
484
+ const column = match.index + 1;
485
+ const length = match[0].length;
486
+ range = [ column, length ];
487
+ }
488
+ return range;
489
+ };
490
+
491
+ // Determines if the front matter includes a title
492
+ module.exports.frontMatterHasTitle =
493
+ function frontMatterHasTitle(frontMatterLines, frontMatterTitlePattern) {
494
+ const ignoreFrontMatter =
495
+ (frontMatterTitlePattern !== undefined) && !frontMatterTitlePattern;
496
+ const frontMatterTitleRe =
497
+ new RegExp(String(frontMatterTitlePattern || "^\\s*title\\s*[:=]"), "i");
498
+ return !ignoreFrontMatter &&
499
+ frontMatterLines.some((line) => frontMatterTitleRe.test(line));
500
+ };
501
+
502
+ /**
503
+ * Returns a list of emphasis markers in code spans and links.
504
+ *
505
+ * @param {Object} params RuleParams instance.
506
+ * @returns {number[][]} List of markers.
507
+ */
508
+ function emphasisMarkersInContent(params) {
509
+ const { lines } = params;
510
+ const byLine = new Array(lines.length);
511
+ // Search code spans
512
+ filterTokens(params, "inline", (token) => {
513
+ const { children, lineNumber, map } = token;
514
+ if (children.some((child) => child.type === "code_inline")) {
515
+ const tokenLines = lines.slice(map[0], map[1]);
516
+ forEachInlineCodeSpan(
517
+ tokenLines.join("\n"),
518
+ (code, lineIndex, column, tickCount) => {
519
+ const codeLines = code.split(newLineRe);
520
+ codeLines.forEach((codeLine, codeLineIndex) => {
521
+ let match = null;
522
+ while ((match = emphasisMarkersRe.exec(codeLine))) {
523
+ const byLineIndex = lineNumber - 1 + lineIndex + codeLineIndex;
524
+ const inLine = byLine[byLineIndex] || [];
525
+ const codeLineOffset = codeLineIndex ? 0 : column - 1 + tickCount;
526
+ inLine.push(codeLineOffset + match.index);
527
+ byLine[byLineIndex] = inLine;
528
+ }
529
+ });
530
+ }
531
+ );
532
+ }
533
+ });
534
+ // Search links
535
+ lines.forEach((tokenLine, tokenLineIndex) => {
536
+ let linkMatch = null;
537
+ while ((linkMatch = linkRe.exec(tokenLine))) {
538
+ let markerMatch = null;
539
+ while ((markerMatch = emphasisMarkersRe.exec(linkMatch[0]))) {
540
+ const inLine = byLine[tokenLineIndex] || [];
541
+ inLine.push(linkMatch.index + markerMatch.index);
542
+ byLine[tokenLineIndex] = inLine;
543
+ }
544
+ }
545
+ });
546
+ return byLine;
547
+ }
548
+ module.exports.emphasisMarkersInContent = emphasisMarkersInContent;
549
+
550
+ /**
551
+ * Gets the most common line ending, falling back to the platform default.
552
+ *
553
+ * @param {string} input Markdown content to analyze.
554
+ * @returns {string} Preferred line ending.
555
+ */
556
+ function getPreferredLineEnding(input) {
557
+ let cr = 0;
558
+ let lf = 0;
559
+ let crlf = 0;
560
+ const endings = input.match(newLineRe) || [];
561
+ endings.forEach((ending) => {
562
+ // eslint-disable-next-line default-case
563
+ switch (ending) {
564
+ case "\r":
565
+ cr++;
566
+ break;
567
+ case "\n":
568
+ lf++;
569
+ break;
570
+ case "\r\n":
571
+ crlf++;
572
+ break;
573
+ }
574
+ });
575
+ let preferredLineEnding = null;
576
+ if (!cr && !lf && !crlf) {
577
+ preferredLineEnding = os.EOL;
578
+ } else if ((lf >= crlf) && (lf >= cr)) {
579
+ preferredLineEnding = "\n";
580
+ } else if (crlf >= cr) {
581
+ preferredLineEnding = "\r\n";
582
+ } else {
583
+ preferredLineEnding = "\r";
584
+ }
585
+ return preferredLineEnding;
586
+ }
587
+ module.exports.getPreferredLineEnding = getPreferredLineEnding;
588
+
589
+ /**
590
+ * Normalizes the fields of a RuleOnErrorFixInfo instance.
591
+ *
592
+ * @param {Object} fixInfo RuleOnErrorFixInfo instance.
593
+ * @param {number} [lineNumber] Line number.
594
+ * @returns {Object} Normalized RuleOnErrorFixInfo instance.
595
+ */
596
+ function normalizeFixInfo(fixInfo, lineNumber) {
597
+ return {
598
+ "lineNumber": fixInfo.lineNumber || lineNumber,
599
+ "editColumn": fixInfo.editColumn || 1,
600
+ "deleteCount": fixInfo.deleteCount || 0,
601
+ "insertText": fixInfo.insertText || ""
602
+ };
603
+ }
604
+
605
+ /**
606
+ * Fixes the specified error on a line of Markdown content.
607
+ *
608
+ * @param {string} line Line of Markdown content.
609
+ * @param {Object} fixInfo RuleOnErrorFixInfo instance.
610
+ * @param {string} lineEnding Line ending to use.
611
+ * @returns {string} Fixed content.
612
+ */
613
+ function applyFix(line, fixInfo, lineEnding) {
614
+ const { editColumn, deleteCount, insertText } = normalizeFixInfo(fixInfo);
615
+ const editIndex = editColumn - 1;
616
+ return (deleteCount === -1) ?
617
+ null :
618
+ line.slice(0, editIndex) +
619
+ insertText.replace(/\n/g, lineEnding || "\n") +
620
+ line.slice(editIndex + deleteCount);
621
+ }
622
+ module.exports.applyFix = applyFix;
623
+
624
+ // Applies as many fixes as possible to the input lines
625
+ module.exports.applyFixes = function applyFixes(input, errors) {
626
+ const lineEnding = getPreferredLineEnding(input);
627
+ const lines = input.split(newLineRe);
628
+ // Normalize fixInfo objects
629
+ let fixInfos = errors
630
+ .filter((error) => error.fixInfo)
631
+ .map((error) => normalizeFixInfo(error.fixInfo, error.lineNumber));
632
+ // Sort bottom-to-top, line-deletes last, right-to-left, long-to-short
633
+ fixInfos.sort((a, b) => {
634
+ const aDeletingLine = (a.deleteCount === -1);
635
+ const bDeletingLine = (b.deleteCount === -1);
636
+ return (
637
+ (b.lineNumber - a.lineNumber) ||
638
+ (aDeletingLine ? 1 : (bDeletingLine ? -1 : 0)) ||
639
+ (b.editColumn - a.editColumn) ||
640
+ (b.insertText.length - a.insertText.length)
641
+ );
642
+ });
643
+ // Remove duplicate entries (needed for following collapse step)
644
+ let lastFixInfo = {};
645
+ fixInfos = fixInfos.filter((fixInfo) => {
646
+ const unique = (
647
+ (fixInfo.lineNumber !== lastFixInfo.lineNumber) ||
648
+ (fixInfo.editColumn !== lastFixInfo.editColumn) ||
649
+ (fixInfo.deleteCount !== lastFixInfo.deleteCount) ||
650
+ (fixInfo.insertText !== lastFixInfo.insertText)
651
+ );
652
+ lastFixInfo = fixInfo;
653
+ return unique;
654
+ });
655
+ // Collapse insert/no-delete and no-insert/delete for same line/column
656
+ lastFixInfo = {};
657
+ fixInfos.forEach((fixInfo) => {
658
+ if (
659
+ (fixInfo.lineNumber === lastFixInfo.lineNumber) &&
660
+ (fixInfo.editColumn === lastFixInfo.editColumn) &&
661
+ !fixInfo.insertText &&
662
+ (fixInfo.deleteCount > 0) &&
663
+ lastFixInfo.insertText &&
664
+ !lastFixInfo.deleteCount) {
665
+ fixInfo.insertText = lastFixInfo.insertText;
666
+ lastFixInfo.lineNumber = 0;
667
+ }
668
+ lastFixInfo = fixInfo;
669
+ });
670
+ fixInfos = fixInfos.filter((fixInfo) => fixInfo.lineNumber);
671
+ // Apply all (remaining/updated) fixes
672
+ let lastLineIndex = -1;
673
+ let lastEditIndex = -1;
674
+ fixInfos.forEach((fixInfo) => {
675
+ const { lineNumber, editColumn, deleteCount } = fixInfo;
676
+ const lineIndex = lineNumber - 1;
677
+ const editIndex = editColumn - 1;
678
+ if (
679
+ (lineIndex !== lastLineIndex) ||
680
+ ((editIndex + deleteCount) < lastEditIndex) ||
681
+ (deleteCount === -1)
682
+ ) {
683
+ lines[lineIndex] = applyFix(lines[lineIndex], fixInfo, lineEnding);
684
+ }
685
+ lastLineIndex = lineIndex;
686
+ lastEditIndex = editIndex;
687
+ });
688
+ // Return corrected input
689
+ return lines.filter((line) => line !== null).join(lineEnding);
690
+ };