markdownlint 0.12.0 → 0.14.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/.eslintrc.json +52 -275
  2. package/.travis.yml +2 -2
  3. package/LICENSE +1 -1
  4. package/README.md +54 -27
  5. package/doc/CustomRules.md +7 -2
  6. package/doc/Rules.md +120 -33
  7. package/example/standalone.js +2 -2
  8. package/helpers/LICENSE +21 -0
  9. package/helpers/README.md +50 -0
  10. package/{lib/shared.js → helpers/helpers.js} +91 -82
  11. package/helpers/package.json +18 -0
  12. package/lib/cache.js +24 -0
  13. package/lib/markdownlint.js +78 -45
  14. package/lib/md001.js +3 -3
  15. package/lib/md002.js +2 -2
  16. package/lib/md003.js +5 -4
  17. package/lib/md004.js +11 -9
  18. package/lib/md005.js +12 -10
  19. package/lib/md006.js +7 -5
  20. package/lib/md007.js +7 -5
  21. package/lib/md009.js +10 -8
  22. package/lib/md010.js +5 -5
  23. package/lib/md011.js +4 -4
  24. package/lib/md012.js +4 -3
  25. package/lib/md013.js +41 -32
  26. package/lib/md014.js +7 -6
  27. package/lib/md018.js +6 -4
  28. package/lib/md019.js +6 -5
  29. package/lib/md020.js +5 -4
  30. package/lib/md021.js +6 -5
  31. package/lib/md022.js +22 -22
  32. package/lib/md023.js +5 -4
  33. package/lib/md024.js +6 -6
  34. package/lib/md025.js +11 -5
  35. package/lib/md026.js +4 -4
  36. package/lib/md027.js +6 -6
  37. package/lib/md028.js +2 -2
  38. package/lib/md029.js +17 -9
  39. package/lib/md030.js +8 -6
  40. package/lib/md031.js +7 -6
  41. package/lib/md032.js +10 -12
  42. package/lib/md033.js +27 -26
  43. package/lib/md034.js +15 -10
  44. package/lib/md035.js +3 -3
  45. package/lib/md036.js +2 -2
  46. package/lib/md037.js +12 -9
  47. package/lib/md038.js +6 -5
  48. package/lib/md039.js +7 -6
  49. package/lib/md040.js +3 -3
  50. package/lib/md041.js +17 -19
  51. package/lib/md042.js +5 -4
  52. package/lib/md043.js +5 -4
  53. package/lib/md044.js +14 -13
  54. package/lib/md045.js +3 -3
  55. package/lib/md046.js +32 -0
  56. package/lib/md047.js +18 -0
  57. package/lib/rules.js +3 -1
  58. package/package.json +12 -11
  59. package/schema/build-config-schema.js +36 -2
  60. package/schema/markdownlint-config-schema.json +178 -14
  61. package/style/cirosantilli.json +1 -1
@@ -7,7 +7,8 @@
7
7
  module.exports.newLineRe = /\r[\n\u0085]?|[\n\u2424\u2028\u0085]/;
8
8
 
9
9
  // Regular expression for matching common front matter (YAML and TOML)
10
- module.exports.frontMatterRe = /^(---|\+\+\+)$[^]*?^\1$(\r\n|\r|\n|$)/m;
10
+ module.exports.frontMatterRe =
11
+ /((^---$[^]*?^---$)|(^\+\+\+$[^]*?^(\+\+\+|\.\.\.)$))(\r\n|\r|\n|$)/m;
11
12
 
12
13
  // Regular expression for matching inline disable/enable comments
13
14
  const inlineCommentRe =
@@ -23,31 +24,6 @@ module.exports.orderedListItemMarkerRe = /^[\s>]*0*(\d+)[.)]/;
23
24
  // readFile options for reading with the UTF-8 encoding
24
25
  module.exports.utf8Encoding = { "encoding": "utf8" };
25
26
 
