mdrefine 0.1.0-beta.1 → 0.1.0-beta.3

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 (41) hide show
  1. package/CONTRIBUTING.md +6 -0
  2. package/README.md +10 -6
  3. package/dist/cli/main.js +9 -2
  4. package/dist/cli/main.js.map +1 -1
  5. package/dist/config/resolve.d.ts +12 -0
  6. package/dist/config/resolve.js +7 -1
  7. package/dist/config/resolve.js.map +1 -1
  8. package/dist/core/engine.js +29 -9
  9. package/dist/core/engine.js.map +1 -1
  10. package/dist/core/types.d.ts +7 -1
  11. package/dist/core/types.js.map +1 -1
  12. package/dist/index.d.ts +1 -1
  13. package/dist/index.js.map +1 -1
  14. package/dist/rules/links.js +28 -5
  15. package/dist/rules/links.js.map +1 -1
  16. package/dist/rules/structure.d.ts +2 -0
  17. package/dist/rules/structure.js +24 -0
  18. package/dist/rules/structure.js.map +1 -0
  19. package/dist/rules/style.js +138 -29
  20. package/dist/rules/style.js.map +1 -1
  21. package/dist/syntax/obsidian.js +17 -0
  22. package/dist/syntax/obsidian.js.map +1 -1
  23. package/dist/workspace/files.d.ts +5 -2
  24. package/dist/workspace/files.js +15 -6
  25. package/dist/workspace/files.js.map +1 -1
  26. package/dist/workspace/index.d.ts +4 -1
  27. package/dist/workspace/index.js +53 -15
  28. package/dist/workspace/index.js.map +1 -1
  29. package/dist/workspace/selection.d.ts +2 -0
  30. package/dist/workspace/selection.js +31 -0
  31. package/dist/workspace/selection.js.map +1 -0
  32. package/docs/cli.md +18 -0
  33. package/docs/configuration.md +91 -25
  34. package/docs/contributing.md +87 -5
  35. package/docs/design.md +28 -5
  36. package/docs/plugins.md +11 -4
  37. package/docs/quick-start.md +1 -1
  38. package/docs/releases/0.1.0-beta.2.md +83 -0
  39. package/docs/releases/0.1.0-beta.3.md +61 -0
  40. package/docs/releases.md +34 -10
  41. package/package.json +1 -1
@@ -1,12 +1,14 @@
1
1
  import { lstat, readdir, readFile, realpath, open, rename, unlink } from "node:fs/promises";
2
2
  import path from "node:path";
3
+ import { readFileSync } from "node:fs";
3
4
  import { randomUUID } from "node:crypto";
4
5
  import ignore from "ignore";
5
6
  import { minimatch } from "minimatch";
6
7
  import { exists } from "../config/load.js";
