mdrefine 0.1.0-beta.1 → 0.1.0-beta.2

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 (40) 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 +19 -4
  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 +78 -15
  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 +86 -25
  34. package/docs/contributing.md +87 -5
  35. package/docs/design.md +15 -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.md +33 -10
  40. package/package.json +1 -1
@@ -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,62 @@ 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 over-width paragraphs protected by hard breaks,
104
+ block IDs, inline HTML, callout headers, or unsupported multiline syntax or
105
+ containers. `reportUnbreakable: true` reports overflow remaining after reflow
106
+ because an atom cannot be split. Both default to false, propose no edits, and
107
+ use the rule's severity. Use severity `error` or `--max-warnings 0` to make
108
+ these diagnostics fail a check; `--check` does not change rule settings.
109
+
110
+ Enable `style/inline-code` to join multiline code spans before paragraph reflow.
111
+ This opt-in rule handles single and multiple backticks, including list and quote
112
+ containers. It preserves the parsed code value, converting each line ending to
113
+ one space under CommonMark's code-span rules. Other code whitespace remains
114
+ significant. It is useful for Obsidian editors that display split spans
115
+ differently from Reading view.
76
116
 
77
117
  ## Link policies
78
118
 
@@ -95,6 +135,9 @@ vault-relative path. `leadingDot: true` prefixes ordinary relative paths with
95
135
  Markdown destinations are percent-encoded. `extension` accepts `preserve`
96
136
  (default), `include`, or `omit`; omission is Obsidian specific. Fragments are
97
137
  resolved according to the dialect, separately from percent-decoded filenames.
138
+ Already compliant destinations retain their percent-encoding spelling, including
139
+ literal `?` and `%` in resolving Obsidian paths. Newly generated paths encode
140
+ literal percent signs and hashes to preserve target identity.
98
141
 
99
142
  Path edits require a resolved target and valid fragment. Unaliased ordinary
100
143
  wikilinks retain their target spelling because changing it may change the
@@ -110,10 +153,15 @@ separate passes.
110
153
 
111
154
  Network URLs are recognized but never fetched. Website-root paths and
112
155
  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.
156
+ Missing or ambiguous targets are reported, never guessed. Obsidian resolution
157
+ uses explicit `./` or `../` paths as source-relative; otherwise an exact
158
+ vault-root match precedes an exact source-relative match, followed by unique
159
+ suffix matching. Multiple candidates within the chosen tier remain ambiguous.
160
+ Relative rewrites use `./` when needed to avoid a vault-root collision. Existing
161
+ directories get a distinct diagnostic in Obsidian; they are not rewritten to
162
+ README or index notes. CommonMark/GitHub directory links are accepted because a
163
+ hosting site can serve them. Shortest-name/alias resolution is not a complete
164
+ clone of Obsidian.
117
165
 
118
166
  ## Suppressions
119
167
 
@@ -139,11 +187,24 @@ it directly above the content to suppress.
139
187
 
140
188
  Commands accept explicit files/directories, or no paths to select the workspace.
141
189
  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.
190
+ documents remain available for link resolution. Git-ignored content and nested
191
+ repositories are not indexed by default. To make their files available only as
192
+ link targets, set:
193
+
194
+ ```jsonc
195
+ {
196
+ "resolve": { "gitIgnored": true, "nestedRepositories": true },
197
+ }
198
+ ```
199
+
200
+ These options are independent and default to false. They never select those
201
+ files for linting or formatting, including when a path is supplied explicitly.
202
+ Markdown targets are read and parsed only for fragment checks; attachments need
203
+ only a discovered file entry. Built-in traversal exclusions still apply. `.git`,
204
+ `.obsidian`, `node_modules`, `.npm-cache`, `dist`, and `coverage` are excluded
205
+ from traversal, as are symlinks. Nested Git repositories are traversed for
206
+ targets only when opted in. Explicit symlink inputs are rejected. Shell-expanded
207
+ globs work; the CLI does not expand path globs.
147
208
 
148
209
  `lint --json` and `format --json` print a single object with `version`, `mode`,
149
210
  `files`, and `written`. Each file carries diagnostics with rule ID, severity,
@@ -28,8 +28,8 @@ respectful and focused on reproducible behavior; harassment and disclosure of
28
28
  private data are not acceptable. Contributions are provided under the MIT
29
29
  license.
30
30
 
31
- The branch policy requires the four platform checks and resolved review threads.
32
- It does not require approval from a second account: this project is
31
+ The branch policy requires the three CI checks and resolved review threads. It
32
+ does not require approval from a second account: this project is
33
33
  agent-maintained and cannot assume another maintainer is available. Request
34
34
  independent review when available, especially for file writes and release
35
35
  automation. Passing CI is not a claim of human review.
@@ -41,6 +41,77 @@ versions and release tags are never replaced. Follow the
41
41
  reports belong in the private channel described in
