markdownlint 0.37.4 → 0.39.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 (73) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/CONTRIBUTING.md +2 -3
  3. package/README.md +175 -87
  4. package/doc/Rules.md +273 -47
  5. package/doc/md001.md +14 -0
  6. package/doc/md009.md +8 -9
  7. package/doc/md010.md +6 -0
  8. package/doc/md027.md +8 -0
  9. package/doc/md029.md +2 -0
  10. package/doc/md033.md +7 -1
  11. package/doc/md038.md +23 -11
  12. package/doc/md041.md +31 -16
  13. package/doc/md043.md +22 -4
  14. package/doc/md045.md +9 -1
  15. package/doc/md051.md +17 -0
  16. package/doc/md052.md +12 -0
  17. package/doc/md053.md +3 -3
  18. package/doc/md055.md +1 -1
  19. package/doc/md056.md +1 -1
  20. package/doc/md059.md +33 -0
  21. package/doc/md060.md +70 -0
  22. package/helpers/README.md +2 -2
  23. package/helpers/helpers.cjs +174 -20
  24. package/helpers/micromark-helpers.cjs +37 -7
  25. package/helpers/package.json +1 -1
  26. package/lib/cache.mjs +25 -7
  27. package/lib/configuration-strict.d.ts +1494 -292
  28. package/lib/constants.mjs +6 -5
  29. package/lib/defer-require.cjs +2 -0
  30. package/lib/exports.d.mts +2 -2
  31. package/lib/exports.mjs +2 -2
  32. package/lib/markdownit.cjs +8 -14
  33. package/lib/markdownlint.d.mts +136 -52
  34. package/lib/markdownlint.mjs +469 -409
  35. package/lib/md001.mjs +6 -2
  36. package/lib/md004.mjs +7 -18
  37. package/lib/md007.mjs +2 -1
  38. package/lib/md009.mjs +9 -5
  39. package/lib/md010.mjs +1 -1
  40. package/lib/md022.mjs +5 -2
  41. package/lib/md025.mjs +23 -15
  42. package/lib/md027.mjs +11 -1
  43. package/lib/md029.mjs +25 -14
  44. package/lib/md031.mjs +0 -1
  45. package/lib/md032.mjs +3 -1
  46. package/lib/md033.mjs +28 -20
  47. package/lib/md034.mjs +2 -1
  48. package/lib/md036.mjs +16 -4
  49. package/lib/md037.mjs +10 -7
  50. package/lib/md038.mjs +52 -71
  51. package/lib/md041.mjs +54 -16
  52. package/lib/md042.mjs +4 -2
  53. package/lib/md043.mjs +4 -2
  54. package/lib/md044.mjs +1 -1
  55. package/lib/md045.mjs +3 -1
  56. package/lib/md046.mjs +6 -6
  57. package/lib/md051.mjs +13 -5
  58. package/lib/md052.mjs +2 -1
  59. package/lib/md053.mjs +1 -1
  60. package/lib/md054.mjs +7 -5
  61. package/lib/md055.mjs +7 -6
  62. package/lib/md056.mjs +1 -1
  63. package/lib/md059.mjs +71 -0
  64. package/lib/md060.mjs +121 -0
  65. package/lib/micromark-parse.mjs +17 -3
  66. package/lib/node-imports-browser.mjs +2 -0
  67. package/lib/parse-configuration.mjs +47 -0
  68. package/lib/rules.mjs +5 -1
  69. package/package.json +32 -30
  70. package/schema/.markdownlint.jsonc +97 -62
  71. package/schema/.markdownlint.yaml +86 -57
  72. package/schema/markdownlint-config-schema-strict.json +4713 -1436
  73. package/schema/markdownlint-config-schema.json +4713 -1436
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/md042.mjs CHANGED
@@ -4,6 +4,8 @@ import { addErrorContext } from "../helpers/helpers.cjs";
4
4
  import { getDescendantsByType } from "../helpers/micromark-helpers.cjs";
5
5
  import { getReferenceLinkImageData, filterByTypesCached } from "./cache.mjs";
6
6
 
7
+ /** @typedef {import("markdownlint").MicromarkToken} MicromarkToken */
8
+
7
9
  /** @type {import("markdownlint").Rule} */
