mdrefine 0.1.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CONTRIBUTING.md +10 -0
- package/LICENSE +21 -0
- package/README.md +133 -0
- package/SECURITY.md +16 -0
- package/dist/cli/main.d.ts +2 -0
- package/dist/cli/main.js +158 -0
- package/dist/cli/main.js.map +1 -0
- package/dist/config/load.d.ts +9 -0
- package/dist/config/load.js +75 -0
- package/dist/config/load.js.map +1 -0
- package/dist/config/resolve.d.ts +93 -0
- package/dist/config/resolve.js +110 -0
- package/dist/config/resolve.js.map +1 -0
- package/dist/core/directives.d.ts +3 -0
- package/dist/core/directives.js +48 -0
- package/dist/core/directives.js.map +1 -0
- package/dist/core/engine.d.ts +8 -0
- package/dist/core/engine.js +192 -0
- package/dist/core/engine.js.map +1 -0
- package/dist/core/types.d.ts +92 -0
- package/dist/core/types.js +2 -0
- package/dist/core/types.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -0
- package/dist/rules/dialects.d.ts +2 -0
- package/dist/rules/dialects.js +105 -0
- package/dist/rules/dialects.js.map +1 -0
- package/dist/rules/links.d.ts +2 -0
- package/dist/rules/links.js +238 -0
- package/dist/rules/links.js.map +1 -0
- package/dist/rules/style.d.ts +12 -0
- package/dist/rules/style.js +244 -0
- package/dist/rules/style.js.map +1 -0
- package/dist/syntax/obsidian.d.ts +33 -0
- package/dist/syntax/obsidian.js +126 -0
- package/dist/syntax/obsidian.js.map +1 -0
- package/dist/syntax/parse.d.ts +5 -0
- package/dist/syntax/parse.js +45 -0
- package/dist/syntax/parse.js.map +1 -0
- package/dist/workspace/files.d.ts +8 -0
- package/dist/workspace/files.js +98 -0
- package/dist/workspace/files.js.map +1 -0
- package/dist/workspace/index.d.ts +10 -0
- package/dist/workspace/index.js +100 -0
- package/dist/workspace/index.js.map +1 -0
- package/docs/cli.md +71 -0
- package/docs/configuration.md +171 -0
- package/docs/contributing.md +103 -0
- package/docs/design.md +79 -0
- package/docs/index.md +48 -0
- package/docs/obsidian.md +69 -0
- package/docs/plugins.md +114 -0
- package/docs/quick-start.md +101 -0
- package/docs/releases/0.1.0-beta.1.md +39 -0
- package/docs/releases.md +90 -0
- package/examples/github.jsonc +7 -0
- package/examples/obsidian.jsonc +8 -0
- package/package.json +101 -0
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { visit } from "unist-util-visit";
|
|
2
|
+
import { range } from "../syntax/parse.js";
|
|
3
|
+
import { optionsSchema } from "./style.js";
|
|
4
|
+
function calloutRule(obsidian) {
|
|
5
|
+
return {
|
|
6
|
+
description: obsidian
|
|
7
|
+
? "Use lowercase Obsidian callout types."
|
|
8
|
+
: "Use uppercase GitHub alert types.",
|
|
9
|
+
kind: "style",
|
|
10
|
+
phase: "inline",
|
|
11
|
+
schema: optionsSchema({}),
|
|
12
|
+
check({ document }) {
|
|
13
|
+
if (document.dialect !== (obsidian ? "obsidian" : "github"))
|
|
14
|
+
return [];
|
|
15
|
+
const findings = [];
|
|
16
|
+
visit(document.tree, "blockquote", (node) => {
|
|
17
|
+
const child = node.children[0];
|
|
18
|
+
if (child?.type !== "paragraph")
|
|
19
|
+
return;
|
|
20
|
+
const start = range(child)[0];
|
|
21
|
+
const match = /^\[!([\w-]+)\]/.exec(document.source.slice(start));
|
|
22
|
+
if (!match)
|
|
23
|
+
return;
|
|
24
|
+
const original = match[1];
|
|
25
|
+
if (!obsidian &&
|
|
26
|
+
!["NOTE", "TIP", "IMPORTANT", "WARNING", "CAUTION"].includes(original.toUpperCase()))
|
|
27
|
+
return;
|
|
28
|
+
const replacement = obsidian ? original.toLowerCase() : original.toUpperCase();
|
|
29
|
+
if (original !== replacement)
|
|
30
|
+
findings.push({
|
|
31
|
+
start: start + 2,
|
|
32
|
+
end: start + 2 + original.length,
|
|
33
|
+
message: `Use ${replacement} for the callout type.`,
|
|
34
|
+
edit: { start: start + 2, end: start + 2 + original.length, text: replacement },
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
return findings;
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
export const dialectRules = {
|
|
42
|
+
"github/alert-marker": calloutRule(false),
|
|
43
|
+
"obsidian/callout-marker": calloutRule(true),
|
|
44
|
+
"github/task-marker": {
|
|
45
|
+
description: "Use lowercase x in completed task markers.",
|
|
46
|
+
kind: "style",
|
|
47
|
+
phase: "inline",
|
|
48
|
+
schema: optionsSchema({}),
|
|
49
|
+
check({ document }) {
|
|
50
|
+
const findings = [];
|
|
51
|
+
visit(document.tree, "listItem", (node) => {
|
|
52
|
+
if (node.checked !== true)
|
|
53
|
+
return;
|
|
54
|
+
const start = range(node)[0];
|
|
55
|
+
const match = /^(?:[-+*]|\d+[.)])\s+\[X\]/.exec(document.source.slice(start));
|
|
56
|
+
if (!match)
|
|
57
|
+
return;
|
|
58
|
+
const offset = start + match[0].length - 2;
|
|
59
|
+
findings.push({
|
|
60
|
+
start: offset,
|
|
61
|
+
message: "Use lowercase x in task markers.",
|
|
62
|
+
edit: { start: offset, end: offset + 1, text: "x" },
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
return findings;
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
"obsidian/block-reference": {
|
|
69
|
+
description: "Report duplicate block identifiers.",
|
|
70
|
+
kind: "problem",
|
|
71
|
+
schema: optionsSchema({}),
|
|
72
|
+
check({ document }) {
|
|
73
|
+
if (document.dialect !== "obsidian")
|
|
74
|
+
return [];
|
|
75
|
+
const seen = new Set();
|
|
76
|
+
const findings = [];
|
|
77
|
+
visit(document.tree, "text", (node) => {
|
|
78
|
+
const match = /(?:^|\s)\^([A-Za-z0-9-]+)\s*$/.exec(node.value);
|
|
79
|
+
if (!match)
|
|
80
|
+
return;
|
|
81
|
+
const id = match[1];
|
|
82
|
+
if (seen.has(id))
|
|
83
|
+
findings.push({ start: range(node)[0], message: `Duplicate block identifier ^${id}.` });
|
|
84
|
+
seen.add(id);
|
|
85
|
+
});
|
|
86
|
+
return findings;
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
"obsidian/strict-line-breaks": {
|
|
90
|
+
description: "Require verified soft-line-break behavior before reflowing an Obsidian document.",
|
|
91
|
+
kind: "problem",
|
|
92
|
+
schema: optionsSchema({}),
|
|
93
|
+
check({ document, workspace }) {
|
|
94
|
+
return document.dialect === "obsidian" && workspace?.strictLineBreaks !== true
|
|
95
|
+
? [
|
|
96
|
+
{
|
|
97
|
+
start: 0,
|
|
98
|
+
message: "Obsidian strictLineBreaks is not verified true; paragraph reflow is disabled. Set it in Obsidian or supply workspace.strictLineBreaks to the library.",
|
|
99
|
+
},
|
|
100
|
+
]
|
|
101
|
+
: [];
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
//# sourceMappingURL=dialects.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dialects.js","sourceRoot":"","sources":["../../src/rules/dialects.ts"],"names":[],"mappings":"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","sourcesContent":["import { visit } from \"unist-util-visit\";\nimport type { Finding, Rule } from \"../core/types.js\";\nimport { range } from \"../syntax/parse.js\";\nimport { optionsSchema } from \"./style.js\";\n\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 if (document.dialect !== (obsidian ? \"obsidian\" : \"github\")) return [];\n const findings: Finding[] = [];\n visit(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 visit(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 visit(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 \"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"]}
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { visit } from "unist-util-visit";
|
|
3
|
+
import { range } from "../syntax/parse.js";
|
|
4
|
+
import { splitDestination } from "../workspace/index.js";
|
|
5
|
+
import { optionsSchema } from "./style.js";
|
|
6
|
+
function destination(node) {
|
|
7
|
+
if (node.type === "wikiLink")
|
|
8
|
+
return node.target;
|
|
9
|
+
if (node.type === "link" || node.type === "image" || node.type === "definition")
|
|
10
|
+
return node.url;
|
|
11
|
+
return undefined;
|
|
12
|
+
}
|
|
13
|
+
/** Locate an inline destination within a parsed link, retaining labels and optional titles. */
|
|
14
|
+
function destinationRange(raw) {
|
|
15
|
+
let bracket = 0;
|
|
16
|
+
let opening = -1;
|
|
17
|
+
for (let i = raw.startsWith("!") ? 1 : 0; i < raw.length; i++) {
|
|
18
|
+
if (raw[i] === "\\") {
|
|
19
|
+
i++;
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
if (raw[i] === "[")
|
|
23
|
+
bracket++;
|
|
24
|
+
if (raw[i] === "]" && --bracket === 0 && raw[i + 1] === "(") {
|
|
25
|
+
opening = i + 2;
|
|
26
|
+
break;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
if (opening < 0)
|
|
30
|
+
return undefined;
|
|
31
|
+
while (/\s/.test(raw[opening] ?? ""))
|
|
32
|
+
opening++;
|
|
33
|
+
if (raw[opening] === "<") {
|
|
34
|
+
for (let i = opening + 1; i < raw.length; i++) {
|
|
35
|
+
if (raw[i] === "\\") {
|
|
36
|
+
i++;
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
if (raw[i] === ">")
|
|
40
|
+
return [opening, i + 1];
|
|
41
|
+
}
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
let depth = 0;
|
|
45
|
+
for (let i = opening; i < raw.length; i++) {
|
|
46
|
+
const char = raw[i];
|
|
47
|
+
if (char === "\\") {
|
|
48
|
+
i++;
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
if (char === "(")
|
|
52
|
+
depth++;
|
|
53
|
+
if ((char === ")" && depth-- === 0) || /\s/.test(char))
|
|
54
|
+
return [opening, i];
|
|
55
|
+
}
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
export const linkRules = {
|
|
59
|
+
"links/valid": {
|
|
60
|
+
description: "Check local link destinations and dialect-specific heading/block references.",
|
|
61
|
+
kind: "problem",
|
|
62
|
+
schema: optionsSchema({}),
|
|
63
|
+
check({ document, workspace }) {
|
|
64
|
+
const findings = [];
|
|
65
|
+
const definitions = new Set();
|
|
66
|
+
visit(document.tree, "definition", (node) => {
|
|
67
|
+
definitions.add(node.identifier);
|
|
68
|
+
});
|
|
69
|
+
visit(document.tree, (node) => {
|
|
70
|
+
if ((node.type === "linkReference" || node.type === "imageReference") &&
|
|
71
|
+
!definitions.has(node.identifier))
|
|
72
|
+
findings.push({
|
|
73
|
+
start: range(node)[0],
|
|
74
|
+
message: `Missing link definition: ${node.identifier}.`,
|
|
75
|
+
});
|
|
76
|
+
const url = destination(node);
|
|
77
|
+
if (url === undefined || !workspace)
|
|
78
|
+
return;
|
|
79
|
+
const result = workspace.resolve(document.path, url, document.dialect, node.type === "wikiLink");
|
|
80
|
+
if (result.status === "missing" || result.status === "ambiguous")
|
|
81
|
+
findings.push({
|
|
82
|
+
start: range(node)[0],
|
|
83
|
+
message: `${result.status === "missing" ? "Missing" : "Ambiguous"} local target: ${url}.`,
|
|
84
|
+
});
|
|
85
|
+
else if (result.status === "resolved" && !result.fragmentExists)
|
|
86
|
+
findings.push({ start: range(node)[0], message: `Missing fragment in ${url}.` });
|
|
87
|
+
});
|
|
88
|
+
return findings;
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
"links/path": {
|
|
92
|
+
description: "Rewrite verified local destinations using the configured path and bracket style.",
|
|
93
|
+
kind: "style",
|
|
94
|
+
phase: "inline",
|
|
95
|
+
schema: optionsSchema({
|
|
96
|
+
style: { enum: ["relative", "root", "shortest", "preserve"] },
|
|
97
|
+
brackets: { enum: ["angle", "bare", "preserve"] },
|
|
98
|
+
extension: { enum: ["include", "omit", "preserve"] },
|
|
99
|
+
leadingDot: { type: "boolean" },
|
|
100
|
+
}),
|
|
101
|
+
check({ document, options, workspace }) {
|
|
102
|
+
const findings = [];
|
|
103
|
+
visit(document.tree, (node) => {
|
|
104
|
+
const url = destination(node);
|
|
105
|
+
if (url === undefined || !workspace)
|
|
106
|
+
return;
|
|
107
|
+
const result = workspace.resolve(document.path, url, document.dialect, node.type === "wikiLink");
|
|
108
|
+
if (result.status !== "resolved" || !result.target || !result.fragmentExists)
|
|
109
|
+
return;
|
|
110
|
+
const parts = splitDestination(url);
|
|
111
|
+
let target = parts.path;
|
|
112
|
+
const style = options.style ?? "preserve";
|
|
113
|
+
// Vault roots and extensionless Markdown are renderer-specific; don't change other dialects.
|
|
114
|
+
if (document.dialect !== "obsidian" &&
|
|
115
|
+
(style === "root" || style === "shortest" || options.extension === "omit"))
|
|
116
|
+
return;
|
|
117
|
+
if (parts.path) {
|
|
118
|
+
if (style === "root")
|
|
119
|
+
target = result.target;
|
|
120
|
+
else if (style === "relative")
|
|
121
|
+
target = path.posix.relative(path.posix.dirname(document.path), result.target);
|
|
122
|
+
else if (style === "shortest") {
|
|
123
|
+
const basename = path.posix.basename(result.target);
|
|
124
|
+
const resolved = workspace.resolve(document.path, basename, document.dialect, true);
|
|
125
|
+
target =
|
|
126
|
+
resolved.status === "resolved" && resolved.target === result.target
|
|
127
|
+
? basename
|
|
128
|
+
: result.target;
|
|
129
|
+
}
|
|
130
|
+
if (document.dialect === "obsidian" && result.target.endsWith(".md")) {
|
|
131
|
+
if (options.extension === "omit" ||
|
|
132
|
+
((options.extension ?? "preserve") === "preserve" && !parts.path.endsWith(".md")))
|
|
133
|
+
target = target.replace(/\.md$/, "");
|
|
134
|
+
else if (options.extension === "include" && !target.endsWith(".md"))
|
|
135
|
+
target += ".md";
|
|
136
|
+
}
|
|
137
|
+
if (style === "relative" && options.leadingDot === true && !target.startsWith("."))
|
|
138
|
+
target = `./${target}`;
|
|
139
|
+
}
|
|
140
|
+
// Encode reserved path characters before reattaching a separately parsed fragment.
|
|
141
|
+
const encodedPath = target
|
|
142
|
+
.replaceAll("%", "%25")
|
|
143
|
+
.replaceAll("#", "%23")
|
|
144
|
+
.replaceAll("?", "%3F");
|
|
145
|
+
let replacement = encodedPath + (parts.fragment ? `#${parts.fragment.replaceAll("%", "%25")}` : "");
|
|
146
|
+
const [start, end] = range(node);
|
|
147
|
+
if (node.type === "wikiLink") {
|
|
148
|
+
if (/[[\]|\r\n]/.test(replacement))
|
|
149
|
+
return;
|
|
150
|
+
if (!node.embed && node.label === undefined && replacement !== node.target)
|
|
151
|
+
return;
|
|
152
|
+
replacement = `${node.embed ? "!" : ""}[[${replacement}${node.label === undefined ? "" : `|${node.label}`}]]`;
|
|
153
|
+
if (replacement !== document.source.slice(start, end))
|
|
154
|
+
findings.push({
|
|
155
|
+
start,
|
|
156
|
+
end,
|
|
157
|
+
message: "Normalize wikilink destination.",
|
|
158
|
+
edit: { start, end, text: replacement },
|
|
159
|
+
});
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
if (node.type === "definition")
|
|
163
|
+
return; // Definitions are validated; retain their exact layout in this release.
|
|
164
|
+
const raw = document.source.slice(start, end);
|
|
165
|
+
const span = destinationRange(raw);
|
|
166
|
+
if (!span)
|
|
167
|
+
return; // Autolinks have no parenthesized destination.
|
|
168
|
+
const original = raw.slice(...span);
|
|
169
|
+
const angle = options.brackets === "angle" ||
|
|
170
|
+
((options.brackets ?? "preserve") === "preserve" && original.startsWith("<"));
|
|
171
|
+
if (angle)
|
|
172
|
+
replacement = `<${replacement.replaceAll("<", "%3C").replaceAll(">", "%3E").replaceAll("\\", "%5C")}>`;
|
|
173
|
+
else
|
|
174
|
+
replacement = replacement
|
|
175
|
+
.replaceAll(" ", "%20")
|
|
176
|
+
.replaceAll("(", "%28")
|
|
177
|
+
.replaceAll(")", "%29")
|
|
178
|
+
.replaceAll("<", "%3C")
|
|
179
|
+
.replaceAll(">", "%3E");
|
|
180
|
+
if (original !== replacement)
|
|
181
|
+
findings.push({
|
|
182
|
+
start: start + span[0],
|
|
183
|
+
end: start + span[1],
|
|
184
|
+
message: "Normalize link destination.",
|
|
185
|
+
edit: { start: start + span[0], end: start + span[1], text: replacement },
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
return findings;
|
|
189
|
+
},
|
|
190
|
+
},
|
|
191
|
+
"links/notation": {
|
|
192
|
+
description: "Convert plain-label Obsidian links between Markdown and wikilink notation.",
|
|
193
|
+
kind: "style",
|
|
194
|
+
phase: "inline",
|
|
195
|
+
schema: optionsSchema({ style: { enum: ["markdown", "wiki"] } }),
|
|
196
|
+
check({ document, options, workspace }) {
|
|
197
|
+
if (document.dialect !== "obsidian" || !workspace)
|
|
198
|
+
return [];
|
|
199
|
+
const findings = [];
|
|
200
|
+
visit(document.tree, (node) => {
|
|
201
|
+
const [start, end] = range(node);
|
|
202
|
+
let replacement;
|
|
203
|
+
if (options.style === "markdown" &&
|
|
204
|
+
node.type === "wikiLink" &&
|
|
205
|
+
!node.embed &&
|
|
206
|
+
node.label !== undefined) {
|
|
207
|
+
if (/[[\]<>\\\r\n]/.test(node.target ?? "") || /[*_`[\]\\]/.test(node.label))
|
|
208
|
+
return;
|
|
209
|
+
replacement = `[${node.label}](<${node.target}>)`;
|
|
210
|
+
}
|
|
211
|
+
else if (options.style === "wiki" &&
|
|
212
|
+
node.type === "link" &&
|
|
213
|
+
!node.title &&
|
|
214
|
+
node.children.length === 1 &&
|
|
215
|
+
node.children[0]?.type === "text") {
|
|
216
|
+
const label = node.children[0].value;
|
|
217
|
+
if (/[[\]|\r\n]/.test(node.url + label))
|
|
218
|
+
return;
|
|
219
|
+
replacement = `[[${node.url}|${label}]]`;
|
|
220
|
+
}
|
|
221
|
+
const url = destination(node);
|
|
222
|
+
if (!replacement ||
|
|
223
|
+
url === undefined ||
|
|
224
|
+
workspace.resolve(document.path, url, document.dialect, node.type === "wikiLink")
|
|
225
|
+
.status !== "resolved")
|
|
226
|
+
return;
|
|
227
|
+
findings.push({
|
|
228
|
+
start,
|
|
229
|
+
end,
|
|
230
|
+
message: `Use ${String(options.style)} link notation.`,
|
|
231
|
+
edit: { start, end, text: replacement },
|
|
232
|
+
});
|
|
233
|
+
});
|
|
234
|
+
return findings;
|
|
235
|
+
},
|
|
236
|
+
},
|
|
237
|
+
};
|
|
238
|
+
//# sourceMappingURL=links.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"links.js","sourceRoot":"","sources":["../../src/rules/links.ts"],"names":[],"mappings":"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","sourcesContent":["import path from \"node:path\";\nimport { visit } from \"unist-util-visit\";\nimport type { Nodes } from \"mdast\";\nimport type { Finding, Rule } from \"../core/types.js\";\nimport { range } from \"../syntax/parse.js\";\nimport { splitDestination } from \"../workspace/index.js\";\nimport { optionsSchema } from \"./style.js\";\n\nfunction destination(node: Nodes): string | undefined {\n if (node.type === \"wikiLink\") return node.target;\n if (node.type === \"link\" || node.type === \"image\" || node.type === \"definition\") return node.url;\n return undefined;\n}\n/** Locate an inline destination within a parsed link, retaining labels and optional titles. */\nfunction destinationRange(raw: string): [number, number] | undefined {\n let bracket = 0;\n let opening = -1;\n for (let i = raw.startsWith(\"!\") ? 1 : 0; i < raw.length; i++) {\n if (raw[i] === \"\\\\\") {\n i++;\n continue;\n }\n if (raw[i] === \"[\") bracket++;\n if (raw[i] === \"]\" && --bracket === 0 && raw[i + 1] === \"(\") {\n opening = i + 2;\n break;\n }\n }\n if (opening < 0) return undefined;\n while (/\\s/.test(raw[opening] ?? \"\")) opening++;\n if (raw[opening] === \"<\") {\n for (let i = opening + 1; i < raw.length; i++) {\n if (raw[i] === \"\\\\\") {\n i++;\n continue;\n }\n if (raw[i] === \">\") return [opening, i + 1];\n }\n return undefined;\n }\n let depth = 0;\n for (let i = opening; i < raw.length; i++) {\n const char = raw[i]!;\n if (char === \"\\\\\") {\n i++;\n continue;\n }\n if (char === \"(\") depth++;\n if ((char === \")\" && depth-- === 0) || /\\s/.test(char)) return [opening, i];\n }\n return undefined;\n}\n\nexport const linkRules: Record<string, Rule> = {\n \"links/valid\": {\n description: \"Check local link destinations and dialect-specific heading/block references.\",\n kind: \"problem\",\n schema: optionsSchema({}),\n check({ document, workspace }) {\n const findings: Finding[] = [];\n const definitions = new Set<string>();\n visit(document.tree, \"definition\", (node) => {\n definitions.add(node.identifier);\n });\n visit(document.tree, (node) => {\n if (\n (node.type === \"linkReference\" || node.type === \"imageReference\") &&\n !definitions.has(node.identifier)\n )\n findings.push({\n start: range(node)[0],\n message: `Missing link definition: ${node.identifier}.`,\n });\n const url = destination(node);\n if (url === undefined || !workspace) return;\n const result = workspace.resolve(\n document.path,\n url,\n document.dialect,\n node.type === \"wikiLink\",\n );\n if (result.status === \"missing\" || result.status === \"ambiguous\")\n findings.push({\n start: range(node)[0],\n message: `${result.status === \"missing\" ? \"Missing\" : \"Ambiguous\"} local target: ${url}.`,\n });\n else if (result.status === \"resolved\" && !result.fragmentExists)\n findings.push({ start: range(node)[0], message: `Missing fragment in ${url}.` });\n });\n return findings;\n },\n },\n \"links/path\": {\n description: \"Rewrite verified local destinations using the configured path and bracket style.\",\n kind: \"style\",\n phase: \"inline\",\n schema: optionsSchema({\n style: { enum: [\"relative\", \"root\", \"shortest\", \"preserve\"] },\n brackets: { enum: [\"angle\", \"bare\", \"preserve\"] },\n extension: { enum: [\"include\", \"omit\", \"preserve\"] },\n leadingDot: { type: \"boolean\" },\n }),\n check({ document, options, workspace }) {\n const findings: Finding[] = [];\n visit(document.tree, (node) => {\n const url = destination(node);\n if (url === undefined || !workspace) return;\n const result = workspace.resolve(\n document.path,\n url,\n document.dialect,\n node.type === \"wikiLink\",\n );\n if (result.status !== \"resolved\" || !result.target || !result.fragmentExists) return;\n const parts = splitDestination(url);\n let target = parts.path;\n const style = options.style ?? \"preserve\";\n // Vault roots and extensionless Markdown are renderer-specific; don't change other dialects.\n if (\n document.dialect !== \"obsidian\" &&\n (style === \"root\" || style === \"shortest\" || options.extension === \"omit\")\n )\n return;\n if (parts.path) {\n if (style === \"root\") target = result.target;\n else if (style === \"relative\")\n target = path.posix.relative(path.posix.dirname(document.path), result.target);\n else if (style === \"shortest\") {\n const basename = path.posix.basename(result.target);\n const resolved = workspace.resolve(document.path, basename, document.dialect, true);\n target =\n resolved.status === \"resolved\" && resolved.target === result.target\n ? basename\n : result.target;\n }\n if (document.dialect === \"obsidian\" && result.target.endsWith(\".md\")) {\n if (\n options.extension === \"omit\" ||\n ((options.extension ?? \"preserve\") === \"preserve\" && !parts.path.endsWith(\".md\"))\n )\n target = target.replace(/\\.md$/, \"\");\n else if (options.extension === \"include\" && !target.endsWith(\".md\")) target += \".md\";\n }\n if (style === \"relative\" && options.leadingDot === true && !target.startsWith(\".\"))\n target = `./${target}`;\n }\n // Encode reserved path characters before reattaching a separately parsed fragment.\n const encodedPath = target\n .replaceAll(\"%\", \"%25\")\n .replaceAll(\"#\", \"%23\")\n .replaceAll(\"?\", \"%3F\");\n let replacement =\n encodedPath + (parts.fragment ? `#${parts.fragment.replaceAll(\"%\", \"%25\")}` : \"\");\n const [start, end] = range(node);\n if (node.type === \"wikiLink\") {\n if (/[[\\]|\\r\\n]/.test(replacement)) return;\n if (!node.embed && node.label === undefined && replacement !== node.target) return;\n replacement = `${node.embed ? \"!\" : \"\"}[[${replacement}${node.label === undefined ? \"\" : `|${node.label}`}]]`;\n if (replacement !== document.source.slice(start, end))\n findings.push({\n start,\n end,\n message: \"Normalize wikilink destination.\",\n edit: { start, end, text: replacement },\n });\n return;\n }\n if (node.type === \"definition\") return; // Definitions are validated; retain their exact layout in this release.\n const raw = document.source.slice(start, end);\n const span = destinationRange(raw);\n if (!span) return; // Autolinks have no parenthesized destination.\n const original = raw.slice(...span);\n const angle =\n options.brackets === \"angle\" ||\n ((options.brackets ?? \"preserve\") === \"preserve\" && original.startsWith(\"<\"));\n if (angle)\n replacement = `<${replacement.replaceAll(\"<\", \"%3C\").replaceAll(\">\", \"%3E\").replaceAll(\"\\\\\", \"%5C\")}>`;\n else\n replacement = replacement\n .replaceAll(\" \", \"%20\")\n .replaceAll(\"(\", \"%28\")\n .replaceAll(\")\", \"%29\")\n .replaceAll(\"<\", \"%3C\")\n .replaceAll(\">\", \"%3E\");\n if (original !== replacement)\n findings.push({\n start: start + span[0],\n end: start + span[1],\n message: \"Normalize link destination.\",\n edit: { start: start + span[0], end: start + span[1], text: replacement },\n });\n });\n return findings;\n },\n },\n \"links/notation\": {\n description: \"Convert plain-label Obsidian links between Markdown and wikilink notation.\",\n kind: \"style\",\n phase: \"inline\",\n schema: optionsSchema({ style: { enum: [\"markdown\", \"wiki\"] } }),\n check({ document, options, workspace }) {\n if (document.dialect !== \"obsidian\" || !workspace) return [];\n const findings: Finding[] = [];\n visit(document.tree, (node) => {\n const [start, end] = range(node);\n let replacement: string | undefined;\n if (\n options.style === \"markdown\" &&\n node.type === \"wikiLink\" &&\n !node.embed &&\n node.label !== undefined\n ) {\n if (/[[\\]<>\\\\\\r\\n]/.test(node.target ?? \"\") || /[*_`[\\]\\\\]/.test(node.label)) return;\n replacement = `[${node.label}](<${node.target}>)`;\n } else if (\n options.style === \"wiki\" &&\n node.type === \"link\" &&\n !node.title &&\n node.children.length === 1 &&\n node.children[0]?.type === \"text\"\n ) {\n const label = node.children[0].value;\n if (/[[\\]|\\r\\n]/.test(node.url + label)) return;\n replacement = `[[${node.url}|${label}]]`;\n }\n const url = destination(node);\n if (\n !replacement ||\n url === undefined ||\n workspace.resolve(document.path, url, document.dialect, node.type === \"wikiLink\")\n .status !== \"resolved\"\n )\n return;\n findings.push({\n start,\n end,\n message: `Use ${String(options.style)} link notation.`,\n edit: { start, end, text: replacement },\n });\n });\n return findings;\n },\n },\n};\n"]}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Nodes } from "mdast";
|
|
2
|
+
import type { Rule } from "../core/types.js";
|
|
3
|
+
export declare function optionsSchema(properties: Record<string, unknown>): {
|
|
4
|
+
type: string;
|
|
5
|
+
properties: Record<string, unknown>;
|
|
6
|
+
additionalProperties: boolean;
|
|
7
|
+
};
|
|
8
|
+
export declare const styleRules: Record<string, Rule>;
|
|
9
|
+
/** Collect source-positioned nodes for rule/plugin consumers. */
|
|
10
|
+
export declare function nodesOfType(document: {
|
|
11
|
+
tree: import("mdast").Root;
|
|
12
|
+
}, type: Nodes["type"]): Nodes[];
|