markdownlint 0.37.3 → 0.38.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.
Files changed (50) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/CONTRIBUTING.md +2 -3
  3. package/README.md +39 -21
  4. package/doc/Rules.md +158 -35
  5. package/doc/md027.md +8 -0
  6. package/doc/md038.md +23 -11
  7. package/doc/md041.md +31 -16
  8. package/doc/md043.md +22 -4
  9. package/doc/md045.md +9 -1
  10. package/doc/md051.md +17 -0
  11. package/doc/md052.md +12 -0
  12. package/doc/md053.md +3 -3
  13. package/doc/md059.md +30 -0
  14. package/helpers/README.md +2 -2
  15. package/helpers/helpers.cjs +4 -6
  16. package/helpers/micromark-helpers.cjs +29 -3
  17. package/helpers/package.json +1 -1
  18. package/lib/cache.mjs +19 -5
  19. package/lib/configuration-strict.d.ts +190 -126
  20. package/lib/constants.mjs +1 -1
  21. package/lib/defer-require.cjs +16 -0
  22. package/lib/exports.d.mts +2 -0
  23. package/lib/exports.mjs +2 -0
  24. package/lib/markdownit.cjs +6 -13
  25. package/lib/markdownlint.d.mts +36 -2
  26. package/lib/markdownlint.mjs +314 -292
  27. package/lib/md025.mjs +23 -15
  28. package/lib/md027.mjs +11 -1
  29. package/lib/md036.mjs +12 -2
  30. package/lib/md038.mjs +52 -71
  31. package/lib/md039.mjs +3 -22
  32. package/lib/md041.mjs +54 -16
  33. package/lib/md043.mjs +2 -0
  34. package/lib/md045.mjs +3 -1
  35. package/lib/md051.mjs +13 -5
  36. package/lib/md052.mjs +2 -1
  37. package/lib/md059.mjs +71 -0
  38. package/lib/micromark-parse.mjs +14 -3
  39. package/lib/node-imports-browser.mjs +0 -2
  40. package/lib/node-imports-node.mjs +0 -3
  41. package/lib/parse-configuration.mjs +46 -0
  42. package/lib/resolve-module.cjs +52 -0
  43. package/lib/resolve-module.d.cts +34 -0
  44. package/lib/rules.mjs +3 -1
  45. package/package.json +30 -27
  46. package/schema/.markdownlint.jsonc +82 -60
  47. package/schema/.markdownlint.yaml +74 -56
  48. package/schema/markdownlint-config-schema-strict.json +259 -144
  49. package/schema/markdownlint-config-schema.json +259 -144
  50. package/lib/node-imports-browser-module.cjs +0 -7
package/lib/md025.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  // @ts-check
2
2
 
3
3
  import { addErrorContext, frontMatterHasTitle } from "../helpers/helpers.cjs";
4
- import { getHeadingLevel, getHeadingText } from "../helpers/micromark-helpers.cjs";
4
+ import { getHeadingLevel, getHeadingText, isDocfxTab, isHtmlFlowComment, nonContentTokens } from "../helpers/micromark-helpers.cjs";
5
5
  import { filterByTypesCached } from "./cache.mjs";
6
6
 
7
7
  /** @type {import("markdownlint").Rule} */