8
10
  export default {
9
11
  "names": [ "MD042", "no-empty-links" ],
@@ -12,9 +14,9 @@ export default {
12
14
  "parser": "micromark",
13
15
  "function": function MD042(params, onError) {
14
16
  const { definitions } = getReferenceLinkImageData();
15
- const isReferenceDefinitionHash = (token) => {
17
+ const isReferenceDefinitionHash = (/** @type {MicromarkToken} */ token) => {
16
18
  const definition = definitions.get(token.text.trim());
17
- return (definition && (definition[1] === "#"));
19
+ return Boolean(definition && (definition[1] === "#"));
18
20
  };
19
21
  const links = filterByTypesCached([ "link" ]);
20
22
  for (const link of links) {
package/lib/md043.mjs CHANGED
@@ -21,8 +21,8 @@ export default {
21
21
  let matchAny = false;
22
22
  let hasError = false;
23
23
  let anyHeadings = false;
24
- const getExpected = () => requiredHeadings[i++] || "[None]";
25
- const handleCase = (str) => (matchCase ? str : str.toLowerCase());
24
+ const getExpected = () => String(requiredHeadings[i++] || "[None]");
25
+ const handleCase = (/** @type {string} */ str) => (matchCase ? str : str.toLowerCase());
26
26
  for (const heading of filterByTypesCached([ "atxHeading", "setextHeading" ])) {
27
27
  if (!hasError) {
28
28
  const headingText = getHeadingText(heading);
@@ -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/md044.mjs CHANGED
@@ -17,7 +17,7 @@ export default {
17
17
  "function": function MD044(params, onError) {
18
18
  let names = params.config.names;
19
19
  names = Array.isArray(names) ? names : [];
20
- names.sort((a, b) => (b.length - a.length) || a.localeCompare(b));
20
+ names.sort((/** @type {string} */ a, /** @type {string} */ b) => (b.length - a.length) || a.localeCompare(b));
21
21
  if (names.length === 0) {
22
22
  // Nothing to check; avoid doing any work
23
23
  return;
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/md046.mjs CHANGED
@@ -3,10 +3,9 @@
3
3
  import { addErrorDetailIf } from "../helpers/helpers.cjs";
4
4
  import { filterByTypesCached } from "./cache.mjs";
5
5
 
6
- const tokenTypeToStyle = {
7
- "codeFenced": "fenced",
8
- "codeIndented": "indented"
9
- };
6
+ /** @typedef {import("markdownlint").MicromarkTokenType} MicromarkTokenType */
7
+
8
+ const tokenTypeToStyle = (/** @type {MicromarkTokenType} */ tokenType) => (tokenType === "codeFenced") ? "fenced" : "indented";
10
9
 
11
10
  /** @type {import("markdownlint").Rule} */
12
11
  export default {
@@ -19,13 +18,14 @@ export default {
19
18
  for (const token of filterByTypesCached([ "codeFenced", "codeIndented" ])) {
20
19
  const { startLine, type } = token;
21
20
  if (expectedStyle === "consistent") {
22
- expectedStyle = tokenTypeToStyle[type];
21
+ expectedStyle = tokenTypeToStyle(type);
23
22
  }
24
23
  addErrorDetailIf(
25
24
  onError,
26
25
  startLine,
27
26
  expectedStyle,
28
- tokenTypeToStyle[type]);
27
+ tokenTypeToStyle(type)
28
+ );
29
29
  }
30
30
  }
31
31
  };
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/md053.mjs CHANGED
@@ -16,7 +16,7 @@ export default {
16
16
  const lines = params.lines;
17
17
  const { references, shortcuts, definitions, duplicateDefinitions } =
18
18
  getReferenceLinkImageData();
19
- const singleLineDefinition = (line) => (
19
+ const singleLineDefinition = (/** @type {string} */ line) => (
20
20
  line.replace(linkReferenceDefinitionRe, "").trim().length > 0
21
21
  );
22
22
  const deleteFixInfo = {
package/lib/md054.mjs CHANGED
@@ -5,9 +5,9 @@ import { getDescendantsByType } from "../helpers/micromark-helpers.cjs";
5
5
  import { getReferenceLinkImageData, filterByTypesCached } from "./cache.mjs";
6
6
 
7
7
  const backslashEscapeRe = /\\([!"#$%&'()*+,\-./:;<=>?@[\\\]^_`{|}~])/g;
8
- const removeBackslashEscapes = (text) => text.replace(backslashEscapeRe, "$1");
8
+ const removeBackslashEscapes = (/** @type {string} **/ text) => text.replace(backslashEscapeRe, "$1");
9
9
  const autolinkDisallowedRe = /[ <>]/;
10
- const autolinkAble = (destination) => {
10
+ const autolinkAble = (/** @type {string} */ destination) => {
11
11
  try {
12
12
  // eslint-disable-next-line no-new
13
13
  new URL(destination);
@@ -73,9 +73,11 @@ export default {
73
73
  const referenceString = getDescendantsByType(link, [ "reference", "referenceString" ])[0]?.text;
74
74
  const isCollapsed = (referenceString === undefined);
75
75
  const definition = definitions.get(referenceString || label);
76
- destination = definition && definition[1];
77
- isError = destination &&
78
- (isShortcut ? !shortcut : (isCollapsed ? !collapsed : !full));
76
+ destination = (definition && definition[1]) || "";
77
+ isError = Boolean(
78
+ destination &&
79
+ (isShortcut ? !shortcut : (isCollapsed ? !collapsed : !full))
80
+ );
79
81
  }
80
82
  }
81
83
  if (isError) {
package/lib/md055.mjs CHANGED
@@ -3,15 +3,16 @@
3
3
  import { addErrorDetailIf } from "../helpers/helpers.cjs";
4
4
  import { filterByTypesCached } from "./cache.mjs";
5
5
 
6
+ /** @typedef {import("markdownlint").MicromarkToken} MicromarkToken */
7
+ /** @typedef {import("micromark-extension-gfm-table")} */
8
+
6
9
  const whitespaceTypes = new Set([ "linePrefix", "whitespace" ]);
7
- const ignoreWhitespace = (tokens) => tokens.filter(
10
+ const ignoreWhitespace = (/** @type {MicromarkToken[]} */ tokens) => tokens.filter(
8
11
  (token) => !whitespaceTypes.has(token.type)
9
12
  );
10
- const firstOrNothing = (items) => items[0];
11
- const lastOrNothing = (items) => items[items.length - 1];
12
- const makeRange = (start, end) => [ start, end - start + 1 ];
13
-
14
- /** @typedef {import("micromark-extension-gfm-table")} */
13
+ const firstOrNothing = (/** @type {MicromarkToken[]} */ items) => items[0];
14
+ const lastOrNothing = (/** @type {MicromarkToken[]} */ items) => items[items.length - 1];
15
+ const makeRange = (/** @type {number} */ start, /** @type {number} */ end) => [ start, end - start + 1 ];
15
16
 
16
17
  /** @type {import("markdownlint").Rule} */
17
18
  export default {
package/lib/md056.mjs CHANGED
@@ -4,7 +4,7 @@ import { addErrorDetailIf } from "../helpers/helpers.cjs";
4
4
  import { getParentOfType } from "../helpers/micromark-helpers.cjs";
5
5
  import { filterByTypesCached } from "./cache.mjs";
6
6
 
7
- const makeRange = (start, end) => [ start, end - start + 1 ];
7
+ const makeRange = (/** @type {number} */ start, /** @type {number} */ end) => [ start, end - start + 1 ];
8
8
 
9
9
  /** @typedef {import("micromark-extension-gfm-table")} */
10
10
 
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
+ };
package/lib/md060.mjs ADDED
@@ -0,0 +1,121 @@
1
+ // @ts-check
2
+
3
+ import { filterByTypes } from "../helpers/micromark-helpers.cjs";
4
+ import { filterByTypesCached } from "./cache.mjs";
5
+
6
+ /** @typedef {import("micromark-extension-gfm-table")} */
7
+ /** @typedef {import("markdownlint").RuleOnErrorInfo} RuleOnErrorInfo */
8
+
9
+ /**
10
+ * Adds a RuleOnErrorInfo object to a list of RuleOnErrorInfo objects.
11
+ *
12
+ * @param {RuleOnErrorInfo[]} errors List of errors.
13
+ * @param {number} lineNumber Line number.
14
+ * @param {number} column Column number.
15
+ * @param {string} detail Detail message.
16
+ */
17
+ function addError(errors, lineNumber, column, detail) {
18
+ errors.push({
19
+ lineNumber,
20
+ detail,
21
+ "range": [ column, 1 ]
22
+ });
23
+ }
24
+
25
+ /** @type {import("markdownlint").Rule} */
26
+ export default {
27
+ "names": [ "MD060", "table-column-style" ],
28
+ "description": "Table column style",
29
+ "tags": [ "table" ],
30
+ "parser": "micromark",
31
+ "function": function MD060(params, onError) {
32
+ const style = String(params.config.style || "any");
33
+ const styleAlignedAllowed = (style === "any") || (style === "aligned");
34
+ const styleCompactAllowed = (style === "any") || (style === "compact");
35
+ const styleTightAllowed = (style === "any") || (style === "tight");
36
+
37
+ // Scan all tables/rows
38
+ const tables = filterByTypesCached([ "table" ]);
39
+ for (const table of tables) {
40
+ const rows = filterByTypes(table.children, [ "tableDelimiterRow", "tableRow" ]);
41
+ const headingRow = rows[0];
42
+
43
+ // Determine errors for style "aligned"
44
+ /** @type {RuleOnErrorInfo[]} */
45
+ const errorsIfAligned = [];
46
+ if (styleAlignedAllowed) {
47
+ const headingDividerColumns = filterByTypes(headingRow.children, [ "tableCellDivider" ]).map((divider) => divider.startColumn);
48
+ for (const row of rows.slice(1)) {
49
+ const remainingHeadingDividerColumns = new Set(headingDividerColumns);
50
+ const rowDividerColumns = filterByTypes(row.children, [ "tableCellDivider" ]).map((divider) => divider.startColumn);
51
+ for (const dividerColumn of rowDividerColumns) {
52
+ if ((remainingHeadingDividerColumns.size > 0) && !remainingHeadingDividerColumns.delete(dividerColumn)) {
53
+ addError(errorsIfAligned, row.startLine, dividerColumn, "Table pipe does not align with heading for style \"aligned\"");
54
+ }
55
+ }
56
+ }
57
+ }
58
+
59
+ // Determine errors for styles "compact" and "tight"
60
+ /** @type {RuleOnErrorInfo[]} */
61
+ const errorsIfCompact = [];
62
+ /** @type {RuleOnErrorInfo[]} */
63
+ const errorsIfTight = [];
64
+ if (
65
+ (styleCompactAllowed || styleTightAllowed) &&
66
+ !(styleAlignedAllowed && (errorsIfAligned.length === 0))
67
+ ) {
68
+ for (const row of rows) {
69
+ const tokensOfInterest = filterByTypes(row.children, [ "tableCellDivider", "tableContent", "whitespace" ]);
70
+ for (let i = 0; i < tokensOfInterest.length; i++) {
71
+ const { startColumn, startLine, type } = tokensOfInterest[i];
72
+ if (type === "tableCellDivider") {
73
+ const previous = tokensOfInterest[i - 1];
74
+ if (previous) {
75
+ if (previous.type === "whitespace") {
76
+ if (previous.text.length !== 1) {
77
+ addError(errorsIfCompact, startLine, startColumn, "Table pipe has extra space to the left for style \"compact\"");
78
+ }
79
+ addError(errorsIfTight, startLine, startColumn, "Table pipe has space to the left for style \"tight\"");
80
+ } else {
81
+ addError(errorsIfCompact, startLine, startColumn, "Table pipe is missing space to the left for style \"compact\"");
82
+ }
83
+ }
84
+ const next = tokensOfInterest[i + 1];
85
+ if (next) {
86
+ if (next.type === "whitespace") {
87
+ if (next.endColumn !== row.endColumn) {
88
+ if (next.text.length !== 1) {
89
+ addError(errorsIfCompact, startLine, startColumn, "Table pipe has extra space to the right for style \"compact\"");
90
+ }
91
+ addError(errorsIfTight, startLine, startColumn, "Table pipe has space to the right for style \"tight\"");
92
+ }
93
+ } else {
94
+ addError(errorsIfCompact, startLine, startColumn, "Table pipe is missing space to the right for style \"compact\"");
95
+ }
96
+ }
97
+ }
98
+ }
99
+ }
100
+ }
101
+
102
+ // Report errors for whatever (allowed) style has the fewest
103
+ let errorInfos = errorsIfAligned;
104
+ if (
105
+ styleCompactAllowed &&
106
+ ((errorsIfCompact.length < errorInfos.length) || !styleAlignedAllowed)
107
+ ) {
108
+ errorInfos = errorsIfCompact;
109
+ }
110
+ if (
111
+ styleTightAllowed &&
112
+ ((errorsIfTight.length < errorInfos.length) || (!styleAlignedAllowed && !styleCompactAllowed))
113
+ ) {
114
+ errorInfos = errorsIfTight;
115
+ }
116
+ for (const errorInfo of errorInfos) {
117
+ onError(errorInfo);
118
+ }
119
+ }
120
+ }
121
+ };
@@ -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("]")) {
@@ -201,7 +212,9 @@ function parseInternal(
201
212
  const events = getEvents(markdown, micromarkParseOptions);
202
213
 
203
214
  // Create Token objects
215
+ /** @type {MicromarkToken[]} */
204
216
  const document = [];
217
+ /** @type {MicromarkToken[]} */
205
218
  let flatTokens = [];
206
219
  /** @type {MicromarkToken} */
207
220
  const root = {
@@ -221,11 +234,11 @@ function parseInternal(
221
234
  let lines = null;
222
235
  let skipHtmlFlowChildren = false;
223
236
  for (const event of events) {
224
- const [ kind, token, context ] = event;
237
+ const [ kind, token ] = event;
225
238
  const { type, start, end } = token;
226
239
  const { "column": startColumn, "line": startLine } = start;
227
240
  const { "column": endColumn, "line": endLine } = end;
228
- const text = context.sliceSerialize(token);
241
+ const text = getText(markdown, token);
229
242
  if ((kind === "enter") && !skipHtmlFlowChildren) {
230
243
  const previous = current;
231
244
  history.push(previous);
@@ -271,6 +284,7 @@ function parseInternal(
271
284
  );
272
285
  current.children = tokens;
273
286
  // Avoid stack overflow of Array.push(...spread)
287
+ // @ts-ignore
274
288
  // eslint-disable-next-line unicorn/prefer-spread
275
289
  flatTokens = flatTokens.concat(tokens[flatTokensSymbol]);
276
290
  }
@@ -6,8 +6,10 @@ const throwForSync = () => {
6
6
  };
7
7
 
8
8
  export const fs = {
9
+ // @ts-ignore
9
10
  "access": (path, callback) => callback(getError()),
10
11
  "accessSync": throwForSync,
12
+ // @ts-ignore
11
13
  "readFile": (path, options, callback) => callback(getError()),
12
14
  "readFileSync": throwForSync
13
15
  };
@@ -0,0 +1,47 @@
1
+ // @ts-check
2
+
3
+ /**
4
+ * Result of a call to parseConfiguration.
5
+ *
6
+ * @typedef {Object} ParseConfigurationResult
7
+ * @property {import("markdownlint").Configuration | null} config Configuration object if successful.
8
+ * @property {string | null} message Error message if an error occurred.
9
+ */
10
+
11
+ /**
12
+ * Parse the content of a configuration file.
13
+ *
14
+ * @param {string} name Name of the configuration file.
15
+ * @param {string} content Configuration content.
16
+ * @param {import("markdownlint").ConfigurationParser[]} [parsers] Parsing function(s).
17
+ * @returns {ParseConfigurationResult} Parse configuration result.
18
+ */
19
+ export default function parseConfiguration(name, content, parsers) {
20
+ let config = null;
21
+ let message = null;
22
+ const errors = [];
23
+ let index = 0;
24
+ // Try each parser
25
+ const failed = (parsers || [ JSON.parse ]).every((parser) => {
26
+ try {
27
+ const result = parser(content);
28
+ config = (result && (typeof result === "object") && !Array.isArray(result)) ? result : {};
29
+ // Succeeded
30
+ return false;
31
+ // eslint-disable-next-line jsdoc/reject-any-type
32
+ } catch(/** @type {any} */ error) {
33
+ errors.push(`Parser ${index++}: ${error?.message}`);
34
+ }
35
+ // Failed, try the next parser
36
+ return true;
37
+ });
38
+ // Message if unable to parse
39
+ if (failed) {
40
+ errors.unshift(`Unable to parse '${name}'`);
41
+ message = errors.join("; ");
42
+ }
43
+ return {
44
+ config,
45
+ message
46
+ };
47
+ }
package/lib/rules.mjs CHANGED
@@ -53,6 +53,8 @@ import md054 from "./md054.mjs";
53
53
  import md055 from "./md055.mjs";
54
54
  import md056 from "./md056.mjs";
55
55
  import md058 from "./md058.mjs";
56
+ import md059 from "./md059.mjs";
57
+ import md060 from "./md060.mjs";
56
58
 
57
59
  const rules = [
58
60
  md001,
@@ -108,7 +110,9 @@ const rules = [
108
110
  md055,
109
111
  md056,
110
112
  // md057: See https://github.com/markdownlint/markdownlint
111
- md058
113
+ md058,
114
+ md059,
115
+ md060
112
116
  ];
113
117
  for (const rule of rules) {
114
118
  const name = rule.names[0].toLowerCase();