mdrefine 0.2.0-rc.2 → 0.2.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 (130) hide show
  1. package/CHANGELOG.md +577 -0
  2. package/CODE_OF_CONDUCT.md +30 -0
  3. package/CONTRIBUTING.md +189 -16
  4. package/README.md +100 -154
  5. package/SECURITY.md +15 -13
  6. package/config.schema.json +91 -0
  7. package/dist/cli/diff.d.ts +10 -0
  8. package/dist/cli/diff.js +247 -0
  9. package/dist/cli/diff.js.map +1 -0
  10. package/dist/cli/main.js +270 -150
  11. package/dist/cli/main.js.map +1 -1
  12. package/dist/config/load.js +67 -6
  13. package/dist/config/load.js.map +1 -1
  14. package/dist/config/problems.d.ts +7 -0
  15. package/dist/config/problems.js +208 -0
  16. package/dist/config/problems.js.map +1 -0
  17. package/dist/config/resolve.d.ts +42 -109
  18. package/dist/config/resolve.js +193 -52
  19. package/dist/config/resolve.js.map +1 -1
  20. package/dist/core/directives.d.ts +7 -0
  21. package/dist/core/directives.js +25 -13
  22. package/dist/core/directives.js.map +1 -1
  23. package/dist/core/engine.d.ts +42 -4
  24. package/dist/core/engine.js +518 -79
  25. package/dist/core/engine.js.map +1 -1
  26. package/dist/core/frozen.d.ts +6 -0
  27. package/dist/core/frozen.js +15 -0
  28. package/dist/core/frozen.js.map +1 -0
  29. package/dist/core/scope.d.ts +30 -0
  30. package/dist/core/scope.js +122 -0
  31. package/dist/core/scope.js.map +1 -0
  32. package/dist/core/types.d.ts +202 -23
  33. package/dist/core/types.js.map +1 -1
  34. package/dist/index.d.ts +2 -2
  35. package/dist/index.js.map +1 -1
  36. package/dist/node.d.ts +3 -0
  37. package/dist/node.js +4 -0
  38. package/dist/node.js.map +1 -0
  39. package/dist/rules/blocks.d.ts +34 -0
  40. package/dist/rules/blocks.js +550 -0
  41. package/dist/rules/blocks.js.map +1 -0
  42. package/dist/rules/content.d.ts +2 -0
  43. package/dist/rules/content.js +85 -0
  44. package/dist/rules/content.js.map +1 -0
  45. package/dist/rules/dialects.js +10 -6
  46. package/dist/rules/dialects.js.map +1 -1
  47. package/dist/rules/links.js +302 -6
  48. package/dist/rules/links.js.map +1 -1
  49. package/dist/rules/lists.d.ts +2 -0
  50. package/dist/rules/lists.js +924 -0
  51. package/dist/rules/lists.js.map +1 -0
  52. package/dist/rules/structure.js +127 -1
  53. package/dist/rules/structure.js.map +1 -1
  54. package/dist/rules/style.d.ts +8 -2
  55. package/dist/rules/style.js +1137 -165
  56. package/dist/rules/style.js.map +1 -1
  57. package/dist/syntax/block-identifier.d.ts +18 -0
  58. package/dist/syntax/block-identifier.js +24 -0
  59. package/dist/syntax/block-identifier.js.map +1 -0
  60. package/dist/syntax/obsidian.d.ts +29 -11
  61. package/dist/syntax/obsidian.js +9 -10
  62. package/dist/syntax/obsidian.js.map +1 -1
  63. package/dist/syntax/parse.d.ts +87 -3
  64. package/dist/syntax/parse.js +247 -19
  65. package/dist/syntax/parse.js.map +1 -1
  66. package/dist/workspace/access.d.ts +12 -3
  67. package/dist/workspace/access.js +33 -5
  68. package/dist/workspace/access.js.map +1 -1
  69. package/dist/workspace/discovery.d.ts +85 -0
  70. package/dist/workspace/discovery.js +155 -0
  71. package/dist/workspace/discovery.js.map +1 -0
  72. package/dist/workspace/encoding.d.ts +24 -0
  73. package/dist/workspace/encoding.js +113 -0
  74. package/dist/workspace/encoding.js.map +1 -0
  75. package/dist/workspace/files.d.ts +92 -6
  76. package/dist/workspace/files.js +715 -59
  77. package/dist/workspace/files.js.map +1 -1
  78. package/dist/workspace/git.d.ts +54 -0
  79. package/dist/workspace/git.js +198 -0
  80. package/dist/workspace/git.js.map +1 -0
  81. package/dist/workspace/index.d.ts +58 -3
  82. package/dist/workspace/index.js +136 -37
  83. package/dist/workspace/index.js.map +1 -1
  84. package/dist/workspace/selection.d.ts +2 -2
  85. package/dist/workspace/selection.js +3 -3
  86. package/dist/workspace/selection.js.map +1 -1
  87. package/dist/workspace/slug.js +8 -1
  88. package/dist/workspace/slug.js.map +1 -1
  89. package/dist/workspace/vault.js +3 -1
  90. package/dist/workspace/vault.js.map +1 -1
  91. package/docs/api.md +239 -0
  92. package/docs/benchmarks.md +140 -200
  93. package/docs/changelog.md +1 -0
  94. package/docs/cli.md +281 -89
  95. package/docs/configuration.md +193 -480
  96. package/docs/design.md +163 -284
  97. package/docs/dialects.md +169 -0
  98. package/docs/examples.md +151 -109
  99. package/docs/index.md +21 -37
  100. package/docs/integrations.md +144 -0
  101. package/docs/obsidian.md +91 -115
  102. package/docs/plugins.md +147 -212
  103. package/docs/quick-start.md +68 -74
  104. package/docs/rules.md +1237 -0
  105. package/docs/versions.md +56 -0
  106. package/examples/README.md +17 -14
  107. package/examples/automation/summarize-report.mjs +5 -2
  108. package/examples/library/lint-folder.mjs +5 -4
  109. package/examples/library/lint-workspace.mjs +32 -0
  110. package/examples/plugin/notes/release-checklist.md +2 -2
  111. package/examples/plugin/rules.mjs +17 -23
  112. package/examples/workspaces/obsidian-vault/after/mdtools.config.jsonc +2 -2
  113. package/examples/workspaces/obsidian-vault/before/mdtools.config.jsonc +2 -2
  114. package/examples/workspaces/repository-docs/after/README.md +2 -2
  115. package/examples/workspaces/repository-docs/after/mdtools.config.jsonc +0 -2
  116. package/examples/workspaces/repository-docs/before/README.md +2 -2
  117. package/examples/workspaces/repository-docs/before/docs/guide.md +7 -7
  118. package/examples/workspaces/repository-docs/before/docs/reference.md +3 -1
  119. package/examples/workspaces/repository-docs/before/mdtools.config.jsonc +0 -2
  120. package/package.json +38 -12
  121. package/docs/contributing.md +0 -218
  122. package/docs/releases/0.1.0-beta.1.md +0 -39
  123. package/docs/releases/0.1.0-beta.2.md +0 -83
  124. package/docs/releases/0.1.0-beta.3.md +0 -61
  125. package/docs/releases/0.1.0-beta.4.md +0 -50
  126. package/docs/releases/0.1.0-rc.1.md +0 -99
  127. package/docs/releases/0.2.0-rc.1.md +0 -141
  128. package/docs/releases/0.2.0-rc.2.md +0 -225
  129. package/docs/releases.md +0 -144
  130. package/docs/testing.md +0 -93
