markdownlint 0.28.2 → 0.30.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/md032.js CHANGED
@@ -4,46 +4,72 @@
4
4
 
5
5
  const { addErrorContext, blockquotePrefixRe, isBlankLine } =
6
6
  require("../helpers");
7
- const { flattenedLists } = require("./cache");
7
+ const { filterByPredicate, flattenedChildren } =
8
+ require("../helpers/micromark.cjs");
9
+
10
+ const nonContentTokens = new Set([
11
+ "blockQuoteMarker",
12
+ "blockQuotePrefix",
13
+ "blockQuotePrefixWhitespace",
14
+ "lineEnding",
15
+ "lineEndingBlank",
16
+ "linePrefix",
17
+ "listItemIndent"
18
+ ]);
19
+ const isList = (token) => (
20
+ (token.type === "listOrdered") || (token.type === "listUnordered")
21
+ );
22
+ const addBlankLineError = (onError, lines, lineIndex, lineNumber) => {
23
+ const line = lines[lineIndex];
24
+ const quotePrefix = line.match(blockquotePrefixRe)[0].trimEnd();
25
+ addErrorContext(
26
+ onError,
27
+ lineIndex + 1,
28
+ line.trim(),
29
+ null,
30
+ null,
31
+ null,
32
+ {
33
+ lineNumber,
34
+ "insertText": `${quotePrefix}\n`
35
+ }
36
+ );
37
+ };
8
38
 
9
39
  module.exports = {
10
40
  "names": [ "MD032", "blanks-around-lists" ],
11
41
  "description": "Lists should be surrounded by blank lines",
12
42
  "tags": [ "bullet", "ul", "ol", "blank_lines" ],
13
43
  "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];
44
+ const { lines, parsers } = params;
45
+
46
+ // For every top-level list...
47
+ const topLevelLists = filterByPredicate(
48
+ parsers.micromark.tokens,
49
+ isList,
50
+ (token) => (isList(token) ? [] : token.children)
51
+ );
52
+ for (const list of topLevelLists) {
53
+
54
+ // Look for a blank line above the list
55
+ const firstIndex = list.startLine - 1;
18
56
  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
- });
57
+ addBlankLineError(onError, lines, firstIndex);
58
+ }
59
+
60
+ // Find the "visual" end of the list
61
+ let endLine = list.endLine;
62
+ for (const child of flattenedChildren(list).reverse()) {
63
+ if (!nonContentTokens.has(child.type)) {
64
+ endLine = child.endLine;
65
+ break;
66
+ }
31
67
  }
32
- const lastIndex = list.lastLineIndex - 1;
68
+
69
+ // Look for a blank line below the list
70
+ const lastIndex = endLine - 1;
33
71
  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
- });
72
+ addBlankLineError(onError, lines, lastIndex, lastIndex + 2);
47
73
  }
48
74
  }
49
75
  }
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 { filterByHtmlTokens, getHtmlTagInfo } =
7
7
  require("../helpers/micromark.cjs");
8
8
 
9
9
  const nextLinesRe = /[\r\n][\s\S]*$/;
@@ -16,50 +16,24 @@ 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
+ for (const token of filterByHtmlTokens(params.parsers.micromark.tokens)) {
20
+ const htmlTagInfo = getHtmlTagInfo(token);
21
+ if (
22
+ htmlTagInfo &&
23
+ !htmlTagInfo.close &&
24
+ !allowedElements.includes(htmlTagInfo.name.toLowerCase())
25
+ ) {
26
+ const range = [
27
+ token.startColumn,
28
+ token.text.replace(nextLinesRe, "").length
29
+ ];
30
+ addError(
31
+ onError,
32
+ token.startLine,
33
+ "Element: " + htmlTagInfo.name,
34
+ undefined,
35
+ range
36
+ );
63
37
  }
64
38
  }
65
39
  }
package/lib/md034.js CHANGED
@@ -3,7 +3,7 @@
3
3
  "use strict";
4
4
 
5
5
  const { addErrorContext } = require("../helpers");
6
- const { filterByPredicate, getHtmlTagInfo } =
6
+ const { filterByPredicate, getHtmlTagInfo, parse } =
7
7
  require("../helpers/micromark.cjs");
8
8
 
