markdownlint 0.20.0 → 0.20.4

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