@@ -0,0 +1,85 @@
1
+ import { walk } from "../syntax/walk.js";
2
+ import { range } from "../syntax/parse.js";
3
+ import { optionsSchema } from "./style.js";
4
+ /** `|100` or `|100x50` at the end of an Obsidian image's text sets its size, not its text. */
5
+ const obsidianSize = /\|\d+(?:x\d+)?$/;
6
+ export const contentRules = {
7
+ "content/image-alt": {
8
+ description: "Report images without alternative text.",
9
+ kind: "problem",
10
+ schema: optionsSchema({}),
11
+ check({ document }) {
12
+ const findings = [];
13
+ // Obsidian's `![[image.png]]` embeds are wikilinks, which have no place for the text.
14
+ walk(document.tree, (node) => {
15
+ if (node.type !== "image" && node.type !== "imageReference")
16
+ return;
17
+ let alt = node.alt ?? "";
18
+ if (document.dialect === "obsidian")
19
+ alt = alt.replace(obsidianSize, "");
20
+ if (alt.trim())
21
+ return;
22
+ const [start, end] = range(node);
23
+ findings.push({
24
+ start,
25
+ end,
26
+ message: "Add alternative text to the image, for readers who cannot see it.",
27
+ });
28
+ });
29
+ return findings;
30
+ },
31
+ },
32
+ "content/table-columns": {
33
+ description: "Report table rows with more or fewer cells than the header row.",
34
+ kind: "problem",
35
+ schema: optionsSchema({}),
36
+ check({ document }) {
37
+ const findings = [];
38
+ walk(document.tree, "table", (node) => {
39
+ const [header, ...rows] = node.children;
40
+ const columns = header?.children.length ?? 0;
41
+ for (const row of rows) {
42
+ const cells = row.children.length;
43
+ if (cells === columns)
44
+ continue;
45
+ const [start, end] = range(row);
46
+ const counted = `Row has ${cells} ${cells === 1 ? "cell" : "cells"} and the header ${columns}`;
47
+ findings.push({
48
+ start,
49
+ end,
50
+ message: cells > columns
51
+ ? `${counted}; ${cells - columns === 1 ? "the extra cell is" : "the extra cells are"} not shown.`
52
+ : `${counted}; the row is shown with ${columns - cells === 1 ? "an empty cell" : "empty cells"} added.`,
53
+ });
54
+ }
55
+ });
56
+ return findings;
57
+ },
58
+ },
59
+ "content/code-language": {
60
+ description: "Report fenced code blocks that do not name a language.",
61
+ kind: "problem",
62
+ schema: optionsSchema({}),
63
+ check({ document }) {
64
+ const findings = [];
65
+ walk(document.tree, "code", (node) => {
66
+ const [start] = range(node);
67
+ // An indented code block starts with its indentation, and has no place for a language.
68
+ const fence = document.source[start];
69
+ if (node.lang || (fence !== "`" && fence !== "~"))
70
+ return;
71
+ let end = start;
72
+ while (document.source[end] === fence)
73
+ end++;
74
+ const marker = document.source.slice(start, end);
75
+ findings.push({
76
+ start,
77
+ end,
78
+ message: `Name the code block's language after its opening fence, such as ${marker}sh, or ${marker}text for plain text.`,
79
+ });
80
+ });
81
+ return findings;
82
+ },
83
+ },
84
+ };
85
+ //# sourceMappingURL=content.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"content.js","sourceRoot":"","sources":["../../src/rules/content.ts"],"names":[],"mappings":"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","sourcesContent":["import { walk } from \"../syntax/walk.js\";\nimport type { Finding, Rule } from \"../core/types.js\";\nimport { range } from \"../syntax/parse.js\";\nimport { optionsSchema } from \"./style.js\";\n\n/** `|100` or `|100x50` at the end of an Obsidian image's text sets its size, not its text. */\nconst obsidianSize = /\\|\\d+(?:x\\d+)?$/;\n\nexport const contentRules: Record<string, Rule> = {\n \"content/image-alt\": {\n description: \"Report images without alternative text.\",\n kind: \"problem\",\n schema: optionsSchema({}),\n check({ document }) {\n const findings: Finding[] = [];\n // Obsidian's `![[image.png]]` embeds are wikilinks, which have no place for the text.\n walk(document.tree, (node) => {\n if (node.type !== \"image\" && node.type !== \"imageReference\") return;\n let alt = node.alt ?? \"\";\n if (document.dialect === \"obsidian\") alt = alt.replace(obsidianSize, \"\");\n if (alt.trim()) return;\n const [start, end] = range(node);\n findings.push({\n start,\n end,\n message: \"Add alternative text to the image, for readers who cannot see it.\",\n });\n });\n return findings;\n },\n },\n \"content/table-columns\": {\n description: \"Report table rows with more or fewer cells than the header row.\",\n kind: \"problem\",\n schema: optionsSchema({}),\n check({ document }) {\n const findings: Finding[] = [];\n walk(document.tree, \"table\", (node) => {\n const [header, ...rows] = node.children;\n const columns = header?.children.length ?? 0;\n for (const row of rows) {\n const cells = row.children.length;\n if (cells === columns) continue;\n const [start, end] = range(row);\n const counted = `Row has ${cells} ${cells === 1 ? \"cell\" : \"cells\"} and the header ${columns}`;\n findings.push({\n start,\n end,\n message:\n cells > columns\n ? `${counted}; ${cells - columns === 1 ? \"the extra cell is\" : \"the extra cells are\"} not shown.`\n : `${counted}; the row is shown with ${columns - cells === 1 ? \"an empty cell\" : \"empty cells\"} added.`,\n });\n }\n });\n return findings;\n },\n },\n \"content/code-language\": {\n description: \"Report fenced code blocks that do not name a language.\",\n kind: \"problem\",\n schema: optionsSchema({}),\n check({ document }) {\n const findings: Finding[] = [];\n walk(document.tree, \"code\", (node) => {\n const [start] = range(node);\n // An indented code block starts with its indentation, and has no place for a language.\n const fence = document.source[start];\n if (node.lang || (fence !== \"`\" && fence !== \"~\")) return;\n let end = start;\n while (document.source[end] === fence) end++;\n const marker = document.source.slice(start, end);\n findings.push({\n start,\n end,\n message: `Name the code block's language after its opening fence, such as ${marker}sh, or ${marker}text for plain text.`,\n });\n });\n return findings;\n },\n },\n};\n"]}
@@ -1,4 +1,5 @@
1
1
  import { walk } from "../syntax/walk.js";
