mdrefine 0.1.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CONTRIBUTING.md +10 -0
- package/LICENSE +21 -0
- package/README.md +133 -0
- package/SECURITY.md +16 -0
- package/dist/cli/main.d.ts +2 -0
- package/dist/cli/main.js +158 -0
- package/dist/cli/main.js.map +1 -0
- package/dist/config/load.d.ts +9 -0
- package/dist/config/load.js +75 -0
- package/dist/config/load.js.map +1 -0
- package/dist/config/resolve.d.ts +93 -0
- package/dist/config/resolve.js +110 -0
- package/dist/config/resolve.js.map +1 -0
- package/dist/core/directives.d.ts +3 -0
- package/dist/core/directives.js +48 -0
- package/dist/core/directives.js.map +1 -0
- package/dist/core/engine.d.ts +8 -0
- package/dist/core/engine.js +192 -0
- package/dist/core/engine.js.map +1 -0
- package/dist/core/types.d.ts +92 -0
- package/dist/core/types.js +2 -0
- package/dist/core/types.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -0
- package/dist/rules/dialects.d.ts +2 -0
- package/dist/rules/dialects.js +105 -0
- package/dist/rules/dialects.js.map +1 -0
- package/dist/rules/links.d.ts +2 -0
- package/dist/rules/links.js +238 -0
- package/dist/rules/links.js.map +1 -0
- package/dist/rules/style.d.ts +12 -0
- package/dist/rules/style.js +244 -0
- package/dist/rules/style.js.map +1 -0
- package/dist/syntax/obsidian.d.ts +33 -0
- package/dist/syntax/obsidian.js +126 -0
- package/dist/syntax/obsidian.js.map +1 -0
- package/dist/syntax/parse.d.ts +5 -0
- package/dist/syntax/parse.js +45 -0
- package/dist/syntax/parse.js.map +1 -0
- package/dist/workspace/files.d.ts +8 -0
- package/dist/workspace/files.js +98 -0
- package/dist/workspace/files.js.map +1 -0
- package/dist/workspace/index.d.ts +10 -0
- package/dist/workspace/index.js +100 -0
- package/dist/workspace/index.js.map +1 -0
- package/docs/cli.md +71 -0
- package/docs/configuration.md +171 -0
- package/docs/contributing.md +103 -0
- package/docs/design.md +79 -0
- package/docs/index.md +48 -0
- package/docs/obsidian.md +69 -0
- package/docs/plugins.md +114 -0
- package/docs/quick-start.md +101 -0
- package/docs/releases/0.1.0-beta.1.md +39 -0
- package/docs/releases.md +90 -0
- package/examples/github.jsonc +7 -0
- package/examples/obsidian.jsonc +8 -0
- package/package.json +101 -0
|
@@ -0,0 +1 @@
|
|
|
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"]}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Dialect, Workspace } from "../core/types.js";
|
|
2
|
+
export interface WorkspaceOptions {
|
|
3
|
+
dialect?: Dialect;
|
|
4
|
+
strictLineBreaks?: boolean;
|
|
5
|
+
}
|
|
6
|
+
export declare function splitDestination(destination: string): {
|
|
7
|
+
path: string;
|
|
8
|
+
fragment: string;
|
|
9
|
+
};
|
|
10
|
+
export declare function createWorkspace(files: Record<string, string | null>, options?: WorkspaceOptions): Workspace;
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import GithubSlugger from "github-slugger";
|
|
3
|
+
import { visit } from "unist-util-visit";
|
|
4
|
+
import { parse, textContent } from "../syntax/parse.js";
|
|
5
|
+
export function splitDestination(destination) {
|
|
6
|
+
const hash = destination.indexOf("#");
|
|
7
|
+
const decode = (value) => {
|
|
8
|
+
try {
|
|
9
|
+
return decodeURIComponent(value);
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
return value;
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
return {
|
|
16
|
+
path: decode(hash < 0 ? destination : destination.slice(0, hash)),
|
|
17
|
+
fragment: decode(hash < 0 ? "" : destination.slice(hash + 1)),
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
export function createWorkspace(files, options = {}) {
|
|
21
|
+
const entries = new Map();
|
|
22
|
+
for (const [name, source] of Object.entries(files)) {
|
|
23
|
+
const entry = { headings: new Set(), slugs: new Set(), blocks: new Set() };
|
|
24
|
+
if (source !== null && /\.md$/i.test(name)) {
|
|
25
|
+
const document = parse(source, options.dialect ?? "commonmark", name);
|
|
26
|
+
const slugger = new GithubSlugger();
|
|
27
|
+
visit(document.tree, "heading", (node) => {
|
|
28
|
+
const text = textContent(node);
|
|
29
|
+
entry.headings.add(text);
|
|
30
|
+
entry.slugs.add(slugger.slug(text));
|
|
31
|
+
});
|
|
32
|
+
visit(document.tree, "text", (node) => {
|
|
33
|
+
const match = /(?:^|\s)\^([A-Za-z0-9-]+)\s*$/.exec(node.value);
|
|
34
|
+
if (match)
|
|
35
|
+
entry.blocks.add(match[1]);
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
entries.set(name.replaceAll("\\", "/").replace(/^\.\//, ""), entry);
|
|
39
|
+
}
|
|
40
|
+
return {
|
|
41
|
+
...(options.strictLineBreaks !== undefined
|
|
42
|
+
? { strictLineBreaks: options.strictLineBreaks }
|
|
43
|
+
: {}),
|
|
44
|
+
resolve(source, destination, dialect) {
|
|
45
|
+
if (/^[a-z][a-z\d+.-]*:/i.test(destination) || destination.startsWith("//"))
|
|
46
|
+
return { status: "external" };
|
|
47
|
+
const parts = splitDestination(destination);
|
|
48
|
+
const targetPath = parts.path;
|
|
49
|
+
if (dialect !== "obsidian" && (targetPath.startsWith("/") || targetPath.includes("?")))
|
|
50
|
+
return { status: "unavailable" };
|
|
51
|
+
const candidates = new Set();
|
|
52
|
+
const add = (candidate) => {
|
|
53
|
+
const normalized = path.posix.normalize(candidate);
|
|
54
|
+
if (normalized.startsWith("../") || path.posix.isAbsolute(normalized))
|
|
55
|
+
return;
|
|
56
|
+
if (entries.has(normalized))
|
|
57
|
+
candidates.add(normalized);
|
|
58
|
+
if (dialect === "obsidian" &&
|
|
59
|
+
!path.posix.extname(normalized) &&
|
|
60
|
+
entries.has(`${normalized}.md`))
|
|
61
|
+
candidates.add(`${normalized}.md`);
|
|
62
|
+
};
|
|
63
|
+
if (!targetPath)
|
|
64
|
+
add(source);
|
|
65
|
+
else if (dialect === "obsidian") {
|
|
66
|
+
if (/^\.{1,2}\//.test(targetPath))
|
|
67
|
+
add(path.posix.join(path.posix.dirname(source), targetPath));
|
|
68
|
+
else {
|
|
69
|
+
add(targetPath.replace(/^\//, ""));
|
|
70
|
+
if (!targetPath.startsWith("/"))
|
|
71
|
+
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
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
else
|
|
84
|
+
add(path.posix.join(path.posix.dirname(source), targetPath));
|
|
85
|
+
if (candidates.size !== 1)
|
|
86
|
+
return { status: candidates.size ? "ambiguous" : "missing" };
|
|
87
|
+
const target = [...candidates][0];
|
|
88
|
+
const entry = entries.get(target);
|
|
89
|
+
const fragment = parts.fragment;
|
|
90
|
+
const fragmentExists = !fragment ||
|
|
91
|
+
(dialect === "obsidian"
|
|
92
|
+
? fragment.startsWith("^")
|
|
93
|
+
? entry.blocks.has(fragment.slice(1))
|
|
94
|
+
: entry.headings.has(fragment)
|
|
95
|
+
: entry.slugs.has(fragment));
|
|
96
|
+
return { status: "resolved", target, fragment, fragmentExists };
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +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"]}
|
package/docs/cli.md
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# CLI workflows
|
|
2
|
+
|
|
3
|
+
Examples use the built checkout. The installed executable is named `mdtools`;
|
|
4
|
+
`node dist/cli/main.js` invokes the same CLI without a global installation.
|
|
5
|
+
|
|
6
|
+
## Select files
|
|
7
|
+
|
|
8
|
+
With no file arguments, commands process the workspace. Pass files or
|
|
9
|
+
directories to narrow the selection while retaining the workspace for link
|
|
10
|
+
resolution:
|
|
11
|
+
|
|
12
|
+
```sh
|
|
13
|
+
node dist/cli/main.js lint --root /path/to/documents /path/to/documents/notes/example.md
|
|
14
|
+
node dist/cli/main.js format --root /path/to/documents /path/to/documents/notes --diff
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Input paths are absolute or relative to the current working directory. The CLI
|
|
18
|
+
honors `.gitignore` and configuration ignore patterns, skips symlinks and nested
|
|
19
|
+
repositories, and does not expand path globs itself. See the
|
|
20
|
+
[selection details](configuration.md#selection-output-and-exit-codes).
|
|
21
|
+
|
|
22
|
+
## Use an explicit configuration
|
|
23
|
+
|
|
24
|
+
```sh
|
|
25
|
+
node dist/cli/main.js lint --root /path/to/documents --config /path/to/mdtools.config.jsonc
|
|
26
|
+
node dist/cli/main.js config --root /path/to/documents explain /path/to/documents/notes/example.md
|
|
27
|
+
node dist/cli/main.js rules
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
`config explain` shows the configuration resolved for a document, including path
|
|
31
|
+
overrides. `rules` lists available rules. JavaScript configuration and plugins
|
|
32
|
+
execute code with the caller's permissions, so load trusted code only.
|
|
33
|
+
|
|
34
|
+
## Check documents in CI
|
|
35
|
+
|
|
36
|
+
After installing and building the checkout, run both checks against your
|
|
37
|
+
document workspace:
|
|
38
|
+
|
|
39
|
+
```sh
|
|
40
|
+
node dist/cli/main.js lint --root /path/to/documents --max-warnings 0
|
|
41
|
+
node dist/cli/main.js format --root /path/to/documents --check
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Lint catches enabled problem and style rules. Format checking verifies that the
|
|
45
|
+
formatter would leave the selected documents unchanged. Neither command writes.
|
|
46
|
+
|
|
47
|
+
## Work with agents and scripts
|
|
48
|
+
|
|
49
|
+
```sh
|
|
50
|
+
node dist/cli/main.js lint --root /path/to/documents --json
|
|
51
|
+
node dist/cli/main.js format --root /path/to/documents --check --json
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
JSON output contains file results and diagnostics with rule IDs, severity,
|
|
55
|
+
messages, and source positions. Exit code `0` means success, `1` means a lint or
|
|
56
|
+
formatting check failed, and `2` means a configuration, execution, or safety
|
|
57
|
+
failure. Warnings fail only when they exceed `--max-warnings`.
|
|
58
|
+
|
|
59
|
+
For a formatting pass, inspect a diff, apply it with `--write`, then run the
|
|
60
|
+
checks again. Report reproducible document defects using a synthetic example;
|
|
61
|
+
see [contributing](contributing.md). Automatic issue reporting is not built in.
|
|
62
|
+
|
|
63
|
+
## Read from stdin
|
|
64
|
+
|
|
65
|
+
```sh
|
|
66
|
+
node dist/cli/main.js format - --stdin-filepath notes/example.md < input.md > output.md
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
`--stdin-filepath` supplies the path used for configuration and link resolution.
|
|
70
|
+
Stdin formatting writes Markdown to stdout and diagnostics to stderr unless
|
|
71
|
+
`--json` is selected. It cannot be combined with `--write`.
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
# Configuration and rules
|
|
2
|
+
|
|
3
|
+
## Loading and precedence
|
|
4
|
+
|
|
5
|
+
Discovery searches upward from `--root` or the working directory for
|
|
6
|
+
`mdtools.config.jsonc`, `mdtools.config.json`, or `mdtools.config.mjs`. It stops
|
|
7
|
+
at a Git repository, an Obsidian vault, or the filesystem root. Multiple config
|
|
8
|
+
files in one directory are an error. `--config` chooses one explicitly. The
|
|
9
|
+
configuration directory is the workspace root unless `--root` overrides it.
|
|
10
|
+
|
|
11
|
+
JSONC permits comments and trailing commas. JavaScript configuration exports a
|
|
12
|
+
default object. JavaScript configs and plugins execute code with the caller's
|
|
13
|
+
permissions; load trusted code only. Unknown configuration keys, rule IDs,
|
|
14
|
+
preset names, and invalid built-in rule options are errors.
|
|
15
|
+
|
|
16
|
+
Presets merge in listed order, followed by the configuration's own settings,
|
|
17
|
+
followed by matching `overrides` in order. Each rule setting replaces the
|
|
18
|
+
previous setting as a whole. Paths in overrides and ignores use forward slashes
|
|
19
|
+
relative to the workspace root. Presets can extend other presets; cycles are
|
|
20
|
+
rejected. Config files do not implicitly cascade or merge across directories.
|
|
21
|
+
|
|
22
|
+
`--dialect` replaces the top-level dialect; an explicit file override still
|
|
23
|
+
takes precedence. `config explain <file>` prints the resolved configuration and
|
|
24
|
+
its source file.
|
|
25
|
+
|
|
26
|
+
```jsonc
|
|
27
|
+
{
|
|
28
|
+
"extends": ["recommended", "github"],
|
|
29
|
+
"ignore": ["generated/**"],
|
|
30
|
+
"overrides": [
|
|
31
|
+
{
|
|
32
|
+
"files": ["vault/**/*.md"],
|
|
33
|
+
"dialect": "obsidian",
|
|
34
|
+
"rules": { "style/emphasis": "off" },
|
|
35
|
+
},
|
|
36
|
+
],
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
For Obsidian reflow, use a workspace rooted at the vault so `.obsidian/app.json`
|
|
41
|
+
can be inspected. Multiple vaults with independent settings should be processed
|
|
42
|
+
separately.
|
|
43
|
+
|
|
44
|
+
## Presets
|
|
45
|
+
|
|
46
|
+
| Preset | Dialect | Enabled rules |
|
|
47
|
+
| ------------- | --------- | ---------------------------------------------------------------------------------------------- |
|
|
48
|
+
| `recommended` | Unchanged | Wrap at 80, `_` emphasis, `**` strong, final newline, valid links |
|
|
49
|
+
| `github` | GitHub | Table alignment, lowercase completed task marker, uppercase alert marker |
|
|
50
|
+
| `obsidian` | Obsidian | Table alignment, task marker, lowercase callout type, duplicate block IDs, soft-break settings |
|
|
51
|
+
|
|
52
|
+
An omitted `extends` selects `recommended`. Explicit `extends` replaces that
|
|
53
|
+
default, so use `["recommended", "obsidian"]` for both. `extends: []` enables
|
|
54
|
+
nothing. Severity is `off`, `warn`, or `error`; supply options as
|
|
55
|
+
`["warn", { ... }]`. Both `warn` and `error` style rules format when enabled.
|
|
56
|
+
Problem rules report findings and never become formatting edits.
|
|
57
|
+
|
|
58
|
+
## Built-in rules
|
|
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 |
|
|
76
|
+
|
|
77
|
+
## Link policies
|
|
78
|
+
|
|
79
|
+
```jsonc
|
|
80
|
+
{
|
|
81
|
+
"extends": ["recommended", "obsidian"],
|
|
82
|
+
"rules": {
|
|
83
|
+
"links/path": ["warn", { "style": "root", "brackets": "angle", "extension": "preserve" }],
|
|
84
|
+
},
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
`style` accepts `preserve` (default), `relative`, `root`, or `shortest`. `root`
|
|
89
|
+
means vault-relative, without a leading slash, and is available only for
|
|
90
|
+
Obsidian. `shortest` uses the basename if it resolves uniquely, otherwise the
|
|
91
|
+
vault-relative path. `leadingDot: true` prefixes ordinary relative paths with
|
|
92
|
+
`./`. Filesystem absolute paths are not generated.
|
|
93
|
+
|
|
94
|
+
`brackets` accepts `preserve` (default), `angle`, or `bare`. Spaces in bare
|
|
95
|
+
Markdown destinations are percent-encoded. `extension` accepts `preserve`
|
|
96
|
+
(default), `include`, or `omit`; omission is Obsidian specific. Fragments are
|
|
97
|
+
resolved according to the dialect, separately from percent-decoded filenames.
|
|
98
|
+
|
|
99
|
+
Path edits require a resolved target and valid fragment. Unaliased ordinary
|
|
100
|
+
wikilinks retain their target spelling because changing it may change the
|
|
101
|
+
visible label. Reference definitions are validated but their source spelling is
|
|
102
|
+
currently preserved.
|
|
103
|
+
|
|
104
|
+
`links/notation` converts simple explicit-label wikilinks to Markdown links and
|
|
105
|
+
plain-text Markdown links to wikilinks. Titles, rich labels, unaliased
|
|
106
|
+
wikilinks, and embeds are preserved. A note embed is not interchangeable with a
|
|
107
|
+
Markdown image. Do not enable notation conversion and path rewriting for the
|
|
108
|
+
same link in one pass: overlapping edits are reported; run those policies in
|
|
109
|
+
separate passes.
|
|
110
|
+
|
|
111
|
+
Network URLs are recognized but never fetched. Website-root paths and
|
|
112
|
+
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.
|
|
117
|
+
|
|
118
|
+
## Suppressions
|
|
119
|
+
|
|
120
|
+
```markdown
|
|
121
|
+
<!-- mdtools-disable style/wrap -->
|
|
122
|
+
|
|
123
|
+
Keep the wrapping in this region.
|
|
124
|
+
|
|
125
|
+
<!-- mdtools-enable style/wrap -->
|
|
126
|
+
|
|
127
|
+
<!-- mdtools-disable-next-line style/emphasis -->
|
|
128
|
+
Keep *this* delimiter.
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Omit rule names to disable all rules. A disabled region suppresses edits
|
|
132
|
+
intersecting it; a paragraph spanning a disabled line is therefore preserved as
|
|
133
|
+
a whole. An enable directive closes matching disabled regions; `enable` without
|
|
134
|
+
names closes all. Directives inside code are ordinary code. `disable-next-line`
|
|
135
|
+
targets the immediately following physical line, including a blank line; place
|
|
136
|
+
it directly above the content to suppress.
|
|
137
|
+
|
|
138
|
+
## Selection, output, and exit codes
|
|
139
|
+
|
|
140
|
+
Commands accept explicit files/directories, or no paths to select the workspace.
|
|
141
|
+
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.
|
|
147
|
+
|
|
148
|
+
`lint --json` and `format --json` print a single object with `version`, `mode`,
|
|
149
|
+
`files`, and `written`. Each file carries diagnostics with rule ID, severity,
|
|
150
|
+
message, offsets, and one-based line/column locations. Source offsets and
|
|
151
|
+
columns use JavaScript UTF-16 units; wrapping widths use display columns.
|
|
152
|
+
Formatting diagnostics refer to the resulting source. `written` records whether
|
|
153
|
+
the requested write phase was allowed, not how many files changed; inspect each
|
|
154
|
+
file's `changed` field.
|
|
155
|
+
|
|
156
|
+
`format --diff` is the default for file input. `--check`, `--diff`, and
|
|
157
|
+
`--write` are mutually exclusive. `format - --stdin-filepath notes/example.md`
|
|
158
|
+
reads stdin with configuration and resolution context; it cannot be used with
|
|
159
|
+
`--write`.
|
|
160
|
+
|
|
161
|
+
| Exit | Meaning |
|
|
162
|
+
| ---- | ------------------------------------------------------------------------------- |
|
|
163
|
+
| `0` | Completed; no errors or failed formatting check |
|
|
164
|
+
| `1` | Lint errors, exceeded `--max-warnings`, or changes required by `format --check` |
|
|
165
|
+
| `2` | Configuration, execution, or formatting safety failure |
|
|
166
|
+
|
|
167
|
+
Warnings do not fail a command unless `--max-warnings` is supplied. A successful
|
|
168
|
+
formatting command can still report remaining lint problems. Inspect its exit
|
|
169
|
+
code and diagnostics. An unsafe-format diagnostic blocks the complete batch's
|
|
170
|
+
write phase. An I/O error during writing can leave earlier files written;
|
|
171
|
+
replacement is atomic per file, not a workspace-wide transaction.
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# Contributing and development
|
|
2
|
+
|
|
3
|
+
This project is developed and maintained by AI agents. It is **not
|
|
4
|
+
human-maintained**; do not assume that a person has reviewed generated changes.
|
|
5
|
+
Issues and pull requests from people and agents are welcome.
|
|
6
|
+
|
|
7
|
+
## Report a defect
|
|
8
|
+
|
|
9
|
+
Include the version, command, configuration, minimal Markdown input, actual
|
|
10
|
+
output, and expected rendering. A screenshot can explain a visible defect, but
|
|
11
|
+
the Markdown source is needed to reproduce it. Include JSON diagnostics when
|
|
12
|
+
available, and remove private content before posting examples.
|
|
13
|
+
|
|
14
|
+
Use the [issue tracker](https://github.com/viell-dev/slop-markdown-tools/issues)
|
|
15
|
+
for reproducible defects and feature requests. Agents should open issues only
|
|
16
|
+
when their operator has authorized publishing them.
|
|
17
|
+
|
|
18
|
+
Document corruption and changed rendering take priority over new style options.
|
|
19
|
+
Fixes should include a synthetic regression example, preserve meaning, and
|
|
20
|
+
verify that a second formatting pass leaves the output unchanged.
|
|
21
|
+
|
|
22
|
+
## Pull requests and compatibility
|
|
23
|
+
|
|
24
|
+
All changes to `main` require a pull request and passing CI. This includes
|
|
25
|
+
prereleases, version bumps, documentation, and dependency updates. Do not push
|
|
26
|
+
directly, bypass required checks, or rewrite shared history. Keep discussion
|
|
27
|
+
respectful and focused on reproducible behavior; harassment and disclosure of
|
|
28
|
+
private data are not acceptable. Contributions are provided under the MIT
|
|
29
|
+
license.
|
|
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
|
|
33
|
+
agent-maintained and cannot assume another maintainer is available. Request
|
|
34
|
+
independent review when available, especially for file writes and release
|
|
35
|
+
automation. Passing CI is not a claim of human review.
|
|
36
|
+
|
|
37
|
+
The beta API and configuration may change; record changes in versioned release
|
|
38
|
+
notes and explain migration steps for incompatible behavior. Once published,
|
|
39
|
+
versions and release tags are never replaced. Follow the
|
|
40
|
+
[release procedure](releases.md) for preparation and publication. Security
|
|
41
|
+
reports belong in the private channel described in
|
|
42
|
+
[SECURITY.md](https://github.com/viell-dev/slop-markdown-tools/blob/main/SECURITY.md).
|
|
43
|
+
|
|
44
|
+
## Develop locally
|
|
45
|
+
|
|
46
|
+
Follow the [checkout setup](quick-start.md#install-from-the-repository), then:
|
|
47
|
+
|
|
48
|
+
```sh
|
|
49
|
+
npm run check
|
|
50
|
+
npm run test:package
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
`check` runs TypeScript checks, ESLint, formatting checks, the documentation
|
|
54
|
+
site build, and regression/property/CLI tests. `test:package` packs the tool,
|
|
55
|
+
installs it into a temporary consumer project, and exercises the installed CLI
|
|
56
|
+
and library. It can require access to the npm registry.
|
|
57
|
+
|
|
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
|
|
61
|
+
[AGENTS.md](https://github.com/viell-dev/slop-markdown-tools/blob/main/AGENTS.md).
|
|
62
|
+
|
|
63
|
+
## Write and preview documentation
|
|
64
|
+
|
|
65
|
+
The site source lives in `docs/` on `main`. Edit these Markdown files directly;
|
|
66
|
+
VitePress supplies navigation, local search, and rendering. The site uses
|
|
67
|
+
VitePress 2 alpha because the stable 1.6 line currently retains vulnerable
|
|
68
|
+
development dependencies; site builds are verified in CI. Markdown Tools formats
|
|
69
|
+
and lints the site's own Markdown at its default 80-column width.
|
|
70
|
+
|
|
71
|
+
```sh
|
|
72
|
+
npm run format:docs
|
|
73
|
+
npm run format:docs:check
|
|
74
|
+
npm run docs:dev
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
To inspect the production build:
|
|
78
|
+
|
|
79
|
+
```sh
|
|
80
|
+
npm run docs:build
|
|
81
|
+
npm run docs:preview
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Open the URL printed by the preview server, including the
|
|
85
|
+
`/slop-markdown-tools/` base path. The site build fails on unresolved internal
|
|
86
|
+
page links; Markdown linting also checks local heading references.
|
|
87
|
+
|
|
88
|
+
Prettier formats implementation and configuration files and excludes Markdown.
|
|
89
|
+
`npm run format` runs both formatters. Synthetic fixtures under
|
|
90
|
+
`tests/fixtures/` are excluded from documentation formatting.
|
|
91
|
+
|
|
92
|
+
## Publish the site
|
|
93
|
+
|
|
94
|
+
The Pages workflow builds the static site from `main` and deploys only
|
|
95
|
+
`docs/.vitepress/dist/` as a GitHub Pages artifact. Generated output is ignored
|
|
96
|
+
by Git and excluded from the npm package. There is no publishing branch to
|
|
97
|
+
maintain. Pull requests build the site through CI without deploying it.
|
|
98
|
+
|
|
99
|
+
GitHub Pages must use **GitHub Actions** as its publishing source. Deployment
|
|
100
|
+
uses the `github-pages` environment and the workflow's short-lived token. See
|
|
101
|
+
GitHub's
|
|
102
|
+
[custom workflow documentation](https://docs.github.com/en/pages/getting-started-with-github-pages/using-custom-workflows-with-github-pages)
|
|
103
|
+
for deployment settings.
|
package/docs/design.md
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# Design and current limitations
|
|
2
|
+
|
|
3
|
+
## Architecture
|
|
4
|
+
|
|
5
|
+
The project uses TypeScript, Node.js, and npm. mdast/micromark provide parsing
|
|
6
|
+
and syntax extensions. The formatter retains original source and applies
|
|
7
|
+
source-positioned edits rather than serializing the whole AST. This allows
|
|
8
|
+
disabled formatting rules and opaque constructs to retain their spelling.
|
|
9
|
+
|
|
10
|
+
`src/syntax/` owns parsing and Obsidian tokens. `src/config/` owns configuration
|
|
11
|
+
and plugin discovery. `src/core/` owns the shared contracts, suppression
|
|
12
|
+
directives, diagnostics, and edit engine. `src/rules/` implements built-in
|
|
13
|
+
rules. `src/workspace/` resolves targets and handles filesystem
|
|
14
|
+
selection/writes. `src/cli/` connects those services; the public `src/index.ts`
|
|
15
|
+
library has no implicit filesystem access.
|
|
16
|
+
|
|
17
|
+
Lint and format use the same rule settings. A style rule checks a preferred
|
|
18
|
+
representation and proposes edits. A problem rule reports defects without
|
|
19
|
+
inventing repairs. Inline, block, and document phases separate operations that
|
|
20
|
+
would otherwise commonly overlap. Each phase reparses its result and compares a
|
|
21
|
+
semantic fingerprint, using resolved destinations for link identity. Formatting
|
|
22
|
+
must converge; otherwise the original document is returned with an error
|
|
23
|
+
diagnostic.
|
|
24
|
+
|
|
25
|
+
## Safety boundaries
|
|
26
|
+
|
|
27
|
+
The semantic fingerprint protects the supported AST model, not every
|
|
28
|
+
application's rendered output. It normalizes ordinary prose whitespace and known
|
|
29
|
+
callout-marker casing. It does not run Obsidian or GitHub's renderer. Syntax
|
|
30
|
+
extensions require regression tests for their own meaning and source
|
|
31
|
+
preservation. Tests use synthetic documents, deterministic property checks, and
|
|
32
|
+
filesystem/CLI integration cases, including the list-collapse failure that
|
|
33
|
+
motivated replacing heuristic reflow.
|
|
34
|
+
|
|
35
|
+
Obsidian callout headers remain intact; their body paragraphs can reflow when
|
|
36
|
+
supported. Highlight, comment, math, code, HTML, and front-matter contents are
|
|
37
|
+
preserved. Standalone Obsidian comment blocks may span blank lines. Inline
|
|
38
|
+
comments spanning multiple paragraphs are not fully modeled; suppress formatting
|
|
39
|
+
around those constructs or use standalone comment blocks.
|
|
40
|
+
|
|
41
|
+
The source-preserving wrapper supports ordinary paragraphs, list items, and
|
|
42
|
+
blockquote containers. It preserves explicit hard-break paragraphs, lazy/unusual
|
|
43
|
+
continuations it cannot safely reconstruct, and paragraphs carrying block IDs.
|
|
44
|
+
Long unbreakable atoms can exceed the width. Intraword emphasis retains
|
|
45
|
+
asterisks when underscores would change parsing. Formatting width is a target,
|
|
46
|
+
not a license to split protected syntax.
|
|
47
|
+
|
|
48
|
+
GitHub alert support normalizes known alert type markers. Footnotes, math,
|
|
49
|
+
strikethrough, and autolinks are parsed, but do not each have dedicated style
|
|
50
|
+
rules. Table alignment currently applies only to top-level tables and preserves
|
|
51
|
+
cell contents. Embedded code formatting, metadata mutation, document generation,
|
|
52
|
+
external URL fetching, and file renames are outside this release.
|
|
53
|
+
|
|
54
|
+
Obsidian links resolve against indexed paths, extensionless Markdown candidates,
|
|
55
|
+
and unique suffixes for internal links. Duplicate candidates remain ambiguous.
|
|
56
|
+
Heading fragments use exact text; GitHub uses slugged headings including
|
|
57
|
+
duplicate suffixes. Block identifiers are indexed from trailing text markers.
|
|
58
|
+
Heading nesting paths, property aliases, PDF subpaths, and every Obsidian
|
|
59
|
+
plugin's syntax are not fully supported. Embeds are preserved as embeds; image
|
|
60
|
+
dimensions in their aliases remain intact.
|
|
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.
|
|
67
|
+
|
|
68
|
+
## Maintenance priorities
|
|
69
|
+
|
|
70
|
+
Prioritize reproducible document corruption and renderer disagreement over new
|
|
71
|
+
style options. Add a minimal regression case before a fix; verify that disabling
|
|
72
|
+
the affected rule preserves the source. Broaden dialect fixtures and resolver
|
|
73
|
+
compatibility before promising complete application parity. Benchmark real
|
|
74
|
+
synthetic vault sizes before adding caching or worker processes.
|
|
75
|
+
|
|
76
|
+
Future changes can add richer link policies, reference-definition formatting,
|
|
77
|
+
more container layout, versioned plugin contracts, and optional standalone
|
|
78
|
+
distribution. Each should extend the shared configuration and edit contracts
|
|
79
|
+
instead of creating a second formatter pipeline.
|
package/docs/index.md
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
---
|
|
2
|
+
layout: home
|
|
3
|
+
hero:
|
|
4
|
+
name: Markdown Tools
|
|
5
|
+
text: Your Markdown. Your rules.
|
|
6
|
+
tagline: Separate lint and format commands. Shared configuration. Built for CommonMark, GitHub, and Obsidian.
|
|
7
|
+
actions:
|
|
8
|
+
- theme: brand
|
|
9
|
+
text: Quick start
|
|
10
|
+
link: /quick-start
|
|
11
|
+
- theme: alt
|
|
12
|
+
text: Explore the rules
|
|
13
|
+
link: /configuration
|
|
14
|
+
features:
|
|
15
|
+
- title: Check, preview, then write
|
|
16
|
+
details: Lint without changing files, inspect formatting diffs, and write when ready. JSON diagnostics work with agents and automation.
|
|
17
|
+
- title: Format what you configure
|
|
18
|
+
details: Wrap at 80 columns by default. Choose link paths, delimiters, and dialect rules. Disabled rules preserve their syntax choices.
|
|
19
|
+
- title: Extend the shared core
|
|
20
|
+
details: Add JavaScript rules, presets, and syntax extensions. Use the same engine through the CLI or the TypeScript library.
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## A small, reviewable workflow
|
|
24
|
+
|
|
25
|
+
From a [built checkout](quick-start.md), point the CLI at your documents:
|
|
26
|
+
|
|
27
|
+
```sh
|
|
28
|
+
node dist/cli/main.js lint --root /path/to/documents
|
|
29
|
+
node dist/cli/main.js format --root /path/to/documents --diff
|
|
30
|
+
node dist/cli/main.js format --root /path/to/documents --write
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Formatting applies edits to the original source and checks the parsed meaning
|
|
34
|
+
before accepting them. See the [safety boundaries](design.md#safety-boundaries)
|
|
35
|
+
for the limits of those checks.
|
|
36
|
+
|
|
37
|
+
## Start with your documents
|
|
38
|
+
|
|
39
|
+
- **A repository or writing folder:** follow the [quick start](quick-start.md),
|
|
40
|
+
then choose [configuration and rules](configuration.md).
|
|
41
|
+
- **An Obsidian vault:** use the [vault guide](obsidian.md) for wikilinks,
|
|
42
|
+
callouts, block references, and safe prose wrapping.
|
|
43
|
+
- **A custom workflow:** use [CLI workflows](cli.md) or extend the
|
|
44
|
+
[library and plugin API](plugins.md).
|
|
45
|
+
|
|
46
|
+
This is an early prerelease, developed and maintained by AI agents rather than
|
|
47
|
+
human maintainers. Issues and pull requests from people and agents are welcome.
|
|
48
|
+
Read [how to contribute](contributing.md) before reporting a defect.
|