26
- // Trims whitespace from the left (start) of a string
27
- function trimLeft(str) {
28
- return str.replace(/^\s*/, "");
29
- }
30
- module.exports.trimLeft = trimLeft;
31
-
32
- // Trims whitespace from the right (end) of a string
33
- module.exports.trimRight = function trimRight(str) {
34
- return str.replace(/\s*$/, "");
35
- };
36
-
37
- // Applies key/value pairs from src to dst, returning dst
38
- function assign(dst, src) {
39
- Object.keys(src).forEach(function forKey(key) {
40
- dst[key] = src[key];
41
- });
42
- return dst;
43
- }
44
- module.exports.assign = assign;
45
-
46
- // Clones the key/value pairs of obj, returning the clone
47
- module.exports.clone = function clone(obj) {
48
- return assign({}, obj);
49
- };
50
-
51
27
  // Returns true iff the input is a number
52
28
  module.exports.isNumber = function isNumber(obj) {
53
29
  return typeof obj === "number";
@@ -63,6 +39,31 @@ module.exports.isEmptyString = function isEmptyString(str) {
63
39
  return str.length === 0;
64
40
  };
65
41
 
42
+ // Returns true iff the input line is blank (no content)
43
+ // Example: Contains nothing, whitespace, or comments
44
+ const blankLineRe = />|(?:<!--.*?-->)/g;
45
+ module.exports.isBlankLine = function isBlankLine(line) {
46
+ return !line || !line.trim() || !line.replace(blankLineRe, "").trim();
47
+ };
48
+
49
+ // Returns true iff the sorted array contains the specified element
50
+ module.exports.includesSorted = function includesSorted(array, element) {
51
+ let left = 0;
52
+ let right = array.length - 1;
53
+ while (left <= right) {
54
+ /* eslint-disable no-bitwise */
55
+ const mid = (left + right) >> 1;
56
+ if (array[mid] < element) {
57
+ left = mid + 1;
58
+ } else if (array[mid] > element) {
59
+ right = mid - 1;
60
+ } else {
61
+ return true;
62
+ }
63
+ }
64
+ return false;
65
+ };
66
+
66
67
  // Replaces the text of all properly-formatted HTML comments with whitespace
67
68
  // This preserves the line/column information for the rest of the document
68
69
  // Trailing whitespace is avoided with a '\' character in the last column
@@ -75,13 +76,13 @@ module.exports.clearHtmlCommentText = function clearHtmlCommentText(text) {
75
76
  let j = text.indexOf(htmlCommentEnd, i);
76
77
  if (j === -1) {
77
78
  j = text.length;
78
- text += "\\";
79
+ text += "\\\n";
79
80
  }
80
81
  const comment = text.slice(i + htmlCommentBegin.length, j);
81
82
  if ((comment.length > 0) &&
82
83
  (comment[0] !== ">") &&
83
84
  (comment[comment.length - 1] !== "-") &&
84
- (comment.indexOf("--") === -1) &&
85
+ !comment.includes("--") &&
85
86
  (text.slice(i, j + htmlCommentEnd.length)
86
87
  .search(inlineCommentRe) === -1)) {
87
88
  const blanks = comment
@@ -100,10 +101,23 @@ module.exports.escapeForRegExp = function escapeForRegExp(str) {
100
101
  return str.replace(/[-/\\^$*+?.()|[\]{}]/g, "\\$&");
101
102
  };
102
103
 
104
+ // Un-escapes Markdown content (simple algorithm; not a parser)
105
+ const escapedMarkdownRe = /\\./g;
106
+ module.exports.unescapeMarkdown =
107
+ function unescapeMarkdown(markdown, replacement) {
108
+ return markdown.replace(escapedMarkdownRe, (match) => {
109
+ const char = match[1];
110
+ if ("!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~".includes(char)) {
111
+ return replacement || char;
112
+ }
113
+ return match;
114
+ });
115
+ };
116
+
103
117
  // Returns the indent for a token
104
118
  function indentFor(token) {
105
119
  const line = token.line.replace(/^[\s>]*(> |>)/, "");
106
- return line.length - trimLeft(line).length;
120
+ return line.length - line.trimLeft().length;
107
121
  }
108
122
  module.exports.indentFor = indentFor;
109
123
 
@@ -119,24 +133,17 @@ module.exports.headingStyleFor = function headingStyleFor(token) {
119
133
  };
120
134
 
121
135
  // Calls the provided function for each matching token
122
- function filterTokens(params, type, callback) {
136
+ function filterTokens(params, type, handler) {
123
137
  params.tokens.forEach(function forToken(token) {
124
138
  if (token.type === type) {
125
- callback(token);
139
+ handler(token);
126
140
  }
127
141
  });
128
142
  }
129
143
  module.exports.filterTokens = filterTokens;
130
144
 
131
- let tokenCache = null;
132
- // Caches line metadata and flattened lists for reuse
133
- function makeTokenCache(params) {
134
- if (!params) {
135
- tokenCache = null;
136
- return;
137
- }
138
-
139
- // Initialize line metadata array
145
+ // Get line metadata array
146
+ module.exports.getLineMetadata = function getLineMetadata(params) {
140
147
  const lineMetadata = params.lines.map(function mapLine(line, index) {
141
148
  return [ line, index, false, 0, false ];
142
149
  });
@@ -157,8 +164,19 @@ function makeTokenCache(params) {
157
164
  lineMetadata[i][4] = true;
158
165
  }
159
166
  });
167
+ return lineMetadata;
168
+ };
169
+
170
+ // Calls the provided function for each line (with context)
171
+ module.exports.forEachLine = function forEachLine(lineMetadata, handler) {
172
+ lineMetadata.forEach(function forMetadata(metadata) {
173
+ // Parameters: line, lineIndex, inCode, onFence, inTable
174
+ handler(...metadata);
175
+ });
176
+ };
160
177
 
161
- // Flatten lists
178
+ // Returns (nested) lists as a flat array (in order)
179
+ module.exports.flattenLists = function flattenLists(params) {
162
180
  const flattenedLists = [];
163
181
  const stack = [];
164
182
  let current = null;
@@ -195,38 +213,23 @@ function makeTokenCache(params) {
195
213
  lastWithMap = token;
196
214
  }
197
215
  });
198
-
199
- // Cache results
200
- tokenCache = {
201
- "params": params,
202
- "lineMetadata": lineMetadata,
203
- "flattenedLists": flattenedLists
204
- };
205
- }
206
- module.exports.makeTokenCache = makeTokenCache;
207
-
208
- // Calls the provided function for each line (with context)
209
- module.exports.forEachLine = function forEachLine(callback) {
210
- tokenCache.lineMetadata.forEach(function forMetadata(metadata) {
211
- // Parameters: line, lineIndex, inCode, onFence, inTable
212
- callback.apply(this, metadata);
213
- });
216
+ return flattenedLists;
214
217
  };
215
218
 
216
219
  // Calls the provided function for each specified inline child token
217
220
  module.exports.forEachInlineChild =
218
- function forEachInlineChild(params, type, callback) {
221
+ function forEachInlineChild(params, type, handler) {
219
222
  filterTokens(params, "inline", function forToken(token) {
220
223
  token.children.forEach(function forChild(child) {
221
224
  if (child.type === type) {
222
- callback(child, token);
225
+ handler(child, token);
223
226
  }
224
227
  });
225
228
  });
226
229
  };
227
230
 
228
231
  // Calls the provided function for each heading's content
229
- module.exports.forEachHeading = function forEachHeading(params, callback) {
232
+ module.exports.forEachHeading = function forEachHeading(params, handler) {
230
233
  let heading = null;
231
234
  params.tokens.forEach(function forToken(token) {
232
235
  if (token.type === "heading_open") {
@@ -234,14 +237,14 @@ module.exports.forEachHeading = function forEachHeading(params, callback) {
234
237
  } else if (token.type === "heading_close") {
235
238
  heading = null;
236
239
  } else if ((token.type === "inline") && heading) {
237
- callback(heading, token.content);
240
+ handler(heading, token.content);
238
241
  }
239
242
  });
240
243
  };
241
244
 
242
245
  // Calls the provided function for each inline code span's content
243
246
  module.exports.forEachInlineCodeSpan =
244
- function forEachInlineCodeSpan(input, callback) {
247
+ function forEachInlineCodeSpan(input, handler) {
245
248
  let currentLine = 0;
246
249
  let currentColumn = 0;
247
250
  let index = 0;
@@ -265,7 +268,7 @@ module.exports.forEachInlineCodeSpan =
265
268
  (startColumn >= 0) &&
266
269
  (tickCount === currentTicks)) {
267
270
  // Found end backticks; invoke callback for code span
268
- callback(
271
+ handler(
269
272
  input.substring(startIndex, index - currentTicks),
270
273
  startLine, startColumn, tickCount);
271
274
  startIndex = -1;
@@ -302,11 +305,6 @@ module.exports.forEachInlineCodeSpan =
302
305
  }
303
306
  };
304
307
 
305
- // Returns (nested) lists as a flat array (in order)
306
- module.exports.flattenLists = function flattenLists() {
307
- return tokenCache.flattenedLists;
308
- };
309
-
310
308
  // Adds a generic error object via the onError callback
311
309
  function addError(onError, lineNumber, detail, context, range) {
312
310
  onError({
@@ -320,32 +318,32 @@ module.exports.addError = addError;
320
318
 
321
319
  // Adds an error object with details conditionally via the onError callback
322
320
  module.exports.addErrorDetailIf = function addErrorDetailIf(
323
- onError, lineNumber, expected, actual, detail, range) {
321
+ onError, lineNumber, expected, actual, detail, context, range) {
324
322
  if (expected !== actual) {
325
323
  addError(
326
324
  onError,
327
325
  lineNumber,
328
326
  "Expected: " + expected + "; Actual: " + actual +
329
327
  (detail ? "; " + detail : ""),
330
- null,
328
+ context,
331
329
  range);
332
330
  }
333
331
  };
334
332
 
335
333
  // Adds an error object with context via the onError callback
336
334
  module.exports.addErrorContext =
337
- function addErrorContext(onError, lineNumber, context, left, right, range) {
338
- if (context.length <= 30) {
339
- // Nothing to do
340
- } else if (left && right) {
341
- context = context.substr(0, 15) + "..." + context.substr(-15);
342
- } else if (right) {
343
- context = "..." + context.substr(-30);
344
- } else {
345
- context = context.substr(0, 30) + "...";
346
- }
347
- addError(onError, lineNumber, null, context, range);
348
- };
335
+ function addErrorContext(onError, lineNumber, context, left, right, range) {
336
+ if (context.length <= 30) {
337
+ // Nothing to do
338
+ } else if (left && right) {
339
+ context = context.substr(0, 15) + "..." + context.substr(-15);
340
+ } else if (right) {
341
+ context = "..." + context.substr(-30);
342
+ } else {
343
+ context = context.substr(0, 30) + "...";
344
+ }
345
+ addError(onError, lineNumber, null, context, range);
346
+ };
349
347
 
350
348
  // Returns a range object for a line by applying a RegExp
351
349
  module.exports.rangeFromRegExp = function rangeFromRegExp(line, regexp) {
@@ -362,3 +360,14 @@ module.exports.rangeFromRegExp = function rangeFromRegExp(line, regexp) {
362
360
  }
363
361
  return range;
364
362
  };
363
+
364
+ // Determines if the front matter includes a title
365
+ module.exports.frontMatterHasTitle =
366
+ function frontMatterHasTitle(frontMatterLines, frontMatterTitlePattern) {
367
+ const ignoreFrontMatter =
368
+ (frontMatterTitlePattern !== undefined) && !frontMatterTitlePattern;
369
+ const frontMatterTitleRe =
370
+ new RegExp(frontMatterTitlePattern || "^\\s*title\\s*[:=]", "i");
371
+ return !ignoreFrontMatter &&
372
+ frontMatterLines.some((line) => frontMatterTitleRe.test(line));
373
+ };
@@ -0,0 +1,18 @@
1
+ {
2
+ "name": "markdownlint-rule-helpers",
3
+ "version": "0.2.1",
4
+ "description": "A collection of markdownlint helper functions for custom rules",
5
+ "main": "helpers.js",
6
+ "author": "David Anson (https://dlaa.me/)",
7
+ "license": "MIT",
8
+ "homepage": "https://github.com/DavidAnson/markdownlint",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "https://github.com/DavidAnson/markdownlint.git"
12
+ },
13
+ "bugs": "https://github.com/DavidAnson/markdownlint/issues",
14
+ "keywords": [
15
+ "markdownlint",
16
+ "markdownlint-rule"
17
+ ]
18
+ }
package/lib/cache.js ADDED
@@ -0,0 +1,24 @@
1
+ // @ts-check
2
+
3
+ "use strict";
4
+
5
+ let lineMetadata = null;
6
+ module.exports.lineMetadata = (value) => {
7
+ if (value) {
8
+ lineMetadata = value;
9
+ }
10
+ return lineMetadata;
11
+ };
12
+
13
+ let flattenedLists = null;
14
+ module.exports.flattenedLists = (value) => {
15
+ if (value) {
16
+ flattenedLists = value;
17
+ }
18
+ return flattenedLists;
19
+ };
20
+
21
+ module.exports.clear = () => {
22
+ lineMetadata = null;
23
+ flattenedLists = null;
24
+ };
@@ -7,7 +7,10 @@ const path = require("path");
7
7
  const { URL } = require("url");
8
8
  const markdownIt = require("markdown-it");
9
9
  const rules = require("./rules");
10
- const shared = require("./shared");
10
+ const helpers = require("../helpers");
11
+ const cache = require("./cache");
12
+
13
+ const deprecatedRuleNames = [ "MD002" ];
11
14
 
12
15
  // Validates the list of rules for structure and reuse
13
16
  function validateRuleList(ruleList) {
@@ -28,7 +31,7 @@ function validateRuleList(ruleList) {
28
31
  const value = rule[property];
29
32
  if (!result &&
30
33
  (!value || !Array.isArray(value) || (value.length === 0) ||
31
- !value.every(shared.isString) || value.some(shared.isEmptyString))) {
34
+ !value.every(helpers.isString) || value.some(helpers.isEmptyString))) {
32
35
  result = newError(property);
33
36
  }
34
37
  });
@@ -72,7 +75,7 @@ function validateRuleList(ruleList) {
72
75
  // Class for results with toString for pretty display
73
76
  function newResults(ruleList) {
74
77
  function Results() {}
75
- Results.prototype.toString = function resultsToString(useAlias) {
78
+ Results.prototype.toString = function toString(useAlias) {
76
79
  const that = this;
77
80
  let ruleNameToRule = null;
78
81
  const results = [];
@@ -131,7 +134,7 @@ function removeFrontMatter(content, frontMatter) {
131
134
  if (frontMatterMatch && !frontMatterMatch.index) {
132
135
  const contentMatched = frontMatterMatch[0];
133
136
  content = content.slice(contentMatched.length);
134
- frontMatterLines = contentMatched.split(shared.newLineRe);
137
+ frontMatterLines = contentMatched.split(helpers.newLineRe);
135
138
  if (frontMatterLines.length &&
136
139
  (frontMatterLines[frontMatterLines.length - 1] === "")) {
137
140
  frontMatterLines.length--;
@@ -164,7 +167,7 @@ function annotateTokens(tokens, lines) {
164
167
  token.line = lines[token.map[0]];
165
168
  token.lineNumber = token.map[0] + 1;
166
169
  // Trim bottom of token to exclude whitespace lines
167
- while (token.map[1] && !(lines[token.map[1] - 1].trim())) {
170
+ while (token.map[1] && !((lines[token.map[1] - 1] || "").trim())) {
168
171
  token.map[1]--;
169
172
  }
170
173
  // Annotate children with lineNumber
@@ -212,16 +215,19 @@ function mapAliasToRuleNames(ruleList) {
212
215
 
213
216
  // Apply (and normalize) config
214
217
  function getEffectiveConfig(ruleList, config, aliasToRuleNames) {
215
- const defaultKey = Object.keys(config).filter(function forKey(key) {
216
- return key.toUpperCase() === "DEFAULT";
217
- });
218
+ const defaultKey = Object.keys(config).filter(
219
+ (key) => key.toUpperCase() === "DEFAULT"
220
+ );
218
221
  const ruleDefault = (defaultKey.length === 0) || !!config[defaultKey[0]];
219
222
  const effectiveConfig = {};
220
- ruleList.forEach(function forRule(rule) {
223
+ ruleList.forEach((rule) => {
221
224
  const ruleName = rule.names[0].toUpperCase();
222
225
  effectiveConfig[ruleName] = ruleDefault;
223
226
  });
224
- Object.keys(config).forEach(function forKey(key) {
227
+ deprecatedRuleNames.forEach((ruleName) => {
228
+ effectiveConfig[ruleName] = false;
229
+ });
230
+ Object.keys(config).forEach((key) => {
225
231
  let value = config[key];
226
232
  if (value) {
227
233
  if (!(value instanceof Object)) {
@@ -231,7 +237,7 @@ function getEffectiveConfig(ruleList, config, aliasToRuleNames) {
231
237
  value = false;
232
238
  }
233
239
  const keyUpper = key.toUpperCase();
234
- (aliasToRuleNames[keyUpper] || []).forEach(function forRule(ruleName) {
240
+ (aliasToRuleNames[keyUpper] || []).forEach((ruleName) => {
235
241
  effectiveConfig[ruleName] = value;
236
242
  });
237
243
  });
@@ -263,12 +269,12 @@ function getEnabledRulesPerLineNumber(
263
269
  const enabledRulesPerLineNumber = new Array(1 + frontMatterLines.length);
264
270
  lines.forEach(function forLine(line) {
265
271
  if (!noInlineConfig) {
266
- let match = shared.inlineCommentRe.exec(line);
272
+ let match = helpers.inlineCommentRe.exec(line);
267
273
  if (match) {
268
- enabledRules = shared.clone(enabledRules);
274
+ enabledRules = { ...enabledRules };
269
275
  while (match) {
270
276
  forMatch(match);
271
- match = shared.inlineCommentRe.exec(line);
277
+ match = helpers.inlineCommentRe.exec(line);
272
278
  }
273
279
  }
274
280
  }
@@ -295,6 +301,7 @@ function lintContent(
295
301
  md,
296
302
  config,
297
303
  frontMatter,
304
+ handleRuleFailures,
298
305
  noInlineConfig,
299
306
  resultVersion,
300
307
  callback) {
@@ -304,10 +311,10 @@ function lintContent(
304
311
  const removeFrontMatterResult = removeFrontMatter(content, frontMatter);
305
312
  const frontMatterLines = removeFrontMatterResult.frontMatterLines;
306
313
  // Ignore the content of HTML comments
307
- content = shared.clearHtmlCommentText(removeFrontMatterResult.content);
314
+ content = helpers.clearHtmlCommentText(removeFrontMatterResult.content);
308
315
  // Parse content into tokens and lines
309
316
  const tokens = md.parse(content, {});
310
- const lines = content.split(shared.newLineRe);
317
+ const lines = content.split(helpers.newLineRe);
311
318
  annotateTokens(tokens, lines);
312
319
  const aliasToRuleNames = mapAliasToRuleNames(ruleList);
313
320
  const effectiveConfig =
@@ -322,7 +329,8 @@ function lintContent(
322
329
  lines,
323
330
  frontMatterLines
324
331
  };
325
- shared.makeTokenCache(params);
332
+ cache.lineMetadata(helpers.getLineMetadata(params));
333
+ cache.flattenedLists(helpers.flattenLists(params));
326
334
  // Function to run for each rule
327
335
  const result = (resultVersion === 0) ? {} : [];
328
336
  function forRule(rule) {
@@ -337,23 +345,28 @@ function lintContent(
337
345
  const errors = [];
338
346
  function onError(errorInfo) {
339
347
  if (!errorInfo ||
340
- !errorInfo.lineNumber ||
341
- !shared.isNumber(errorInfo.lineNumber)) {
348
+ !helpers.isNumber(errorInfo.lineNumber) ||
349
+ (errorInfo.lineNumber < 1) ||
350
+ (errorInfo.lineNumber > lines.length)) {
342
351
  throwError("lineNumber");
343
352
  }
344
353
  if (errorInfo.detail &&
345
- !shared.isString(errorInfo.detail)) {
354
+ !helpers.isString(errorInfo.detail)) {
346
355
  throwError("detail");
347
356
  }
348
357
  if (errorInfo.context &&
349
- !shared.isString(errorInfo.context)) {
358
+ !helpers.isString(errorInfo.context)) {
350
359
  throwError("context");
351
360
  }
352
361
  if (errorInfo.range &&
353
362
  (!Array.isArray(errorInfo.range) ||
354
363
  (errorInfo.range.length !== 2) ||
355
- !shared.isNumber(errorInfo.range[0]) ||
356
- !shared.isNumber(errorInfo.range[1]))) {
364
+ !helpers.isNumber(errorInfo.range[0]) ||
365
+ (errorInfo.range[0] < 1) ||
366
+ !helpers.isNumber(errorInfo.range[1]) ||
367
+ (errorInfo.range[1] < 1) ||
368
+ ((errorInfo.range[0] + errorInfo.range[1] - 1) >
369
+ lines[errorInfo.lineNumber - 1].length))) {
357
370
  throwError("range");
358
371
  }
359
372
  errors.push({
@@ -364,7 +377,18 @@ function lintContent(
364
377
  });
365
378
  }
366
379
  // Call (possibly external) rule function
367
- rule.function(params, onError);
380
+ if (handleRuleFailures) {
381
+ try {
382
+ rule.function(params, onError);
383
+ } catch (ex) {
384
+ onError({
385
+ "lineNumber": 1,
386
+ "detail": `This rule threw an exception: ${ex.message}`
387
+ });
388
+ }
389
+ } else {
390
+ rule.function(params, onError);
391
+ }
368
392
  // Record any errors (significant performance benefit from length check)
369
393
  if (errors.length) {
370
394
  errors.sort(lineNumberComparison);
@@ -406,11 +430,11 @@ function lintContent(
406
430
  try {
407
431
  ruleList.forEach(forRule);
408
432
  } catch (ex) {
409
- shared.makeTokenCache(null);
433
+ cache.clear();
410
434
  return callback(ex);
411
435
  }
412
- shared.makeTokenCache(null);
413
- callback(null, result);
436
+ cache.clear();
437
+ return callback(null, result);
414
438
  }
415
439
 
416
440
  // Lints a single file
@@ -420,6 +444,7 @@ function lintFile(
420
444
  md,
421
445
  config,
422
446
  frontMatter,
447
+ handleRuleFailures,
423
448
  noInlineConfig,
424
449
  resultVersion,
425
450
  synchronous,
@@ -428,14 +453,14 @@ function lintFile(
428
453
  if (err) {
429
454
  return callback(err);
430
455
  }
431
- lintContent(ruleList, file, content, md, config, frontMatter,
432
- noInlineConfig, resultVersion, callback);
456
+ return lintContent(ruleList, file, content, md, config, frontMatter,
457
+ handleRuleFailures, noInlineConfig, resultVersion, callback);
433
458
  }
434
459
  // Make a/synchronous call to read file
435
460
  if (synchronous) {
436
- lintContentWrapper(null, fs.readFileSync(file, shared.utf8Encoding));
461
+ lintContentWrapper(null, fs.readFileSync(file, helpers.utf8Encoding));
437
462
  } else {
438
- fs.readFile(file, shared.utf8Encoding, lintContentWrapper);
463
+ fs.readFile(file, helpers.utf8Encoding, lintContentWrapper);
439
464
  }
440
465
  }
441
466
 
@@ -459,7 +484,8 @@ function lintInput(options, synchronous, callback) {
459
484
  const stringsKeys = Object.keys(strings);
460
485
  const config = options.config || { "default": true };
461
486
  const frontMatter = (options.frontMatter === undefined) ?
462
- shared.frontMatterRe : options.frontMatter;
487
+ helpers.frontMatterRe : options.frontMatter;
488
+ const handleRuleFailures = !!options.handleRuleFailures;
463
489
  const noInlineConfig = !!options.noInlineConfig;
464
490
  const resultVersion = (options.resultVersion === undefined) ?
465
491
  2 : options.resultVersion;
@@ -471,6 +497,7 @@ function lintInput(options, synchronous, callback) {
471
497
  });
472
498
  const results = newResults(ruleList);
473
499
  // Helper to lint the next string or file
500
+ /* eslint-disable consistent-return */
474
501
  function lintNextItem() {
475
502
  let iterating = true;
476
503
  let item = null;
@@ -480,9 +507,7 @@ function lintInput(options, synchronous, callback) {
480
507
  return callback(err);
481
508
  }
482
509
  results[item] = result;
483
- if (!iterating) {
484
- lintNextItem();
485
- }
510
+ return iterating || lintNextItem();
486
511
  }
487
512
  while (iterating) {
488
513
  if ((item = stringsKeys.shift())) {
@@ -493,6 +518,7 @@ function lintInput(options, synchronous, callback) {
493
518
  md,
494
519
  config,
495
520
  frontMatter,
521
+ handleRuleFailures,
496
522
  noInlineConfig,
497
523
  resultVersion,
498
524
  lintNextItemCallback);
@@ -504,6 +530,7 @@ function lintInput(options, synchronous, callback) {
504
530
  md,
505
531
  config,
506
532
  frontMatter,
533
+ handleRuleFailures,
507
534
  noInlineConfig,
508
535
  resultVersion,
509
536
  synchronous,
@@ -513,7 +540,7 @@ function lintInput(options, synchronous, callback) {
513
540
  }
514
541
  }
515
542
  }
516
- lintNextItem();
543
+ return lintNextItem();
517
544
  }
518
545
 
519
546
  /**
@@ -584,7 +611,7 @@ function readConfig(file, parsers, callback) {
584
611
  parsers = null;
585
612
  }
586
613
  // Read file
587
- fs.readFile(file, shared.utf8Encoding, (err, content) => {
614
+ fs.readFile(file, helpers.utf8Encoding, (err, content) => {
588
615
  if (err) {
589
616
  return callback(err);
590
617
  }
@@ -598,15 +625,17 @@ function readConfig(file, parsers, callback) {
598
625
  if (configExtends) {
599
626
  delete config.extends;
600
627
  const extendsFile = path.resolve(path.dirname(file), configExtends);
601
- readConfig(extendsFile, parsers, (errr, extendsConfig) => {
628
+ return readConfig(extendsFile, parsers, (errr, extendsConfig) => {
602
629
  if (errr) {
603
630
  return callback(errr);
604
631
  }
605
- callback(null, shared.assign(extendsConfig, config));
632
+ return callback(null, {
633
+ ...extendsConfig,
634
+ ...config
635
+ });
606
636
  });
607
- } else {
608
- callback(null, config);
609
637
  }
638
+ return callback(null, config);
610
639
  });
611
640
  }
612
641
 
@@ -619,7 +648,7 @@ function readConfig(file, parsers, callback) {
619
648
  */
620
649
  function readConfigSync(file, parsers) {
621
650
  // Read file
622
- const content = fs.readFileSync(file, shared.utf8Encoding);
651
+ const content = fs.readFileSync(file, helpers.utf8Encoding);
623
652
  // Try to parse file
624
653
  const { config, message } = parseConfiguration(file, content, parsers);
625
654
  if (!config) {
@@ -629,9 +658,13 @@ function readConfigSync(file, parsers) {
629
658
  const configExtends = config.extends;
630
659
  if (configExtends) {
631
660
  delete config.extends;
632
- return shared.assign(
633
- readConfigSync(path.resolve(path.dirname(file), configExtends), parsers),
634
- config);
661
+ return {
662
+ ...readConfigSync(
663
+ path.resolve(path.dirname(file), configExtends),
664
+ parsers
665
+ ),
666
+ ...config
667
+ };
635
668
  }
636
669
  return config;
637
670
  }