42
42
  [SECURITY.md](https://github.com/viell-dev/slop-markdown-tools/blob/main/SECURITY.md).
43
43
 
44
+ ## Issue and pull request labels
45
+
46
+ Use one primary `Type:` label on every issue and PR, and at most one `Status:`
47
+ label describing the current disposition or next action. The type describes what
48
+ the item is about; a feature implementation PR also uses
49
+ `Type: Feature Request`. For mixed historical items, choose the main purpose and
50
+ explain separate decisions in comments. Split new requests when their parts need
51
+ independent scope decisions. Do not add priority labels.
52
+
53
+ The
54
+ [label catalog](https://github.com/viell-dev/slop-markdown-tools/blob/main/.github/labels.json)
55
+ is the source of truth for exact names, descriptions, and hexadecimal colors.
56
+ Keep it, this guide, GitHub, issue templates, and Dependabot settings aligned.
57
+ Use the GitHub UI or the existing `gh` CLI; no label-sync service is required.
58
+
59
+ | Label | Color | Use |
60
+ | --------------------------- | --------- | ---------------------------------------------------------------------------- |
61
+ | `Type: Bug` | `#d73a4a` | Incorrect behavior, broken functionality, or a regression. |
62
+ | `Type: Enhancement` | `#a2eeef` | Improve an existing capability, including usability or performance. |
63
+ | `Type: Feature Request` | `#0e8a16` | Add a new independently useful capability or rule. |
64
+ | `Type: Maintenance` | `#6a737d` | Dependencies, CI, releases, refactoring, or repository upkeep. |
65
+ | `Type: Documentation` | `#0075ca` | Documentation, examples, or contributor instructions. |
66
+ | `Type: Discussion` | `#5319e7` | Explore a design or project direction before an actionable proposal. |
67
+ | `Type: Question` | `#d876e3` | Ask how the project works or how to use it. |
68
+ | `Status: Needs Triage` | `#ededed` | Awaiting maintainer review and a scope decision. |
69
+ | `Status: Confirmed` | `#0e8a16` | Reproduced defect or accepted actionable request; not a delivery commitment. |
70
+ | `Status: Needs Information` | `#fbca04` | Waiting for reporter details, answers, or validation results. |
71
+ | `Status: Blocked` | `#d93f0b` | Accepted work is waiting on an identified external dependency. |
72
+ | `Status: Can't Reproduce` | `#fef2c0` | A documented reproduction attempt did not reproduce the report. |
73
+ | `Status: Duplicate` | `#cfd3d7` | Already tracked elsewhere; link the canonical issue or PR. |
74
+ | `Status: Won't Do` | `#666666` | Declined as out of scope or unsuitable in its proposed form. |
75
+ | `Good First Issue` | `#7057ff` | A scoped, ready task with a clear approach and beginner-friendly validation. |
76
+ | `Help Wanted` | `#008672` | A scoped, accepted task for which outside contributions are welcome. |
77
+
78
+ Review labels whenever answering, triaging, implementing, closing, reopening, or
79
+ otherwise updating an issue or PR:
80
+
81
+ - Replace `Status: Needs Triage` after review. Use `Status: Confirmed` only for
82
+ a reproduced defect or an accepted request within project scope; neither
83
+ reproduction nor acceptance follows automatically from a report.
84
+ - When waiting on the reporter, use `Status: Needs Information` and state what
85
+ evidence is needed. Use `Status: Blocked` for an identified external
86
+ dependency, with a link and an unblock condition. Choose the immediate next
87
+ action rather than stacking statuses.
88
+ - Use `Status: Can't Reproduce` only after documenting the attempted version,
89
+ input, and result. An incomplete report first needs information.
90
+ - On duplicate closure, link the canonical item and retain `Status: Duplicate`.
91
+ On rejection, explain the scope or suitability decision and retain
92
+ `Status: Won't Do`. A declined proposal can be reconsidered with new evidence.
93
+ - On successful completion or merge, remove transient status labels and retain
94
+ the type. GitHub's open/closed/merged state records completion; there is no
95
+ redundant Done or Merged label. A closed, unmerged PR should retain its
96
+ applicable disposition, rather than being labeled as completed.
97
+ - On reopening, reassess the evidence and replace the old disposition; do not
98
+ carry a stale rejection, duplicate, or information request forward.
99
+ - Apply `Good First Issue` only to a ready, well-explained beginner task,
100
+ together with `Help Wanted`. Remove both when the item closes or is no longer
101
+ ready for contribution. Waiting for a reporter's private-workspace benchmark
102
+ is not a general help request.
103
+
104
+ Colors follow familiar GitHub cues: red for defects, blue for documentation,
105
+ green for accepted work, yellow/orange for missing information or blockers, and
106
+ gray for neutral or declined dispositions. Names carry the meaning without
107
+ relying on color. The unprefixed contributor labels follow
108
+ [GitHub's label conventions](https://docs.github.com/en/issues/using-labels-and-milestones-to-track-work/managing-labels);
109
+ the readiness criteria draw on the
110
+ [Kubernetes contributor guide](https://www.kubernetes.dev/docs/guide/help-wanted/).
111
+ The `Type:` / `Status:` grouping also appears in
112
+ [community label catalogs](https://gist.github.com/dysfunc/fc722e865a6a960a2d9c5ecf43f5a963);
113
+ this repository intentionally uses a smaller set.
114
+
44
115
  ## Develop locally
45
116
 
46
117
  Follow the [checkout setup](quick-start.md#install-from-the-repository), then:
@@ -55,9 +126,20 @@ site build, and regression/property/CLI tests. `test:package` packs the tool,
55
126
  installs it into a temporary consumer project, and exercises the installed CLI
56
127
  and library. It can require access to the npm registry.
57
128
 
58
- GitHub Actions verifies Linux with Node.js 22 and 24, plus macOS and Windows
59
- with Node.js 24. Keep changes scoped and describe remaining limitations in the
60
- pull request. Agent contributors should also read the repository's
129
+ GitHub Actions runs the full `check` and dependency audit on Linux with
130
+ Node.js 24. Linux with Node.js 22 and Windows with Node.js 24 run the build and
131
+ tests; all three jobs verify the installed package. Linting, formatting checks,
132
+ and documentation builds run once rather than on every platform. Superseded PR
133
+ runs are canceled.
134
+
135
+ Runtime dependencies have no OS-specific installation constraints. Development
136
+ tooling does include native platform packages, but that alone does not justify
137
+ repeating the full toolchain matrix. Windows coverage protects filesystem path
138
+ handling and installed command behavior; it has already caught a path-alias
139
+ regression. macOS is not a routine CI target.
140
+
141
+ Keep changes scoped and describe remaining limitations in the pull request.
142
+ Agent contributors should also read the repository's
61
143
  [AGENTS.md](https://github.com/viell-dev/slop-markdown-tools/blob/main/AGENTS.md).
62
144
 
63
145
  ## Write and preview documentation
package/docs/design.md CHANGED
@@ -59,11 +59,21 @@ Heading nesting paths, property aliases, PDF subpaths, and every Obsidian
59
59
  plugin's syntax are not fully supported. Embeds are preserved as embeds; image
60
60
  dimensions in their aliases remain intact.
61
61
 
62
- All eligible Markdown is currently parsed into an in-memory workspace index.
63
- There is no persistent cache, watch service, editor extension, or language
64
- server yet. Use directory selection to scope output; indexing still needs the
65
- containing workspace for cross-document links. The library has a pluggable
66
- workspace interface for specialized hosts.
62
+ The workspace indexes eligible paths first and parses target Markdown only when
63
+ a fragment is checked, caching the result for that invocation. CLI discovery
64
+ also defers reading Markdown until it is selected or needed for a fragment.
65
+ Rule-schema validators are reused across documents. A suffix lookup index is
66
+ built on first use instead of scanning every path for each shortened link. There
67
+ is no persistent cache, worker pool, watch service, editor extension, or
68
+ language server yet. The library has a pluggable workspace interface for
69
+ specialized hosts.
70
+
71
+ Run `npm run build` and `node scripts/benchmark.mjs 1000` for a reproducible
72
+ synthetic library benchmark. It reports path-index construction and lint time
73
+ separately for 1,000 short interlinked notes; pass a different count to scale
74
+ it. These timings exclude filesystem traversal and do not predict a particular
75
+ vault's throughput. Measure the original workload before adding caches or
76
+ workers.
67
77
 
68
78
  ## Maintenance priorities
69
79
 
package/docs/plugins.md CHANGED
@@ -26,10 +26,17 @@ const result = format(files["note.md"], options);
26
26
 
27
27
  Library calls do not read or write files. `path` and workspace keys use
28
28
  forward-slash paths relative to the workspace root. Use `null` values for
29
- non-Markdown attachments. Without a workspace, local target checks and path
30
- rewriting are unavailable. Obsidian reflow also requires an explicit
31
- `workspace.strictLineBreaks: true`; callers are responsible for verifying that
32
- renderer setting.
29
+ non-Markdown attachments. A Markdown value can also be a synchronous loader
30
+ `() => string`; `createWorkspace` calls it only when that target needs fragment
31
+ validation, then caches its parsed heading/block index. Callers must provide a
32
+ stable source snapshot for each workspace instance. The CLI provides its own
33
+ per-invocation file loaders. `WorkspaceOptions.directories` can list existing
34
+ empty directories; parent directories of file entries are inferred. Resolution
35
+ returns `status: "directory"` for a directory instead of `"missing"`.
36
+
37
+ Without a workspace, local target checks and path rewriting are unavailable.
38
+ Obsidian reflow also requires an explicit `workspace.strictLineBreaks: true`;
39
+ callers are responsible for verifying that renderer setting.
33
40
 
34
41
  `parse`, `range`, `textContent`, `resolveConfig`, `configSchema`, `presets`,
35
42
  `builtInRules`, `ruleRegistry`, `applyEdits`, and `semanticFingerprint` are also