markdownlint 0.21.1 → 0.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/md011.js CHANGED
@@ -2,40 +2,44 @@
2
2
 
3
3
  "use strict";
4
4
 
5
- const { addError, forEachInlineChild, unescapeMarkdown } =
6
- require("../helpers");
5
+ const { addError, forEachLine, overlapsAnyRange } = require("../helpers");
6
+ const { inlineCodeSpanRanges, lineMetadata } = require("./cache");
7
7
 
8
- const reversedLinkRe = /\(([^)]+)\)\[([^\]^][^\]]*)]/g;
8
+ const reversedLinkRe =
9
+ /(^|[^\\])\(([^)]+)\)\[([^\]^][^\]]*)](?!\()/g;
9
10
 
10
11
  module.exports = {
11
12
  "names": [ "MD011", "no-reversed-links" ],
12
13
  "description": "Reversed link syntax",
13
14
  "tags": [ "links" ],
14
15
  "function": function MD011(params, onError) {
15
- forEachInlineChild(params, "text", (token) => {
16
- const { lineNumber, content } = token;
17
- let match = null;
18
- while ((match = reversedLinkRe.exec(content)) !== null) {
19
- const [ reversedLink, linkText, linkDestination ] = match;
20
- const line = params.lines[lineNumber - 1];
21
- const column = unescapeMarkdown(line).indexOf(reversedLink) + 1;
22
- const length = reversedLink.length;
23
- const range = column ? [ column, length ] : null;
24
- const fixInfo = column ?
25
- {
26
- "editColumn": column,
27
- "deleteCount": length,
28
- "insertText": `[${linkText}](${linkDestination})`
29
- } :
30
- null;
31
- addError(
32
- onError,
33
- lineNumber,
34
- reversedLink,
35
- null,
36
- range,
37
- fixInfo
38
- );
16
+ const exclusions = inlineCodeSpanRanges();
17
+ forEachLine(lineMetadata(), (line, lineIndex, inCode, onFence) => {
18
+ if (!inCode && !onFence) {
19
+ let match = null;
20
+ while ((match = reversedLinkRe.exec(line)) !== null) {
21
+ const [ reversedLink, preChar, linkText, linkDestination ] = match;
22
+ const index = match.index + preChar.length;
23
+ const length = match[0].length - preChar.length;
24
+ if (
25
+ !linkText.endsWith("\\") &&
26
+ !linkDestination.endsWith("\\") &&
27
+ !overlapsAnyRange(exclusions, lineIndex, index, length)
28
+ ) {
29
+ addError(
30
+ onError,
31
+ lineIndex + 1,
32
+ reversedLink.slice(preChar.length),
33
+ null,
34
+ [ index + 1, length ],
35
+ {
36
+ "editColumn": index + 1,
37
+ "deleteCount": length,
38
+ "insertText": `[${linkText}](${linkDestination})`
39
+ }
40
+ );
41
+ }
42
+ }
39
43
  }
40
44
  });
41
45
  }
package/lib/md018.js CHANGED
@@ -12,7 +12,7 @@ module.exports = {
12
12
  "function": function MD018(params, onError) {
13
13
  forEachLine(lineMetadata(), (line, lineIndex, inCode) => {
14
14
  if (!inCode &&
15
- /^#+[^#\s]/.test(line) &&
15
+ /^#+[^# \t]/.test(line) &&
16
16
  !/#\s*$/.test(line) &&
17
17
  !line.startsWith("#️⃣")) {
18
18
  const hashCount = /^#+/.exec(line)[0].length;
package/lib/md019.js CHANGED
@@ -13,7 +13,7 @@ module.exports = {
13
13
  filterTokens(params, "heading_open", (token) => {
14
14
  if (headingStyleFor(token) === "atx") {
15
15
  const { line, lineNumber } = token;
16
- const match = /^(#+)(\s{2,})(?:\S)/.exec(line);
16
+ const match = /^(#+)([ \t]{2,})(?:\S)/.exec(line);
17
17
  if (match) {
18
18
  const [
19
19
  ,
package/lib/md020.js CHANGED
@@ -13,7 +13,7 @@ module.exports = {
13
13
  forEachLine(lineMetadata(), (line, lineIndex, inCode) => {
14
14
  if (!inCode) {
15
15
  const match =
16
- /^(#+)(\s*)([^#]*?[^#\\])(\s*)((?:\\#)?)(#+)(\s*)$/.exec(line);
16
+ /^(#+)([ \t]*)([^#]*?[^#\\])([ \t]*)((?:\\#)?)(#+)(\s*)$/.exec(line);
17
17
  if (match) {
18
18
  const [
19
19
  ,
package/lib/md021.js CHANGED
@@ -13,7 +13,7 @@ module.exports = {
13
13
  filterTokens(params, "heading_open", (token) => {
14
14
  if (headingStyleFor(token) === "atx_closed") {
15
15
  const { line, lineNumber } = token;
16
- const match = /^(#+)(\s+)([^#]+?)(\s+)(#+)(\s*)$/.exec(line);
16
+ const match = /^(#+)([ \t]+)([^#]+?)([ \t]+)(#+)(\s*)$/.exec(line);
17
17
  if (match) {
18
18
  const [
19
19
  ,
package/lib/md023.js CHANGED
@@ -17,7 +17,7 @@ module.exports = {
17
17
  if (match) {
18
18
  const [ prefixAndFirstChar, prefix ] = match;
19
19
  let deleteCount = prefix.length;
20
- const prefixLengthNoSpace = prefix.trimRight().length;
20
+ const prefixLengthNoSpace = prefix.trimEnd().length;
21
21
  if (prefixLengthNoSpace) {
22
22
  deleteCount -= prefixLengthNoSpace - 1;
23
23
  }
package/lib/md025.js CHANGED
@@ -7,7 +7,7 @@ const { addErrorContext, filterTokens, frontMatterHasTitle } =
7
7
 
8
8
  module.exports = {
9
9
  "names": [ "MD025", "single-title", "single-h1" ],
10
- "description": "Multiple top level headings in the same document",
10
+ "description": "Multiple top-level headings in the same document",
11
11
  "tags": [ "headings", "headers" ],
12
12
  "function": function MD025(params, onError) {
13
13
  const level = Number(params.config.level || 1);
package/lib/md026.js CHANGED
@@ -2,24 +2,27 @@
2
2
 
3
3
  "use strict";
4
4
 
5
- const { addError, allPunctuation, escapeForRegExp, forEachHeading } =
5
+ const { addError, allPunctuationNoQuestion, escapeForRegExp, forEachHeading } =
6
6
  require("../helpers");
7
7
 
8
+ const endOfLineHtmlEntityRe = /&#?[0-9a-zA-Z]+;$/;
9
+
8
10
  module.exports = {
9
11
  "names": [ "MD026", "no-trailing-punctuation" ],
10
12
  "description": "Trailing punctuation in heading",
11
13
  "tags": [ "headings", "headers" ],
12
14
  "function": function MD026(params, onError) {
13
15
  let punctuation = params.config.punctuation;
14
- punctuation =
15
- String((punctuation === undefined) ? allPunctuation : punctuation);
16
+ punctuation = String(
17
+ (punctuation === undefined) ? allPunctuationNoQuestion : punctuation
18
+ );
16
19
  const trailingPunctuationRe =
17
20
  new RegExp("\\s*[" + escapeForRegExp(punctuation) + "]+$");
18
21
  forEachHeading(params, (heading) => {
19
22
  const { line, lineNumber } = heading;
20
23
  const trimmedLine = line.replace(/[\s#]*$/, "");
21
24
  const match = trailingPunctuationRe.exec(trimmedLine);
22
- if (match) {
25
+ if (match && !endOfLineHtmlEntityRe.test(trimmedLine)) {
23
26
  const fullMatch = match[0];
24
27
  const column = match.index + 1;
25
28
  const length = fullMatch.length;
package/lib/md028.js CHANGED
@@ -18,15 +18,7 @@ module.exports = {
18
18
  let lineNumber = prevLineNumber;
19
19
  lineNumber < token.lineNumber;
20
20
  lineNumber++) {
21
- addError(
22
- onError,
23
- lineNumber,
24
- null,
25
- null,
26
- null,
27
- {
28
- "deleteCount": -1
29
- });
21
+ addError(onError, lineNumber);
30
22
  }
31
23
  }
32
24
  prevToken = token;
package/lib/md032.js CHANGED
@@ -17,7 +17,7 @@ module.exports = {
17
17
  const firstIndex = list.open.map[0];
18
18
  if (!isBlankLine(lines[firstIndex - 1])) {
19
19
  const line = lines[firstIndex];
20
- const quotePrefix = line.match(quotePrefixRe)[0].trimRight();
20
+ const quotePrefix = line.match(quotePrefixRe)[0].trimEnd();
21
21
  addErrorContext(
22
22
  onError,
23
23
  firstIndex + 1,
@@ -32,7 +32,7 @@ module.exports = {
32
32
  const lastIndex = list.lastLineIndex - 1;
33
33
  if (!isBlankLine(lines[lastIndex + 1])) {
34
34
  const line = lines[lastIndex];
35
- const quotePrefix = line.match(quotePrefixRe)[0].trimRight();
35
+ const quotePrefix = line.match(quotePrefixRe)[0].trimEnd();
36
36
  addErrorContext(
37
37
  onError,
38
38
  lastIndex + 1,
package/lib/md037.js CHANGED
@@ -10,6 +10,7 @@ const emphasisRe = /(^|[^\\]|\\\\)(?:(\*\*?\*?)|(__?_?))/g;
10
10
  const asteriskListItemMarkerRe = /^([\s>]*)\*(\s+)/;
11
11
  const leftSpaceRe = /^\s+/;
12
12
  const rightSpaceRe = /\s+$/;
13
+ const tablePipeRe = /\|/;
13
14
 
14
15
  module.exports = {
15
16
  "names": [ "MD037", "no-space-in-emphasis" ],
@@ -28,18 +29,23 @@ module.exports = {
28
29
  pendingError = null;
29
30
  }
30
31
  // eslint-disable-next-line jsdoc/require-jsdoc
31
- function handleRunEnd(line, lineIndex, contextLength, match, matchIndex) {
32
+ function handleRunEnd(
33
+ line, lineIndex, contextLength, match, matchIndex, inTable
34
+ ) {
32
35
  // Close current run
33
36
  let content = line.substring(emphasisIndex, matchIndex);
34
37
  if (!emphasisLength) {
35
- content = content.trimLeft();
38
+ content = content.trimStart();
36
39
  }
37
40
  if (!match) {
38
- content = content.trimRight();
41
+ content = content.trimEnd();
39
42
  }
40
43
  const leftSpace = leftSpaceRe.test(content);
41
44
  const rightSpace = rightSpaceRe.test(content);
42
- if (leftSpace || rightSpace) {
45
+ if (
46
+ (leftSpace || rightSpace) &&
47
+ (!inTable || !tablePipeRe.test(content))
48
+ ) {
43
49
  // Report the violation
44
50
  const contextStart = emphasisIndex - emphasisLength;
45
51
  const contextEnd = matchIndex + contextLength;
@@ -70,13 +76,25 @@ module.exports = {
70
76
  resetRunTracking();
71
77
  forEachLine(
72
78
  lineMetadata(),
73
- (line, lineIndex, inCode, onFence, inTable, inItem, onBreak) => {
79
+ (line, lineIndex, inCode, onFence, inTable, inItem, onBreak, inMath) => {
74
80
  const onItemStart = (inItem === 1);
75
- if (inCode || inTable || onBreak || onItemStart || isBlankLine(line)) {
81
+ if (
82
+ inCode ||
83
+ onFence ||
84
+ inTable ||
85
+ onBreak ||
86
+ onItemStart ||
87
+ isBlankLine(line)
88
+ ) {
76
89
  // Emphasis resets when leaving a block
77
90
  resetRunTracking();
78
91
  }
79
- if (inCode || onBreak) {
92
+ if (
93
+ inCode ||
94
+ onFence ||
95
+ onBreak ||
96
+ inMath
97
+ ) {
80
98
  // Emphasis has no meaning here
81
99
  return;
82
100
  }
@@ -106,12 +124,20 @@ module.exports = {
106
124
  if (matchLength === effectiveEmphasisLength) {
107
125
  // Ending an existing run, report any pending error
108
126
  if (pendingError) {
127
+ // @ts-ignore
109
128
  addErrorContext(...pendingError);
110
129
  pendingError = null;
111
130
  }
112
131
  const error = handleRunEnd(
113
- line, lineIndex, effectiveEmphasisLength, match, matchIndex);
132
+ line,
133
+ lineIndex,
134
+ effectiveEmphasisLength,
135
+ match,
136
+ matchIndex,
137
+ inTable
138
+ );
114
139
  if (error) {
140
+ // @ts-ignore
115
141
  addErrorContext(...error);
116
142
  }
117
143
  // Reset
@@ -139,7 +165,7 @@ module.exports = {
139
165
  }
140
166
  if (emphasisIndex !== -1) {
141
167
  pendingError = pendingError ||
142
- handleRunEnd(line, lineIndex, 0, null, line.length);
168
+ handleRunEnd(line, lineIndex, 0, null, line.length, inTable);
143
169
  // Adjust for pending run on new line
144
170
  emphasisIndex = 0;
145
171
  emphasisLength = 0;
package/lib/md039.js CHANGED
@@ -24,8 +24,8 @@ module.exports = {
24
24
  linkText = "";
25
25
  } else if (type === "link_close") {
26
26
  inLink = false;
27
- const left = linkText.trimLeft().length !== linkText.length;
28
- const right = linkText.trimRight().length !== linkText.length;
27
+ const left = linkText.trimStart().length !== linkText.length;
28
+ const right = linkText.trimEnd().length !== linkText.length;
29
29
  if (left || right) {
30
30
  const line = params.lines[lineNumber - 1];
31
31
  let range = null;
package/lib/md041.js CHANGED
@@ -6,7 +6,7 @@ const { addErrorContext, frontMatterHasTitle } = require("../helpers");
6
6
 
7
7
  module.exports = {
8
8
  "names": [ "MD041", "first-line-heading", "first-line-h1" ],
9
- "description": "First line in file should be a top level heading",
9
+ "description": "First line in a file should be a top-level heading",
10
10
  "tags": [ "headings", "headers" ],
11
11
  "function": function MD041(params, onError) {
12
12
  const level = Number(params.config.level || 1);
@@ -17,11 +17,22 @@ module.exports = {
17
17
  params.config.front_matter_title
18
18
  );
19
19
  if (!foundFrontMatterTitle) {
20
+ const htmlHeadingRe = new RegExp(`^<h${level}[ />]`, "i");
20
21
  params.tokens.every((token) => {
22
+ let isError = false;
21
23
  if (token.type === "html_block") {
22
- return true;
24
+ if (token.content.startsWith("<!--")) {
25
+ // Ignore leading HTML comments
26
+ return true;
27
+ } else if (!htmlHeadingRe.test(token.content)) {
28
+ // Something other than an HTML heading
29
+ isError = true;
30
+ }
31
+ } else if ((token.type !== "heading_open") || (token.tag !== tag)) {
32
+ // Something other than a Markdown heading
33
+ isError = true;
23
34
  }
24
- if ((token.type !== "heading_open") || (token.tag !== tag)) {
35
+ if (isError) {
25
36
  addErrorContext(onError, token.lineNumber, token.line);
26
37
  }
27
38
  return false;
package/lib/md043.js CHANGED
@@ -13,30 +13,46 @@ module.exports = {
13
13
  const requiredHeadings = params.config.headings || params.config.headers;
14
14
  if (Array.isArray(requiredHeadings)) {
15
15
  const levels = {};
16
- [ 1, 2, 3, 4, 5, 6 ].forEach(function forLevel(level) {
16
+ [ 1, 2, 3, 4, 5, 6 ].forEach((level) => {
17
17
  levels["h" + level] = "######".substr(-level);
18
18
  });
19
19
  let i = 0;
20
- let optional = false;
21
- let errorCount = 0;
22
- forEachHeading(params, function forHeading(heading, content) {
23
- if (!errorCount) {
20
+ let matchAny = false;
21
+ let hasError = false;
22
+ let anyHeadings = false;
23
+ // eslint-disable-next-line func-style
24
+ const getExpected = () => requiredHeadings[i++] || "[None]";
25
+ forEachHeading(params, (heading, content) => {
26
+ if (!hasError) {
27
+ anyHeadings = true;
24
28
  const actual = levels[heading.tag] + " " + content;
25
- const expected = requiredHeadings[i++] || "[None]";
29
+ const expected = getExpected();
26
30
  if (expected === "*") {
27
- optional = true;
31
+ const nextExpected = getExpected();
32
+ if (nextExpected.toLowerCase() !== actual.toLowerCase()) {
33
+ matchAny = true;
34
+ i--;
35
+ }
36
+ } else if (expected === "+") {
37
+ matchAny = true;
28
38
  } else if (expected.toLowerCase() === actual.toLowerCase()) {
29
- optional = false;
30
- } else if (optional) {
39
+ matchAny = false;
40
+ } else if (matchAny) {
31
41
  i--;
32
42
  } else {
33
43
  addErrorDetailIf(onError, heading.lineNumber,
34
44
  expected, actual);
35
- errorCount++;
45
+ hasError = true;
36
46
  }
37
47
  }
38
48
  });
39
- if ((i < requiredHeadings.length) && !errorCount) {
49
+ const extraHeadings = requiredHeadings.length - i;
50
+ if (
51
+ !hasError &&
52
+ ((extraHeadings > 1) ||
53
+ ((extraHeadings === 1) && (requiredHeadings[i] !== "*"))) &&
54
+ (anyHeadings || !requiredHeadings.every((heading) => heading === "*"))
55
+ ) {
40
56
  addErrorContext(onError, params.lines.length,
41
57
  requiredHeadings[i]);
42
58
  }
package/lib/md044.js CHANGED
@@ -2,11 +2,9 @@
2
2
 
3
3
  "use strict";
4
4
 
5
- const { addErrorDetailIf, bareUrlRe, escapeForRegExp, filterTokens,
6
- forEachInlineChild, newLineRe } = require("../helpers");
7
-
8
- const startNonWordRe = /^\W/;
9
- const endNonWordRe = /\W$/;
5
+ const { addErrorDetailIf, bareUrlRe, escapeForRegExp, forEachLine,
6
+ overlapsAnyRange, linkRe, linkReferenceRe } = require("../helpers");
7
+ const { inlineCodeSpanRanges, lineMetadata } = require("./cache");
10
8
 
11
9
  module.exports = {
12
10
  "names": [ "MD044", "proper-names" ],
@@ -15,80 +13,65 @@ module.exports = {
15
13
  "function": function MD044(params, onError) {
16
14
  let names = params.config.names;
17
15
  names = Array.isArray(names) ? names : [];
16
+ names.sort((a, b) => (b.length - a.length) || a.localeCompare(b));
18
17
  const codeBlocks = params.config.code_blocks;
19
18
  const includeCodeBlocks = (codeBlocks === undefined) ? true : !!codeBlocks;
20
- // Text of automatic hyperlinks is implicitly a URL
21
- const autolinkText = new Set();
22
- filterTokens(params, "inline", (token) => {
23
- let inAutoLink = false;
24
- token.children.forEach((child) => {
25
- const { info, type } = child;
26
- if ((type === "link_open") && (info === "auto")) {
27
- inAutoLink = true;
28
- } else if (type === "link_close") {
29
- inAutoLink = false;
30
- } else if ((type === "text") && inAutoLink) {
31
- autolinkText.add(child);
19
+ const exclusions = [];
20
+ forEachLine(lineMetadata(), (line, lineIndex) => {
21
+ if (linkReferenceRe.test(line)) {
22
+ exclusions.push([ lineIndex, 0, line.length ]);
23
+ } else {
24
+ let match = null;
25
+ while ((match = bareUrlRe.exec(line)) !== null) {
26
+ exclusions.push([ lineIndex, match.index, match[0].length ]);
32
27
  }
33
- });
28
+ while ((match = linkRe.exec(line)) !== null) {
29
+ const [ , text, destination ] = match;
30
+ if (destination) {
31
+ exclusions.push(
32
+ [ lineIndex, match.index + text.length, destination.length ]
33
+ );
34
+ }
35
+ }
36
+ }
34
37
  });
35
- // For each proper name...
36
- names.forEach((name) => {
38
+ if (!includeCodeBlocks) {
39
+ exclusions.push(...inlineCodeSpanRanges());
40
+ }
41
+ for (const name of names) {
37
42
  const escapedName = escapeForRegExp(name);
38
- const startNamePattern = startNonWordRe.test(name) ? "" : "\\S*\\b";
39
- const endNamePattern = endNonWordRe.test(name) ? "" : "\\b\\S*";
43
+ const startNamePattern = /^\W/.test(name) ? "" : "\\b_*";
44
+ const endNamePattern = /\W$/.test(name) ? "" : "_*\\b";
40
45
  const namePattern =
41
- `(${startNamePattern})(${escapedName})(${endNamePattern})`;
42
- const anyNameRe = new RegExp(namePattern, "gi");
43
- // eslint-disable-next-line jsdoc/require-jsdoc
44
- function forToken(token) {
45
- if (!autolinkText.has(token)) {
46
- const fenceOffset = (token.type === "fence") ? 1 : 0;
47
- token.content.split(newLineRe).forEach((line, index) => {
48
- let match = null;
49
- while ((match = anyNameRe.exec(line)) !== null) {
50
- const [ fullMatch, leftMatch, nameMatch, rightMatch ] = match;
51
- if (fullMatch.search(bareUrlRe) === -1) {
52
- const wordMatch = fullMatch
53
- .replace(new RegExp(`^\\W{0,${leftMatch.length}}`), "")
54
- .replace(new RegExp(`\\W{0,${rightMatch.length}}$`), "");
55
- if (!names.includes(wordMatch)) {
56
- const lineNumber = token.lineNumber + index + fenceOffset;
57
- const fullLine = params.lines[lineNumber - 1];
58
- const matchLength = wordMatch.length;
59
- const matchIndex = fullLine.indexOf(wordMatch);
60
- const range = (matchIndex === -1) ?
61
- null :
62
- [ matchIndex + 1, matchLength ];
63
- const fixInfo = (matchIndex === -1) ?
64
- null :
65
- {
66
- "editColumn": matchIndex + 1,
67
- "deleteCount": matchLength,
68
- "insertText": name
69
- };
70
- addErrorDetailIf(
71
- onError,
72
- lineNumber,
73
- name,
74
- nameMatch,
75
- null,
76
- null,
77
- range,
78
- fixInfo
79
- );
46
+ `(${startNamePattern})(${escapedName})${endNamePattern}`;
47
+ const nameRe = new RegExp(namePattern, "gi");
48
+ forEachLine(lineMetadata(), (line, lineIndex, inCode, onFence) => {
49
+ if (includeCodeBlocks || (!inCode && !onFence)) {
50
+ let match = null;
51
+ while ((match = nameRe.exec(line)) !== null) {
52
+ const [ , leftMatch, nameMatch ] = match;
53
+ const index = match.index + leftMatch.length;
54
+ const length = nameMatch.length;
55
+ if (!overlapsAnyRange(exclusions, lineIndex, index, length)) {
56
+ addErrorDetailIf(
57
+ onError,
58
+ lineIndex + 1,
59
+ name,
60
+ nameMatch,
61
+ null,
62
+ null,
63
+ [ index + 1, length ],
64
+ {
65
+ "editColumn": index + 1,
66
+ "deleteCount": length,
67
+ "insertText": name
80
68
  }
81
- }
69
+ );
82
70
  }
83
- });
71
+ exclusions.push([ lineIndex, index, length ]);
72
+ }
84
73
  }
85
- }
86
- forEachInlineChild(params, "text", forToken);
87
- if (includeCodeBlocks) {
88
- forEachInlineChild(params, "code_inline", forToken);
89
- filterTokens(params, "code_block", forToken);
90
- filterTokens(params, "fence", forToken);
91
- }
92
- });
74
+ });
75
+ }
93
76
  }
94
77
  };
package/lib/rules.js CHANGED
@@ -54,7 +54,8 @@ const rules = [
54
54
  ];
55
55
  rules.forEach((rule) => {
56
56
  const name = rule.names[0].toLowerCase();
57
- rule.information =
57
+ // eslint-disable-next-line dot-notation
58
+ rule["information"] =
58
59
  new URL(`${homepage}/blob/v${version}/doc/Rules.md#${name}`);
59
60
  });
60
61
  module.exports = rules;