@@ -12,24 +12,32 @@ export default {
12
12
  "parser": "micromark",
13
13
  "function": function MD025(params, onError) {
14
14
  const level = Number(params.config.level || 1);
15
- const foundFrontMatterTitle =
16
- frontMatterHasTitle(
17
- params.frontMatterLines,
18
- params.config.front_matter_title
19
- );
20
- let hasTopLevelHeading = false;
21
- for (const heading of filterByTypesCached([ "atxHeading", "setextHeading" ])) {
22
- const headingLevel = getHeadingLevel(heading);
23
- if (headingLevel === level) {
24
- if (hasTopLevelHeading || foundFrontMatterTitle) {
25
- const headingText = getHeadingText(heading);
15
+ const { tokens } = params.parsers.micromark;
16
+ const matchingHeadings = filterByTypesCached([ "atxHeading", "setextHeading" ])
17
+ .filter((heading) => (level === getHeadingLevel(heading)) && !isDocfxTab(heading));
18
+ if (matchingHeadings.length > 0) {
19
+ const foundFrontMatterTitle =
20
+ frontMatterHasTitle(
21
+ params.frontMatterLines,
22
+ params.config.front_matter_title
23
+ );
24
+ // Front matter title counts as a top-level heading if present
25
+ let hasTopLevelHeading = foundFrontMatterTitle;
26
+ if (!hasTopLevelHeading) {
27
+ // Check if the first matching heading is a top-level heading
28
+ const previousTokens = tokens.slice(0, tokens.indexOf(matchingHeadings[0]));
29
+ hasTopLevelHeading = previousTokens.every(
30
+ (token) => nonContentTokens.has(token.type) || isHtmlFlowComment(token)
31
+ );
32
+ }
33
+ if (hasTopLevelHeading) {
34
+ // All other matching headings are violations
35
+ for (const heading of matchingHeadings.slice(foundFrontMatterTitle ? 0 : 1)) {
26
36
  addErrorContext(
27
37
  onError,
28
38
  heading.startLine,
29
- headingText
39
+ getHeadingText(heading)
30
40
  );
31
- } else if (heading.startLine === 1) {
32
- hasTopLevelHeading = true;
33
41
  }
34
42
  }
35
43
  }
package/lib/md027.mjs CHANGED
@@ -1,8 +1,12 @@
1
1
  // @ts-check
2
2
 
3
3
  import { addErrorContext } from "../helpers/helpers.cjs";
4
+ import { getParentOfType } from "../helpers/micromark-helpers.cjs";
4
5
  import { filterByTypesCached } from "./cache.mjs";
5
6
 
7
+ /** @type {import("../helpers/micromark-helpers.cjs").TokenType[]} */
8
+ const listTypes = [ "listOrdered", "listUnordered" ];
9
+
6
10
  /** @type {import("markdownlint").Rule} */
7
11
  export default {
8
12
  "names": [ "MD027", "no-multiple-space-blockquote" ],
@@ -10,6 +14,8 @@ export default {
10
14
  "tags": [ "blockquote", "whitespace", "indentation" ],
11
15
  "parser": "micromark",
12
16
  "function": function MD027(params, onError) {
17
+ const listItems = params.config.list_items;
18
+ const includeListItems = (listItems === undefined) ? true : !!listItems;
13
19
  const { tokens } = params.parsers.micromark;
14
20
  for (const token of filterByTypesCached([ "linePrefix" ])) {
15
21
  const parent = token.parent;
@@ -17,7 +23,11 @@ export default {
17
23
  const siblings = parent?.children || tokens;
18
24
  if (
19
25
  !codeIndented &&
20
- (siblings[siblings.indexOf(token) - 1]?.type === "blockQuotePrefix")
26
+ (siblings[siblings.indexOf(token) - 1]?.type === "blockQuotePrefix") &&
27
+ (includeListItems || (
28
+ !listTypes.includes(siblings[siblings.indexOf(token) + 1]?.type) &&
29
+ !getParentOfType(token, listTypes)
30
+ ))
21
31
  ) {
22
32
  const { startColumn, startLine, text } = token;
23
33
  const { length } = text;
package/lib/md036.mjs CHANGED
@@ -11,6 +11,11 @@ const emphasisTypes = [
11
11
  [ "strong", "strongText" ]
12
12
  ];
13
13
 
14
+ const isParagraphChildMeaningful = (token) => !(
15
+ (token.type === "htmlText") ||
16
+ ((token.type === "data") && (token.text.trim().length === 0))
17
+ );
18
+
14
19
  /** @type {import("markdownlint").Rule} */
15
20
  export default {
16
21
  "names": [ "MD036", "no-emphasis-as-heading" ],
@@ -22,9 +27,14 @@ export default {
22
27
  punctuation = String((punctuation === undefined) ? allPunctuation : punctuation);
23
28
  const punctuationRe = new RegExp("[" + punctuation + "]$");
24
29
  const paragraphTokens =
25
- filterByTypesCached([ "paragraph" ])
30
+ filterByTypesCached([ "paragraph" ], true)
26
31
  .filter((token) =>
27
- (token.parent?.type === "content") && !token.parent?.parent && (token.children.length === 1)
32
+ (token.parent?.type === "content") &&
33
+ (
34
+ !token.parent?.parent ||
35
+ ((token.parent?.parent?.type === "htmlFlow") && !token.parent?.parent?.parent)
36
+ ) &&
37
+ (token.children.filter(isParagraphChildMeaningful).length === 1)
28
38
  );
29
39
  for (const emphasisType of emphasisTypes) {
30
40
  const textTokens = getDescendantsByType(paragraphTokens, emphasisType);
package/lib/md038.mjs CHANGED
@@ -4,19 +4,6 @@ import { addErrorContext } from "../helpers/helpers.cjs";
4
4
  import { getDescendantsByType } from "../helpers/micromark-helpers.cjs";
5
5
  import { filterByTypesCached } from "./cache.mjs";
6
6
 
7
- const leftSpaceRe = /^\s(?:[^`]|$)/;
8
- const rightSpaceRe = /[^`]\s$/;
9
- const trimCodeText = (text, start, end) => {
10
- text = text.replace(/^\s+$/, "");
11
- if (start) {
12
- text = text.replace(/^\s+?(\s`|\S)/, "$1");
13
- }
14
- if (end) {
15
- text = text.replace(/(`\s|\S)\s+$/, "$1");
16
- }
17
- return text;
18
- };
19
-
20
7
  /** @type {import("markdownlint").Rule} */
21
8
  export default {
22
9
  "names": [ "MD038", "no-space-in-code" ],
@@ -26,65 +13,59 @@ export default {
26
13
  "function": function MD038(params, onError) {
27
14
  const codeTexts = filterByTypesCached([ "codeText" ]);
28
15
  for (const codeText of codeTexts) {
29
- const sequences = getDescendantsByType(codeText, [ "codeTextSequence" ]);
30
- const startSequence = sequences[0];
31
- const endSequence = sequences[sequences.length - 1];
32
16
  const datas = getDescendantsByType(codeText, [ "codeTextData" ]);
33
- const startData = datas[0];
34
- const endData = datas[datas.length - 1];
35
- if (startSequence && endSequence && startData && endData) {
36
- const spaceLeft = leftSpaceRe.test(startData.text);
37
- const spaceRight = rightSpaceRe.test(endData.text);
38
- if (spaceLeft || spaceRight) {
39
- let lineNumber = startSequence.startLine;
40
- let range = undefined;
41
- let fixInfo = undefined;
42
- if (startSequence.startLine === endSequence.endLine) {
43
- range = [
44
- startSequence.startColumn,
45
- endSequence.endColumn - startSequence.startColumn
46
- ];
47
- fixInfo = {
48
- "editColumn": startSequence.endColumn,
49
- "deleteCount": endSequence.startColumn - startSequence.endColumn,
50
- "insertText": trimCodeText(startData.text, true, true)
51
- };
52
- } else if (spaceLeft && (startSequence.endLine === startData.startLine)) {
53
- range = [
54
- startSequence.startColumn,
55
- startData.endColumn - startSequence.startColumn
56
- ];
57
- fixInfo = {
58
- "editColumn": startSequence.endColumn,
59
- "deleteCount": startData.endColumn - startData.startColumn,
60
- "insertText": trimCodeText(startData.text, true, false)
61
- };
62
- } else if (spaceRight && (endData.text.trim().length > 0)) {
63
- lineNumber = endSequence.endLine;
64
- range = [
65
- endData.startColumn,
66
- endSequence.endColumn - endData.startColumn
67
- ];
68
- fixInfo = {
69
- "editColumn": endData.startColumn,
70
- "deleteCount": endData.endColumn - endData.startColumn,
71
- "insertText": trimCodeText(endData.text, false, true)
72
- };
73
- }
74
- if (range) {
75
- const context = params
76
- .lines[lineNumber - 1]
77
- .substring(range[0] - 1, range[0] - 1 + range[1]);
78
- addErrorContext(
79
- onError,
80
- lineNumber,
81
- context,
82
- spaceLeft,
83
- spaceRight,
84
- range,
85
- fixInfo
86
- );
87
- }
17
+ if (datas.length > 0) {
18
+ const paddings = getDescendantsByType(codeText, [ "codeTextPadding" ]);
19
+ // Check for extra space at start of code
20
+ const startPadding = paddings[0];
21
+ const startData = datas[0];
22
+ const startMatch = /^(\s+)(\S)/.exec(startData.text) || [ null, "", "" ];
23
+ const startBacktick = (startMatch[2] === "`");
24
+ const startCount = startMatch[1].length - ((startBacktick && !startPadding) ? 1 : 0);
25
+ const startSpaces = startCount > 0;
26
+ // Check for extra space at end of code
27
+ const endPadding = paddings[paddings.length - 1];
28
+ const endData = datas[datas.length - 1];
29
+ const endMatch = /(\S)(\s+)$/.exec(endData.text) || [ null, "", "" ];
30
+ const endBacktick = (endMatch[1] === "`");
31
+ const endCount = endMatch[2].length - ((endBacktick && !endPadding) ? 1 : 0);
32
+ const endSpaces = endCount > 0;
33
+ // Check if safe to remove 1-space padding
34
+ const removePadding = startSpaces && endSpaces && startPadding && endPadding && !startBacktick && !endBacktick;
35
+ const context = codeText.text;
36
+ // If extra space at start, report violation
37
+ if (startSpaces) {
38
+ const startColumn = (removePadding ? startPadding : startData).startColumn;
39
+ const length = startCount + (removePadding ? startPadding.text.length : 0);
40
+ addErrorContext(
41
+ onError,
42
+ startData.startLine,
43
+ context,
44
+ true,
45
+ false,
46
+ [ startColumn, length ],
47
+ {
48
+ "editColumn": startColumn,
49
+ "deleteCount": length
50
+ }
51
+ );
52
+ }
53
+ // If extra space at end, report violation
54
+ if (endSpaces) {
55
+ const endColumn = (removePadding ? endPadding : endData).endColumn;
56
+ const length = endCount + (removePadding ? endPadding.text.length : 0);
57
+ addErrorContext(
58
+ onError,
59
+ endData.endLine,
60
+ context,
61
+ false,
62
+ true,
63
+ [ endColumn - length, length ],
64
+ {
65
+ "editColumn": endColumn - length,
66
+ "deleteCount": length
67
+ }
68
+ );
88
69
  }
89
70
  }
90
71
  }
package/lib/md039.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  // @ts-check
2
2
 
3
3
  import { addErrorContext } from "../helpers/helpers.cjs";
4
- import { getReferenceLinkImageData, filterByTypesCached } from "./cache.mjs";
4
+ import { filterByTypesCached } from "./cache.mjs";
5
5
 
6
6
  /**
7
7
  * Adds an error for a label space issue.
@@ -35,18 +35,6 @@ function addLabelSpaceError(onError, label, labelText, isStart) {
35
35
  );
36
36
  }
37
37
 
38
- /**
39
- * Determines if a link is a valid link (and not a fake shortcut link due to parser tricks).
40
- *
41
- * @param {import("markdownlint").MicromarkToken} label Label token.
42
- * @param {import("markdownlint").MicromarkToken} labelText LabelText token.
43
- * @param {Map<string, any>} definitions Map of link definitions.
44
- * @returns {boolean} True iff the link is valid.
45
- */
46
- function validLink(label, labelText, definitions) {
47
- return (label.parent?.children.length !== 1) || definitions.has(labelText.text.trim());
48
- }
49
-
50
38
  /** @type {import("markdownlint").Rule} */
51
39
  export default {
52
40
  "names": [ "MD039", "no-space-in-links" ],
@@ -54,22 +42,15 @@ export default {
54
42
  "tags": [ "whitespace", "links" ],
55
43
  "parser": "micromark",
56
44
  "function": function MD039(params, onError) {
57
- const { definitions } = getReferenceLinkImageData();
58
45
  const labels = filterByTypesCached([ "label" ])
59
46
  .filter((label) => label.parent?.type === "link");
60
47
  for (const label of labels) {
61
48
  const labelTexts = label.children.filter((child) => child.type === "labelText");
62
49
  for (const labelText of labelTexts) {
63
- if (
64
- (labelText.text.trimStart().length !== labelText.text.length) &&
65
- validLink(label, labelText, definitions)
66
- ) {
50
+ if (labelText.text.trimStart().length !== labelText.text.length) {
67
51
  addLabelSpaceError(onError, label, labelText, true);
68
52
  }
69
- if (
70
- (labelText.text.trimEnd().length !== labelText.text.length) &&
71
- validLink(label, labelText, definitions)
72
- ) {
53
+ if (labelText.text.trimEnd().length !== labelText.text.length) {
73
54
  addLabelSpaceError(onError, label, labelText, false);
74
55
  }
75
56
  }
package/lib/md041.mjs CHANGED
@@ -3,6 +3,26 @@
3
3
  import { addErrorContext, frontMatterHasTitle } from "../helpers/helpers.cjs";
4
4
  import { filterByTypes, getHeadingLevel, getHtmlTagInfo, isHtmlFlowComment, nonContentTokens } from "../helpers/micromark-helpers.cjs";
5
5
 
6
+ const headingTagNameRe = /^h[1-6]$/;
7
+
8
+ /**
9
+ * Gets the HTML tag name of an htmlFlow token.
10
+ *
11
+ * @param {import("markdownlint").MicromarkToken} token Micromark Token.
12
+ * @returns {string | null} Tag name.
13
+ */
14
+ function getHtmlFlowTagName(token) {
15
+ const { children, type } = token;
16
+ if (type === "htmlFlow") {
17
+ const htmlTexts = filterByTypes(children, [ "htmlText" ], true);
18
+ const tagInfo = (htmlTexts.length > 0) && getHtmlTagInfo(htmlTexts[0]);
19
+ if (tagInfo) {
20
+ return tagInfo.name.toLowerCase();
21
+ }
22
+ }
23
+ return null;
24
+ }
25
+
6
26
  /** @type {import("markdownlint").Rule} */
7
27
  export default {
8
28
  "names": [ "MD041", "first-line-heading", "first-line-h1" ],
@@ -10,24 +30,42 @@ export default {
10
30
  "tags": [ "headings" ],
11
31
  "parser": "micromark",
12
32
  "function": function MD041(params, onError) {
33
+ const allowPreamble = !!params.config.allow_preamble;
13
34
  const level = Number(params.config.level || 1);
14
- if (!frontMatterHasTitle(params.frontMatterLines, params.config.front_matter_title)) {
15
- params.parsers.micromark.tokens
16
- .filter((token) => !nonContentTokens.has(token.type) && !isHtmlFlowComment(token))
17
- .every((token) => {
18
- let isError = true;
19
- if ((token.type === "atxHeading") || (token.type === "setextHeading")) {
20
- isError = (getHeadingLevel(token) !== level);
21
- } else if (token.type === "htmlFlow") {
22
- const htmlTexts = filterByTypes(token.children, [ "htmlText" ], true);
23
- const tagInfo = (htmlTexts.length > 0) && getHtmlTagInfo(htmlTexts[0]);
24
- isError = !tagInfo || (tagInfo.name.toLowerCase() !== `h${level}`);
25
- }
26
- if (isError) {
27
- addErrorContext(onError, token.startLine, params.lines[token.startLine - 1]);
35
+ const { tokens } = params.parsers.micromark;
36
+ if (
37
+ !frontMatterHasTitle(
38
+ params.frontMatterLines,
39
+ params.config.front_matter_title
40
+ )
41
+ ) {
42
+ let errorLineNumber = 0;
43
+ for (const token of tokens) {
44
+ const { startLine, type } = token;
45
+ if (!nonContentTokens.has(type) && !isHtmlFlowComment(token)) {
46
+ let tagName = null;
47
+ if ((type === "atxHeading") || (type === "setextHeading")) {
48
+ // First heading needs to have the expected level
49
+ if (getHeadingLevel(token) !== level) {
50
+ errorLineNumber = startLine;
51
+ }
52
+ break;
53
+ } else if ((tagName = getHtmlFlowTagName(token)) && headingTagNameRe.test(tagName)) {
54
+ // First HTML element needs to have an <h?> with the expected level
55
+ if (tagName !== `h${level}`) {
56
+ errorLineNumber = startLine;
57
+ }
58
+ break;
59
+ } else if (!allowPreamble) {
60
+ // First non-content needs to be a heading with the expected level
61
+ errorLineNumber = startLine;
62
+ break;
28
63
  }
29
- return false;
30
- });
64
+ }
65
+ }
66
+ if (errorLineNumber > 0) {
67
+ addErrorContext(onError, errorLineNumber, params.lines[errorLineNumber - 1]);
68
+ }
31
69
  }
32
70
  }
33
71
  };
package/lib/md043.mjs CHANGED
@@ -38,6 +38,8 @@ export default {
38
38
  }
39
39
  } else if (expected === "+") {
40
40
  matchAny = true;
41
+ } else if (expected === "?") {
42
+ // Allow current, match next
41
43
  } else if (handleCase(expected) === handleCase(actual)) {
42
44
  matchAny = false;
43
45
  } else if (matchAny) {
package/lib/md045.mjs CHANGED
@@ -5,6 +5,7 @@ import { getHtmlTagInfo, getDescendantsByType } from "../helpers/micromark-helpe
5
5
  import { filterByTypesCached } from "./cache.mjs";
6
6
 
7
7
  const altRe = getHtmlAttributeRe("alt");
8
+ const ariaHiddenRe = getHtmlAttributeRe("aria-hidden");
8
9
 
9
10
  /** @type {import("markdownlint").Rule} */
10
11
  export default {
@@ -40,7 +41,8 @@ export default {
40
41
  htmlTagInfo &&
41
42
  !htmlTagInfo.close &&
42
43
  (htmlTagInfo.name.toLowerCase() === "img") &&
43
- !altRe.test(text)
44
+ !altRe.test(text) &&
45
+ (ariaHiddenRe.exec(text)?.[1].toLowerCase() !== "true")
44
46
  ) {
45
47
  const range = [
46
48
  startColumn,
package/lib/md051.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  // @ts-check
2
2
 
3
3
  import { addError, getHtmlAttributeRe } from "../helpers/helpers.cjs";
4
- import { filterByPredicate, filterByTypes, getHtmlTagInfo } from "../helpers/micromark-helpers.cjs";
4
+ import { filterByPredicate, filterByTypes, getHtmlTagInfo, isDocfxTab } from "../helpers/micromark-helpers.cjs";
5
5
  import { filterByTypesCached } from "./cache.mjs";
6
6
 
7
7
  // Regular expression for identifying HTML anchor names
@@ -66,7 +66,10 @@ export default {
66
66
  "parser": "micromark",
67
67
  "function": function MD051(params, onError) {
68
68
  const ignoreCase = params.config.ignore_case || false;
69
- const fragments = new Map();
69
+ const ignoredPattern = params.config.ignored_pattern || "";
70
+ const ignoredPatternRe = new RegExp(ignoredPattern || "^$");
71
+ /** @type {Map<string, number>} */
72
+ const fragments = new Map([ [ "#top", 0 ] ]);
70
73
 
71
74
  // Process headings
72
75
  const headingTexts = filterByTypesCached([ "atxHeadingText", "setextHeadingText" ]);
@@ -107,18 +110,23 @@ export default {
107
110
  [ "definition", "definitionDestinationString" ]
108
111
  ];
109
112
  for (const [ parentType, definitionType ] of parentChilds) {
110
- const links = filterByTypesCached([ parentType ]);
113
+ const links = filterByTypesCached([ parentType ])
114
+ .filter(
115
+ (link) => !((link.parent?.type === "atxHeadingText") && isDocfxTab(link.parent.parent))
116
+ );
111
117
  for (const link of links) {
112
118
  const definitions = filterByTypes(link.children, [ definitionType ]);
113
119
  for (const definition of definitions) {
114
120
  const { endColumn, startColumn } = definition;
115
121
  const text = unescapeStringTokenText(definition);
116
- const encodedText = `#${encodeURIComponent(text.slice(1))}`;
122
+ const textSliceOne = text.slice(1);
123
+ const encodedText = `#${encodeURIComponent(textSliceOne)}`;
117
124
  if (
118
125
  (text.length > 1) &&
119
126
  text.startsWith("#") &&
120
127
  !fragments.has(encodedText) &&
121
- !lineFragmentRe.test(encodedText)
128
+ !lineFragmentRe.test(encodedText) &&
129
+ !ignoredPatternRe.test(textSliceOne)
122
130
  ) {
123
131
  let context = undefined;
124
132
  let range = undefined;
package/lib/md052.mjs CHANGED
@@ -13,6 +13,7 @@ export default {
13
13
  "function": function MD052(params, onError) {
14
14
  const { config, lines } = params;
15
15
  const shortcutSyntax = config.shortcut_syntax || false;
16
+ const ignoredLabels = new Set(config.ignored_labels || [ "x" ]);
16
17
  const { definitions, references, shortcuts } = getReferenceLinkImageData();
17
18
  const entries = shortcutSyntax ?
18
19
  [ ...references.entries(), ...shortcuts.entries() ] :
@@ -20,7 +21,7 @@ export default {
20
21
  // Look for links/images that use an undefined link reference
21
22
  for (const reference of entries) {
22
23
  const [ label, datas ] = reference;
23
- if (!definitions.has(label)) {
24
+ if (!definitions.has(label) && !ignoredLabels.has(label)) {
24
25
  for (const data of datas) {
25
26
  const [ lineIndex, index, length ] = data;
26
27
  // Context will be incomplete if reporting for a multi-line link
package/lib/md059.mjs ADDED
@@ -0,0 +1,71 @@
1
+ // @ts-check
2
+
3
+ import { addErrorContext } from "../helpers/helpers.cjs";
4
+ import { getDescendantsByType } from "../helpers/micromark-helpers.cjs";
5
+ import { filterByTypesCached } from "./cache.mjs";
6
+
7
+ /** @typedef {import("markdownlint").MicromarkTokenType} MicromarkTokenType */
8
+ /** @type {Set<MicromarkTokenType>} */
9
+ const allowedChildrenTypes = new Set([
10
+ "codeText",
11
+ "htmlText"
12
+ ]);
13
+ const defaultProhibitedTexts = [
14
+ "click here",
15
+ "here",
16
+ "link",
17
+ "more"
18
+ ];
19
+
20
+ /**
21
+ * Normalizes a string by removing extra whitespaces and punctuation.
22
+ *
23
+ * @param {string} str String to normalize.
24
+ * @returns {string} Normalized string.
25
+ */
26
+ function normalize(str) {
27
+ return str
28
+ .replace(/[\W_]+/g, " ")
29
+ .replace(/\s+/g, " ")
30
+ .toLowerCase()
31
+ .trim();
32
+ }
33
+
34
+ /** @type {import("markdownlint").Rule} */
35
+ export default {
36
+ "names": [ "MD059", "descriptive-link-text" ],
37
+ "description": "Link text should be descriptive",
38
+ "tags": [ "accessibility", "links" ],
39
+ "parser": "micromark",
40
+ "function": function MD059(params, onError) {
41
+ const prohibitedTexts = new Set(
42
+ (params.config.prohibited_texts || defaultProhibitedTexts).map(normalize)
43
+ );
44
+ if (prohibitedTexts.size > 0) {
45
+ const links = filterByTypesCached([ "link" ]);
46
+ for (const link of links) {
47
+ const labelTexts = getDescendantsByType(link, [ "label", "labelText" ]);
48
+ for (const labelText of labelTexts) {
49
+ const { children, endColumn, endLine, parent, startColumn, startLine, text } = labelText;
50
+ if (
51
+ !children.some((child) => allowedChildrenTypes.has(child.type)) &&
52
+ prohibitedTexts.has(normalize(text))
53
+ ) {
54
+ const range = (startLine === endLine) ?
55
+ [ startColumn, endColumn - startColumn ] :
56
+ undefined;
57
+ addErrorContext(
58
+ onError,
59
+ startLine,
60
+ // @ts-ignore
61
+ parent.text,
62
+ undefined,
63
+ undefined,
64
+ range
65
+ );
66
+ }
67
+ }
68
+ }
69
+ }
70
+ }
71
+ };
@@ -20,6 +20,17 @@ import { flatTokensSymbol, htmlFlowSymbol, newLineRe } from "../helpers/shared.c
20
20
  /** @typedef {import("markdownlint").MicromarkToken} MicromarkToken */
21
21
  /** @typedef {import("./micromark-types.d.mts")} */
22
22
 
23
+ /**
24
+ * Gets the Markdown text for a Micromark token.
25
+ *
26
+ * @param {string} markdown Markdown content.
27
+ * @param {Token} token Micromark token.
28
+ * @returns {string} Token text.
29
+ */
30
+ function getText(markdown, token) {
31
+ return markdown.slice(token.start.offset, token.end.offset);
32
+ }
33
+
23
34
  /**
24
35
  * Parse options.
25
36
  *
@@ -126,7 +137,7 @@ export function getEvents(
126
137
  // Create artificial event list and replicate content
127
138
  const text = eventsToReplicate
128
139
  .filter((event) => event[0] === "enter")
129
- .map((event) => tokenizeContext.sliceSerialize(event[1]))
140
+ .map((event) => getText(markdown, event[1]))
130
141
  .join("")
131
142
  .trim();
132
143
  if ((text.length > 0) && !text.includes("]")) {
@@ -221,11 +232,11 @@ function parseInternal(
221
232
  let lines = null;
222
233
  let skipHtmlFlowChildren = false;
223
234
  for (const event of events) {
224
- const [ kind, token, context ] = event;
235
+ const [ kind, token ] = event;
225
236
  const { type, start, end } = token;
226
237
  const { "column": startColumn, "line": startLine } = start;
227
238
  const { "column": endColumn, "line": endLine } = end;
228
- const text = context.sliceSerialize(token);
239
+ const text = getText(markdown, token);
229
240
  if ((kind === "enter") && !skipHtmlFlowChildren) {
230
241
  const previous = current;
231
242
  history.push(previous);
@@ -12,8 +12,6 @@ export const fs = {
12
12
  "readFileSync": throwForSync
13
13
  };
14
14
 
15
- export { default as module } from "./node-imports-browser-module.cjs";
16
-
17
15
  export const os = {};
18
16
 
19
17
  export const path = {
@@ -3,9 +3,6 @@
3
3
  import { access, accessSync, readFile, readFileSync } from "node:fs";
4
4
  export const fs = { access, accessSync, readFile, readFileSync };
5
5
 
6
- import { createRequire } from "node:module";
7
- export const module = { createRequire };
8
-
9
6
  import { EOL, homedir } from "node:os";
10
7
  export const os = { EOL, homedir };
11
8