markdownlint 0.29.0 → 0.31.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/md022.js CHANGED
@@ -2,8 +2,25 @@
2
2
 
3
3
  "use strict";
4
4
 
5
- const { addErrorDetailIf, blockquotePrefixRe, filterTokens, isBlankLine } =
5
+ const { addErrorDetailIf, blockquotePrefixRe, isBlankLine } =
6
6
  require("../helpers");
7
+ const { filterByTypes, getHeadingLevel } =
8
+ require("../helpers/micromark.cjs");
9
+
10
+ const defaultLines = 1;
11
+
12
+ const getLinesFunction = (linesParam) => {
13
+ if (Array.isArray(linesParam)) {
14
+ const linesArray = new Array(6).fill(defaultLines);
15
+ for (const [ index, value ] of [ ...linesParam.entries() ].slice(0, 6)) {
16
+ linesArray[index] = value;
17
+ }
18
+ return (heading) => linesArray[getHeadingLevel(heading) - 1];
19
+ }
20
+ // Coerce linesParam to a number
21
+ const lines = (linesParam === undefined) ? defaultLines : Number(linesParam);
22
+ return () => lines;
23
+ };
7
24
 
8
25
  const getBlockQuote = (str, count) => (
9
26
  (str || "")
@@ -19,50 +36,73 @@ module.exports = {
19
36
  "description": "Headings should be surrounded by blank lines",
20
37
  "tags": [ "headings", "headers", "blank_lines" ],
21
38
  "function": function MD022(params, onError) {
22
- let linesAbove = params.config.lines_above;
23
- linesAbove = Number((linesAbove === undefined) ? 1 : linesAbove);
24
- let linesBelow = params.config.lines_below;
25
- linesBelow = Number((linesBelow === undefined) ? 1 : linesBelow);
26
- const { lines } = params;
27
- filterTokens(params, "heading_open", (token) => {
28
- const [ topIndex, nextIndex ] = token.map;
29
- let actualAbove = 0;
30
- for (let i = 0; i < linesAbove; i++) {
31
- if (isBlankLine(lines[topIndex - i - 1])) {
39
+ const getLinesAbove = getLinesFunction(params.config.lines_above);
40
+ const getLinesBelow = getLinesFunction(params.config.lines_below);
41
+ const { lines, parsers } = params;
42
+ const headings = filterByTypes(
43
+ parsers.micromark.tokens,
44
+ [ "atxHeading", "setextHeading" ]
45
+ );
46
+ for (const heading of headings) {
47
+ const { startLine, endLine } = heading;
48
+ const line = lines[startLine - 1].trim();
49
+
50
+ // Check lines above
51
+ const linesAbove = getLinesAbove(heading);
52
+ if (linesAbove >= 0) {
53
+ let actualAbove = 0;
54
+ for (
55
+ let i = 0;
56
+ (i < linesAbove) && isBlankLine(lines[startLine - 2 - i]);
57
+ i++
58
+ ) {
32
59
  actualAbove++;
33
60
  }
61
+ addErrorDetailIf(
62
+ onError,
63
+ startLine,
64
+ linesAbove,
65
+ actualAbove,
66
+ "Above",
67
+ line,
68
+ null,
69
+ {
70
+ "insertText": getBlockQuote(
71
+ lines[startLine - 2],
72
+ linesAbove - actualAbove
73
+ )
74
+ }
75
+ );
34
76
  }
35
- addErrorDetailIf(
36
- onError,
37
- topIndex + 1,
38
- linesAbove,
39
- actualAbove,
40
- "Above",
41
- lines[topIndex].trim(),
42
- null,
43
- {
44
- "insertText":
45
- getBlockQuote(lines[topIndex - 1], linesAbove - actualAbove)
46
- });
47
- let actualBelow = 0;
48
- for (let i = 0; i < linesBelow; i++) {
49
- if (isBlankLine(lines[nextIndex + i])) {
77
+
78
+ // Check lines below
79
+ const linesBelow = getLinesBelow(heading);
80
+ if (linesBelow >= 0) {
81
+ let actualBelow = 0;
82
+ for (
83
+ let i = 0;
84
+ (i < linesBelow) && isBlankLine(lines[endLine + i]);
85
+ i++
86
+ ) {
50
87
  actualBelow++;
51
88
  }
89
+ addErrorDetailIf(
90
+ onError,
91
+ startLine,
92
+ linesBelow,
93
+ actualBelow,
94
+ "Below",
95
+ line,
96
+ null,
97
+ {
98
+ "lineNumber": endLine + 1,
99
+ "insertText": getBlockQuote(
100
+ lines[endLine],
101
+ linesBelow - actualBelow
102
+ )
103
+ }
104
+ );
52
105
  }
53
- addErrorDetailIf(
54
- onError,
55
- topIndex + 1,
56
- linesBelow,
57
- actualBelow,
58
- "Below",
59
- lines[topIndex].trim(),
60
- null,
61
- {
62
- "lineNumber": nextIndex + 1,
63
- "insertText":
64
- getBlockQuote(lines[nextIndex], linesBelow - actualBelow)
65
- });
66
- });
106
+ }
67
107
  }
68
108
  };
package/lib/md026.js CHANGED
@@ -2,10 +2,10 @@
2
2
 
3
3
  "use strict";
4
4
 
5
- const { addError, allPunctuationNoQuestion, escapeForRegExp, forEachHeading } =
6
- require("../helpers");
5
+ const { addError, allPunctuationNoQuestion, endOfLineGemojiCodeRe,
6
+ endOfLineHtmlEntityRe, escapeForRegExp } = require("../helpers");
7
+ const { filterByTypes } = require("../helpers/micromark.cjs");
7
8
 
8
- const endOfLineHtmlEntityRe = /&#?[\da-zA-Z]+;$/;
9
9
 
10
10
  module.exports = {
11
11
  "names": [ "MD026", "no-trailing-punctuation" ],
@@ -18,19 +18,26 @@ module.exports = {
18
18
  );
19
19
  const trailingPunctuationRe =
20
20
  new RegExp("\\s*[" + escapeForRegExp(punctuation) + "]+$");
21
- forEachHeading(params, (heading) => {
22
- const { line, lineNumber } = heading;
23
- const trimmedLine = line.replace(/([^\s#])[\s#]+$/, "$1");
24
- const match = trailingPunctuationRe.exec(trimmedLine);
25
- if (match && !endOfLineHtmlEntityRe.test(trimmedLine)) {
21
+ const headings = filterByTypes(
22
+ params.parsers.micromark.tokens,
23
+ [ "atxHeadingText", "setextHeadingText" ]
24
+ );
25
+ for (const heading of headings) {
26
+ const { endLine, startColumn, text } = heading;
27
+ const match = trailingPunctuationRe.exec(text);
28
+ if (
29
+ match &&
30
+ !endOfLineHtmlEntityRe.test(text) &&
31
+ !endOfLineGemojiCodeRe.test(text)
32
+ ) {
26
33
  const fullMatch = match[0];
27
- const column = match.index + 1;
34
+ const column = startColumn + match.index;
28
35
  const length = fullMatch.length;
29
36
  addError(
30
37
  onError,
31
- lineNumber,
38
+ endLine,
32
39
  `Punctuation: '${fullMatch}'`,
33
- null,
40
+ undefined,
34
41
  [ column, length ],
35
42
  {
36
43
  "editColumn": column,
@@ -38,6 +45,6 @@ module.exports = {
38
45
  }
39
46
  );
40
47
  }
41
- });
48
+ }
42
49
  }
43
50
  };
package/lib/md032.js CHANGED
@@ -4,46 +4,77 @@
4
4
 
5
5
  const { addErrorContext, blockquotePrefixRe, isBlankLine } =
6
6
  require("../helpers");
7
- const { flattenedLists } = require("./cache");
7
+ const { filterByPredicate } = require("../helpers/micromark.cjs");
8
+
9
+ const nonContentTokens = new Set([
10
+ "blockQuoteMarker",
11
+ "blockQuotePrefix",
12
+ "blockQuotePrefixWhitespace",
13
+ "lineEnding",
14
+ "lineEndingBlank",
15
+ "linePrefix",
16
+ "listItemIndent"
17
+ ]);
18
+ const isList = (token) => (
19
+ (token.type === "listOrdered") || (token.type === "listUnordered")
20
+ );
21
+ const addBlankLineError = (onError, lines, lineIndex, lineNumber) => {
22
+ const line = lines[lineIndex];
23
+ const quotePrefix = line.match(blockquotePrefixRe)[0].trimEnd();
24
+ addErrorContext(
25
+ onError,
26
+ lineIndex + 1,
27
+ line.trim(),
28
+ null,
29
+ null,
30
+ null,
31
+ {
32
+ lineNumber,
33
+ "insertText": `${quotePrefix}\n`
34
+ }
35
+ );
36
+ };
8
37
 
9
38
  module.exports = {
10
39
  "names": [ "MD032", "blanks-around-lists" ],
11
40
  "description": "Lists should be surrounded by blank lines",
12
41
  "tags": [ "bullet", "ul", "ol", "blank_lines" ],
13
42
  "function": function MD032(params, onError) {
14
- const { lines } = params;
15
- const filteredLists = flattenedLists().filter((list) => !list.nesting);
16
- for (const list of filteredLists) {
17
- const firstIndex = list.open.map[0];
43
+ const { lines, parsers } = params;
44
+
45
+ // For every top-level list...
46
+ const topLevelLists = filterByPredicate(
47
+ parsers.micromark.tokens,
48
+ isList,
49
+ (token) => (
50
+ (isList(token) || (token.type === "htmlFlow")) ? [] : token.children
51
+ )
52
+ );
53
+ for (const list of topLevelLists) {
54
+
55
+ // Look for a blank line above the list
56
+ const firstIndex = list.startLine - 1;
18
57
  if (!isBlankLine(lines[firstIndex - 1])) {
19
- const line = lines[firstIndex];
20
- const quotePrefix = line.match(blockquotePrefixRe)[0].trimEnd();
21
- addErrorContext(
22
- onError,
23
- firstIndex + 1,
24
- line.trim(),
25
- null,
26
- null,
27
- null,
28
- {
29
- "insertText": `${quotePrefix}\n`
30
- });
58
+ addBlankLineError(onError, lines, firstIndex);
59
+ }
60
+
61
+ // Find the "visual" end of the list
62
+ let endLine = list.endLine;
63
+ const flattenedChildren = filterByPredicate(
64
+ list.children,
65
+ () => true
66
+ );
67
+ for (const child of flattenedChildren.reverse()) {
68
+ if (!nonContentTokens.has(child.type)) {
69
+ endLine = child.endLine;
70
+ break;
71
+ }
31
72
  }
32
- const lastIndex = list.lastLineIndex - 1;
73
+
74
+ // Look for a blank line below the list
75
+ const lastIndex = endLine - 1;
33
76
  if (!isBlankLine(lines[lastIndex + 1])) {
34
- const line = lines[lastIndex];
35
- const quotePrefix = line.match(blockquotePrefixRe)[0].trimEnd();
36
- addErrorContext(
37
- onError,
38
- lastIndex + 1,
39
- line.trim(),
40
- null,
41
- null,
42
- null,
43
- {
44
- "lineNumber": lastIndex + 2,
45
- "insertText": `${quotePrefix}\n`
46
- });
77
+ addBlankLineError(onError, lines, lastIndex, lastIndex + 2);
47
78
  }
48
79
  }
49
80
  }
package/lib/md033.js CHANGED
@@ -3,7 +3,7 @@
3
3
  "use strict";
4
4
 
5
5
  const { addError } = require("../helpers");
6
- const { filterByTypes, getHtmlTagInfo, parse } =
6
+ const { filterByTypes, getHtmlTagInfo } =
7
7
  require("../helpers/micromark.cjs");
8
8
 
9
9
  const nextLinesRe = /[\r\n][\s\S]*$/;
@@ -16,50 +16,25 @@ module.exports = {
16
16
  let allowedElements = params.config.allowed_elements;
17
17
  allowedElements = Array.isArray(allowedElements) ? allowedElements : [];
18
18
  allowedElements = allowedElements.map((element) => element.toLowerCase());
19
- const pending = [ [ 0, params.parsers.micromark.tokens ] ];
20
- let current = null;
21
- while ((current = pending.shift())) {
22
- const [ offset, tokens ] = current;
23
- for (const token of filterByTypes(tokens, [ "htmlFlow", "htmlText" ])) {
24
- if (token.type === "htmlText") {
25
- const htmlTagInfo = getHtmlTagInfo(token);
26
- if (
27
- htmlTagInfo &&
28
- !htmlTagInfo.close &&
29
- !allowedElements.includes(htmlTagInfo.name.toLowerCase())
30
- ) {
31
- const range = [
32
- token.startColumn,
33
- token.text.replace(nextLinesRe, "").length
34
- ];
35
- addError(
36
- onError,
37
- token.startLine + offset,
38
- "Element: " + htmlTagInfo.name,
39
- undefined,
40
- range
41
- );
42
- }
43
- } else {
44
- // token.type === "htmlFlow"
45
- // Re-parse without "htmlFlow" to get only "htmlText" tokens
46
- const options = {
47
- "extensions": [
48
- {
49
- "disable": {
50
- "null": [ "codeIndented", "htmlFlow" ]
51
- }
52
- }
53
- ]
54
- };
55
- // Use lines instead of token.text for accurate columns
56
- const lines =
57
- params.lines.slice(token.startLine - 1, token.endLine).join("\n");
58
- const flowTokens = parse(lines, options);
59
- pending.push(
60
- [ token.startLine - 1, flowTokens ]
61
- );
62
- }
19
+ const { tokens } = params.parsers.micromark;
20
+ for (const token of filterByTypes(tokens, [ "htmlText" ])) {
21
+ const htmlTagInfo = getHtmlTagInfo(token);
22
+ if (
23
+ htmlTagInfo &&
24
+ !htmlTagInfo.close &&
25
+ !allowedElements.includes(htmlTagInfo.name.toLowerCase())
26
+ ) {
27
+ const range = [
28
+ token.startColumn,
29
+ token.text.replace(nextLinesRe, "").length
30
+ ];
31
+ addError(
32
+ onError,
33
+ token.startLine,
34
+ "Element: " + htmlTagInfo.name,
35
+ undefined,
36
+ range
37
+ );
63
38
  }
64
39
  }
65
40
  }
package/lib/md037.js CHANGED
@@ -2,184 +2,90 @@
2
2
 
3
3
  "use strict";
4
4
 
5
- const { addErrorContext, emphasisMarkersInContent, forEachLine, isBlankLine,
6
- withinAnyRange } = require("../helpers");
7
- const { htmlElementRanges, lineMetadata } = require("./cache");
8
-
9
- const emphasisRe = /(^|[^\\]|\\\\)(?:(\*{1,3})|(_{1,3}))/g;
10
- const embeddedUnderscoreRe = /([A-Za-z\d])(_([A-Za-z\d]))+/g;
11
- const asteriskListItemMarkerRe = /^([\s>]*)\*(\s+)/;
12
- const leftSpaceRe = /^\s+/;
13
- const rightSpaceRe = /\s+$/;
14
- const tablePipeRe = /\|/;
15
- const allUnderscoresRe = /_/g;
5
+ const { addError } = require("../helpers");
6
+ const { filterByPredicate } = require("../helpers/micromark.cjs");
16
7
 
17
8
  module.exports = {
18
9
  "names": [ "MD037", "no-space-in-emphasis" ],
19
10
  "description": "Spaces inside emphasis markers",
20
11
  "tags": [ "whitespace", "emphasis" ],
21
12
  "function": function MD037(params, onError) {
22
- const exclusions = htmlElementRanges();
23
- // eslint-disable-next-line init-declarations
24
- let effectiveEmphasisLength, emphasisIndex, emphasisKind, emphasisLength,
25
- pendingError = null;
26
- // eslint-disable-next-line jsdoc/require-jsdoc
27
- function resetRunTracking() {
28
- emphasisIndex = -1;
29
- emphasisLength = 0;
30
- emphasisKind = "";
31
- effectiveEmphasisLength = 0;
32
- pendingError = null;
13
+
14
+ // Initialize variables
15
+ const { lines, parsers } = params;
16
+ const emphasisTokensByMarker = new Map();
17
+ for (const marker of [ "_", "__", "___", "*", "**", "***" ]) {
18
+ emphasisTokensByMarker.set(marker, []);
33
19
  }
34
- // eslint-disable-next-line jsdoc/require-jsdoc
35
- function handleRunEnd(
36
- line, lineIndex, contextLength, match, matchIndex, inTable
37
- ) {
38
- // Close current run
39
- let content = line.substring(emphasisIndex, matchIndex);
40
- if (!emphasisLength) {
41
- content = content.trimStart();
42
- }
43
- if (!match) {
44
- content = content.trimEnd();
20
+ const tokens = filterByPredicate(
21
+ parsers.micromark.tokens,
22
+ (token) => token.children.some((child) => child.type === "data")
23
+ );
24
+ for (const token of tokens) {
25
+
26
+ // Build lists of bare tokens for each emphasis marker type
27
+ for (const emphasisTokens of emphasisTokensByMarker.values()) {
28
+ emphasisTokens.length = 0;
45
29
  }
46
- const leftSpace = leftSpaceRe.test(content);
47
- const rightSpace = rightSpaceRe.test(content);
48
- if (
49
- (leftSpace || rightSpace) &&
50
- (!inTable || !tablePipeRe.test(content))
51
- ) {
52
- // Report the violation
53
- const contextStart = emphasisIndex - emphasisLength;
54
- const contextEnd = matchIndex + contextLength;
55
- const column = contextStart + 1;
56
- const length = contextEnd - contextStart;
57
- if (!withinAnyRange(exclusions, lineIndex, column, length)) {
58
- const context = line.substring(contextStart, contextEnd);
59
- const leftMarker = line.substring(contextStart, emphasisIndex);
60
- const rightMarker = match ? (match[2] || match[3]) : "";
61
- const fixedText = `${leftMarker}${content.trim()}${rightMarker}`;
62
- return [
63
- onError,
64
- lineIndex + 1,
65
- context,
66
- leftSpace,
67
- rightSpace,
68
- [ column, length ],
69
- {
70
- "editColumn": column,
71
- "deleteCount": length,
72
- "insertText": fixedText
73
- }
74
- ];
30
+ for (const child of token.children) {
31
+ const { text, type } = child;
32
+ if ((type === "data") && (text.length <= 3)) {
33
+ const emphasisTokens = emphasisTokensByMarker.get(text);
34
+ if (emphasisTokens) {
35
+ emphasisTokens.push(child);
36
+ }
75
37
  }
76
38
  }
77
- return null;
78
- }
79
- // Initialize
80
- const ignoreMarkersByLine = emphasisMarkersInContent(params);
81
- resetRunTracking();
82
- forEachLine(
83
- lineMetadata(),
84
- (line, lineIndex, inCode, onFence, inTable, inItem, onBreak, inMath) => {
85
- const onItemStart = (inItem === 1);
86
- if (
87
- inCode ||
88
- onFence ||
89
- inTable ||
90
- onBreak ||
91
- onItemStart ||
92
- isBlankLine(line)
93
- ) {
94
- // Emphasis resets when leaving a block
95
- resetRunTracking();
96
- }
97
- if (
98
- inCode ||
99
- onFence ||
100
- onBreak ||
101
- inMath
102
- ) {
103
- // Emphasis has no meaning here
104
- return;
105
- }
106
- let patchedLine = line.replace(
107
- embeddedUnderscoreRe,
108
- (match) => match.replace(allUnderscoresRe, " ")
109
- );
110
- if (onItemStart) {
111
- // Trim overlapping '*' list item marker
112
- patchedLine = patchedLine.replace(asteriskListItemMarkerRe, "$1 $2");
113
- }
114
- let match = null;
115
- // Match all emphasis-looking runs in the line...
116
- while ((match = emphasisRe.exec(patchedLine))) {
117
- const ignoreMarkersForLine = ignoreMarkersByLine[lineIndex];
118
- const matchIndex = match.index + match[1].length;
119
- if (ignoreMarkersForLine.includes(matchIndex)) {
120
- // Ignore emphasis markers inside code spans and links
121
- continue;
122
- }
123
- const matchLength = match[0].length - match[1].length;
124
- const matchKind = (match[2] || match[3])[0];
125
- if (emphasisIndex === -1) {
126
- // New run
127
- emphasisIndex = matchIndex + matchLength;
128
- emphasisLength = matchLength;
129
- emphasisKind = matchKind;
130
- effectiveEmphasisLength = matchLength;
131
- } else if (matchKind === emphasisKind) {
132
- // Matching emphasis markers
133
- if (matchLength === effectiveEmphasisLength) {
134
- // Ending an existing run, report any pending error
135
- if (pendingError) {
136
- // @ts-ignore
137
- addErrorContext(...pendingError);
138
- pendingError = null;
39
+
40
+ // Process bare tokens for each emphasis marker type
41
+ for (const entry of emphasisTokensByMarker.entries()) {
42
+ const [ marker, emphasisTokens ] = entry;
43
+ for (let i = 0; i + 1 < emphasisTokens.length; i += 2) {
44
+
45
+ // Process start token of start/end pair
46
+ const startToken = emphasisTokens[i];
47
+ const startLine = lines[startToken.startLine - 1];
48
+ const startSlice = startLine.slice(startToken.endColumn - 1);
49
+ const startMatch = startSlice.match(/^\s+\S/);
50
+ if (startMatch) {
51
+ const [ startSpaceCharacter ] = startMatch;
52
+ const startContext = `${marker}${startSpaceCharacter}`;
53
+ addError(
54
+ onError,
55
+ startToken.startLine,
56
+ undefined,
57
+ startContext,
58
+ [ startToken.startColumn, startContext.length ],
59
+ {
60
+ "editColumn": startToken.endColumn,
61
+ "deleteCount": startSpaceCharacter.length - 1
139
62
  }
140
- const error = handleRunEnd(
141
- line,
142
- lineIndex,
143
- effectiveEmphasisLength,
144
- match,
145
- matchIndex,
146
- inTable
147
- );
148
- if (error) {
149
- // @ts-ignore
150
- addErrorContext(...error);
63
+ );
64
+ }
65
+
66
+ // Process end token of start/end pair
67
+ const endToken = emphasisTokens[i + 1];
68
+ const endLine = lines[endToken.startLine - 1];
69
+ const endSlice = endLine.slice(0, endToken.startColumn - 1);
70
+ const endMatch = endSlice.match(/\S\s+$/);
71
+ if (endMatch) {
72
+ const [ endSpaceCharacter ] = endMatch;
73
+ const endContext = `${endSpaceCharacter}${marker}`;
74
+ addError(
75
+ onError,
76
+ endToken.startLine,
77
+ undefined,
78
+ endContext,
79
+ [ endToken.endColumn - endContext.length, endContext.length ],
80
+ {
81
+ "editColumn":
82
+ endToken.startColumn - (endSpaceCharacter.length - 1),
83
+ "deleteCount": endSpaceCharacter.length - 1
151
84
  }
152
- // Reset
153
- resetRunTracking();
154
- } else if (matchLength === 3) {
155
- // Swap internal run length (1->2 or 2->1)
156
- effectiveEmphasisLength = matchLength - effectiveEmphasisLength;
157
- } else if (effectiveEmphasisLength === 3) {
158
- // Downgrade internal run (3->1 or 3->2)
159
- effectiveEmphasisLength -= matchLength;
160
- } else {
161
- // Upgrade to internal run (1->3 or 2->3)
162
- effectiveEmphasisLength += matchLength;
163
- }
164
- // Back up one character so RegExp has a chance to match the
165
- // next marker (ex: "**star**_underscore_")
166
- if (emphasisRe.lastIndex > 1) {
167
- emphasisRe.lastIndex--;
168
- }
169
- } else if (emphasisRe.lastIndex > 1) {
170
- // Back up one character so RegExp has a chance to match the
171
- // mis-matched marker (ex: "*text_*")
172
- emphasisRe.lastIndex--;
85
+ );
173
86
  }
174
87
  }
175
- if (emphasisIndex !== -1) {
176
- pendingError = pendingError ||
177
- handleRunEnd(line, lineIndex, 0, null, line.length, inTable);
178
- // Adjust for pending run on new line
179
- emphasisIndex = 0;
180
- emphasisLength = 0;
181
- }
182
88
  }
183
- );
89
+ }
184
90
  }
185
91
  };