2
+ import { blockIdentifier } from "../syntax/block-identifier.js";
2
3
  import { range } from "../syntax/parse.js";
3
4
  import { headingAttributeBlock } from "../workspace/heading-attributes.js";
4
5
  import { optionsSchema } from "./style.js";
@@ -77,13 +78,16 @@ export const dialectRules = {
77
78
  return [];
78
79
  const seen = new Set();
79
80
  const findings = [];
80
- walk(document.tree, "text", (node) => {
81
- const match = /(?:^|\s)\^([A-Za-z0-9-]+)\s*$/.exec(node.value);
82
- if (!match)
81
+ walk(document.tree, (node) => {
82
+ const found = blockIdentifier(document.source, node);
83
+ if (!found)
83
84
  return;
84
- const id = match[1];
85
+ const { id } = found;
85
86
  if (seen.has(id))
86
- findings.push({ start: range(node)[0], message: `Duplicate block identifier ^${id}.` });
87
+ findings.push({
88
+ start: range(found.node)[0],
89
+ message: `Duplicate block identifier ^${id}.`,
90
+ });
87
91
  seen.add(id);
88
92
  });
89
93
  return findings;
@@ -141,7 +145,7 @@ export const dialectRules = {
141
145
  ? [
142
146
  {
143
147
  start: 0,
144
- message: "Obsidian strictLineBreaks is not verified true; paragraph reflow is disabled. Set it in Obsidian or supply workspace.strictLineBreaks to the library.",
148
+ message: "Obsidian's \"Strict line breaks\" setting is not known to be on, so paragraphs are not reflowed. Turn it on in the vault's settings, which keep it in .obsidian/app.json; a program that calls the library passes strictLineBreaks to createWorkspace.",
145
149
  },
146
150
  ]
147
151
  : [];
@@ -1 +1 @@
1
- {"version":3,"file":"dialects.js","sourceRoot":"","sources":["../../src/rules/dialects.ts"],"names":[],"mappings":"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","sourcesContent":["import { walk } from \"../syntax/walk.js\";\nimport type { Dialect, Finding, Rule } from \"../core/types.js\";\nimport { range } from \"../syntax/parse.js\";\nimport { headingAttributeBlock } from \"../workspace/heading-attributes.js\";\nimport { optionsSchema } from \"./style.js\";\n\nconst githubAlerts = new Set<Dialect>([\"github\", \"forgejo\", \"gitea\"]);\nfunction calloutRule(obsidian: boolean): Rule {\n return {\n description: obsidian\n ? \"Use lowercase Obsidian callout types.\"\n : \"Use uppercase GitHub alert types.\",\n kind: \"style\",\n phase: \"inline\",\n schema: optionsSchema({}),\n check({ document }) {\n // Forgejo and Gitea render the same five GitHub alert types, case-insensitively.\n if (obsidian ? document.dialect !== \"obsidian\" : !githubAlerts.has(document.dialect))\n return [];\n const findings: Finding[] = [];\n walk(document.tree, \"blockquote\", (node) => {\n const child = node.children[0];\n if (child?.type !== \"paragraph\") return;\n const start = range(child)[0];\n const match = /^\\[!([\\w-]+)\\]/.exec(document.source.slice(start));\n if (!match) return;\n const original = match[1]!;\n if (\n !obsidian &&\n ![\"NOTE\", \"TIP\", \"IMPORTANT\", \"WARNING\", \"CAUTION\"].includes(original.toUpperCase())\n )\n return;\n const replacement = obsidian ? original.toLowerCase() : original.toUpperCase();\n if (original !== replacement)\n findings.push({\n start: start + 2,\n end: start + 2 + original.length,\n message: `Use ${replacement} for the callout type.`,\n edit: { start: start + 2, end: start + 2 + original.length, text: replacement },\n });\n });\n return findings;\n },\n };\n}\nexport const dialectRules: Record<string, Rule> = {\n \"github/alert-marker\": calloutRule(false),\n \"obsidian/callout-marker\": calloutRule(true),\n \"github/task-marker\": {\n description: \"Use lowercase x in completed task markers.\",\n kind: \"style\",\n phase: \"inline\",\n schema: optionsSchema({}),\n check({ document }) {\n const findings: Finding[] = [];\n walk(document.tree, \"listItem\", (node) => {\n if (node.checked !== true) return;\n const start = range(node)[0];\n const match = /^(?:[-+*]|\\d+[.)])\\s+\\[X\\]/.exec(document.source.slice(start));\n if (!match) return;\n const offset = start + match[0].length - 2;\n findings.push({\n start: offset,\n message: \"Use lowercase x in task markers.\",\n edit: { start: offset, end: offset + 1, text: \"x\" },\n });\n });\n return findings;\n },\n },\n \"obsidian/block-reference\": {\n description: \"Report duplicate block identifiers.\",\n kind: \"problem\",\n schema: optionsSchema({}),\n check({ document }) {\n if (document.dialect !== \"obsidian\") return [];\n const seen = new Set<string>();\n const findings: Finding[] = [];\n walk(document.tree, \"text\", (node) => {\n const match = /(?:^|\\s)\\^([A-Za-z0-9-]+)\\s*$/.exec(node.value);\n if (!match) return;\n const id = match[1]!;\n if (seen.has(id))\n findings.push({ start: range(node)[0], message: `Duplicate block identifier ^${id}.` });\n seen.add(id);\n });\n return findings;\n },\n },\n \"forgejo/heading-id\": {\n description:\n \"Report heading attribute ids that are not text, which Forgejo and Gitea before 1.26 cannot render.\",\n kind: \"problem\",\n schema: optionsSchema({}),\n check({ document }) {\n if (document.dialect !== \"forgejo\" && document.dialect !== \"gitea\") return [];\n const findings: Finding[] = [];\n const { source } = document;\n walk(document.tree, \"heading\", (node) => {\n const [start, end] = range(node);\n // The block ends an ATX heading's line or the last text line of a\n // Setext heading, whose last line is its underline.\n const breaks = [...source.slice(start, end).matchAll(/\\r\\n?|\\n/g)];\n const underline = breaks.at(-1);\n const previous = breaks.at(-2);\n const lineStart = previous ? start + previous.index + previous[0].length : start;\n let lineEnd = underline ? start + underline.index : end;\n while (source[lineEnd - 1] === \" \" || source[lineEnd - 1] === \"\\t\") lineEnd--;\n let line = source.slice(lineStart, lineEnd);\n let block = headingAttributeBlock(line);\n // Gitea before 1.26 also reads a block followed by a closing `#` sequence.\n if (!block && !underline) {\n const closed = /[ \\t]+#+$/.exec(line);\n if (closed) {\n line = line.slice(0, closed.index);\n block = headingAttributeBlock(line);\n }\n }\n // goldmark hands a number, boolean, null, or list to the renderers,\n // which expect text: Gitea 1.26 and later give the heading an empty\n // id, Forgejo and earlier Gitea versions fail on the whole document.\n if (block?.id === null)\n findings.push({\n start: lineStart + block.start,\n end: lineStart + line.length,\n message:\n 'Heading id is not text: Forgejo, and Gitea before 1.26, cannot render a document containing it. Quote it: {id=\"5\"}.',\n });\n });\n return findings;\n },\n },\n \"obsidian/strict-line-breaks\": {\n description: \"Require verified soft-line-break behavior before reflowing an Obsidian document.\",\n kind: \"problem\",\n schema: optionsSchema({}),\n check({ document, workspace }) {\n return document.dialect === \"obsidian\" && workspace?.strictLineBreaks !== true\n ? [\n {\n start: 0,\n message:\n \"Obsidian strictLineBreaks is not verified true; paragraph reflow is disabled. Set it in Obsidian or supply workspace.strictLineBreaks to the library.\",\n },\n ]\n : [];\n },\n },\n};\n"]}
1
+ {"version":3,"file":"dialects.js","sourceRoot":"","sources":["../../src/rules/dialects.ts"],"names":[],"mappings":"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","sourcesContent":["import { walk } from \"../syntax/walk.js\";\nimport type { Dialect, Finding, Rule } from \"../core/types.js\";\nimport { blockIdentifier } from \"../syntax/block-identifier.js\";\nimport { range } from \"../syntax/parse.js\";\nimport { headingAttributeBlock } from \"../workspace/heading-attributes.js\";\nimport { optionsSchema } from \"./style.js\";\n\nconst githubAlerts = new Set<Dialect>([\"github\", \"forgejo\", \"gitea\"]);\nfunction calloutRule(obsidian: boolean): Rule {\n return {\n description: obsidian\n ? \"Use lowercase Obsidian callout types.\"\n : \"Use uppercase GitHub alert types.\",\n kind: \"style\",\n phase: \"inline\",\n schema: optionsSchema({}),\n check({ document }) {\n // Forgejo and Gitea render the same five GitHub alert types, case-insensitively.\n if (obsidian ? document.dialect !== \"obsidian\" : !githubAlerts.has(document.dialect))\n return [];\n const findings: Finding[] = [];\n walk(document.tree, \"blockquote\", (node) => {\n const child = node.children[0];\n if (child?.type !== \"paragraph\") return;\n const start = range(child)[0];\n const match = /^\\[!([\\w-]+)\\]/.exec(document.source.slice(start));\n if (!match) return;\n const original = match[1]!;\n if (\n !obsidian &&\n ![\"NOTE\", \"TIP\", \"IMPORTANT\", \"WARNING\", \"CAUTION\"].includes(original.toUpperCase())\n )\n return;\n const replacement = obsidian ? original.toLowerCase() : original.toUpperCase();\n if (original !== replacement)\n findings.push({\n start: start + 2,\n end: start + 2 + original.length,\n message: `Use ${replacement} for the callout type.`,\n edit: { start: start + 2, end: start + 2 + original.length, text: replacement },\n });\n });\n return findings;\n },\n };\n}\nexport const dialectRules: Record<string, Rule> = {\n \"github/alert-marker\": calloutRule(false),\n \"obsidian/callout-marker\": calloutRule(true),\n \"github/task-marker\": {\n description: \"Use lowercase x in completed task markers.\",\n kind: \"style\",\n phase: \"inline\",\n schema: optionsSchema({}),\n check({ document }) {\n const findings: Finding[] = [];\n walk(document.tree, \"listItem\", (node) => {\n if (node.checked !== true) return;\n const start = range(node)[0];\n const match = /^(?:[-+*]|\\d+[.)])\\s+\\[X\\]/.exec(document.source.slice(start));\n if (!match) return;\n const offset = start + match[0].length - 2;\n findings.push({\n start: offset,\n message: \"Use lowercase x in task markers.\",\n edit: { start: offset, end: offset + 1, text: \"x\" },\n });\n });\n return findings;\n },\n },\n \"obsidian/block-reference\": {\n description: \"Report duplicate block identifiers.\",\n kind: \"problem\",\n schema: optionsSchema({}),\n check({ document }) {\n if (document.dialect !== \"obsidian\") return [];\n const seen = new Set<string>();\n const findings: Finding[] = [];\n walk(document.tree, (node) => {\n const found = blockIdentifier(document.source, node);\n if (!found) return;\n const { id } = found;\n if (seen.has(id))\n findings.push({\n start: range(found.node)[0],\n message: `Duplicate block identifier ^${id}.`,\n });\n seen.add(id);\n });\n return findings;\n },\n },\n \"forgejo/heading-id\": {\n description:\n \"Report heading attribute ids that are not text, which Forgejo and Gitea before 1.26 cannot render.\",\n kind: \"problem\",\n schema: optionsSchema({}),\n check({ document }) {\n if (document.dialect !== \"forgejo\" && document.dialect !== \"gitea\") return [];\n const findings: Finding[] = [];\n const { source } = document;\n walk(document.tree, \"heading\", (node) => {\n const [start, end] = range(node);\n // The block ends an ATX heading's line or the last text line of a\n // Setext heading, whose last line is its underline.\n const breaks = [...source.slice(start, end).matchAll(/\\r\\n?|\\n/g)];\n const underline = breaks.at(-1);\n const previous = breaks.at(-2);\n const lineStart = previous ? start + previous.index + previous[0].length : start;\n let lineEnd = underline ? start + underline.index : end;\n while (source[lineEnd - 1] === \" \" || source[lineEnd - 1] === \"\\t\") lineEnd--;\n let line = source.slice(lineStart, lineEnd);\n let block = headingAttributeBlock(line);\n // Gitea before 1.26 also reads a block followed by a closing `#` sequence.\n if (!block && !underline) {\n const closed = /[ \\t]+#+$/.exec(line);\n if (closed) {\n line = line.slice(0, closed.index);\n block = headingAttributeBlock(line);\n }\n }\n // goldmark hands a number, boolean, null, or list to the renderers,\n // which expect text: Gitea 1.26 and later give the heading an empty\n // id, Forgejo and earlier Gitea versions fail on the whole document.\n if (block?.id === null)\n findings.push({\n start: lineStart + block.start,\n end: lineStart + line.length,\n message:\n 'Heading id is not text: Forgejo, and Gitea before 1.26, cannot render a document containing it. Quote it: {id=\"5\"}.',\n });\n });\n return findings;\n },\n },\n \"obsidian/strict-line-breaks\": {\n description: \"Require verified soft-line-break behavior before reflowing an Obsidian document.\",\n kind: \"problem\",\n schema: optionsSchema({}),\n check({ document, workspace }) {\n return document.dialect === \"obsidian\" && workspace?.strictLineBreaks !== true\n ? [\n {\n start: 0,\n message:\n \"Obsidian's \\\"Strict line breaks\\\" setting is not known to be on, so paragraphs are not reflowed. Turn it on in the vault's settings, which keep it in .obsidian/app.json; a program that calls the library passes strictLineBreaks to createWorkspace.\",\n },\n ]\n : [];\n },\n },\n};\n"]}
@@ -64,6 +64,205 @@ function destinationRange(raw) {
64
64
  }
65
65
  return undefined;
66
66
  }
67
+ /**
68
+ * A Markdown link's destination as a wikilink's target, or undefined when a
69
+ * wikilink cannot hold it. Obsidian takes a wikilink's text as the note's
70
+ * name, so percent escapes are decoded: `My%20Note.md` is the note `My
71
+ * Note.md`, which is right whether or not Obsidian decodes escapes in
72
+ * wikilinks. A destination that does not decode, or whose decoded name holds
73
+ * a character that a wikilink cannot contain, or that would change its meaning
74
+ * there, stays a Markdown link.
75
+ */
76
+ function wikiTarget(url) {
77
+ const hash = url.indexOf("#");
78
+ const [name, fragment] = hash < 0 ? [url, undefined] : [url.slice(0, hash), url.slice(hash + 1)];
79
+ let decodedName;
80
+ let decodedFragment;
81
+ try {
82
+ decodedName = decodeURIComponent(name);
83
+ decodedFragment = fragment === undefined ? undefined : decodeURIComponent(fragment);
84
+ }
85
+ catch {
86
+ return undefined;
87
+ }
88
+ if (/[[\]|#^\r\n]/.test(decodedName) || decodedName.trim() !== decodedName)
89
+ return undefined;
90
+ if (decodedFragment !== undefined) {
91
+ if (/[[\]|\r\n]/.test(decodedFragment))
92
+ return undefined;
93
+ // An escaped `#` or `^` would become a separator or a block reference.
94
+ const marks = (text) => text.replace(/[^#^]/g, "");
95
+ if (marks(decodedFragment) !== marks(fragment))
96
+ return undefined;
97
+ }
98
+ return decodedFragment === undefined ? decodedName : `${decodedName}#${decodedFragment}`;
99
+ }
100
+ /**
101
+ * A label as CommonMark matches it: case folded, with its blank space
102
+ * collapsed and trimmed. This is how the parser computes the `identifier` of
103
+ * definitions and references, so the two compare equal.
104
+ */
105
+ function normalizeLabel(label) {
106
+ return label
107
+ .replace(/[\t\n\r ]+/g, " ")
108
+ .replace(/^ | $/g, "")
109
+ .toLowerCase()
110
+ .toUpperCase()
111
+ .toLowerCase();
112
+ }
113
+ /**
114
+ * A label's text as the parser reads it, from its source: without the block
115
+ * quote markers and indentation that continue its lines inside a container.
116
+ */
117
+ function labelText(source) {
118
+ return source.replace(/(\r\n|\r|\n)[ \t>]*/g, "$1");
119
+ }
120
+ /**
121
+ * The end, after its `]`, of the link label that starts with the `[` at `at`
122
+ * and ends before `limit`: the first unescaped `]`, with no unescaped `[`
123
+ * before it, at most 999 characters, and something besides blank space.
124
+ */
125
+ function labelEnd(source, at, limit) {
126
+ let blank = true;
127
+ for (let i = at + 1; i < limit && i - at <= 1000; i++) {
128
+ const char = source[i];
129
+ if (char === "\\" && /[[\]\\]/.test(source[i + 1] ?? "")) {
130
+ blank = false;
131
+ i++;
132
+ }
133
+ else if (char === "[")
134
+ return undefined;
135
+ else if (char === "]")
136
+ return blank ? undefined : i + 1;
137
+ else if (!/[ \t\r\n]/.test(char))
138
+ blank = false;
139
+ }
140
+ return undefined;
141
+ }
142
+ /** A GFM footnote label after `^`: no blank space, and brackets only escaped. */
143
+ const footnoteLabel = /^\^((?:\\[[\]\\]|\\(?![[\]\\])|[^\s[\]\\]){1,999})$/;
144
+ /** Inline nodes whose text is read for brackets; any other node is syntax or a link of its own. */
145
+ const transparent = new Set(["emphasis", "strong", "delete"]);
146
+ /**
147
+ * What before a reference's text makes it no reference to report: an ASCII
148
+ * letter, digit, `_`, or `)`, as in the indexes `matrix[i][j]` and `f(x)[0][1]`
149
+ * in prose, or a `]`, which continues a run like `[a][b][c]` that is reported
150
+ * once, at its start.
151
+ */
152
+ const notAfter = /[\w\])]/;
153
+ /** Characters that a backslash escapes. */
154
+ const asciiPunctuation = /[!-/:-@[-`{-~]/;
155
+ /**
156
+ * The full (`[text][label]`) and collapsed (`[label][]`) references, and with
157
+ * `footnotes` the footnote references (`[^label]`), whose label has no
158
+ * definition. The parser leaves these as plain text, so they are found in the
159
+ * text of each paragraph, heading, and table cell, as CommonMark pairs its
160
+ * brackets. Brackets in code, math, HTML, links, images, and other syntax, and
161
+ * escaped ones, are not read.
162
+ */
163
+ function undefinedReferences(document, definitions, footnotes) {
164
+ const { source } = document;
165
+ if (!source.includes("["))
166
+ return [];
167
+ const references = [];
168
+ const forge = document.dialect === "forgejo" || document.dialect === "gitea";
169
+ // 1 where a character is text that the parser reads brackets in.
170
+ const readable = new Uint8Array(source.length);
171
+ const collect = (node) => {
172
+ if (!("children" in node))
173
+ return;
174
+ for (const child of node.children)
175
+ if (child.type === "text") {
176
+ const [from, to] = range(child);
177
+ for (let i = from; i < to; i++) {
178
+ if (source[i] === "\\" && i + 1 < to && asciiPunctuation.test(source[i + 1]))
179
+ i++;
180
+ else
181
+ readable[i] = 1;
182
+ }
183
+ }
184
+ else if (transparent.has(child.type))
185
+ collect(child);
186
+ };
187
+ walk(document.tree, (node) => {
188
+ if (node.type !== "paragraph" && node.type !== "heading" && node.type !== "tableCell")
189
+ return;
190
+ const [start, end] = range(node);
191
+ // Searched in the block's own text, so that blocks without brackets cost no more than that.
192
+ const text = source.slice(start, end);
193
+ if (!text.includes("["))
194
+ return;
195
+ collect(node);
196
+ // Forgejo and Gitea render `\(...\)` and `\[...\]` as math, which GFM reads as text.
197
+ if (forge)
198
+ for (const [open, close] of [
199
+ ["\\(", "\\)"],
200
+ ["\\[", "\\]"],
201
+ ])
202
+ for (let from = 0;;) {
203
+ const opening = text.indexOf(open, from);
204
+ const closing = opening < 0 ? -1 : text.indexOf(close, opening + 2);
205
+ if (closing < 0)
206
+ break;
207
+ readable.fill(0, start + opening, start + closing + 2);
208
+ from = closing + 2;
209
+ }
210
+ const openers = [];
211
+ // The opening bracket of the last full reference's label, which is not read again.
212
+ let label = -1;
213
+ for (let i = start; i < end; i++) {
214
+ if (!readable[i])
215
+ continue;
216
+ if (source[i] === "[") {
217
+ openers.push(i);
218
+ continue;
219
+ }
220
+ if (source[i] !== "]")
221
+ continue;
222
+ const opener = openers.pop();
223
+ if (opener === undefined || opener === label)
224
+ continue;
225
+ let reference;
226
+ if (source[i + 1] === "[" && readable[i + 1]) {
227
+ if (source[i + 2] === "]" && readable[i + 2]) {
228
+ // Collapsed: the text is the label.
229
+ if (labelEnd(source, opener, i + 1) === i + 1)
230
+ reference = {
231
+ start: opener,
232
+ end: i + 3,
233
+ label: source.slice(opener + 1, i),
234
+ footnote: false,
235
+ };
236
+ }
237
+ else {
238
+ const after = labelEnd(source, i + 1, end);
239
+ if (after !== undefined && readable[after - 1]) {
240
+ reference = {
241
+ start: opener,
242
+ end: after,
243
+ label: source.slice(i + 2, after - 1),
244
+ footnote: false,
245
+ };
246
+ label = i + 1;
247
+ }
248
+ }
249
+ if (reference) {
250
+ if (!definitions.has(normalizeLabel(labelText(reference.label))) &&
251
+ !(opener > 0 && readable[opener - 1] && notAfter.test(source[opener - 1])))
252
+ references.push(reference);
253
+ continue;
254
+ }
255
+ }
256
+ // A footnote label has at most 999 characters after its `^`.
257
+ if (!footnotes || source[opener + 1] !== "^" || i - opener > 1001)
258
+ continue;
259
+ const footnote = footnoteLabel.exec(source.slice(opener + 1, i));
260
+ if (footnote && !footnotes.has(normalizeLabel(footnote[1])))
261
+ references.push({ start: opener, end: i + 1, label: footnote[1], footnote: true });
262
+ }
263
+ });
264
+ return references;
265
+ }
67
266
  /** Names the first few of the paths that could not be read, and counts the rest. */
68
267
  function named(paths) {
69
268
  return paths.length > 3
@@ -109,9 +308,11 @@ export const linkRules = {
109
308
  ];
110
309
  findings.push({
111
310
  start: range(node)[0],
112
- message: result.target === undefined
113
- ? `Local target could not be checked: ${url} (no readable match; ${reasons.join("; ")}).`
114
- : `Fragment could not be checked: ${url} (cannot read ${result.target}).`,
311
+ message: result.leavesRoot !== undefined
312
+ ? `Local target could not be checked: ${url} (${result.leavesRoot} leads outside the workspace root, which the tool does not read).`
313
+ : result.target === undefined
314
+ ? `Local target could not be checked: ${url} (no readable match; ${reasons.join("; ")}).`
315
+ : `Fragment could not be checked: ${url} (cannot read ${result.target}).`,
115
316
  });
116
317
  }
117
318
  else if (result.status === "resolved" && !result.fragmentExists)
@@ -242,7 +443,7 @@ export const linkRules = {
242
443
  .replaceAll("<", "%3C")
243
444
  .replaceAll(">", "%3E");
244
445
  if (document.dialect === "gitea" &&
245
- (giteaPyLine(document.source, start, end) || replacement.includes("_.py")) &&
446
+ (giteaPyLine(document, start, end) || replacement.includes("_.py")) &&
246
447
  giteaUnderscores(original) !== giteaUnderscores(replacement))
247
448
  return;
248
449
  // On Forgejo, and Gitea before 1.26, the destination is part of the
@@ -286,9 +487,10 @@ export const linkRules = {
286
487
  node.children.length === 1 &&
287
488
  node.children[0]?.type === "text") {
288
489
  const label = node.children[0].value;
289
- if (/[[\]|\r\n]/.test(node.url + label))
490
+ const target = wikiTarget(node.url);
491
+ if (target === undefined || /[[\]|\r\n]/.test(label))
290
492
  return;
291
- replacement = `[[${node.url}|${label}]]`;
493
+ replacement = `[[${target}|${label}]]`;
292
494
  }
293
495
  const url = destination(node);
294
496
  if (!replacement || url === undefined)
@@ -307,5 +509,99 @@ export const linkRules = {
307
509
  return findings;
308
510
  },
309
511
  },
512
+ "links/undefined-reference": {
513
+ description: "Report full, collapsed, and footnote references whose label has no definition, which show as plain text.",
514
+ kind: "problem",
515
+ schema: optionsSchema({}),
516
+ check({ document }) {
517
+ const definitions = new Set();
518
+ const footnotes = new Set();
519
+ walk(document.tree, (node) => {
520
+ if (node.type === "definition")
521
+ definitions.add(node.identifier);
522
+ if (node.type === "footnoteDefinition")
523
+ footnotes.add(node.identifier);
524
+ });
525
+ // CommonMark has no footnotes: there `[^label]` is an ordinary shortcut reference.
526
+ return undefinedReferences(document, definitions, document.dialect === "commonmark" ? undefined : footnotes).map(({ start, end, label, footnote }) => {
527
+ const name = (footnote ? `^${label}` : labelText(label)).replace(/\s+/g, " ").trim();
528
+ return {
529
+ start,
530
+ end,
531
+ message: `${footnote ? "Footnote" : "Reference"} [${name}] has no definition and shows as plain text.`,
532
+ };
533
+ });
534
+ },
535
+ },
536
+ "links/unused-definition": {
537
+ description: "Report link and footnote definitions that no reference uses.",
538
+ kind: "problem",
539
+ schema: optionsSchema({}),
540
+ check({ document }) {
541
+ // Link and footnote labels are kept apart, as `link:` and `footnote:` keys.
542
+ const used = new Set();
543
+ const definitions = [];
544
+ walk(document.tree, (node) => {
545
+ // A shortcut reference `[label]` that has a definition is a reference node too.
546
+ if (node.type === "linkReference" || node.type === "imageReference")
547
+ used.add(`link:${node.identifier}`);
548
+ else if (node.type === "footnoteReference")
549
+ used.add(`footnote:${node.identifier}`);
550
+ else if (node.type === "definition" || node.type === "footnoteDefinition")
551
+ definitions.push(node);
552
+ });
553
+ const findings = [];
554
+ const seen = new Set();
555
+ for (const node of definitions) {
556
+ const footnote = node.type === "footnoteDefinition";
557
+ const key = `${footnote ? "footnote" : "link"}:${node.identifier}`;
558
+ const label = (node.label ?? node.identifier).replace(/\s+/g, " ").trim();
559
+ const name = `[${footnote ? "^" : ""}${label}]`;
560
+ const [start] = range(node);
561
+ // CommonMark uses the first definition of a label; a later one is never used.
562
+ if (seen.has(key))
563
+ findings.push({
564
+ start,
565
+ message: `${footnote ? "Footnote" : "Definition"} ${name} repeats an earlier one, which references use instead.`,
566
+ });
567
+ else if (!used.has(key))
568
+ findings.push({
569
+ start,
570
+ message: footnote
571
+ ? `Footnote ${name} is not referenced${document.dialect === "github" ? ", and GitHub does not show it" : ""}.`
572
+ : `Definition ${name} is not used by any reference.`,
573
+ });
574
+ seen.add(key);
575
+ }
576
+ return findings;
577
+ },
578
+ },
579
+ "links/empty": {
580
+ description: "Report links and images whose destination is empty or only #.",
581
+ kind: "problem",
582
+ schema: optionsSchema({}),
583
+ check({ document }) {
584
+ const findings = [];
585
+ // A reference has its definition's destination, which is reported once, at the definition.
586
+ walk(document.tree, (node) => {
587
+ if (node.type !== "link" && node.type !== "image" && node.type !== "definition")
588
+ return;
589
+ if (node.url !== "" && node.url !== "#")
590
+ return;
591
+ const what = node.type === "image"
592
+ ? "Image source"
593
+ : node.type === "definition"
594
+ ? `Destination of definition [${(node.label ?? node.identifier).replace(/\s+/g, " ").trim()}]`
595
+ : "Link destination";
596
+ findings.push({
597
+ start: range(node)[0],
598
+ message: node.url === ""
599
+ ? `${what} is empty.`
600
+ : `${what} is only #${node.type === "link" ? ", which only leads to the top of the page" : ""}.`,
601
+ });
602
+ });
603
+ return findings;
604
+ },
605
+ },
310
606
  };
311
607
  //# sourceMappingURL=links.js.map