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/cli.md CHANGED
@@ -1,116 +1,308 @@
1
- # CLI workflows
1
+ # Command line
2
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.
3
+ The command is `mdtools`; with the package installed in a project, run it as
4
+ `npx mdtools`.
5
5
 
6
- ## Select files
6
+ | Command | What it does |
7
+ | ----------------------- | -------------------------------------------------------------------------------------------------------------- |
8
+ | `lint [paths…]` | Reports the findings of the enabled rules. Never writes. |
9
+ | `format [paths…]` | Formats documents: prints a diff (the default), checks with `--check`, or writes with `--write`. |
10
+ | `config explain <file>` | Prints the configuration in effect for one document; see [configuration](configuration.md#inspect-the-result). |
11
+ | `rules` | Lists the built-in rules and those of the configuration's plugins. |
12
+ | `help [command]` | Prints help. `--help` works for every command, and `--version` prints the version. |
7
13
 
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:
14
+ Help and the version are printed on stdout, with exit code `0`. Without a
15
+ command, `mdtools` prints the help on stderr and exits with code `2`, as it does
16
+ for every mistake on the command line, such as an unknown option.
11
17
 
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
- ```
18
+ `rules` prints one line per rule: its ID, kind, and description, separated by
19
+ tabs. It looks for the configuration from the current directory, as `lint` does,
20
+ or reads the one that `--config` names. With `--json` it prints
21
+ `{ "rules": [...] }`, each rule an object with its `id`, `kind`, and
22
+ `description`.
23
+
24
+ ## Options
25
+
26
+ `lint` and `format` accept these options; the last three are for `format` only
27
+ and exclude one another.
16
28
 
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).
29
+ | Option | Meaning |
30
+ | ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
31
+ | `--root <directory>` | The workspace root. Without it, the configuration file's directory; else the Git repository or vault where the search for one stopped; else the current directory. |
32
+ | `--config <file>` | Use this configuration file instead of searching for one. |
33
+ | `--dialect <name>` | Read documents in this [dialect](dialects.md), replacing the configuration's own `dialect`. |
34
+ | `--exclude <path>` | Leave out a file or a directory. Repeat it for more. |
35
+ | `--json` | Print one [JSON report](#json-report) on stdout instead of text. |
36
+ | `--quiet` | Report errors only. Warnings are left out of the output, the JSON report, and the counts, `--max-warnings` included. |
37
+ | `--no-warn-ignored` | Leave out the warning for a named file or directory that is [ignored](#which-files-are-processed). |
38
+ | `--stdin-filepath <file>` | The path of the document read from [stdin](#read-from-stdin). |
39
+ | `--max-warnings <count>` | Exit with code `1` when there are more warnings than this. |
40
+ | `--check` | Exit with code `1`, naming the documents, when any would change. Writes nothing. |
41
+ | `--diff` | Print the changes as a unified diff. The default for files. |
42
+ | `--write` | Write the changes. |
21
43
 
22
- A directory that the tool is not permitted to read is skipped with a warning
23
- that names it, and the rest of the workspace is processed. Naming such a
24
- directory as an input is an error. See
25
- [paths that cannot be read](configuration.md#paths-that-cannot-be-read).
26
- Versions up to `0.2.0-rc.1` stopped every command instead.
44
+ ## Which files are processed
27
45
 
28
- Use a caller or Git hook to choose changed or staged paths and pass them as
29
- explicit file arguments. The CLI processes working-tree contents, not the Git
30
- index. Pass paths as separate arguments (after `--` when they could begin with
31
- `-`), retain `--root` for workspace-wide link resolution, and skip the
32
- invocation when the selection is empty: no file arguments means the whole
33
- workspace.
46
+ Without paths, a command processes every document of the workspace. Paths,
47
+ absolute or relative to the current directory, narrow the selection to those
48
+ files and directories; the rest of the workspace still counts for checking
49
+ links.
50
+
51
+ - Files whose names end in `.md` or `.markdown`, in any case, are documents. A
52
+ file named on the command line is read as Markdown whatever its name. Other
53
+ files can still be link targets.
54
+ - Files that Git ignores or that match the configuration's `ignore` patterns are
55
+ not processed, and nested Git repositories, symbolic links, and the
56
+ directories `.git`, `.obsidian`, `node_modules`, `dist`, and `coverage` are
57
+ not visited; see [ignored
58
+ files](configuration.md#ignored-files-and-link-targets). A symbolic link named
59
+ on the command line is an error. A file or directory named on the command line
60
+ that these leave out is skipped with a warning that says why, such as
61
+ `Skipped ignored input: generated/x.md (in generated, which "generated/" in .gitignore matches)`,
62
+ and the exit code does not change. `--no-warn-ignored` leaves this warning
63
+ out, so that a hook or script can pass every changed file without repeating
64
+ the ignore rules; the JSON report still lists the input in `skipped`, and the
65
+ other warnings, such as the one for a workspace root that Git ignores, still
66
+ print.
67
+ - Git's ignore rules apply as in Git. A path is checked against each
68
+ `.gitignore` from the deepest up to the repository's top level, including
69
+ those above the workspace root, then the repository's `.git/info/exclude`,
70
+ then the global excludes file (`core.excludesFile`, by default
71
+ `~/.config/git/ignore`), and the first with a pattern that matches decides. So
72
+ a negated pattern includes a file again, but nothing below an excluded
73
+ directory can be included, and nothing in a workspace root that Git ignores is
74
+ processed, except files that Git tracks, which it never ignores. Inside a
75
+ nested repository only its own rules and settings apply, and outside a
76
+ repository only the workspace's `.gitignore` files. The tool asks Git, in each
77
+ repository, which global excludes file it reads, whether it ignores letter
78
+ case (`core.ignorecase`, which Git turns on when it creates a repository on a
79
+ file system that ignores case, as Windows and macOS do by default), and which
80
+ files it tracks. Without Git installed, that file is not read, tracked files
81
+ are ignored like the others, and letter case is ignored on Windows and macOS
82
+ and not elsewhere. The configuration's `ignore` and `overrides` patterns match
83
+ case exactly on every system, so that a shared configuration applies to the
84
+ same files everywhere.
85
+ - A path that does not exist and contains `*`, `?`, `[`, or `{` is a pattern,
86
+ such as `"docs/**/*.md"`, which the CLI expands itself, so it works quoted in
87
+ PowerShell, cmd, and npm scripts. It is relative to the current directory,
88
+ separates directories with `/` on every system, where `\` escapes the next
89
+ character, matches names that start with a dot, and ignores case on Windows
90
+ and macOS. It selects the documents that it matches; one that matches none is
91
+ an error.
92
+ - `--exclude` takes literal paths, not patterns, relative to the current
93
+ directory or absolute, inside the workspace. It leaves out a file, or a
94
+ directory with everything in it, which still count as link targets. A path
95
+ that does not exist excludes nothing; one that the tool may not examine is an
96
+ error. It cannot be combined with stdin.
97
+
98
+ To check only the files that changed, pass them as arguments; see [Git
99
+ hooks](integrations.md#git-hooks).
100
+
101
+ ## Read from stdin
102
+
103
+ With `-` as its only path, a command reads one document from stdin:
34
104
 
35
105
  ```sh
36
- node dist/cli/main.js lint --exclude generated
37
- node dist/cli/main.js format --exclude notes/archive.md --exclude vendor --diff
106
+ npx mdtools format - --stdin-filepath notes/example.md < input.md > output.md
38
107
  ```
39
108
 
40
- Repeat `--exclude <path>` to omit exact files or directory subtrees from any
41
- file selection. Exclusions are literal paths relative to the current directory
42
- (or absolute paths), must be inside the workspace, and do not remove link
43
- targets from the index. They cannot be combined with stdin. Config ignores and
44
- repository boundaries still apply to all selections.
109
+ `--stdin-filepath` gives the document's path, which decides its configuration,
110
+ dialect, and links. It must lie inside the workspace; without it the path is
111
+ `stdin.md` at the workspace root. Ignore rules apply to the path that
112
+ `--stdin-filepath` gives as to a [named file](#which-files-are-processed), also
113
+ where no file or folder of it exists yet: for an ignored path, `lint -` reports
114
+ nothing and `format -` prints the document unchanged, both with the
115
+ `Skipped ignored input` warning, which `--no-warn-ignored` leaves out. A nested
116
+ repository is no such rule: a path in one is read as that repository's document.
117
+ `format -` prints the formatted document on stdout, or the diff with `--diff`,
118
+ or nothing with `--check`; it cannot be combined with `--write`. `lint -` prints
119
+ its findings. Neither prints a summary line. Stdin must be [UTF-8
120
+ text](#files-that-cannot-be-read-or-written), with or without a byte order mark;
121
+ anything else stops the command with exit code `2` and a message such as
122
+ `mdtools: stdin is not UTF-8 text (it starts with a UTF-16LE byte order mark)`.
45
123
 
46
- ## Use an explicit configuration
124
+ The redirection above, in a POSIX shell or in `cmd`, passes the bytes unchanged.
125
+ PowerShell has no `<` operator, and text that it pipes into a program gains a
126
+ line break at its end, or CRLF line endings when piped line by line; Windows
127
+ PowerShell 5.1 also replaces characters outside ASCII and writes `>` output as
128
+ UTF-16, which the tool refuses to read. In PowerShell, run the redirection
129
+ through `cmd`:
47
130
 
48
- ```sh
49
- node dist/cli/main.js lint --root /path/to/documents --config /path/to/mdtools.config.jsonc
50
- node dist/cli/main.js config --root /path/to/documents explain /path/to/documents/notes/example.md
51
- node dist/cli/main.js rules
131
+ ```powershell
132
+ cmd /c "npx mdtools format - --stdin-filepath notes/example.md < input.md > output.md"
52
133
  ```
53
134
 
54
- `config explain` shows the configuration resolved for a document, including path
55
- overrides and the dialect in use. `rules` lists available rules. JavaScript
56
- configuration and plugins execute code with the caller's permissions, so load
57
- trusted code only.
135
+ ## Output
58
136
 
59
- `--dialect <name>` selects the dialect for one invocation of `lint`, `format`,
60
- or `config`, replacing the configuration's top-level dialect. When neither the
61
- configuration nor `--dialect` names a dialect, the commands assume `obsidian`
62
- for a document inside an Obsidian vault and `github` for any other, also for
63
- stdin; version 0.2.0-rc.1 and earlier assumed `commonmark`. See
64
- [dialects](configuration.md#dialects).
137
+ Without `--json`, findings go to stderr, one per line, as
138
+ `path:line:column: severity rule: message`. The path leads from the current
139
+ directory to the document, with forward slashes, as Prettier names files, so
140
+ that a terminal or an editor can open it. `format` prints its diff on stdout
141
+ with the same paths, so that `patch -p1` or `git apply` applies it in the same
142
+ directory. `format --check` names each document that would change, one per line
143
+ after the findings. A run over files ends with one summary line on stderr:
65
144
 
66
- ## Check documents in CI
145
+ | Command | Summary |
146
+ | ---------------- | --------------------------------------------- |
147
+ | `lint` | `3 file(s) linted; 1 error(s), 2 warning(s).` |
148
+ | `format` | `3 file(s) processed; 1 would change.` |
149
+ | `format --write` | `3 file(s) processed; 1 written.` |
67
150
 
68
- After installing and building the checkout, run both checks against your
69
- document workspace:
151
+ Both add `; 1 could not be read` when a selected document [could not be
152
+ read](#files-that-cannot-be-read-or-written), and `format --write` adds
153
+ `; 1 could not be written`. Warnings about the run itself, such as a skipped
154
+ directory, are printed as `mdtools: warning: …`, with paths from the current
155
+ directory as well, and an error that stops the run, a mistake on the command
156
+ line included, as `mdtools: …`. These lines are for people; a script should read
157
+ the JSON report and the exit code.
70
158
 
71
- ```sh
72
- node dist/cli/main.js lint --root /path/to/documents --max-warnings 0
73
- node dist/cli/main.js format --root /path/to/documents --check
159
+ ### JSON report
160
+
161
+ `--json` prints one object:
162
+
163
+ ```json
164
+ {
165
+ "version": "0.2.0",
166
+ "mode": "lint",
167
+ "root": "/home/user/project",
168
+ "files": [
169
+ {
170
+ "path": "notes/release-checklist.md",
171
+ "diagnostics": [
172
+ {
173
+ "start": 149,
174
+ "end": 154,
175
+ "message": "Write one space after the heading's # marks.",
176
+ "edit": { "start": 149, "end": 154, "text": "## " },
177
+ "rule": "house/heading-space",
178
+ "severity": "warn",
179
+ "line": 7,
180
+ "column": 1
181
+ }
182
+ ]
183
+ }
184
+ ],
185
+ "written": []
186
+ }
74
187
  ```
75
188
 
76
- Lint catches enabled problem and style rules. Format checking verifies that the
77
- formatter would leave the selected documents unchanged. Neither command writes.
78
- The [examples](examples.md#automate-checks) include a GitHub Actions workflow
79
- that runs both checks on every pull request, and a script that summarizes a JSON
80
- report.
189
+ - `version` is the tool's version, and `mode` is `lint` or `format`.
190
+ - `root` is the workspace root, as an absolute path in the form of the system.
191
+ - `files` has one entry per selected document: its `path`, relative to the
192
+ workspace root with forward slashes, and its `diagnostics`, without warnings
193
+ when `--quiet` is given. With `format`, `changed` says whether the document
194
+ changed or would change. With stdin, `format` adds `output`, the formatted
195
+ text, unless `--check` is given.
196
+ - `written` lists the paths of the files that `format --write` wrote, in the
197
+ order of `files`. It is empty for `lint`, for `format` without `--write`, and
198
+ when nothing was written. A file whose entry has `changed` but that is not
199
+ listed was not written, and its diagnostics say why.
200
+ - `skipped` appears only when something was skipped: one entry per directory,
201
+ settings file, or ignored input, with `path`, `type` (`directory` or `file`),
202
+ the system's error `code`, `INVALID` for content that could not be used, or
203
+ `IGNORED` for a named input that is ignored, and a `message`. Paths in both
204
+ are relative to the workspace root.
81
205
 
82
- ## Work with agents and scripts
206
+ A diagnostic has the `rule`, the `severity` (`warn` or `error`), the `message`,
207
+ and its position: `start` and optional `end` as offsets into the text, and
208
+ `line` and `column`, both counted from 1. Offsets and columns count UTF-16 code
209
+ units, as JavaScript strings do. A style rule's diagnostic can carry the `edit`
210
+ it proposes, with `start`, `end`, and the replacement `text`. In a `format`
211
+ report, positions refer to the formatted text. Besides the rules, the engine
212
+ reports `engine/unsafe-format` for edits that it could not apply safely (see the
213
+ [safety model](design.md#safety-model)), `engine/rule-error` for a rule that
214
+ failed on a document (see [rules](plugins.md#rules)), `engine/unreadable-file`,
215
+ and `engine/unwritable-file`.
83
216
 
84
- ```sh
85
- node dist/cli/main.js lint --root /path/to/documents --json
86
- node dist/cli/main.js format --root /path/to/documents --check --json
87
- ```
217
+ ## Exit codes
88
218
 
89
- JSON output contains file results and diagnostics with rule IDs, severity,
90
- messages, and source positions. When the run skipped a directory that it was not
91
- permitted to read, or a vault's settings file that it could not read or use, the
92
- report also has a `skipped` list. Each entry has the `path` relative to the
93
- workspace root, a `type` of `directory` or `file`, a `code`, and a `message`
94
- saying what follows from it. The code is the system's error code, or `INVALID`
95
- for a settings file whose content could not be used. The field is absent when
96
- nothing was skipped, and versions up to `0.2.0-rc.1` never print it. Exit code
97
- `0` means success, `1` means a lint or formatting check failed, and `2` means a
98
- configuration, execution, or safety failure. Warnings fail only when they exceed
99
- `--max-warnings`. A selected document that the tool is not permitted to read is
100
- reported with an `engine/unreadable-file` error for that file; the other files
101
- are processed, nothing is written, and the exit code is `2`. Versions up to
102
- `0.2.0-rc.1` stopped without a report.
103
-
104
- For a formatting pass, inspect a diff, apply it with `--write`, then run the
105
- checks again. Report reproducible document defects using a synthetic example;
106
- see [contributing](contributing.md). Automatic issue reporting is not built in.
219
+ | Code | Meaning |
220
+ | ---- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
221
+ | `0` | Done: no errors, no more warnings than `--max-warnings`, and nothing to change for `format --check`. Also after help or the version. |
222
+ | `1` | Errors were found, there were more warnings than `--max-warnings`, or `format --check` found a document to change. |
223
+ | `2` | The command could not do its job: a mistake on the command line; a configuration, path, or plugin error; a rule that failed; or a document that could not be formatted safely, read, or written. |
107
224
 
108
- ## Read from stdin
225
+ Warnings alone never fail a command without `--max-warnings`. `format` can
226
+ succeed and still report problems that only a person can fix, so read its
227
+ findings too. `format --write` writes every document it formatted, also when
228
+ another document cannot be read or written; each problem is reported for its own
229
+ file, and the exit code is then `2`. A document with a rule whose edits were
230
+ refused, or that a rule failed on, is written with the other rules' edits.
109
231
 
110
- ```sh
111
- node dist/cli/main.js format - --stdin-filepath notes/example.md < input.md > output.md
112
- ```
232
+ ## Files that cannot be read or written
233
+
234
+ A directory that the system does not let the tool read is skipped: it is
235
+ reported once as a warning, or in the report's `skipped` list, and the rest of
236
+ the workspace is processed. This includes a directory that can be listed but not
237
+ entered or the reverse, one whose `.gitignore` cannot be read, and a nested
238
+ repository whose `.git/info/exclude` cannot be read. The warning does not change
239
+ the exit code and is not counted by `--max-warnings`. A link into a skipped
240
+ directory is reported as not checkable rather than missing; see
241
+ [`links/valid`](rules.md#linksvalid). Naming the directory, or a path inside it,
242
+ on the command line is an error. To leave a directory out without a warning,
243
+ list it in a `.gitignore`.
244
+
245
+ A selected document that cannot be read gets an `engine/unreadable-file` error;
246
+ the other documents are processed, and written with `--write`, and the exit code
247
+ is `2`. The system errors `EACCES` and `EPERM` count as not being permitted to
248
+ read, and so does `EBUSY`, which Windows reports for a file that another program
249
+ has locked. Any other read error stops the run with exit code `2`. So do a root,
250
+ configuration file, or path on the command line that does not exist, cannot be
251
+ read, or is of the wrong kind, as in `Input does not exist: notes/absent.md`,
252
+ and a configuration file that cannot be read or a directory that refuses the
253
+ search for one. A file of Git's ignore rules that applies to the root, such as a
254
+ `.gitignore` above it, that cannot be read stops the run too.
255
+
256
+ Documents are read as UTF-8, with or without a byte order mark. The tool does
257
+ not guess another encoding: a guess can be wrong, and the supported renderers
258
+ read Markdown as UTF-8. A selected document that is not UTF-8 text gets the same
259
+ `engine/unreadable-file` error, with a message that says what was found, as in
260
+ `The file is not UTF-8 text (byte 0xE9 on line 1 is not valid UTF-8) and was not linted.`
261
+ So does a file that starts with a UTF-16 or UTF-32 byte order mark, which
262
+ Windows PowerShell 5.1 writes for `>`, and a file that contains a NUL byte, as
263
+ UTF-16 text without a mark does. Save such a file as UTF-8 to process it. A link
264
+ to a heading in it is reported as `Fragment could not be checked`.
265
+
266
+ A vault's `.obsidian/app.json` that cannot be read, or that is not valid JSON,
267
+ gets a warning, and the vault's notes are not wrapped. When the system does not
268
+ let the tool find out whether a directory is a vault, the command stops with
269
+ exit code `2`, because reading notes in another dialect is not safe.
270
+
271
+ `format --write` writes each changed file in place, into the existing file,
272
+ which keeps everything about the file but its content; see
273
+ [below](#what-writing-keeps). Before it writes a file, it checks that the file
274
+ may be written and that its directory lets it create a trial file. A file that
275
+ fails the check gets an `engine/unwritable-file` error, such as
276
+ `The file may not be written (EACCES: permission denied); the file is unchanged.`,
277
+ and is left alone; the other files are still written. A read-only file fails the
278
+ check on every platform, and the tool never makes a file writable to write it. A
279
+ failure that the check cannot foresee, such as a file that another program has
280
+ locked on Windows or a full disk, is reported for that file in the same way.
281
+ Either way the file is not listed in `written`, and the exit code is `2`.
282
+
283
+ ### What writing keeps
284
+
285
+ Writing into the existing file keeps everything about it except its content and
286
+ its modification time: its permissions, including access control entries of its
287
+ own, its owner and group, attributes such as Hidden, extended attributes,
288
+ alternate data streams such as the mark of a downloaded file, its creation time,
289
+ and its other names if it has hard links. A file whose two names are both
290
+ selected documents is written once and counted for each.
291
+
292
+ Writing in place is not atomic, and the tool makes up for that as far as it can.
293
+ Before it writes a file, it writes a trial file of the same size beside it,
294
+ holding random bytes, and removes it again, which shows that the new content
295
+ fits on the disk. Just before writing, it checks through the open file that the
296
+ file still holds exactly the bytes it read. If writing then fails part way, it
297
+ writes those bytes back and says whether that worked, as in
298
+ `The file could not be written (EIO: i/o error), and its content from before formatting was written back.`
299
+ or, when that fails too,
300
+ `… and writing its content from before formatting back failed too (EIO: i/o error); it may be incomplete.`
113
301
 
114
- `--stdin-filepath` supplies the path used for configuration and link resolution.
115
- Stdin formatting writes Markdown to stdout and diagnostics to stderr unless
116
- `--json` is selected. It cannot be combined with `--write`.
302
+ Nothing protects a file from a crash of the computer or a power failure while it
303
+ is being written. The file can then be left incomplete, with part of the new
304
+ content and part of the old, and a later run cannot repair it; keep documents
305
+ that matter in version control or a backup. A program that reads the file at
306
+ that moment can see it half written too. A crash during the trial write can
307
+ leave the trial file behind; it is named `.mdtools-` followed by a random name
308
+ and `.tmp`, holds no document text, and can be deleted.