7
- export async function discover(rootPath, supplied, excluded) {
8
+ export async function discover(rootPath, supplied, excluded, resolve = {}) {
8
9
  const root = await realpath(rootPath);
9
10
  const files = {};
11
+ const directories = [];
10
12
  const selected = [];
11
13
  const requests = [];
12
14
  for (const input of supplied.length ? supplied : [root]) {
@@ -19,7 +21,7 @@ export async function discover(rootPath, supplied, excluded) {
19
21
  throw new Error(`Input is outside the workspace: ${input}`);
20
22
  requests.push(relative);
21
23
  }
22
- async function walk(relative, inherited) {
24
+ async function walk(relative, inherited, targetOnly = false) {
23
25
  const directory = path.join(root, relative);
24
26
  const layers = [...inherited];
25
27
  if (await exists(path.join(directory, ".gitignore")))
@@ -32,17 +34,23 @@ export async function discover(rootPath, supplied, excluded) {
32
34
  entry.isSymbolicLink())
33
35
  continue;
34
36
  const name = relative ? `${relative}/${entry.name}` : entry.name;
35
- if (layers.some((layer) => layer.matcher.ignores(name.slice(layer.base ? layer.base.length + 1 : 0) + (entry.isDirectory() ? "/" : ""))))
37
+ const gitIgnored = layers.some((layer) => layer.matcher.ignores(name.slice(layer.base ? layer.base.length + 1 : 0) + (entry.isDirectory() ? "/" : "")));
38
+ if (gitIgnored && resolve.gitIgnored !== true)
36
39
  continue;
37
40
  const full = path.join(root, name);
38
41
  if (entry.isDirectory()) {
39
- if (!(await exists(path.join(full, ".git"))))
40
- await walk(name, layers);
42
+ directories.push(name);
43
+ const nested = await exists(path.join(full, ".git"));
44
+ if (!nested || resolve.nestedRepositories === true)
45
+ await walk(name, layers, targetOnly || gitIgnored || nested);
41
46
  }
42
47
  else if (entry.isFile()) {
43
48
  const markdown = /\.md$/i.test(name);
44
- files[name] = markdown ? await readFile(full, "utf8") : null;
49
+ let cached;
50
+ files[name] = markdown ? () => (cached ??= readFileSync(full, "utf8")) : null;
45
51
  if (markdown &&
52
+ !targetOnly &&
53
+ !gitIgnored &&
46
54
  requests.some((request) => !request || name === request || name.startsWith(`${request}/`)) &&
47
55
  !excluded.some((pattern) => minimatch(name, pattern, { dot: true })))
48
56
  selected.push(name);
@@ -63,6 +71,7 @@ export async function discover(rootPath, supplied, excluded) {
63
71
  return {
64
72
  root,
65
73
  files,
74
+ directories,
66
75
  selected: selected.sort(),
67
76
  ...(strictLineBreaks !== undefined ? { strictLineBreaks } : {}),
68
77
  };
@@ -1 +1 @@
1
- {"version":3,"file":"files.js","sourceRoot":"","sources":["../../src/workspace/files.ts"],"names":[],"mappings":"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","sourcesContent":["import { lstat, readdir, readFile, realpath, open, rename, unlink } from \"node:fs/promises\";\nimport path from \"node:path\";\nimport { randomUUID } from \"node:crypto\";\nimport ignore from \"ignore\";\nimport { minimatch } from \"minimatch\";\nimport { exists } from \"../config/load.js\";\n\nexport interface FileSet {\n root: string;\n files: Record<string, string | null>;\n selected: string[];\n strictLineBreaks?: boolean;\n}\nexport async function discover(\n rootPath: string,\n supplied: string[],\n excluded: string[],\n): Promise<FileSet> {\n const root = await realpath(rootPath);\n const files: Record<string, string | null> = {};\n const selected: string[] = [];\n const requests: string[] = [];\n for (const input of supplied.length ? supplied : [root]) {\n const absolute = path.resolve(input);\n if ((await lstat(absolute)).isSymbolicLink())\n throw new Error(`Refusing symbolic-link input: ${input}`);\n const resolved = await realpath(absolute);\n const relative = path.relative(root, resolved).split(path.sep).join(\"/\");\n if (relative.startsWith(\"../\") || relative === \"..\" || path.isAbsolute(relative))\n throw new Error(`Input is outside the workspace: ${input}`);\n requests.push(relative);\n }\n interface IgnoreLayer {\n base: string;\n matcher: ReturnType<typeof ignore>;\n }\n async function walk(relative: string, inherited: IgnoreLayer[]) {\n const directory = path.join(root, relative);\n const layers = [...inherited];\n if (await exists(path.join(directory, \".gitignore\")))\n layers.push({\n base: relative,\n matcher: ignore().add(await readFile(path.join(directory, \".gitignore\"), \"utf8\")),\n });\n for (const entry of await readdir(directory, { withFileTypes: true })) {\n if (\n [\".git\", \".obsidian\", \"node_modules\", \".npm-cache\", \"dist\", \"coverage\"].includes(\n entry.name,\n ) ||\n entry.isSymbolicLink()\n )\n continue;\n const name = relative ? `${relative}/${entry.name}` : entry.name;\n if (\n layers.some((layer) =>\n layer.matcher.ignores(\n name.slice(layer.base ? layer.base.length + 1 : 0) + (entry.isDirectory() ? \"/\" : \"\"),\n ),\n )\n )\n continue;\n const full = path.join(root, name);\n if (entry.isDirectory()) {\n if (!(await exists(path.join(full, \".git\")))) await walk(name, layers);\n } else if (entry.isFile()) {\n const markdown = /\\.md$/i.test(name);\n files[name] = markdown ? await readFile(full, \"utf8\") : null;\n if (\n markdown &&\n requests.some(\n (request) => !request || name === request || name.startsWith(`${request}/`),\n ) &&\n !excluded.some((pattern) => minimatch(name, pattern, { dot: true }))\n )\n selected.push(name);\n }\n }\n }\n await walk(\"\", []);\n let strictLineBreaks: boolean | undefined;\n const app = path.join(root, \".obsidian\", \"app.json\");\n if (await exists(app)) {\n const settings: unknown = JSON.parse(await readFile(app, \"utf8\"));\n strictLineBreaks =\n typeof settings === \"object\" &&\n settings !== null &&\n \"strictLineBreaks\" in settings &&\n settings.strictLineBreaks === true;\n }\n return {\n root,\n files,\n selected: selected.sort(),\n ...(strictLineBreaks !== undefined ? { strictLineBreaks } : {}),\n };\n}\nexport async function writeAtomic(file: string, before: string, after: string): Promise<void> {\n const info = await lstat(file);\n if (!info.isFile() || info.isSymbolicLink())\n throw new Error(`Refusing non-regular file: ${file}`);\n if ((await readFile(file, \"utf8\")) !== before)\n throw new Error(`File changed during formatting: ${file}`);\n const temporary = path.join(path.dirname(file), `.mdtools-${randomUUID()}.tmp`);\n const handle = await open(temporary, \"wx\", info.mode);\n try {\n await handle.writeFile(after, \"utf8\");\n await handle.chmod(info.mode);\n await handle.sync();\n await handle.close();\n const current = await lstat(file);\n if (\n current.isSymbolicLink() ||\n current.ino !== info.ino ||\n (await readFile(file, \"utf8\")) !== before\n )\n throw new Error(`File changed before replacement: ${file}`);\n await rename(temporary, file);\n } finally {\n await handle.close();\n await unlink(temporary).catch((error: NodeJS.ErrnoException) => {\n if (error.code !== \"ENOENT\") throw error;\n });\n }\n}\n"]}
1
+ {"version":3,"file":"files.js","sourceRoot":"","sources":["../../src/workspace/files.ts"],"names":[],"mappings":"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","sourcesContent":["import { lstat, readdir, readFile, realpath, open, rename, unlink } from \"node:fs/promises\";\nimport path from \"node:path\";\nimport { readFileSync } from \"node:fs\";\nimport type { ResolveOptions } from \"../core/types.js\";\nimport type { WorkspaceSource } from \"./index.js\";\nimport { randomUUID } from \"node:crypto\";\nimport ignore from \"ignore\";\nimport { minimatch } from \"minimatch\";\nimport { exists } from \"../config/load.js\";\n\nexport interface FileSet {\n root: string;\n files: Record<string, WorkspaceSource>;\n directories: string[];\n selected: string[];\n strictLineBreaks?: boolean;\n}\nexport async function discover(\n rootPath: string,\n supplied: string[],\n excluded: string[],\n resolve: ResolveOptions = {},\n): Promise<FileSet> {\n const root = await realpath(rootPath);\n const files: Record<string, WorkspaceSource> = {};\n const directories: string[] = [];\n const selected: string[] = [];\n const requests: string[] = [];\n for (const input of supplied.length ? supplied : [root]) {\n const absolute = path.resolve(input);\n if ((await lstat(absolute)).isSymbolicLink())\n throw new Error(`Refusing symbolic-link input: ${input}`);\n const resolved = await realpath(absolute);\n const relative = path.relative(root, resolved).split(path.sep).join(\"/\");\n if (relative.startsWith(\"../\") || relative === \"..\" || path.isAbsolute(relative))\n throw new Error(`Input is outside the workspace: ${input}`);\n requests.push(relative);\n }\n interface IgnoreLayer {\n base: string;\n matcher: ReturnType<typeof ignore>;\n }\n async function walk(relative: string, inherited: IgnoreLayer[], targetOnly = false) {\n const directory = path.join(root, relative);\n const layers = [...inherited];\n if (await exists(path.join(directory, \".gitignore\")))\n layers.push({\n base: relative,\n matcher: ignore().add(await readFile(path.join(directory, \".gitignore\"), \"utf8\")),\n });\n for (const entry of await readdir(directory, { withFileTypes: true })) {\n if (\n [\".git\", \".obsidian\", \"node_modules\", \".npm-cache\", \"dist\", \"coverage\"].includes(\n entry.name,\n ) ||\n entry.isSymbolicLink()\n )\n continue;\n const name = relative ? `${relative}/${entry.name}` : entry.name;\n const gitIgnored = layers.some((layer) =>\n layer.matcher.ignores(\n name.slice(layer.base ? layer.base.length + 1 : 0) + (entry.isDirectory() ? \"/\" : \"\"),\n ),\n );\n if (gitIgnored && resolve.gitIgnored !== true) continue;\n const full = path.join(root, name);\n if (entry.isDirectory()) {\n directories.push(name);\n const nested = await exists(path.join(full, \".git\"));\n if (!nested || resolve.nestedRepositories === true)\n await walk(name, layers, targetOnly || gitIgnored || nested);\n } else if (entry.isFile()) {\n const markdown = /\\.md$/i.test(name);\n let cached: string | undefined;\n files[name] = markdown ? () => (cached ??= readFileSync(full, \"utf8\")) : null;\n if (\n markdown &&\n !targetOnly &&\n !gitIgnored &&\n requests.some(\n (request) => !request || name === request || name.startsWith(`${request}/`),\n ) &&\n !excluded.some((pattern) => minimatch(name, pattern, { dot: true }))\n )\n selected.push(name);\n }\n }\n }\n await walk(\"\", []);\n let strictLineBreaks: boolean | undefined;\n const app = path.join(root, \".obsidian\", \"app.json\");\n if (await exists(app)) {\n const settings: unknown = JSON.parse(await readFile(app, \"utf8\"));\n strictLineBreaks =\n typeof settings === \"object\" &&\n settings !== null &&\n \"strictLineBreaks\" in settings &&\n settings.strictLineBreaks === true;\n }\n return {\n root,\n files,\n directories,\n selected: selected.sort(),\n ...(strictLineBreaks !== undefined ? { strictLineBreaks } : {}),\n };\n}\nexport async function writeAtomic(file: string, before: string, after: string): Promise<void> {\n const info = await lstat(file);\n if (!info.isFile() || info.isSymbolicLink())\n throw new Error(`Refusing non-regular file: ${file}`);\n if ((await readFile(file, \"utf8\")) !== before)\n throw new Error(`File changed during formatting: ${file}`);\n const temporary = path.join(path.dirname(file), `.mdtools-${randomUUID()}.tmp`);\n const handle = await open(temporary, \"wx\", info.mode);\n try {\n await handle.writeFile(after, \"utf8\");\n await handle.chmod(info.mode);\n await handle.sync();\n await handle.close();\n const current = await lstat(file);\n if (\n current.isSymbolicLink() ||\n current.ino !== info.ino ||\n (await readFile(file, \"utf8\")) !== before\n )\n throw new Error(`File changed before replacement: ${file}`);\n await rename(temporary, file);\n } finally {\n await handle.close();\n await unlink(temporary).catch((error: NodeJS.ErrnoException) => {\n if (error.code !== \"ENOENT\") throw error;\n });\n }\n}\n"]}
@@ -1,10 +1,13 @@
1
1
  import type { Dialect, Workspace } from "../core/types.js";
2
+ export type WorkspaceSource = string | null | (() => string);
2
3
  export interface WorkspaceOptions {
3
4
  dialect?: Dialect;
4
5
  strictLineBreaks?: boolean;
6
+ /** Existing directories, including empty ones; never treated as note targets. */
7
+ directories?: string[];
5
8
  }
6
9
  export declare function splitDestination(destination: string): {
7
10
  path: string;
8
11
  fragment: string;
9
12
  };
10
- export declare function createWorkspace(files: Record<string, string | null>, options?: WorkspaceOptions): Workspace;
13
+ export declare function createWorkspace(files: Record<string, WorkspaceSource>, options?: WorkspaceOptions): Workspace;
@@ -18,10 +18,28 @@ export function splitDestination(destination) {
18
18
  };
19
19
  }
20
20
  export function createWorkspace(files, options = {}) {
21
+ const sources = new Map(Object.entries(files).map(([name, source]) => [
22
+ name.replaceAll("\\", "/").replace(/^\.\//, ""),
23
+ source,
24
+ ]));
25
+ const directories = new Set((options.directories ?? []).map((name) => name.replaceAll("\\", "/")));
26
+ directories.add(".");
27
+ for (const name of sources.keys()) {
28
+ let directory = path.posix.dirname(name);
29
+ while (directory !== "." && directory !== path.posix.dirname(directory)) {
30
+ directories.add(directory);
31
+ directory = path.posix.dirname(directory);
32
+ }
33
+ }
21
34
  const entries = new Map();
22
- for (const [name, source] of Object.entries(files)) {
35
+ function fragments(name) {
36
+ const cached = entries.get(name);
37
+ if (cached)
38
+ return cached;
23
39
  const entry = { headings: new Set(), slugs: new Set(), blocks: new Set() };
24
- if (source !== null && /\.md$/i.test(name)) {
40
+ const value = sources.get(name);
41
+ if (value !== null && value !== undefined && /\.md$/i.test(name)) {
42
+ const source = typeof value === "function" ? value() : value;
25
43
  const document = parse(source, options.dialect ?? "commonmark", name);
26
44
  const slugger = new GithubSlugger();
27
45
  visit(document.tree, "heading", (node) => {
@@ -35,7 +53,26 @@ export function createWorkspace(files, options = {}) {
35
53
  entry.blocks.add(match[1]);
36
54
  });
37
55
  }
38
- entries.set(name.replaceAll("\\", "/").replace(/^\.\//, ""), entry);
56
+ entries.set(name, entry);
57
+ return entry;
58
+ }
59
+ let suffixes;
60
+ function suffixCandidates(target) {
61
+ if (!suffixes) {
62
+ suffixes = new Map();
63
+ for (const name of sources.keys()) {
64
+ const parts = name.split("/");
65
+ for (let i = 0; i < parts.length; i++) {
66
+ const suffix = parts.slice(i).join("/");
67
+ for (const key of suffix.endsWith(".md") ? [suffix, suffix.slice(0, -3)] : [suffix]) {
68
+ if (!suffixes.has(key))
69
+ suffixes.set(key, new Set());
70
+ suffixes.get(key).add(name);
71
+ }
72
+ }
73
+ }
74
+ }
75
+ return suffixes.get(target);
39
76
  }
40
77
  return {
41
78
  ...(options.strictLineBreaks !== undefined
@@ -44,20 +81,24 @@ export function createWorkspace(files, options = {}) {
44
81
  resolve(source, destination, dialect) {
45
82
  if (/^[a-z][a-z\d+.-]*:/i.test(destination) || destination.startsWith("//"))
46
83
  return { status: "external" };
84
+ source = source.replaceAll("\\", "/");
47
85
  const parts = splitDestination(destination);
48
86
  const targetPath = parts.path;
49
87
  if (dialect !== "obsidian" && (targetPath.startsWith("/") || targetPath.includes("?")))
50
88
  return { status: "unavailable" };
51
89
  const candidates = new Set();
90
+ let directory = false;
52
91
  const add = (candidate) => {
53
92
  const normalized = path.posix.normalize(candidate);
54
93
  if (normalized.startsWith("../") || path.posix.isAbsolute(normalized))
55
94
  return;
56
- if (entries.has(normalized))
95
+ if (directories.has(normalized.replace(/\/$/, "")))
96
+ directory = true;
97
+ if (sources.has(normalized))
57
98
  candidates.add(normalized);
58
99
  if (dialect === "obsidian" &&
59
100
  !path.posix.extname(normalized) &&
60
- entries.has(`${normalized}.md`))
101
+ sources.has(`${normalized}.md`))
61
102
  candidates.add(`${normalized}.md`);
62
103
  };
63
104
  if (!targetPath)
@@ -67,26 +108,23 @@ export function createWorkspace(files, options = {}) {
67
108
  add(path.posix.join(path.posix.dirname(source), targetPath));
68
109
  else {
69
110
  add(targetPath.replace(/^\//, ""));
70
- if (!targetPath.startsWith("/"))
111
+ if (candidates.size === 0 && !directory && !targetPath.startsWith("/"))
71
112
  add(path.posix.join(path.posix.dirname(source), targetPath));
72
- if (candidates.size === 0) {
73
- for (const name of entries.keys()) {
74
- if (name === targetPath ||
75
- name.endsWith(`/${targetPath}`) ||
76
- name === `${targetPath}.md` ||
77
- name.endsWith(`/${targetPath}.md`))
78
- candidates.add(name);
79
- }
113
+ if (candidates.size === 0 && !directory && !targetPath.startsWith("/")) {
114
+ for (const name of suffixCandidates(targetPath) ?? [])
115
+ candidates.add(name);
80
116
  }
81
117
  }
82
118
  }
83
119
  else
84
120
  add(path.posix.join(path.posix.dirname(source), targetPath));
121
+ if (candidates.size === 0 && directory)
122
+ return { status: "directory" };
85
123
  if (candidates.size !== 1)
86
124
  return { status: candidates.size ? "ambiguous" : "missing" };
87
125
  const target = [...candidates][0];
88
- const entry = entries.get(target);
89
126
  const fragment = parts.fragment;
127
+ const entry = fragment ? fragments(target) : undefined;
90
128
  const fragmentExists = !fragment ||
91
129
  (dialect === "obsidian"
92
130
  ? fragment.startsWith("^")
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/workspace/index.ts"],"names":[],"mappings":"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","sourcesContent":["import path from \"node:path\";\nimport GithubSlugger from \"github-slugger\";\nimport { visit } from \"unist-util-visit\";\nimport type { Dialect, LinkResolution, Workspace } from \"../core/types.js\";\nimport { parse, textContent } from \"../syntax/parse.js\";\n\ninterface Entry {\n headings: Set<string>;\n slugs: Set<string>;\n blocks: Set<string>;\n}\nexport interface WorkspaceOptions {\n dialect?: Dialect;\n strictLineBreaks?: boolean;\n}\nexport function splitDestination(destination: string): { path: string; fragment: string } {\n const hash = destination.indexOf(\"#\");\n const decode = (value: string) => {\n try {\n return decodeURIComponent(value);\n } catch {\n return value;\n }\n };\n return {\n path: decode(hash < 0 ? destination : destination.slice(0, hash)),\n fragment: decode(hash < 0 ? \"\" : destination.slice(hash + 1)),\n };\n}\nexport function createWorkspace(\n files: Record<string, string | null>,\n options: WorkspaceOptions = {},\n): Workspace {\n const entries = new Map<string, Entry>();\n for (const [name, source] of Object.entries(files)) {\n const entry: Entry = { headings: new Set(), slugs: new Set(), blocks: new Set() };\n if (source !== null && /\\.md$/i.test(name)) {\n const document = parse(source, options.dialect ?? \"commonmark\", name);\n const slugger = new GithubSlugger();\n visit(document.tree, \"heading\", (node) => {\n const text = textContent(node);\n entry.headings.add(text);\n entry.slugs.add(slugger.slug(text));\n });\n visit(document.tree, \"text\", (node) => {\n const match = /(?:^|\\s)\\^([A-Za-z0-9-]+)\\s*$/.exec(node.value);\n if (match) entry.blocks.add(match[1]!);\n });\n }\n entries.set(name.replaceAll(\"\\\\\", \"/\").replace(/^\\.\\//, \"\"), entry);\n }\n return {\n ...(options.strictLineBreaks !== undefined\n ? { strictLineBreaks: options.strictLineBreaks }\n : {}),\n resolve(source, destination, dialect): LinkResolution {\n if (/^[a-z][a-z\\d+.-]*:/i.test(destination) || destination.startsWith(\"//\"))\n return { status: \"external\" };\n const parts = splitDestination(destination);\n const targetPath = parts.path;\n if (dialect !== \"obsidian\" && (targetPath.startsWith(\"/\") || targetPath.includes(\"?\")))\n return { status: \"unavailable\" };\n const candidates = new Set<string>();\n const add = (candidate: string) => {\n const normalized = path.posix.normalize(candidate);\n if (normalized.startsWith(\"../\") || path.posix.isAbsolute(normalized)) return;\n if (entries.has(normalized)) candidates.add(normalized);\n if (\n dialect === \"obsidian\" &&\n !path.posix.extname(normalized) &&\n entries.has(`${normalized}.md`)\n )\n candidates.add(`${normalized}.md`);\n };\n if (!targetPath) add(source);\n else if (dialect === \"obsidian\") {\n if (/^\\.{1,2}\\//.test(targetPath))\n add(path.posix.join(path.posix.dirname(source), targetPath));\n else {\n add(targetPath.replace(/^\\//, \"\"));\n if (!targetPath.startsWith(\"/\"))\n add(path.posix.join(path.posix.dirname(source), targetPath));\n if (candidates.size === 0) {\n for (const name of entries.keys()) {\n if (\n name === targetPath ||\n name.endsWith(`/${targetPath}`) ||\n name === `${targetPath}.md` ||\n name.endsWith(`/${targetPath}.md`)\n )\n candidates.add(name);\n }\n }\n }\n } else add(path.posix.join(path.posix.dirname(source), targetPath));\n if (candidates.size !== 1) return { status: candidates.size ? \"ambiguous\" : \"missing\" };\n const target = [...candidates][0]!;\n const entry = entries.get(target)!;\n const fragment = parts.fragment;\n const fragmentExists =\n !fragment ||\n (dialect === \"obsidian\"\n ? fragment.startsWith(\"^\")\n ? entry.blocks.has(fragment.slice(1))\n : entry.headings.has(fragment)\n : entry.slugs.has(fragment));\n return { status: \"resolved\", target, fragment, fragmentExists };\n },\n };\n}\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/workspace/index.ts"],"names":[],"mappings":"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","sourcesContent":["import path from \"node:path\";\nimport GithubSlugger from \"github-slugger\";\nimport { visit } from \"unist-util-visit\";\nimport type { Dialect, LinkResolution, Workspace } from \"../core/types.js\";\nimport { parse, textContent } from \"../syntax/parse.js\";\n\nexport type WorkspaceSource = string | null | (() => string);\n\ninterface Entry {\n headings: Set<string>;\n slugs: Set<string>;\n blocks: Set<string>;\n}\nexport interface WorkspaceOptions {\n dialect?: Dialect;\n strictLineBreaks?: boolean;\n /** Existing directories, including empty ones; never treated as note targets. */\n directories?: string[];\n}\nexport function splitDestination(destination: string): { path: string; fragment: string } {\n const hash = destination.indexOf(\"#\");\n const decode = (value: string) => {\n try {\n return decodeURIComponent(value);\n } catch {\n return value;\n }\n };\n return {\n path: decode(hash < 0 ? destination : destination.slice(0, hash)),\n fragment: decode(hash < 0 ? \"\" : destination.slice(hash + 1)),\n };\n}\nexport function createWorkspace(\n files: Record<string, WorkspaceSource>,\n options: WorkspaceOptions = {},\n): Workspace {\n const sources = new Map(\n Object.entries(files).map(([name, source]) => [\n name.replaceAll(\"\\\\\", \"/\").replace(/^\\.\\//, \"\"),\n source,\n ]),\n );\n const directories = new Set(\n (options.directories ?? []).map((name) => name.replaceAll(\"\\\\\", \"/\")),\n );\n directories.add(\".\");\n for (const name of sources.keys()) {\n let directory = path.posix.dirname(name);\n while (directory !== \".\" && directory !== path.posix.dirname(directory)) {\n directories.add(directory);\n directory = path.posix.dirname(directory);\n }\n }\n const entries = new Map<string, Entry>();\n function fragments(name: string): Entry {\n const cached = entries.get(name);\n if (cached) return cached;\n const entry: Entry = { headings: new Set(), slugs: new Set(), blocks: new Set() };\n const value = sources.get(name);\n if (value !== null && value !== undefined && /\\.md$/i.test(name)) {\n const source = typeof value === \"function\" ? value() : value;\n const document = parse(source, options.dialect ?? \"commonmark\", name);\n const slugger = new GithubSlugger();\n visit(document.tree, \"heading\", (node) => {\n const text = textContent(node);\n entry.headings.add(text);\n entry.slugs.add(slugger.slug(text));\n });\n visit(document.tree, \"text\", (node) => {\n const match = /(?:^|\\s)\\^([A-Za-z0-9-]+)\\s*$/.exec(node.value);\n if (match) entry.blocks.add(match[1]!);\n });\n }\n entries.set(name, entry);\n return entry;\n }\n let suffixes: Map<string, Set<string>> | undefined;\n function suffixCandidates(target: string): Set<string> | undefined {\n if (!suffixes) {\n suffixes = new Map();\n for (const name of sources.keys()) {\n const parts = name.split(\"/\");\n for (let i = 0; i < parts.length; i++) {\n const suffix = parts.slice(i).join(\"/\");\n for (const key of suffix.endsWith(\".md\") ? [suffix, suffix.slice(0, -3)] : [suffix]) {\n if (!suffixes.has(key)) suffixes.set(key, new Set());\n suffixes.get(key)!.add(name);\n }\n }\n }\n }\n return suffixes.get(target);\n }\n return {\n ...(options.strictLineBreaks !== undefined\n ? { strictLineBreaks: options.strictLineBreaks }\n : {}),\n resolve(source, destination, dialect): LinkResolution {\n if (/^[a-z][a-z\\d+.-]*:/i.test(destination) || destination.startsWith(\"//\"))\n return { status: \"external\" };\n source = source.replaceAll(\"\\\\\", \"/\");\n const parts = splitDestination(destination);\n const targetPath = parts.path;\n if (dialect !== \"obsidian\" && (targetPath.startsWith(\"/\") || targetPath.includes(\"?\")))\n return { status: \"unavailable\" };\n const candidates = new Set<string>();\n let directory = false;\n const add = (candidate: string) => {\n const normalized = path.posix.normalize(candidate);\n if (normalized.startsWith(\"../\") || path.posix.isAbsolute(normalized)) return;\n if (directories.has(normalized.replace(/\\/$/, \"\"))) directory = true;\n if (sources.has(normalized)) candidates.add(normalized);\n if (\n dialect === \"obsidian\" &&\n !path.posix.extname(normalized) &&\n sources.has(`${normalized}.md`)\n )\n candidates.add(`${normalized}.md`);\n };\n if (!targetPath) add(source);\n else if (dialect === \"obsidian\") {\n if (/^\\.{1,2}\\//.test(targetPath))\n add(path.posix.join(path.posix.dirname(source), targetPath));\n else {\n add(targetPath.replace(/^\\//, \"\"));\n if (candidates.size === 0 && !directory && !targetPath.startsWith(\"/\"))\n add(path.posix.join(path.posix.dirname(source), targetPath));\n if (candidates.size === 0 && !directory && !targetPath.startsWith(\"/\")) {\n for (const name of suffixCandidates(targetPath) ?? []) candidates.add(name);\n }\n }\n } else add(path.posix.join(path.posix.dirname(source), targetPath));\n if (candidates.size === 0 && directory) return { status: \"directory\" };\n if (candidates.size !== 1) return { status: candidates.size ? \"ambiguous\" : \"missing\" };\n const target = [...candidates][0]!;\n const fragment = parts.fragment;\n const entry = fragment ? fragments(target) : undefined;\n const fragmentExists =\n !fragment ||\n (dialect === \"obsidian\"\n ? fragment.startsWith(\"^\")\n ? entry!.blocks.has(fragment.slice(1))\n : entry!.headings.has(fragment)\n : entry!.slugs.has(fragment));\n return { status: \"resolved\", target, fragment, fragmentExists };\n },\n };\n}\n"]}
@@ -0,0 +1,2 @@
1
+ /** Exclusions are literal paths relative to cwd, never glob patterns. */
2
+ export declare function excludeSelection(root: string, selected: string[], exclusions: string[]): Promise<string[]>;
@@ -0,0 +1,31 @@
1
+ import path from "node:path";
2
+ import { realpath } from "node:fs/promises";
3
+ /** Exclusions are literal paths relative to cwd, never glob patterns. */
4
+ export async function excludeSelection(root, selected, exclusions) {
5
+ // Resolve filesystem aliases (including Windows short names) consistently
6
+ // with discovery, while allowing exclusions for paths that do not exist yet.
7
+ async function canonical(file) {
8
+ try {
9
+ return await realpath(file);
10
+ }
11
+ catch (error) {
12
+ if (error.code !== "ENOENT")
13
+ throw error;
14
+ const parent = path.dirname(file);
15
+ if (parent === file)
16
+ throw error;
17
+ return path.join(await canonical(parent), path.basename(file));
18
+ }
19
+ }
20
+ const relative = await Promise.all(exclusions.map(async (input) => {
21
+ const name = path
22
+ .relative(root, await canonical(path.resolve(input)))
23
+ .split(path.sep)
24
+ .join("/");
25
+ if (name === ".." || name.startsWith("../") || path.isAbsolute(name))
26
+ throw new Error(`Excluded path is outside the workspace: ${input}`);
27
+ return name;
28
+ }));
29
+ return selected.filter((name) => !relative.some((excluded) => !excluded || name === excluded || name.startsWith(`${excluded}/`)));
30
+ }
31
+ //# sourceMappingURL=selection.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"selection.js","sourceRoot":"","sources":["../../src/workspace/selection.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAE5C,yEAAyE;AACzE,MAAM,CAAC,KAAK,UAAU,gBAAgB,CACpC,IAAY,EACZ,QAAkB,EAClB,UAAoB;IAEpB,0EAA0E;IAC1E,6EAA6E;IAC7E,KAAK,UAAU,SAAS,CAAC,IAAY;QACnC,IAAI,CAAC;YACH,OAAO,MAAM,QAAQ,CAAC,IAAI,CAAC,CAAC;QAC9B,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,IAAK,KAA+B,CAAC,IAAI,KAAK,QAAQ;gBAAE,MAAM,KAAK,CAAC;YACpE,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;YAClC,IAAI,MAAM,KAAK,IAAI;gBAAE,MAAM,KAAK,CAAC;YACjC,OAAO,IAAI,CAAC,IAAI,CAAC,MAAM,SAAS,CAAC,MAAM,CAAC,EAAE,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC;QACjE,CAAC;IACH,CAAC;IACD,MAAM,QAAQ,GAAG,MAAM,OAAO,CAAC,GAAG,CAChC,UAAU,CAAC,GAAG,CAAC,KAAK,EAAE,KAAK,EAAE,EAAE;QAC7B,MAAM,IAAI,GAAG,IAAI;aACd,QAAQ,CAAC,IAAI,EAAE,MAAM,SAAS,CAAC,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC;aACpD,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC;aACf,IAAI,CAAC,GAAG,CAAC,CAAC;QACb,IAAI,IAAI,KAAK,IAAI,IAAI,IAAI,CAAC,UAAU,CAAC,KAAK,CAAC,IAAI,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC;YAClE,MAAM,IAAI,KAAK,CAAC,2CAA2C,KAAK,EAAE,CAAC,CAAC;QACtE,OAAO,IAAI,CAAC;IACd,CAAC,CAAC,CACH,CAAC;IACF,OAAO,QAAQ,CAAC,MAAM,CACpB,CAAC,IAAI,EAAE,EAAE,CACP,CAAC,QAAQ,CAAC,IAAI,CACZ,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC,QAAQ,IAAI,IAAI,KAAK,QAAQ,IAAI,IAAI,CAAC,UAAU,CAAC,GAAG,QAAQ,GAAG,CAAC,CAChF,CACJ,CAAC;AACJ,CAAC","sourcesContent":["import path from \"node:path\";\nimport { realpath } from \"node:fs/promises\";\n\n/** Exclusions are literal paths relative to cwd, never glob patterns. */\nexport async function excludeSelection(\n root: string,\n selected: string[],\n exclusions: string[],\n): Promise<string[]> {\n // Resolve filesystem aliases (including Windows short names) consistently\n // with discovery, while allowing exclusions for paths that do not exist yet.\n async function canonical(file: string): Promise<string> {\n try {\n return await realpath(file);\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code !== \"ENOENT\") throw error;\n const parent = path.dirname(file);\n if (parent === file) throw error;\n return path.join(await canonical(parent), path.basename(file));\n }\n }\n const relative = await Promise.all(\n exclusions.map(async (input) => {\n const name = path\n .relative(root, await canonical(path.resolve(input)))\n .split(path.sep)\n .join(\"/\");\n if (name === \"..\" || name.startsWith(\"../\") || path.isAbsolute(name))\n throw new Error(`Excluded path is outside the workspace: ${input}`);\n return name;\n }),\n );\n return selected.filter(\n (name) =>\n !relative.some(\n (excluded) => !excluded || name === excluded || name.startsWith(`${excluded}/`),\n ),\n );\n}\n"]}
package/docs/cli.md CHANGED
@@ -19,6 +19,24 @@ honors `.gitignore` and configuration ignore patterns, skips symlinks and nested
19
19
  repositories, and does not expand path globs itself. See the
20
20
  [selection details](configuration.md#selection-output-and-exit-codes).
21
21
 
22
+ Use a caller or Git hook to choose changed or staged paths and pass them as
23
+ explicit file arguments. The CLI processes working-tree contents, not the Git
24
+ index. Pass paths as separate arguments (after `--` when they could begin with
25
+ `-`), retain `--root` for workspace-wide link resolution, and skip the
26
+ invocation when the selection is empty: no file arguments means the whole
27
+ workspace.
28
+
29
+ ```sh
30
+ node dist/cli/main.js lint --exclude generated
31
+ node dist/cli/main.js format --exclude notes/archive.md --exclude vendor --diff
32
+ ```
33
+
34
+ Repeat `--exclude <path>` to omit exact files or directory subtrees from any
35
+ file selection. Exclusions are literal paths relative to the current directory
36
+ (or absolute paths), must be inside the workspace, and do not remove link
37
+ targets from the index. They cannot be combined with stdin. Config ignores and
38
+ repository boundaries still apply to all selections.
39
+
22
40
  ## Use an explicit configuration
23
41
 
24
42
  ```sh
@@ -57,22 +57,67 @@ Problem rules report findings and never become formatting edits.
57
57
 
58
58
  ## Built-in rules
59
59
 
60
- | Rule | Kind | Options |
61
- | ----------------------------- | ------- | --------------------------------------------------------- |
62
- | `style/wrap` | Style | `width`: integer 20–500, default 80 |
63
- | `style/emphasis` | Style | `marker`: `_` (default) or `*` |
64
- | `style/strong` | Style | `marker`: `*` (default) or `_` |
65
- | `style/final-newline` | Style | None; adds a missing final newline |
66
- | `style/table` | Style | None; aligns top-level GFM tables |
67
- | `style/heading` | Style | None; converts eligible Setext headings to ATX |
68
- | `links/valid` | Problem | None; local files and heading/block fragments |
69
- | `links/path` | Style | `style`, `brackets`, `extension`, `leadingDot`; see below |
70
- | `links/notation` | Style | `style`: `markdown` or `wiki`; Obsidian only |
71
- | `github/task-marker` | Style | None; `[X]` becomes `[x]` |
72
- | `github/alert-marker` | Style | None; known GitHub alert types become uppercase |
73
- | `obsidian/callout-marker` | Style | None; Obsidian callout types become lowercase |
74
- | `obsidian/block-reference` | Problem | None; duplicate trailing `^block-id` markers |
75
- | `obsidian/strict-line-breaks` | Problem | None; report unverified/incompatible reflow settings |
60
+ | Rule | Kind | Options |
61
+ | ----------------------------- | ------- | ---------------------------------------------------------------------- |
62
+ | `style/wrap` | Style | `width`, `measure`, `reportUnreflowed`, `reportUnbreakable`; see below |
63
+ | `style/inline-code` | Style | None; joins multiline code spans, opt-in |
64
+ | `style/emphasis` | Style | `marker`: `_` (default) or `*` |
65
+ | `style/strong` | Style | `marker`: `*` (default) or `_` |
66
+ | `style/final-newline` | Style | None; adds a missing final newline |
67
+ | `style/table` | Style | None; aligns top-level GFM tables |
68
+ | `style/heading` | Style | None; converts eligible Setext headings to ATX |
69
+ | `links/valid` | Problem | None; local files and heading/block fragments |
70
+ | `links/path` | Style | `style`, `brackets`, `extension`, `leadingDot`; see below |
71
+ | `links/notation` | Style | `style`: `markdown` or `wiki`; Obsidian only |
72
+ | `github/task-marker` | Style | None; `[X]` becomes `[x]` |
73
+ | `github/alert-marker` | Style | None; known GitHub alert types become uppercase |
74
+ | `obsidian/callout-marker` | Style | None; Obsidian callout types become lowercase |
75
+ | `obsidian/block-reference` | Problem | None; duplicate trailing `^block-id` markers |
76
+ | `obsidian/strict-line-breaks` | Problem | None; report unverified/incompatible reflow settings |
77
+
78
+ ## Document structure
79
+
80
+ Enable `structure/initial-heading` explicitly with severity `warn` or `error`;
81
+ no preset enables it. It accepts no options and reports problems without
82
+ changing content. Use path `overrides` to exempt generated documents or trees
83
+ with different conventions.
84
+
85
+ `structure/initial-heading` skips YAML/TOML front matter and requires a level-1
86
+ heading as the first content block. Both ATX and Setext headings count; use
87
+ `style/heading` separately to require ATX spelling. Empty documents are
88
+ reported. Comments and other content before the title are not skipped.
89
+
90
+ Filename/title matching, metadata placement, and guesses about accidental
91
+ Markdown markers are consumer policies. Implement those checks through
92
+ [rule plugins](plugins.md#rule-plugins), which receive the document source,
93
+ parsed tree, and path. Modified-date updates belong in a consumer wrapper or
94
+ library workflow; formatting rules must preserve document meaning.
95
+
96
+ ## Wrapping and inline code
97
+
98
+ `style/wrap` accepts widths from 20 through 500 and defaults to `width: 80` and
99
+ `measure: "columns"` (display columns). Set `measure: "codepoints"` to count
100
+ Unicode scalar values instead; astral characters count once and combining marks
101
+ count separately. Container prefixes count toward the width in either mode.
102
+
103
+ `reportUnreflowed: true` reports breakable over-width lines in paragraphs
104
+ protected by hard breaks, block IDs, inline HTML, callout headers, or
105
+ unsupported multiline syntax or containers. `reportUnbreakable: true` reports
106
+ overflow from an atom that cannot be split, including in protected paragraphs.
107
+ Whitespace inside a link, code span, or other protected inline node is not a
108
+ wrapping opportunity. A protected paragraph with both breakable prose and an
109
+ over-width atom can report both causes; an atom-only overflow does not trigger
110
+ `reportUnreflowed`. Hard-break markers are excluded from protected-line width.
111
+ Both options default to false, propose no edits, and use the rule's severity.
112
+ Use severity `error` or `--max-warnings 0` to make these diagnostics fail a
113
+ check; `--check` does not change rule settings.
114
+
115
+ Enable `style/inline-code` to join multiline code spans before paragraph reflow.
116
+ This opt-in rule handles single and multiple backticks, including list and quote
117
+ containers. It preserves the parsed code value, converting each line ending to
118
+ one space under CommonMark's code-span rules. Other code whitespace remains
119
+ significant. It is useful for Obsidian editors that display split spans
120
+ differently from Reading view.
76
121
 
77
122
  ## Link policies
78
123
 
@@ -95,6 +140,9 @@ vault-relative path. `leadingDot: true` prefixes ordinary relative paths with
95
140
  Markdown destinations are percent-encoded. `extension` accepts `preserve`
96
141
  (default), `include`, or `omit`; omission is Obsidian specific. Fragments are
97
142
  resolved according to the dialect, separately from percent-decoded filenames.
143
+ Already compliant destinations retain their percent-encoding spelling, including
144
+ literal `?` and `%` in resolving Obsidian paths. Newly generated paths encode
145
+ literal percent signs and hashes to preserve target identity.
98
146
 
99
147
  Path edits require a resolved target and valid fragment. Unaliased ordinary
100
148
  wikilinks retain their target spelling because changing it may change the
@@ -110,10 +158,15 @@ separate passes.
110
158
 
111
159
  Network URLs are recognized but never fetched. Website-root paths and
112
160
  query-bearing destinations in CommonMark/GitHub are outside local resolution.
113
- Missing or ambiguous targets are reported, never guessed. The conservative
114
- Obsidian resolver may report ambiguity where a particular Obsidian version would
115
- select one candidate; shortest-name/alias resolution is not a complete clone of
116
- the app.
161
+ Missing or ambiguous targets are reported, never guessed. Obsidian resolution
162
+ uses explicit `./` or `../` paths as source-relative; otherwise an exact
163
+ vault-root match precedes an exact source-relative match, followed by unique
164
+ suffix matching. Multiple candidates within the chosen tier remain ambiguous.
165
+ Relative rewrites use `./` when needed to avoid a vault-root collision. Existing
166
+ directories get a distinct diagnostic in Obsidian; they are not rewritten to
167
+ README or index notes. CommonMark/GitHub directory links are accepted because a
168
+ hosting site can serve them. Shortest-name/alias resolution is not a complete
169
+ clone of Obsidian.
117
170
 
118
171
  ## Suppressions
119
172
 
@@ -139,11 +192,24 @@ it directly above the content to suppress.
139
192
 
140
193
  Commands accept explicit files/directories, or no paths to select the workspace.
141
194
  The CLI honors `.gitignore` files and configured ignore patterns. Config-ignored
142
- documents remain available for link resolution; Git-ignored content is not
143
- indexed. `.git`, `.obsidian`, `node_modules`, `.npm-cache`, `dist`, and
144
- `coverage` are excluded from traversal, as are nested Git repositories and
145
- symlinks. Explicit symlink inputs are rejected. Shell-expanded globs work; the
146
- CLI does not expand path globs.
195
+ documents remain available for link resolution. Git-ignored content and nested
196
+ repositories are not indexed by default. To make their files available only as
197
+ link targets, set:
198
+
199
+ ```jsonc
200
+ {
201
+ "resolve": { "gitIgnored": true, "nestedRepositories": true },
202
+ }
203
+ ```
204
+
205
+ These options are independent and default to false. They never select those
206
+ files for linting or formatting, including when a path is supplied explicitly.
207
+ Markdown targets are read and parsed only for fragment checks; attachments need
208
+ only a discovered file entry. Built-in traversal exclusions still apply. `.git`,
209
+ `.obsidian`, `node_modules`, `.npm-cache`, `dist`, and `coverage` are excluded
210
+ from traversal, as are symlinks. Nested Git repositories are traversed for
211
+ targets only when opted in. Explicit symlink inputs are rejected. Shell-expanded
212
+ globs work; the CLI does not expand path globs.
147
213
 
148
214
  `lint --json` and `format --json` print a single object with `version`, `mode`,
149
215
  `files`, and `written`. Each file carries diagnostics with rule ID, severity,