9
9
  module.exports = {
@@ -11,9 +11,9 @@ module.exports = {
11
11
  "description": "Bare URL used",
12
12
  "tags": [ "links", "url" ],
13
13
  "function": function MD034(params, onError) {
14
- const literalAutolinks =
14
+ const literalAutolinks = (tokens) => (
15
15
  filterByPredicate(
16
- params.parsers.micromark.tokens,
16
+ tokens,
17
17
  (token) => token.type === "literalAutolink",
18
18
  (token) => {
19
19
  const { children } = token;
@@ -43,26 +43,33 @@ module.exports = {
43
43
  }
44
44
  }
45
45
  return result;
46
- });
47
- for (const token of literalAutolinks) {
48
- const range = [
49
- token.startColumn,
50
- token.endColumn - token.startColumn
51
- ];
52
- const fixInfo = {
53
- "editColumn": range[0],
54
- "deleteCount": range[1],
55
- "insertText": `<${token.text}>`
56
- };
57
- addErrorContext(
58
- onError,
59
- token.startLine,
60
- token.text,
61
- null,
62
- null,
63
- range,
64
- fixInfo
65
- );
46
+ }
47
+ )
48
+ );
49
+ if (literalAutolinks(params.parsers.micromark.tokens).length > 0) {
50
+ // Re-parse with correct link/image reference definition handling
51
+ const document = params.lines.join("\n");
52
+ const tokens = parse(document, undefined, false);
53
+ for (const token of literalAutolinks(tokens)) {
54
+ const range = [
55
+ token.startColumn,
56
+ token.endColumn - token.startColumn
57
+ ];
58
+ const fixInfo = {
59
+ "editColumn": range[0],
60
+ "deleteCount": range[1],
61
+ "insertText": `<${token.text}>`
62
+ };
63
+ addErrorContext(
64
+ onError,
65
+ token.startLine,
66
+ token.text,
67
+ null,
68
+ null,
69
+ range,
70
+ fixInfo
71
+ );
72
+ }
66
73
  }
67
74
  }
68
75
  };
package/lib/md037.js CHANGED
@@ -2,180 +2,97 @@
2
2
 
3
3
  "use strict";
4
4
 
5
- const { addErrorContext, emphasisMarkersInContent, forEachLine, isBlankLine,
6
- withinAnyRange } = require("../helpers");
7
- const { htmlElementRanges, lineMetadata } = require("./cache");
5
+ const { addError } = require("../helpers");
8
6
 
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 = /\|/;
7
+ const emphasisStartTextRe = /^(\S{1,3})(\s+)\S/;
8
+ const emphasisEndTextRe = /\S(\s+)(\S{1,3})$/;
15
9
 
