mdrefine 0.2.0-rc.2 → 0.2.0

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 (130) hide show
  1. package/CHANGELOG.md +577 -0
  2. package/CODE_OF_CONDUCT.md +30 -0
  3. package/CONTRIBUTING.md +189 -16
  4. package/README.md +100 -154
  5. package/SECURITY.md +15 -13
  6. package/config.schema.json +91 -0
  7. package/dist/cli/diff.d.ts +10 -0
  8. package/dist/cli/diff.js +247 -0
  9. package/dist/cli/diff.js.map +1 -0
  10. package/dist/cli/main.js +270 -150
  11. package/dist/cli/main.js.map +1 -1
  12. package/dist/config/load.js +67 -6
  13. package/dist/config/load.js.map +1 -1
  14. package/dist/config/problems.d.ts +7 -0
  15. package/dist/config/problems.js +208 -0
  16. package/dist/config/problems.js.map +1 -0
  17. package/dist/config/resolve.d.ts +42 -109
  18. package/dist/config/resolve.js +193 -52
  19. package/dist/config/resolve.js.map +1 -1
  20. package/dist/core/directives.d.ts +7 -0
  21. package/dist/core/directives.js +25 -13
  22. package/dist/core/directives.js.map +1 -1
  23. package/dist/core/engine.d.ts +42 -4
  24. package/dist/core/engine.js +518 -79
  25. package/dist/core/engine.js.map +1 -1
  26. package/dist/core/frozen.d.ts +6 -0
  27. package/dist/core/frozen.js +15 -0
  28. package/dist/core/frozen.js.map +1 -0
  29. package/dist/core/scope.d.ts +30 -0
  30. package/dist/core/scope.js +122 -0
  31. package/dist/core/scope.js.map +1 -0
  32. package/dist/core/types.d.ts +202 -23
  33. package/dist/core/types.js.map +1 -1
  34. package/dist/index.d.ts +2 -2
  35. package/dist/index.js.map +1 -1
  36. package/dist/node.d.ts +3 -0
  37. package/dist/node.js +4 -0
  38. package/dist/node.js.map +1 -0
  39. package/dist/rules/blocks.d.ts +34 -0
  40. package/dist/rules/blocks.js +550 -0
  41. package/dist/rules/blocks.js.map +1 -0
  42. package/dist/rules/content.d.ts +2 -0
  43. package/dist/rules/content.js +85 -0
  44. package/dist/rules/content.js.map +1 -0
  45. package/dist/rules/dialects.js +10 -6
  46. package/dist/rules/dialects.js.map +1 -1
  47. package/dist/rules/links.js +302 -6
  48. package/dist/rules/links.js.map +1 -1
  49. package/dist/rules/lists.d.ts +2 -0
  50. package/dist/rules/lists.js +924 -0
  51. package/dist/rules/lists.js.map +1 -0
  52. package/dist/rules/structure.js +127 -1
  53. package/dist/rules/structure.js.map +1 -1
  54. package/dist/rules/style.d.ts +8 -2
  55. package/dist/rules/style.js +1137 -165
  56. package/dist/rules/style.js.map +1 -1
  57. package/dist/syntax/block-identifier.d.ts +18 -0
  58. package/dist/syntax/block-identifier.js +24 -0
  59. package/dist/syntax/block-identifier.js.map +1 -0
  60. package/dist/syntax/obsidian.d.ts +29 -11
  61. package/dist/syntax/obsidian.js +9 -10
  62. package/dist/syntax/obsidian.js.map +1 -1
  63. package/dist/syntax/parse.d.ts +87 -3
  64. package/dist/syntax/parse.js +247 -19
  65. package/dist/syntax/parse.js.map +1 -1
  66. package/dist/workspace/access.d.ts +12 -3
  67. package/dist/workspace/access.js +33 -5
  68. package/dist/workspace/access.js.map +1 -1
  69. package/dist/workspace/discovery.d.ts +85 -0
  70. package/dist/workspace/discovery.js +155 -0
  71. package/dist/workspace/discovery.js.map +1 -0
  72. package/dist/workspace/encoding.d.ts +24 -0
  73. package/dist/workspace/encoding.js +113 -0
  74. package/dist/workspace/encoding.js.map +1 -0
  75. package/dist/workspace/files.d.ts +92 -6
  76. package/dist/workspace/files.js +715 -59
  77. package/dist/workspace/files.js.map +1 -1
  78. package/dist/workspace/git.d.ts +54 -0
  79. package/dist/workspace/git.js +198 -0
  80. package/dist/workspace/git.js.map +1 -0
  81. package/dist/workspace/index.d.ts +58 -3
  82. package/dist/workspace/index.js +136 -37
  83. package/dist/workspace/index.js.map +1 -1
  84. package/dist/workspace/selection.d.ts +2 -2
  85. package/dist/workspace/selection.js +3 -3
  86. package/dist/workspace/selection.js.map +1 -1
  87. package/dist/workspace/slug.js +8 -1
  88. package/dist/workspace/slug.js.map +1 -1
  89. package/dist/workspace/vault.js +3 -1
  90. package/dist/workspace/vault.js.map +1 -1
  91. package/docs/api.md +239 -0
  92. package/docs/benchmarks.md +140 -200
  93. package/docs/changelog.md +1 -0
  94. package/docs/cli.md +281 -89
  95. package/docs/configuration.md +193 -480
  96. package/docs/design.md +163 -284
  97. package/docs/dialects.md +169 -0
  98. package/docs/examples.md +151 -109
  99. package/docs/index.md +21 -37
  100. package/docs/integrations.md +144 -0
  101. package/docs/obsidian.md +91 -115
  102. package/docs/plugins.md +147 -212
  103. package/docs/quick-start.md +68 -74
  104. package/docs/rules.md +1237 -0
  105. package/docs/versions.md +56 -0
  106. package/examples/README.md +17 -14
  107. package/examples/automation/summarize-report.mjs +5 -2
  108. package/examples/library/lint-folder.mjs +5 -4
  109. package/examples/library/lint-workspace.mjs +32 -0
  110. package/examples/plugin/notes/release-checklist.md +2 -2
  111. package/examples/plugin/rules.mjs +17 -23
  112. package/examples/workspaces/obsidian-vault/after/mdtools.config.jsonc +2 -2
  113. package/examples/workspaces/obsidian-vault/before/mdtools.config.jsonc +2 -2
  114. package/examples/workspaces/repository-docs/after/README.md +2 -2
  115. package/examples/workspaces/repository-docs/after/mdtools.config.jsonc +0 -2
  116. package/examples/workspaces/repository-docs/before/README.md +2 -2
  117. package/examples/workspaces/repository-docs/before/docs/guide.md +7 -7
  118. package/examples/workspaces/repository-docs/before/docs/reference.md +3 -1
  119. package/examples/workspaces/repository-docs/before/mdtools.config.jsonc +0 -2
  120. package/package.json +38 -12
  121. package/docs/contributing.md +0 -218
  122. package/docs/releases/0.1.0-beta.1.md +0 -39
  123. package/docs/releases/0.1.0-beta.2.md +0 -83
  124. package/docs/releases/0.1.0-beta.3.md +0 -61
  125. package/docs/releases/0.1.0-beta.4.md +0 -50
  126. package/docs/releases/0.1.0-rc.1.md +0 -99
  127. package/docs/releases/0.2.0-rc.1.md +0 -141
  128. package/docs/releases/0.2.0-rc.2.md +0 -225
  129. package/docs/releases.md +0 -144
  130. package/docs/testing.md +0 -93
