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.
Files changed (59) hide show
  1. package/CONTRIBUTING.md +10 -0
  2. package/LICENSE +21 -0
  3. package/README.md +133 -0
  4. package/SECURITY.md +16 -0
  5. package/dist/cli/main.d.ts +2 -0
  6. package/dist/cli/main.js +158 -0
  7. package/dist/cli/main.js.map +1 -0
  8. package/dist/config/load.d.ts +9 -0
  9. package/dist/config/load.js +75 -0
  10. package/dist/config/load.js.map +1 -0
  11. package/dist/config/resolve.d.ts +93 -0
  12. package/dist/config/resolve.js +110 -0
  13. package/dist/config/resolve.js.map +1 -0
  14. package/dist/core/directives.d.ts +3 -0
  15. package/dist/core/directives.js +48 -0
  16. package/dist/core/directives.js.map +1 -0
  17. package/dist/core/engine.d.ts +8 -0
  18. package/dist/core/engine.js +192 -0
  19. package/dist/core/engine.js.map +1 -0
  20. package/dist/core/types.d.ts +92 -0
  21. package/dist/core/types.js +2 -0
  22. package/dist/core/types.js.map +1 -0
  23. package/dist/index.d.ts +7 -0
  24. package/dist/index.js +5 -0
  25. package/dist/index.js.map +1 -0
  26. package/dist/rules/dialects.d.ts +2 -0
  27. package/dist/rules/dialects.js +105 -0
  28. package/dist/rules/dialects.js.map +1 -0
  29. package/dist/rules/links.d.ts +2 -0
  30. package/dist/rules/links.js +238 -0
  31. package/dist/rules/links.js.map +1 -0
  32. package/dist/rules/style.d.ts +12 -0
  33. package/dist/rules/style.js +244 -0
  34. package/dist/rules/style.js.map +1 -0
  35. package/dist/syntax/obsidian.d.ts +33 -0
  36. package/dist/syntax/obsidian.js +126 -0
  37. package/dist/syntax/obsidian.js.map +1 -0
  38. package/dist/syntax/parse.d.ts +5 -0
  39. package/dist/syntax/parse.js +45 -0
  40. package/dist/syntax/parse.js.map +1 -0
  41. package/dist/workspace/files.d.ts +8 -0
  42. package/dist/workspace/files.js +98 -0
  43. package/dist/workspace/files.js.map +1 -0
  44. package/dist/workspace/index.d.ts +10 -0
  45. package/dist/workspace/index.js +100 -0
  46. package/dist/workspace/index.js.map +1 -0
  47. package/docs/cli.md +71 -0
  48. package/docs/configuration.md +171 -0
  49. package/docs/contributing.md +103 -0
  50. package/docs/design.md +79 -0
  51. package/docs/index.md +48 -0
  52. package/docs/obsidian.md +69 -0
  53. package/docs/plugins.md +114 -0
  54. package/docs/quick-start.md +101 -0
  55. package/docs/releases/0.1.0-beta.1.md +39 -0
  56. package/docs/releases.md +90 -0
  57. package/examples/github.jsonc +7 -0
  58. package/examples/obsidian.jsonc +8 -0
  59. 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.