16
10
  module.exports = {
17
11
  "names": [ "MD037", "no-space-in-emphasis" ],
18
12
  "description": "Spaces inside emphasis markers",
19
13
  "tags": [ "whitespace", "emphasis" ],
20
14
  "function": function MD037(params, onError) {
21
- const exclusions = htmlElementRanges();
22
- // eslint-disable-next-line init-declarations
23
- let effectiveEmphasisLength, emphasisIndex, emphasisKind, emphasisLength,
24
- pendingError = null;
25
- // eslint-disable-next-line jsdoc/require-jsdoc
26
- function resetRunTracking() {
27
- emphasisIndex = -1;
28
- emphasisLength = 0;
29
- emphasisKind = "";
30
- effectiveEmphasisLength = 0;
31
- pendingError = null;
15
+
16
+ // Initialize variables
17
+ const { lines, parsers } = params;
18
+ const emphasisTokensByMarker = new Map();
19
+ for (const marker of [ "_", "__", "___", "*", "**", "***" ]) {
20
+ emphasisTokensByMarker.set(marker, []);
32
21
  }
33
- // eslint-disable-next-line jsdoc/require-jsdoc
34
- function handleRunEnd(
35
- line, lineIndex, contextLength, match, matchIndex, inTable
36
- ) {
37
- // Close current run
38
- let content = line.substring(emphasisIndex, matchIndex);
39
- if (!emphasisLength) {
40
- content = content.trimStart();
22
+ const pending = [ ...parsers.micromark.tokens ];
23
+ let token = null;
24
+ while ((token = pending.shift())) {
25
+
26
+ // Use reparsed children of htmlFlow tokens
27
+ if (token.type === "htmlFlow") {
28
+ pending.unshift(...token.htmlFlowChildren);
29
+ continue;
41
30
  }
42
- if (!match) {
43
- content = content.trimEnd();
31
+ pending.push(...token.children);
32
+
33
+ // Build lists of bare tokens for each emphasis marker type
34
+ for (const emphasisTokens of emphasisTokensByMarker.values()) {
35
+ emphasisTokens.length = 0;
44
36
  }
45
- const leftSpace = leftSpaceRe.test(content);
46
- const rightSpace = rightSpaceRe.test(content);
47
- if (
48
- (leftSpace || rightSpace) &&
49
- (!inTable || !tablePipeRe.test(content))
50
- ) {
51
- // Report the violation
52
- const contextStart = emphasisIndex - emphasisLength;
53
- const contextEnd = matchIndex + contextLength;
54
- const column = contextStart + 1;
55
- const length = contextEnd - contextStart;
56
- if (!withinAnyRange(exclusions, lineIndex, column, length)) {
57
- const context = line.substring(contextStart, contextEnd);
58
- const leftMarker = line.substring(contextStart, emphasisIndex);
59
- const rightMarker = match ? (match[2] || match[3]) : "";
60
- const fixedText = `${leftMarker}${content.trim()}${rightMarker}`;
61
- return [
62
- onError,
63
- lineIndex + 1,
64
- context,
65
- leftSpace,
66
- rightSpace,
67
- [ column, length ],
68
- {
69
- "editColumn": column,
70
- "deleteCount": length,
71
- "insertText": fixedText
72
- }
73
- ];
37
+ for (const child of token.children) {
38
+ const { text, type } = child;
39
+ if ((type === "data") && (text.length <= 3)) {
40
+ const emphasisTokens = emphasisTokensByMarker.get(text);
41
+ if (emphasisTokens) {
42
+ emphasisTokens.push(child);
43
+ }
74
44
  }
75
45
  }
76
- return null;
77
- }
78
- // Initialize
79
- const ignoreMarkersByLine = emphasisMarkersInContent(params);
80
- resetRunTracking();
81
- forEachLine(
82
- lineMetadata(),
83
- (line, lineIndex, inCode, onFence, inTable, inItem, onBreak, inMath) => {
84
- const onItemStart = (inItem === 1);
85
- if (
86
- inCode ||
87
- onFence ||
88
- inTable ||
89
- onBreak ||
90
- onItemStart ||
91
- isBlankLine(line)
92
- ) {
93
- // Emphasis resets when leaving a block
94
- resetRunTracking();
95
- }
96
- if (
97
- inCode ||
98
- onFence ||
99
- onBreak ||
100
- inMath
101
- ) {
102
- // Emphasis has no meaning here
103
- return;
104
- }
105
- let patchedLine = line.replace(embeddedUnderscoreRe, "$1 $2");
106
- if (onItemStart) {
107
- // Trim overlapping '*' list item marker
108
- patchedLine = patchedLine.replace(asteriskListItemMarkerRe, "$1 $2");
109
- }
110
- let match = null;
111
- // Match all emphasis-looking runs in the line...
112
- while ((match = emphasisRe.exec(patchedLine))) {
113
- const ignoreMarkersForLine = ignoreMarkersByLine[lineIndex];
114
- const matchIndex = match.index + match[1].length;
115
- if (ignoreMarkersForLine.includes(matchIndex)) {
116
- // Ignore emphasis markers inside code spans and links
117
- continue;
118
- }
119
- const matchLength = match[0].length - match[1].length;
120
- const matchKind = (match[2] || match[3])[0];
121
- if (emphasisIndex === -1) {
122
- // New run
123
- emphasisIndex = matchIndex + matchLength;
124
- emphasisLength = matchLength;
125
- emphasisKind = matchKind;
126
- effectiveEmphasisLength = matchLength;
127
- } else if (matchKind === emphasisKind) {
128
- // Matching emphasis markers
129
- if (matchLength === effectiveEmphasisLength) {
130
- // Ending an existing run, report any pending error
131
- if (pendingError) {
132
- // @ts-ignore
133
- addErrorContext(...pendingError);
134
- pendingError = null;
135
- }
136
- const error = handleRunEnd(
137
- line,
138
- lineIndex,
139
- effectiveEmphasisLength,
140
- match,
141
- matchIndex,
142
- inTable
46
+
47
+ // Process bare tokens for each emphasis marker type
48
+ for (const emphasisTokens of emphasisTokensByMarker.values()) {
49
+ for (let i = 0; i + 1 < emphasisTokens.length; i += 2) {
50
+
51
+ // Process start token of start/end pair
52
+ const startToken = emphasisTokens[i];
53
+ const startText =
54
+ lines[startToken.startLine - 1].slice(startToken.startColumn - 1);
55
+ const startMatch = startText.match(emphasisStartTextRe);
56
+ if (startMatch) {
57
+ const [ startContext, startMarker, startSpaces ] = startMatch;
58
+ if ((startMarker === startToken.text) && (startSpaces.length > 0)) {
59
+ addError(
60
+ onError,
61
+ startToken.startLine,
62
+ undefined,
63
+ startContext,
64
+ [ startToken.startColumn, startContext.length ],
65
+ {
66
+ "editColumn": startToken.endColumn,
67
+ "deleteCount": startSpaces.length
68
+ }
143
69
  );
144
- if (error) {
145
- // @ts-ignore
146
- addErrorContext(...error);
147
- }
148
- // Reset
149
- resetRunTracking();
150
- } else if (matchLength === 3) {
151
- // Swap internal run length (1->2 or 2->1)
152
- effectiveEmphasisLength = matchLength - effectiveEmphasisLength;
153
- } else if (effectiveEmphasisLength === 3) {
154
- // Downgrade internal run (3->1 or 3->2)
155
- effectiveEmphasisLength -= matchLength;
156
- } else {
157
- // Upgrade to internal run (1->3 or 2->3)
158
- effectiveEmphasisLength += matchLength;
159
70
  }
160
- // Back up one character so RegExp has a chance to match the
161
- // next marker (ex: "**star**_underscore_")
162
- if (emphasisRe.lastIndex > 1) {
163
- emphasisRe.lastIndex--;
71
+ }
72
+
73
+ // Process end token of start/end pair
74
+ const endToken = emphasisTokens[i + 1];
75
+ const endText =
76
+ lines[endToken.startLine - 1].slice(0, endToken.endColumn - 1);
77
+ const endMatch = endText.match(emphasisEndTextRe);
78
+ if (endMatch) {
79
+ const [ endContext, endSpace, endMarker ] = endMatch;
80
+ if ((endMarker === endToken.text) && (endSpace.length > 0)) {
81
+ addError(
82
+ onError,
83
+ endToken.startLine,
84
+ undefined,
85
+ endContext,
86
+ [ endToken.endColumn - endContext.length, endContext.length ],
87
+ {
88
+ "editColumn": endToken.startColumn - endSpace.length,
89
+ "deleteCount": endSpace.length
90
+ }
91
+ );
164
92
  }
165
- } else if (emphasisRe.lastIndex > 1) {
166
- // Back up one character so RegExp has a chance to match the
167
- // mis-matched marker (ex: "*text_*")
168
- emphasisRe.lastIndex--;
169
93
  }
170
94
  }
171
- if (emphasisIndex !== -1) {
172
- pendingError = pendingError ||
173
- handleRunEnd(line, lineIndex, 0, null, line.length, inTable);
174
- // Adjust for pending run on new line
175
- emphasisIndex = 0;
176
- emphasisLength = 0;
177
- }
178
95
  }
179
- );
96
+ }
180
97
  }
181
98
  };
package/lib/md038.js CHANGED
@@ -3,7 +3,7 @@
3
3
  "use strict";
4
4
 
5
5
  const { addErrorContext } = require("../helpers");
6
- const { filterByTypes } = require("../helpers/micromark.cjs");
6
+ const { filterByTypes, tokenIfType } = require("../helpers/micromark.cjs");
7
7
 
8
8
  const leftSpaceRe = /^\s(?:[^`]|$)/;
9
9
  const rightSpaceRe = /[^`]\s$/;
@@ -17,7 +17,6 @@ const trimCodeText = (text, start, end) => {
17
17
  }
18
18
  return text;
19
19
  };
20
- const tokenIfType = (token, type) => token && (token.type === type) && token;
21
20
 
22
21
  module.exports = {
23
22
  "names": [ "MD038", "no-space-in-code" ],
package/lib/md044.js CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  "use strict";
4
4
 
5
- const { addErrorDetailIf, escapeForRegExp, withinAnyRange } =
5
+ const { addErrorDetailIf, escapeForRegExp, newLineRe, withinAnyRange } =
6
6
  require("../helpers");
7
7
  const { filterByPredicate, filterByTypes, parse } =
8
8
  require("../helpers/micromark.cjs");
@@ -30,14 +30,32 @@ module.exports = {
30
30
  scannedTypes.add("codeFlowValue");
31
31
  scannedTypes.add("codeTextData");
32
32
  }
33
+ const tokenAdjustments = new Map();
33
34
  const contentTokens =
34
35
  filterByPredicate(
35
36
  params.parsers.micromark.tokens,
36
37
  (token) => scannedTypes.has(token.type),
37
38
  (token) => {
38
39
  let { children } = token;
40
+ const { startLine, text } = token;
39
41
  if (!includeHtmlElements && (token.type === "htmlFlow")) {
40
- children = children.slice(1, -1);
42
+ if (text.startsWith("<!--")) {
43
+ // Remove comment content
44
+ children = [];
45
+ } else {
46
+ // Re-parse to get htmlText elements for detailed tokenization
47
+ const htmlTextLines =
48
+ `<md044>\n${text}\n</md044>`.split(newLineRe);
49
+ children = parse(htmlTextLines.join(""));
50
+ const reTokens = [ ...children ];
51
+ for (const reToken of reTokens) {
52
+ tokenAdjustments.set(reToken, {
53
+ htmlTextLines,
54
+ startLine
55
+ });
56
+ reTokens.push(...reToken.children);
57
+ }
58
+ }
41
59
  }
42
60
  return children.filter((t) => !ignoredChildTypes.has(t.type));
43
61
  }
@@ -78,10 +96,22 @@ module.exports = {
78
96
  autoLinked.add(token);
79
97
  }
80
98
  if (!withinAnyRange(urlRanges, lineIndex, index, length)) {
81
- const column = index + 1;
99
+ let lineNumber = token.startLine;
100
+ let column = index;
101
+ if (tokenAdjustments.has(token)) {
102
+ const { htmlTextLines, startLine } =
103
+ tokenAdjustments.get(token);
104
+ let lineDelta = 0;
105
+ while (htmlTextLines[lineDelta].length <= column) {
106
+ column -= htmlTextLines[lineDelta].length;
107
+ lineDelta++;
108
+ }
109
+ lineNumber = startLine + lineDelta - 1;
110
+ }
111
+ column++;
82
112
  addErrorDetailIf(
83
113
  onError,
84
- token.startLine,
114
+ lineNumber,
85
115
  name,
86
116
  nameMatch,
87
117
  null,
package/lib/md045.js CHANGED
@@ -2,17 +2,29 @@
2
2
 
3
3
  "use strict";
4
4
 
5
- const { addError, forEachInlineChild } = require("../helpers");
5
+ const { addError } = require("../helpers");
6
+ const { filterByTypes } = require("../helpers/micromark.cjs");
6
7
 
7
8
  module.exports = {
8
9
  "names": [ "MD045", "no-alt-text" ],
9
10
  "description": "Images should have alternate text (alt text)",
10
11
  "tags": [ "accessibility", "images" ],
11
12
  "function": function MD045(params, onError) {
12
- forEachInlineChild(params, "image", function forToken(token) {
13
- if (token.content === "") {
14
- addError(onError, token.lineNumber);
13
+ const images = filterByTypes(params.parsers.micromark.tokens, [ "image" ]);
14
+ for (const image of images) {
15
+ const labelTexts = filterByTypes(image.children, [ "labelText" ]);
16
+ if (labelTexts.some((labelText) => labelText.text.length === 0)) {
17
+ const range = (image.startLine === image.endLine) ?
18
+ [ image.startColumn, image.endColumn - image.startColumn ] :
19
+ undefined;
20
+ addError(
21
+ onError,
22
+ image.startLine,
23
+ undefined,
24
+ undefined,
25
+ range
26
+ );
15
27
  }
16
- });
28
+ }
17
29
  }
18
30
  };