package/docs/plugins.md CHANGED
@@ -1,130 +1,11 @@
1
- # Plugins and library API
1
+ # Plugins
2
2
 
3
- The public API is exported from `mdrefine`. Follow the
4
- [installation guide](quick-start.md) to install the npm package or build a
5
- checkout as a local dependency. The API is provisional until a stable release.
3
+ A plugin adds rules, presets, and syntax to the built-in ones. It is a
4
+ JavaScript module whose default export is a plugin object. The [plugin
5
+ example](examples.md#write-a-plugin) is a complete plugin with a problem rule
6
+ and a style rule to start from.
6
7
 
7
- ## Library calls
8
-
9
- ```ts
10
- import { createWorkspace, format, lint } from "mdrefine";
11
-
12
- const files = {
13
- "note.md": "A *short* note with [a link](target.md).\n",
14
- "target.md": "# Target\n",
15
- };
16
- const options = {
17
- path: "note.md",
18
- config: { extends: ["recommended", "github"] },
19
- workspace: createWorkspace(files, { dialect: "github" }),
20
- };
21
-
22
- const diagnostics = lint(files["note.md"], options);
23
- const result = format(files["note.md"], options);
24
- // result: { output, changed, diagnostics }
25
- ```
26
-
27
- Library calls do not read or write files; the
28
- [library examples](examples.md#call-the-library) include a script that reads a
29
- folder and builds the workspace from it. `path` and workspace keys use
30
- forward-slash paths relative to the workspace root. Use `null` values for
31
- non-Markdown attachments. A Markdown value can also be a synchronous loader
32
- `() => string`; `createWorkspace` calls it only when that target needs fragment
33
- validation, then keeps the target's anchors, not its text or its parsed form.
34
- The anchors of Obsidian, of GitHub and CommonMark, and of Forgejo and Gitea
35
- differ, so the loader is called once more when a later link to that target is
36
- checked for a dialect of another of these groups; `0.2.0-rc.1` and earlier
37
- called it at most once and kept the parsed target. Callers must provide a stable
38
- source snapshot for each workspace instance. The CLI provides its own
39
- per-invocation file loaders. `WorkspaceOptions.directories` can list existing
40
- empty directories; parent directories of file entries are inferred. Resolution
41
- returns `status: "directory"` for a directory instead of `"missing"`.
42
-
43
- `WorkspaceOptions.unreadable` lists existing directories whose contents the host
44
- could not read. A link that leads into one resolves with `status: "unreadable"`
45
- instead of `"missing"`, and the result's `unreadable` field names the
46
- directories that stand in the way. In the Obsidian dialect, a search by note
47
- name that finds nothing also resolves as `"unreadable"`. One that finds a single
48
- note is `"resolved"` with `unreadable` set, because those directories may hold
49
- another note of that name. The same marks a note found beside the linking note
50
- when the vault-root path of that spelling leads into an unreadable directory,
51
- where Obsidian would look first. A rule must not rewrite such a link:
52
- `links/path` and `links/notation` leave it as written, and the engine refuses an
53
- edit by any rule that changes its destination, because only the same spelling is
54
- known to name the same note. The CLI passes the directories it
55
- [skipped](configuration.md#paths-that-cannot-be-read). A loader that throws an
56
- error whose `code` is `EACCES` or `EPERM`, as Node.js does for a file the
57
- process is not permitted to read, makes a link to a heading or block in that
58
- file resolve as `"unreadable"` too, with `target` set and `unreadable` naming
59
- the file; a link to the file without a fragment still resolves. Any other error
60
- from a loader propagates. Versions up to `0.2.0-rc.1` have neither the option
61
- nor the status, and let every loader error through.
62
-
63
- `WorkspaceOptions.vaults` lists folders below the workspace root that are
64
- Obsidian vaults of their own. An Obsidian link in a note of such a vault is
65
- resolved within it: a path is counted from the vault's folder, and a search by
66
- name looks only at the vault's files. The result's `vault` field then names the
67
- folder, also when nothing was found; `target` stays relative to the workspace
68
- root, so a rule that writes a path from the vault removes that prefix. A path
69
- that names an existing file or folder outside the vault resolves as `"missing"`
70
- with that file in `outside`. Notes outside the listed folders, and all other
71
- dialects, resolve as without the option. The CLI passes the vaults it found
72
- below the root. Versions up to `0.2.0-rc.1` have neither the option nor the
73
- fields.
74
-
75
- `WorkspaceOptions.rootInVault` is for a workspace whose root is a folder of an
76
- Obsidian vault: the root's path from the vault's folder, such as
77
- `Projects/Notes`. The rest of the vault is then out of sight, and an Obsidian
78
- link whose target could lie there resolves as `"unreadable"`, not `"missing"`. A
79
- target found by a search, or in a place that Obsidian tries after one outside
80
- the root, is `"resolved"` with `unreadable` set and so is not rewritten. In both
81
- cases the `unreadable` list holds an entry that starts with `..`, the way from
82
- the root to the vault's folder. A resolved link also carries `rooted`, the
83
- target's path from the folder of its vault, whenever that differs from `target`;
84
- this applies to a vault listed in `vaults` as well. The CLI passes the option
85
- when it finds the vault above the root. Versions up to `0.2.0-rc.1` do not have
86
- it.
87
-
88
- Without a workspace, local target checks and path rewriting are unavailable.
89
- Obsidian reflow also requires an explicit `workspace.strictLineBreaks: true`;
90
- callers are responsible for verifying that renderer setting.
91
-
92
- A document is read in the dialect its configuration names: through `dialect`, a
93
- preset, or a matching override. When none of them does, including when `config`
94
- is omitted, `lint`, `format`, and `resolveConfig` assume `github`, and
95
- `createWorkspace` parses link targets as `github` when
96
- `WorkspaceOptions.dialect` is omitted. Version 0.2.0-rc.1 and earlier assumed
97
- `commonmark` in both places; name it to keep that reading. Give the workspace
98
- the dialect of the documents that link into it.
99
-
100
- A host that knows more than the configuration says can change what is assumed,
101
- without overriding a dialect that the configuration does name. Pass
102
- `defaultDialect` in the options of `lint` and `format`, or as the fourth
103
- argument of `resolveConfig(config, path, plugins, defaultDialect)`; it accepts a
104
- dialect or an alias. The library reads no files and so recognizes nothing by
105
- itself: the CLI passes `obsidian` for a document that has a `.obsidian` folder
106
- in its own folder or in one above it, and gives the workspace that vault's
107
- `strictLineBreaks` setting. A host for vaults should do the same.
108
-
109
- ```ts
110
- // The host found `.obsidian/` at the root of the folder it read `files` from.
111
- const defaultDialect = "obsidian";
112
- const { dialect } = resolveConfig(config, "Note.md", [], defaultDialect);
113
- const workspace = createWorkspace(files, { dialect, strictLineBreaks });
114
- const result = format(files["Note.md"], { path: "Note.md", config, workspace, defaultDialect });
115
- ```
116
-
117
- `parse`, `range`, `textContent`, `resolveConfig`, `validateConfig`,
118
- `configSchema`, `presets`, `dialects`, `dialectAliases`, `canonicalDialect`,
119
- `builtInRules`, `ruleRegistry`, `applyEdits`, and `semanticFingerprint` are also
120
- exported. `range(node)` returns `[start, end]` UTF-16 offsets into the original
121
- source. `configSchema` is the configuration's JSON Schema, and
122
- `validateConfig(value)` throws when a value does not match it; rule-specific
123
- schemas live on rule objects. `canonicalDialect(name)` maps an alias such as
124
- `codeberg` to the dialect that rules and the workspace see (`forgejo`);
125
- `document.dialect` is always canonical.
126
-
127
- ## Rule plugins
8
+ ## A plugin
128
9
 
129
10
  Create `rules.mjs` beside the configuration:
130
11
 
@@ -151,7 +32,7 @@ export default {
151
32
  };
152
33
  ```
153
34
 
154
- Load it with:
35
+ and load it from the configuration:
155
36
 
156
37
  ```json
157
38
  {
@@ -160,68 +41,92 @@ Load it with:
160
41
  }
161
42
  ```
162
43
 
163
- Each `plugins` entry names a module whose default export is a plugin object. An
164
- entry that starts with `.` or is an absolute path names a file, relative to the
165
- configuration file's directory. Any other entry is resolved the way Node.js
166
- resolves an `import` written in the configuration file, with the conditions that
167
- Node.js itself applies: its defaults, those added with `--conditions` or `-C`,
168
- and without `node-addons` under `--no-addons`, whether the option is on
169
- Node.js's command line or in `NODE_OPTIONS`. Options set in any other way, such
170
- as a Node.js configuration file, are not seen. `0.2.0-rc.1` and earlier applied
171
- no added conditions. Usually such an entry names an installed package,
172
- optionally with a subpath: `sample-plugin`, `@scope/sample-plugin`, or
173
- `sample-plugin/rules`. The package's entry can be an `import` or `default`
174
- condition of its `exports`, or its `main` file. When an import finds no entry,
175
- the lookup falls back to Node.js's CommonJS rules, so a package that declares
176
- only a `require` condition loads too. A package with both an `import` and a
177
- `require` entry is loaded through its `import` entry. `0.2.0-rc.1` and earlier
178
- used only the CommonJS rules, so they could not load a package that declares
179
- only an `import` condition, and loaded a package with both entries through
180
- `require`.
181
-
182
- A plugin that cannot be found or loaded stops the command with exit status `2`
183
- and a message of the form
184
- `Cannot load plugin "sample-plugin" named in FILE: REASON`. `FILE` is the
185
- configuration file and `REASON` is Node.js's own, such as
186
- `Cannot find package 'sample-plugin' imported from FILE`. For an entry that
187
- names a file, the reason is `no file at PATH` when nothing is there or the path
188
- is a folder. A module whose default export is not an object with a string `name`
189
- is reported as `Invalid plugin "sample-plugin" named in FILE`. `0.2.0-rc.1` and
190
- earlier printed Node.js's reason alone.
191
-
192
- Library callers pass plugin objects directly through `plugins`; a string list in
193
- a library configuration does not perform module loading. Plugin names must be
194
- unique lowercase identifiers beginning with a letter, using letters, digits, and
195
- hyphens. Rule IDs are namespaced as `plugin-name/rule-name`; collisions are
196
- rejected.
197
-
198
- `check` receives the parsed `document`, configured `options`, and optional
199
- `workspace`. Return a list of findings containing `start`, optional `end`,
200
- `message`, and optional `edit`. A rule may declare a JSON Schema in `schema` to
201
- validate its options. Rules are synchronous and should not mutate the document,
202
- perform I/O, or retain invocation-specific state.
203
-
204
- Style rules use `kind: "style"` and can return edits of the form
205
- `{ start, end, text }`. Their optional `phase` is `inline` (default), `block`,
206
- or `document`. Problem rules use `kind: "problem"`; their edits are not applied
207
- by `format`. Formatting runs phases repeatedly until stable, with a limit of
208
- eight passes. Conflicting edits, oscillation, or a semantic change reject the
209
- entire document's output.
210
-
211
- The [plugin example](examples.md#write-a-plugin) is a complete plugin with a
212
- problem rule and a style rule to start from. Verify a style rule by formatting
213
- its output again: a correct rule leaves it unchanged. Plugins execute within the
214
- host process and are not sandboxed. Semantic checking constrains the returned
215
- text, not arbitrary JavaScript behavior.
44
+ `name` is the plugin's prefix: a lowercase identifier that starts with a letter
45
+ and holds letters, digits, and hyphens, unique among the loaded plugins. Its
46
+ rules are named `name/rule` and its presets `name/preset`; a rule ID that
47
+ already exists is an error. The name `engine` is reserved for the engine's own
48
+ diagnostics, such as `engine/rule-error`, and a plugin with that name is
49
+ rejected. A plugin can also provide `syntax`, described
50
+ [below](#syntax-extensions). The library takes plugin objects through its
51
+ `plugins` option; a list of strings in a library configuration loads nothing.
52
+
53
+ ## Loading
54
+
55
+ The configuration's `plugins` entries are loaded when the configuration is, and
56
+ run with the caller's permissions; plugins are not sandboxed. An entry that
57
+ starts with `.` or is an absolute path names a file, relative to the
58
+ configuration file's directory. Any other entry is resolved as Node.js resolves
59
+ an `import` written in the configuration file, such as `sample-plugin`,
60
+ `@scope/sample-plugin`, or `sample-plugin/rules`. The package's entry can be an
61
+ `import` or `default` condition of its `exports`, or its `main` file. When the
62
+ import finds no entry, Node.js's CommonJS rules are tried, so a package that
63
+ declares only a `require` condition loads as well; a package with both is loaded
64
+ through `import`. The lookup applies the conditions that Node.js applies: its
65
+ defaults, those added with `--conditions` or `-C`, and without `node-addons`
66
+ under `--no-addons`, from Node.js's command line or `NODE_OPTIONS`. Options set
67
+ in other ways, such as a Node.js configuration file, are not seen.
68
+
69
+ A plugin that cannot be loaded stops the command with exit code `2` and the
70
+ message `Cannot load plugin "NAME" named in FILE: REASON`, where `REASON` is
71
+ Node.js's own, such as `Cannot find package 'sample-plugin' imported from FILE`,
72
+ or `no file at PATH` for a file entry. A module whose default export is not an
73
+ object with a string `name`, or whose plugin is named `engine`, gives
74
+ `Invalid plugin "NAME" named in FILE`.
75
+
76
+ ## Rules
77
+
78
+ A rule is an object with a `description`, a `kind`, an optional `schema` and
79
+ `phase`, and a `check` function.
80
+
81
+ - `kind` is `"problem"` for a rule that only reports, or `"style"` for one that
82
+ can propose edits, which `format` applies. `format` ignores the edits of
83
+ problem rules.
84
+ - `schema` is a JSON Schema for the rule's options. The options a configuration
85
+ gives are checked against it, and a rule without a schema accepts any options;
86
+ `false` means that the rule takes none. Each schema is compiled once, for all
87
+ documents. Options are checked synchronously, so an asynchronous schema
88
+ (`$async`) is refused.
89
+ - `check({ document, options, workspace })` receives the parsed `document`
90
+ (`source`, `path`, `dialect`, and `tree`, an
91
+ [mdast](https://github.com/syntax-tree/mdast) tree with source positions), the
92
+ configured `options`, and the `workspace`, which is undefined when the host
93
+ gave none. It returns a list of findings.
94
+
95
+ A finding has a `message`, a `start` offset, an optional `end`, and, from a
96
+ style rule, an optional `edit` of the form `{ start, end, text }`. Offsets are
97
+ integers that count UTF-16 code units in `document.source`, from 0 to its
98
+ length, with `end` not before `start`; `range(node)` from the library gives a
99
+ node's offsets. `check` must be synchronous, must not change the document or
100
+ perform I/O, and must not keep state between calls.
101
+
102
+ A rule that throws, or returns anything other than a list of such findings, is
103
+ skipped for that document and reported with an `engine/rule-error` error that
104
+ names it and the reason, as in
105
+ `Rule local/example failed (TypeError: …); the rule was skipped for this document.`
106
+ The other rules and documents are still processed, and the CLI's exit code is
107
+ `2`. An error that the workspace throws, such as a file that cannot be read, is
108
+ not the rule's: `lint` and `format` throw it, and the CLI stops. A plugin rule
109
+ without a `check` function, a `kind` of `"style"` or `"problem"`, and a string
110
+ `description`, or with a `phase` or `schema` of another kind, stops the command
111
+ with an error that names it, even while the rule is off.
112
+
113
+ A style rule's `phase` is `"inline"` (the default), `"block"`, or `"document"`.
114
+ `format` runs the phases in that order and repeats them until nothing changes,
115
+ for at most eight passes. After each phase that changes the document, it parses
116
+ the result again and compares its meaning with the original's; the [safety
117
+ model](design.md#safety-model) describes what happens to edits that overlap or
118
+ would change the meaning. The comparison covers the syntax tree, not every
119
+ application's rendering, and it constrains the text a rule returns, not what its
120
+ code does.
216
121
 
217
122
  ## Resolve links in a rule
218
123
 
219
124
  A rule asks the workspace where a link leads with
220
- `workspace.resolve(source, destination, dialect)`: `source` is the path of the
221
- linking document (`document.path`), `destination` the link's destination as
222
- written, and `dialect` the document's dialect. The built-in link rules use this
223
- call and nothing else, so a plugin sees links exactly as they do. `workspace` is
224
- undefined when the host gave none; a rule should then report nothing.
125
+ `workspace.resolve(source, destination, dialect, wiki)`: `source` is the linking
126
+ document's path (`document.path`), `destination` the destination as written,
127
+ `dialect` the document's dialect, and `wiki` true for a wikilink, which never
128
+ resolves to a directory. The built-in link rules use the same call, so a plugin
129
+ sees links as they do. Without a workspace, a rule should report nothing.
225
130
 
226
131
  ```js
227
132
  export default {
@@ -259,7 +164,7 @@ The result's `status` is one of:
259
164
  | `resolved` | One file matches. `target` is its path from the workspace root. |
260
165
  | `missing` | No file matches. |
261
166
  | `ambiguous` | Several files match and nothing decides between them, such as two notes whose names differ only by case. |
262
- | `directory` | The destination names a folder. |
167
+ | `directory` | The destination names a directory. |
263
168
  | `external` | The destination is a URL with a scheme, or starts with `//`. It is not checked. |
264
169
  | `unavailable` | The destination cannot be judged from files: it starts with `/` or holds a `?`, outside the Obsidian dialect. |
265
170
  | `unreadable` | The target could not be checked, because something that may hold it could not be read. |
@@ -267,33 +172,63 @@ The result's `status` is one of:
267
172
  The other fields are present when they apply:
268
173
 
269
174
  - `target`: the file found, with status `resolved`, and with `unreadable` when
270
- the file is there but its content could not be read.
271
- - `fragment`: the part after `#`, decoded, or an empty string.
272
- - `fragmentExists`: with status `resolved`, whether the target has the heading,
273
- block, or anchor that the fragment names, by the rules of `dialect`. It is
274
- true when there is no fragment.
275
- - `unreadable`: what stood in the way; see the paragraphs on
276
- `WorkspaceOptions.unreadable` and `rootInVault` above. When it is set on a
277
- `resolved` link, the match is uncertain and a rule must not rewrite the link.
278
- - `vault`, `rooted`, and `outside`: for Obsidian links in a vault that is not
279
- the workspace root; see `WorkspaceOptions.vaults` above.
280
-
281
- A style rule that rewrites a destination should resolve its replacement as well
282
- and propose the edit only when both lead to the same `target`. The engine checks
283
- this again and refuses an edit that changes where a link leads.
175
+ the file exists but its content could not be read.
176
+ - `fragment`: the part after `#`, decoded, or an empty string; `fragmentExists`:
177
+ with status `resolved`, whether the target has that heading, block, or anchor
178
+ by the rules of `dialect`, and `true` without a fragment and for a target that
179
+ is not Markdown, whose fragments are its own.
180
+ - `unreadable`: the directories or the file that stood in the way. An entry that
181
+ starts with `..` stands for the part of a vault outside the workspace root. In
182
+ the Obsidian dialect, a search by note name that finds nothing readable
183
+ resolves as `unreadable`, and one that finds a single note resolves with
184
+ `unreadable` set, because the unreadable directories may hold another note of
185
+ that name. **A rule must not rewrite a `resolved` link whose `unreadable` is
186
+ set**, and the engine refuses an edit that changes such a link's destination.
187
+ - `vault`: the vault's directory, when an Obsidian link was resolved in a vault
188
+ below the workspace root. `rooted`: the target's vault-relative path, when it
189
+ differs from `target`. `outside`: with status `missing`, an existing file that
190
+ the path names outside the vault. `leavesRoot`: with status `unreadable`, the
191
+ symbolic link or junction through which the path leads outside the workspace
192
+ root.
193
+
194
+ A style rule that rewrites a destination should resolve the new destination as
195
+ well, and propose the edit only when both lead to the same `target`. The engine
196
+ checks this again and refuses an edit that changes where a link leads.
284
197
 
285
198
  ## Syntax extensions
286
199
 
287
- An advanced plugin can provide `syntax: { micromark, mdast }`, containing
288
- compatible tokenizer and AST extensions from the micromark/mdast ecosystem.
289
- Extend the TypeScript mdast node maps for custom node types. Preserve source
290
- positions and expose meaningful semantic fields: the engine compares node fields
291
- other than `position` and `data` when checking formatting safety.
292
-
293
- This is not a general remark-transform adapter. Existing syntax extensions can
294
- be reused, but a remark plugin that mutates a tree must be adapted to the
295
- rule/edit contract. There is no custom link resolver loader in CLI plugins yet;
296
- library users can supply an implementation of the `Workspace` interface. Unknown
297
- syntax is not automatically safe merely because a parser treats it as plain
298
- text; use a supported dialect, an extension, an ignore pattern, or a suppression
299
- for application-specific constructs.
200
+ A plugin can provide `syntax: { micromark, mdast }`, a tokenizer extension and a
201
+ tree extension from the [micromark](https://github.com/micromark/micromark) and
202
+ mdast ecosystem. In TypeScript, extend mdast's node maps for the new node types.
203
+ Keep source positions on the nodes, and expose the meaning of a node in its
204
+ fields: the semantic comparison looks at every field except `position` and
205
+ `data`. Syntax that a parser reads as plain text is not safe to format merely
206
+ for that; test the extension's meaning and its source preservation.
207
+
208
+ This is not a remark plugin adapter. Existing micromark extensions can be
209
+ reused, but a remark plugin that changes the tree must be rewritten as a rule
210
+ that proposes edits. A plugin cannot replace link resolution; a library host can
211
+ pass its own implementation of the `Workspace` interface.
212
+
213
+ ## Test a rule
214
+
215
+ Call the library with the plugin object, and check what it reports and what
216
+ formatting produces:
217
+
218
+ ```js
219
+ import { format, lint } from "mdrefine";
220
+ import plugin from "./rules.mjs";
221
+
222
+ const options = {
223
+ plugins: [plugin],
224
+ config: { extends: [], rules: { "local/initial-heading": "error" } },
225
+ };
226
+ console.assert(lint("Text.\n", options).length === 1);
227
+ console.assert(lint("# Title\n", options).length === 0);
228
+ const once = format("Text.\n", options).output;
229
+ console.assert(format(once, options).output === once);
230
+ ```
231
+
232
+ A correct style rule leaves its own output unchanged when it runs again, and
233
+ changes nothing when it is turned off. Add a case for every construct the rule
234
+ could misread, such as code, HTML, and each dialect it supports.
@@ -1,97 +1,85 @@
1
- # Quick start
1
+ # Getting started
2
2
 
3
- Get the CLI running, select a profile, and preview a formatting pass before
4
- writing changes.
3
+ ## Install
5
4
 
6
- ## Install the release candidate
7
-
8
- The CLI requires Node.js 22.12 or newer. Install the package in your document
9
- workspace:
5
+ mdrefine needs Node.js 22.12 or newer. Install the package in the project that
6
+ holds your documents:
10
7
 
11
8
  ```sh
12
- npm install --save-dev --save-exact mdrefine@0.2.0-rc.2
9
+ npm install --save-dev --save-exact mdrefine@0.2.0
13
10
  npx --no-install mdtools --help
14
11
  ```
15
12
 
16
- The npm package is `mdrefine`; the executable is `mdtools`. Prereleases use the
17
- npm channel named after their kind, `rc` or `beta`. See
18
- [versions and availability](releases.md#versions-and-availability) for registry
19
- checks and release announcements.
13
+ The package is `mdrefine`; its command is `mdtools`. `npx` runs the command from
14
+ the project, and `--no-install` stops it from downloading a package when the
15
+ project has none; the examples below leave it out. The exact version lets you
16
+ choose when a release that formats differently reaches your documents; see
17
+ [versions](versions.md).
18
+
19
+ ## Lint
20
+
21
+ Run the commands from the root of your documents, or point them there with
22
+ `--root`:
20
23
 
21
- Run the examples below from the workspace where you installed the package.
22
- `--no-install` prevents fetching a different package if the local executable is
23
- missing. Replace `/path/to/documents` with your document folder; quote paths
24
- containing spaces.
24
+ ```sh
25
+ npx mdtools lint
26
+ ```
25
27
 
26
- ## Install from the repository
28
+ `lint` reports what the enabled rules find and never changes a file. Without
29
+ paths it checks every document of the workspace: each file ending in `.md` below
30
+ the configuration file's directory, or else below the Git repository or vault
31
+ you run it in; see [which files are
32
+ processed](cli.md#which-files-are-processed). Add `--json` for a report that
33
+ scripts can read, or `--max-warnings 0` to fail on warnings too.
27
34
 
28
- To build a checkout instead, use Node.js 22.22.2+ or 24.15.0+ and npm 10 or
29
- newer; Node.js 24 LTS is preferred.
35
+ ## Preview and write
30
36
 
31
37
  ```sh
32
- git clone https://github.com/viell-dev/slop-markdown-tools.git
33
- cd slop-markdown-tools
34
- npm ci
35
- npm run build
36
- node dist/cli/main.js --help
38
+ npx mdtools format --diff
39
+ npx mdtools format --write
40
+ npx mdtools format --check
37
41
  ```
38
42
 
39
- From that checkout, use `node dist/cli/main.js` in place of
40
- `npx --no-install mdtools` in the examples below.
43
+ `--diff` prints what formatting would change, and is also what `format` does
44
+ without an option. `--write` writes the changes. `--check` writes nothing, names
45
+ each document that would change, and then exits with code `1`, which is the
46
+ check to run in CI. `format` changes only what its rules target, and refuses any
47
+ change that would alter what a document means. It can still report problems that
48
+ need a person, such as a broken link.
41
49
 
42
- ## Choose a configuration
50
+ ## Choose a preset and a dialect
43
51
 
44
- Create `mdtools.config.jsonc` in your document folder:
52
+ Create `mdtools.config.jsonc` at the root of your documents:
45
53
 
46
54
  ```jsonc
47
55
  {
56
+ "$schema": "./node_modules/mdrefine/config.schema.json",
48
57
  "extends": ["recommended", "github"],
49
- "ignore": ["vendor/**", "**/*.external.md"],
58
+ "ignore": ["vendor/**"],
50
59
  }
51
60
  ```
52
61
 
53
- The recommended preset wraps prose at **80 columns**, uses `_` for emphasis and
54
- `**` for strong text, ensures a final newline, and validates local links. The
55
- GitHub preset adds table, task marker, and alert marker rules. The configuration
56
- controls both linting and formatting.
62
+ The `$schema` line lets your editor complete and check the settings; see [JSON
63
+ Schema](configuration.md#json-schema).
57
64
 
58
- For Codeberg or another Forgejo instance, replace `"github"` with `"forgejo"`
59
- (or its alias `"codeberg"`); for a Gitea instance, use `"gitea"`. For a vault,
60
- replace it with `"obsidian"` and follow the [Obsidian setup](obsidian.md). For
61
- plain CommonMark, use only `"recommended"` and add `"dialect": "commonmark"`.
65
+ `recommended` wraps prose at 80 columns; writes emphasis as `_text_`, strong
66
+ text as `**text**`, list items as `- item` and `1. item`, headings with `#`,
67
+ code fences with backticks, and thematic breaks as `---`; aligns tables; removes
68
+ trailing spaces and repeated blank lines; ends every file with a line break; and
69
+ checks local links. It also warns about [problems](rules.md) that show on the
70
+ rendered page, such as a reference without a definition, a skipped heading
71
+ level, or an image without alternative text. The second
72
+ [preset](configuration.md#presets) names where your documents are rendered and
73
+ so their [dialect](dialects.md): replace `github` with `forgejo` (or
74
+ `codeberg`), `gitea`, or `obsidian`. For plain CommonMark, keep only
75
+ `recommended` and add `"dialect": "commonmark"`.
62
76
 
63
- Without a configuration file, the tool applies the recommended preset and
64
- assumes the GitHub dialect, so that tables and footnotes are left as they are,
65
- or the Obsidian dialect for documents inside a vault, so that line breaks and
66
- wikilinks are. A configuration that names no dialect is read the same way.
67
- Version 0.2.0-rc.1 and earlier assumed CommonMark, in which formatting reflows a
68
- table and the lines of a note as an ordinary paragraph, so with those versions
69
- create the configuration first. See [dialects](configuration.md#dialects).
70
-
71
- ## Lint without writing
72
-
73
- ```sh
74
- npx --no-install mdtools lint --root /path/to/documents
75
- ```
77
+ Without a configuration file, the `recommended` rules apply, and documents are
78
+ read as GitHub Markdown, or as Obsidian notes inside a vault. A configuration
79
+ file is still worth creating: it enables the rules for your renderer and makes
80
+ the dialect explicit.
76
81
 
77
- Lint reports diagnostics and never changes files. Add `--json` for structured
78
- output, or `--max-warnings 0` to make warnings fail an automated check.
79
-
80
- ## Preview and apply formatting
81
-
82
- ```sh
83
- npx --no-install mdtools format --root /path/to/documents --diff
84
- npx --no-install mdtools format --root /path/to/documents --write
85
- npx --no-install mdtools format --root /path/to/documents --check
86
- ```
87
-
88
- Review the diff before running `--write`. The final check exits with code `1` if
89
- formatting changes remain. Formatting may still report lint problems that
90
- require a manual correction; inspect diagnostics as well as the exit code.
91
-
92
- ## Adjust the rules
93
-
94
- Override a preset rule by adding a `rules` object:
82
+ Change a rule under `rules`:
95
83
 
96
84
  ```jsonc
97
85
  {
@@ -103,10 +91,16 @@ Override a preset rule by adding a `rules` object:
103
91
  }
104
92
  ```
105
93
 
106
- This wraps at 100 columns and preserves existing emphasis delimiters. Use
107
- `"extends": []` to start with no enabled rules.
94
+ This wraps at 100 columns and leaves emphasis markers as they are.
95
+
96
+ ## Next steps
108
97
 
109
- Continue with [CLI workflows](cli.md) for automation and file selection, or
110
- [configuration and rules](configuration.md) for presets, links, overrides, and
111
- suppression comments. The [examples](examples.md) show complete workspaces
112
- before and after formatting.
98
+ - [Rules](rules.md): what each rule does, with examples.
99
+ - [Configuration](configuration.md): overrides, ignore patterns, plugins, and
100
+ suppression comments.
101
+ - [Command line](cli.md): file selection, stdin, the JSON report, and exit
102
+ codes.
103
+ - [Integrations](integrations.md): CI, Git hooks, editors, and moving from
104
+ Prettier or markdownlint.
105
+ - [Obsidian vaults](obsidian.md) and [examples](examples.md) of documents before
106
+ and after formatting.