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
@@ -1,238 +1,192 @@
1
- # Configuration and rules
1
+ # Configuration
2
+
3
+ One configuration controls both `lint` and `format`. Without a configuration
4
+ file, the `recommended` preset applies and the dialect is
5
+ [assumed](dialects.md#the-assumed-dialect).
6
+
7
+ ## Configuration files
8
+
9
+ The file is named `mdtools.config.jsonc`, `mdtools.config.json`,
10
+ `mdtools.config.mjs`, `mdtools.config.js`, or `mdtools.config.cjs`. The CLI
11
+ looks for it in the directory given with `--root`, or else the current
12
+ directory, and then in each directory above, up to the first one that is a Git
13
+ repository (holds `.git`) or an Obsidian vault (holds `.obsidian`), or the
14
+ filesystem root. Two configuration files in one directory are an error.
15
+ `--config` names a file explicitly. The directory of the configuration file is
16
+ the workspace root unless `--root` names another; without a configuration file,
17
+ it is the repository or vault where the search stopped, or else the current
18
+ directory. Configuration files do not cascade: a file in a subdirectory is not
19
+ merged with one above it.
20
+
21
+ JSONC allows comments and trailing commas. A JSON or JSONC file may start with a
22
+ byte order mark, and a syntax error in one is reported with its line and column.
23
+ A JavaScript file exports the configuration as its default export, or as
24
+ `module.exports` in CommonJS; Node.js reads an `.mjs` file as an ES module, a
25
+ `.cjs` file as CommonJS, and a `.js` file as the nearest `package.json` declares
26
+ with `type`, or by its syntax when none does. JavaScript configuration and
27
+ plugins run with your permissions, so load only code you trust. Unknown keys,
28
+ rule IDs, and preset names are errors, and so are rule options that the rule
29
+ does not accept; the message names the setting, as in
30
+ `"extends[1]" must be a preset name, not 5`.
2
31
 
3
- ## Loading and precedence
32
+ ```jsonc
33
+ {
34
+ "extends": ["recommended", "github"],
35
+ "ignore": ["generated/**"],
36
+ "rules": { "style/wrap": ["warn", { "width": 100 }] },
37
+ "overrides": [
38
+ { "files": ["vault/**/*.md"], "rules": { "style/emphasis": "off" } },
39
+ ],
40
+ }
41
+ ```
42
+
43
+ | Key | Value | Purpose |
44
+ | ----------- | ---------------------------------------------- | -------------------------------------------------------------------- |
45
+ | `$schema` | A string | Ignored by the tool; gives an editor the [JSON Schema](#json-schema) |
46
+ | `extends` | A preset name or a list of them | [Presets](#presets) to start from |
47
+ | `dialect` | A [dialect](dialects.md) name | The renderer whose syntax documents are read in |
48
+ | `rules` | Rule IDs and settings | [Rules and severity](#rules-and-severity) |
49
+ | `overrides` | Objects with `files`, and `dialect` or `rules` | [Settings for some files](#overrides) |
50
+ | `ignore` | Path patterns | [Documents not to process](#ignored-files-and-link-targets) |
51
+ | `resolve` | `{ "gitIgnored": …, "nestedRepositories": … }` | [More link targets](#ignored-files-and-link-targets) |
52
+ | `plugins` | Files or package names | [Plugins](#plugins) to load |
53
+
54
+ ### JSON Schema
4
55
 
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.
56
+ The configuration's JSON Schema lets an editor that reads JSON Schemas, such as
57
+ Visual Studio Code, complete and check the settings. The package ships it as
58
+ `config.schema.json`, matching the installed version; name it with `$schema`,
59
+ relative to the configuration file:
10
60
 
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.
61
+ ```jsonc
62
+ {
63
+ "$schema": "./node_modules/mdrefine/config.schema.json",
64
+ "extends": ["recommended", "github"],
65
+ }
66
+ ```
15
67
 
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.
68
+ The documentation site serves the schema of the version it documents at
69
+ `https://viell-dev.github.io/slop-markdown-tools/config.schema.json`, an address
70
+ that stays the same. In a JavaScript configuration file, a type annotation in a
71
+ comment gives an editor the same help:
72
+
73
+ ```js
74
+ /** @type {import("mdrefine").Config} */
75
+ export default { extends: ["recommended", "github"] };
76
+ ```
77
+
78
+ The schema describes the form of each setting. Rule IDs, preset names, and rule
79
+ options are checked when the tool runs, against the rules and presets of the
80
+ loaded plugins. The library exports the same schema as
81
+ [`configSchema`](api.md#other-exports).
82
+
83
+ ## Presets
21
84
 
22
- `--dialect` replaces the top-level dialect; an explicit file override still
23
- takes precedence. `config explain <file>` prints the resolved configuration,
24
- including the dialect in use for that file, and its source file.
85
+ A **preset** is a named set of rule settings. The **dialect** is a separate
86
+ setting: it names the renderer whose syntax documents are read in, such as
87
+ GitHub's or Obsidian's, and so decides what counts as a table, a link, or a line
88
+ break. Each preset except `recommended` also sets a dialect. The
89
+ [rules](rules.md) page shows which presets enable each rule.
90
+
91
+ Presets are starting points with opinionated defaults, not a fixed style. Every
92
+ rule can be turned off or given another severity, and a rule whose choice
93
+ documents reasonably differ on, such as the emphasis marker or the wrapping
94
+ width, takes options for it; the [rules](rules.md) list them with their
95
+ defaults.
96
+
97
+ | Preset | Sets the dialect | For |
98
+ | ------------- | ---------------- | --------------------------------------------------------------------------------------------------------------- |
99
+ | `recommended` | No | Any document: wrapping at 80 columns, markers, blank space, tables, valid links, problems that show on the page |
100
+ | `github` | `github` | Documents rendered by GitHub |
101
+ | `forgejo` | `forgejo` | Documents rendered by Forgejo, such as on Codeberg |
102
+ | `codeberg` | `forgejo` | The `forgejo` preset under another name |
103
+ | `gitea` | `gitea` | Documents rendered by Gitea |
104
+ | `obsidian` | `obsidian` | An Obsidian vault |
105
+
106
+ Without `extends`, the configuration starts from `recommended`. `extends` names
107
+ one preset or lists several, and replaces that default, so name both, as in
108
+ `["recommended", "github"]`; `"extends": "github"` starts from `github` alone,
109
+ and `"extends": []` enables no rules. Presets apply in the order listed, then
110
+ the configuration's own settings, then each matching override in order. A later
111
+ setting for a rule replaces an earlier one as a whole, options included. A
112
+ plugin's presets are named with the plugin's name as a prefix, such as
113
+ `sample/recommended`, and presets may extend other presets; a cycle is an error.
114
+
115
+ ## Rules and severity
116
+
117
+ A rule's setting is its severity, `"off"`, `"warn"`, or `"error"`, or a list of
118
+ the severity and the rule's options, as in `["warn", { "width": 100 }]`; a list
119
+ of the severity alone, `["warn"]`, keeps the rule's default options. The [rules
120
+ reference](rules.md) lists every rule and its options.
121
+
122
+ `format` applies the edits of every enabled style rule, whether its severity is
123
+ `warn` or `error`. Problem rules only report. Errors make `lint` and `format`
124
+ exit with code `1`; warnings do so only beyond `--max-warnings`.
125
+
126
+ ## Dialect
127
+
128
+ `dialect` sets the dialect of every document, and an override can set another
129
+ for some of them. `codeberg` is accepted as another name for `forgejo`.
130
+ `--dialect` on the command line replaces the dialect of the configuration and
131
+ its presets, though not one set by a matching override. When neither the
132
+ configuration, its presets and overrides, nor `--dialect` names one, the dialect
133
+ is [assumed](dialects.md#the-assumed-dialect).
134
+
135
+ ## Overrides
136
+
137
+ Each entry of `overrides` applies its `dialect` and `rules` to the documents
138
+ that match one of its `files` patterns:
25
139
 
26
140
  ```jsonc
27
141
  {
28
142
  "extends": ["recommended", "github"],
29
- "ignore": ["generated/**"],
30
143
  "overrides": [
31
144
  {
32
- "files": ["vault/**/*.md"],
33
- "dialect": "obsidian",
34
- "rules": { "style/emphasis": "off" },
145
+ "files": ["CHANGELOG.md", "generated/**"],
146
+ "rules": { "style/wrap": "off" },
35
147
  },
36
148
  ],
37
149
  }
38
150
  ```
39
151
 
40
- Obsidian reflow depends on a setting in the vault's `.obsidian/app.json`, which
41
- is read for the vault that holds each note. A workspace can hold several vaults
42
- with different settings. An override like the one above is not needed to have a
43
- vault's notes read in the Obsidian dialect, which is assumed for them when
44
- nothing names a dialect; it is the place for rules that only the vault should
45
- follow. Links in such a vault's notes are resolved within the vault; see
46
- [Obsidian vaults](obsidian.md#vaults-that-are-not-the-workspace-root).
47
-
48
- ## Dialects
49
-
50
- `dialect` selects the renderer whose rules apply: `commonmark`, `github`,
51
- `forgejo`, `gitea`, or `obsidian`. `codeberg` is an alias for `forgejo`, because
52
- Codeberg runs Forgejo's renderer; aliases are accepted wherever a dialect is
53
- named and resolve to the canonical name in `config explain` and plugin
54
- documents.
55
-
56
- When nothing names a dialect, the tool assumes one. That is the case without a
57
- configuration file, and with a configuration in which no `dialect` setting,
58
- preset, or matching override names one, unless `--dialect` is passed. A named
59
- dialect always takes precedence. Version 0.2.0-rc.1 and earlier assumed
60
- `commonmark` everywhere.
61
-
62
- - For a document inside an Obsidian vault, the assumed dialect is `obsidian`.
63
- Obsidian shows a single line break as a line break unless the vault's "Strict
64
- line breaks" setting is on, so no other dialect can reflow a note safely. A
65
- vault is a folder that contains a `.obsidian` folder, and a document belongs
66
- to the nearest vault at or above its own folder. That can be the workspace
67
- root, a folder below it, or a folder above it when a folder of a vault is used
68
- as the root. The search upward ends at a repository, a folder that contains
69
- `.git`: documents of a repository checked out inside a vault are not read as
70
- the vault's notes, just as the tool leaves a repository inside a workspace
71
- alone.
72
- - For every other document, it is `github`. GitHub's syntax is the common base
73
- of the Forgejo, Gitea, and Obsidian dialects, so tables and footnotes are
74
- recognized instead of being reflowed as ordinary paragraphs.
75
-
76
- Only the dialect is assumed: the rules of the `github` and `obsidian` presets,
77
- such as table alignment, still need the preset. For documents that are plain
78
- CommonMark, name the dialect with `"dialect": "commonmark"` or
79
- `--dialect commonmark`.
80
-
81
- Text between dollar signs is read as math under the conditions of the dialect's
82
- renderer, so that sums of money are not taken for a formula. Math is never
83
- restyled, and no line is broken inside it.
84
-
85
- - On GitHub, Forgejo, and Gitea, `$…$` is math only when the opening `$` does
86
- not follow a letter or digit, and the next `$` is not followed by a letter, a
87
- digit, or `_`. `Costs $5 and $6` is prose; `From $5 to 10$` is math, as it is
88
- to all three renderers.
89
- - GitHub also wants a single `$` to touch its formula: no space after the
90
- opening one and none before the closing one. `$ x $` is math on Forgejo and
91
- Gitea and prose on GitHub.
92
- - In Obsidian, the opening `$` may not be followed by a space, and a `$` closes
93
- the formula only if no space stands before it and no digit follows it. Letters
94
- and digits before the opening `$` do not matter there. `Costs $5 and $6` is
95
- prose, and so is `$ x $`; `a$x$b` holds a formula, which it does on none of
96
- the other three.
97
- - Display math, between lines of `$$` or in a `math` code block, is the same in
98
- every dialect.
99
-
100
- Where a renderer is stricter than this, the tool stays on the careful side: it
101
- also protects a formula with emphasis inside, which GitHub does not render, and
102
- one that runs over a line break, which Forgejo and Gitea do not. Obsidian's two
103
- views differ in two cases, and the tool follows the one that shows more as a
104
- formula, Reading view: in `A $5 and $x$` it protects everything from the first
105
- `$` to the last, where Live Preview shows only `$x$` as the formula, and it
106
- protects a formula that runs over a line break, which Live Preview does not
107
- render. GitHub accepts an escaped `\$` as the start of a formula; the tool does
108
- not. Versions up to `0.2.0-rc.1` read any text between two `$` signs as math in
109
- every dialect.
110
-
111
- GitHub and Forgejo share tables, task lists, strikethrough, footnotes, literal
112
- autolinks, `$` math, and the five `[!NOTE]`-style alert types. Forgejo
113
- additionally renders definition lists (`Term` followed by a line starting with
114
- `: `), `\(...\)` and `\[...\]` math, and `[[target|text]]` shortlinks, and
115
- generates heading anchors differently: every run of characters other than
116
- letters, numbers, and `_` becomes one hyphen, so `## test.0.1` is `#test-0-1` on
117
- Forgejo and `#test01` on GitHub. The Forgejo dialect keeps those constructs on
118
- their original lines, leaves emphasis markers and code spans inside `\(...\)`
119
- unchanged, and validates fragments against Forgejo anchors. The GitHub rules
120
- apply to both dialects.
121
-
122
- Gitea, from which Forgejo was forked, renders the same definition lists and
123
- shortlinks. It parses `\(...\)` and `\[...\]` math only when an administrator
124
- enables them with `MATH_CODE_BLOCK_DETECTION`, as was the default before
125
- Gitea 1.24; a document cannot reveal that setting, so the Gitea dialect keeps
126
- those constructs on their lines too. Gitea's heading anchors follow GitHub's:
127
- punctuation is deleted, so `## test.0.1` is `#test01`. Gitea 1.26 and later give
128
- repeated headings the same anchor; the `-1`, `-2` suffixes of earlier versions
129
- are still accepted. Gitea 1.26 and later also render `__init__.py` literally
130
- instead of as emphasis, including in headings, whose anchor is then
131
- `#__init__py`. Whether underscores delimit emphasis depends on the rest of the
132
- line, so on a line containing `_.py` the Gitea dialect leaves emphasis markers
133
- unchanged and keeps link destinations whose rewrite would change their
134
- underscores. It does not reflow such a paragraph unless all its underscores are
135
- inside code spans and it contains no `$` or `\(` math. The GitHub rules apply to
136
- Gitea as well. Gitea renders repository files with soft line breaks by default;
137
- if an instance adds `new-line-hard-break` to `RENDER_OPTIONS_REPO_FILE`, disable
138
- `style/wrap`.
139
-
140
- Forgejo, and Gitea before 1.26, build a heading's anchor from the source of its
141
- last line as written, before any Markdown in it is read. Link destinations,
142
- image syntax, emphasis markers, character references, and inline HTML tags are
143
- part of the anchor, and a heading underlined with `===` or `---` gets the anchor
144
- of its last text line only. `## Link [text](https://example.com/page) end` is
145
- `#link-text-https-example-com-page-end` on Forgejo and
146
- `#link-texthttpsexamplecompage-end` on Gitea 1.25, `## Strong __init__ end` is
147
- `#strong-__init__-end` on both, and `## A <span>B</span>` is `#a-span-b-span`
148
- and `#a-spanbspan`. The opening and closing `#` sequences, the blank space
149
- around the text, block quote and list prefixes, and a trailing attribute block
150
- are not part of the line. GitHub, and Gitea 1.26 and later, build the anchor
151
- from the rendered text instead: `#link-text-end`, `#strong-init-end`, and
152
- `#a-b`, with the text of every line of an underlined heading. GitHub keeps the
153
- tags that it shows as text, such as `<script>`, in the anchor; Gitea 1.26 and
154
- later show `<script>`, `<style>`, `<html>`, and `<head>` as text and keep them
155
- too, drop other tags they do not allow while keeping the text inside, and read a
156
- hard line break as a line break. The Gitea dialect accepts the anchors of both
157
- Gitea generations.
158
-
159
- Forgejo and Gitea read a `{...}` block that ends a heading as attributes instead
160
- of text. `## Install {#setup}` renders as "Install" with the anchor `#setup` and
161
- no `#install`; `## Usage {.note}` keeps the generated anchor `#usage`. The
162
- Forgejo and Gitea dialects validate fragments against those anchors. A block
163
- holds `#id`, `.class`, and `name=value` items, such as
164
- `{#setup .note data-level=2}`, and ends the heading's line or, in a heading
165
- underlined with `===` or `---`, its last text line; after a closing `#` sequence
166
- it still counts (`## Install ## {#setup}`). A block that is not valid, that is
167
- followed by more text, or whose `{` is escaped with a backslash stays heading
168
- text. A custom anchor must be linked exactly as written, including its case, and
169
- it is never numbered: a later `## Setup` heading is also `#setup`, not
170
- `#setup-1`. GitHub and Obsidian show the block as text, so the other dialects
171
- keep it in heading anchors and heading names. An `id` that is not text, such as
172
- `{id=5}`, `{id=true}`, or a list, has no anchor on Gitea 1.26 and later, and
173
- Forgejo, and Gitea before 1.26, render nothing for the whole document; the
174
- Forgejo and Gitea presets report it with `forgejo/heading-id`. Quote the value
175
- (`{id="5"}`) or use `{#5}` instead.
176
-
177
- Formatting with the Forgejo or Gitea dialect leaves a block as written:
178
- `{#bare data-x=__init__}` is a valid block, and `{#bare data-x=**init**}` is
179
- heading text without the anchor `#bare`. A change can also create a block:
180
- `## *Mode {.a*}` is emphasis, while `## _Mode {.a_}` is a heading with a class.
181
- `style/emphasis`, `style/strong`, and `links/path` therefore change nothing from
182
- the first `{` of a heading's last text line on. `style/inline-code` does not
183
- join a code span that spans lines of a heading, and `style/heading` escapes a
184
- `#` run in front of the block (`# Run \## {#id}`), which would otherwise end the
185
- heading there.
186
-
187
- Because Forgejo, and Gitea before 1.26, build a heading's anchor from its last
188
- line as written, `style/emphasis`, `style/strong`, and `links/path` also leave
189
- that line unchanged on these dialects when the heading has no custom `id`.
190
- Rewriting `## Emph *star* end` to `_star_` would move its anchor from
191
- `#emph-star-end` to `#emph-_star_-end` and break every link to it, although the
192
- heading reads the same. Earlier lines of an underlined heading, headings with an
193
- `id`, and the rest of the document are formatted as usual.
152
+ Patterns are globs, such as `docs/**/*.md`, matched against paths relative to
153
+ the workspace root, with forward slashes. A pattern matches files whose names
154
+ start with a dot, too. Patterns match letter case exactly on every system, so
155
+ that a configuration selects the same files in CI on Linux as on Windows or
156
+ macOS, where patterns on the command line and, in most repositories,
157
+ `.gitignore` files [ignore case](cli.md#which-files-are-processed).
194
158
 
195
- ## Presets
159
+ ## Ignored files and link targets
160
+
161
+ `ignore` lists path patterns, written like those of overrides, for documents
162
+ that are never linted or formatted. They remain link targets, so links to them
163
+ are still checked.
164
+
165
+ The CLI also leaves out what Git ignores, decided [as Git decides
166
+ it](cli.md#which-files-are-processed), nested Git repositories, symbolic links,
167
+ and the directories `.git`, `.obsidian`, `node_modules`, `dist`, and `coverage`.
168
+ `resolve` makes two of these available as link targets, without ever selecting
169
+ them for linting or formatting:
170
+
171
+ ```jsonc
172
+ {
173
+ "resolve": { "gitIgnored": true, "nestedRepositories": true },
174
+ }
175
+ ```
196
176
 
197
- | Preset | Dialect | Enabled rules |
198
- | ------------- | --------- | ---------------------------------------------------------------------------------------------- |
199
- | `recommended` | Unchanged | Wrap at 80, `_` emphasis, `**` strong, final newline, valid links |
200
- | `github` | GitHub | Table alignment, lowercase completed task marker, uppercase alert marker |
201
- | `forgejo` | Forgejo | The `github` rules and `forgejo/heading-id` |
202
- | `codeberg` | Forgejo | Alias of `forgejo` |
203
- | `gitea` | Gitea | The `github` rules and `forgejo/heading-id` |
204
- | `obsidian` | Obsidian | Table alignment, task marker, lowercase callout type, duplicate block IDs, soft-break settings |
205
-
206
- An omitted `extends` selects `recommended`. Explicit `extends` replaces that
207
- default, so use `["recommended", "obsidian"]` for both. `extends: []` enables
208
- nothing. Severity is `off`, `warn`, or `error`; supply options as
209
- `["warn", { ... }]`. Both `warn` and `error` style rules format when enabled.
210
- Problem rules report findings and never become formatting edits.
211
-
212
- ## Built-in rules
213
-
214
- | Rule | Kind | Options |
215
- | ----------------------------- | ------- | ---------------------------------------------------------------------- |
216
- | `style/wrap` | Style | `width`, `measure`, `reportUnreflowed`, `reportUnbreakable`; see below |
217
- | `style/inline-code` | Style | None; joins multiline code spans, opt-in |
218
- | `style/emphasis` | Style | `marker`: `_` (default) or `*` |
219
- | `style/strong` | Style | `marker`: `*` (default) or `_` |
220
- | `style/final-newline` | Style | None; adds a missing final newline |
221
- | `style/table` | Style | None; aligns top-level GFM tables |
222
- | `style/heading` | Style | None; converts eligible Setext headings to ATX |
223
- | `links/valid` | Problem | None; local files and heading/block fragments |
224
- | `links/path` | Style | `style`, `brackets`, `extension`, `leadingDot`; see below |
225
- | `links/notation` | Style | `style`: `markdown` or `wiki`; Obsidian only |
226
- | `github/task-marker` | Style | None; `[X]` becomes `[x]` |
227
- | `github/alert-marker` | Style | None; known alert types become uppercase on GitHub, Forgejo, and Gitea |
228
- | `forgejo/heading-id` | Problem | None; heading ids that are not text, which break rendering on Forgejo |
229
- | `obsidian/callout-marker` | Style | None; Obsidian callout types become lowercase |
230
- | `obsidian/block-reference` | Problem | None; duplicate trailing `^block-id` markers |
231
- | `obsidian/strict-line-breaks` | Problem | None; report unverified/incompatible reflow settings |
177
+ Both default to `false`. The Markdown files of such a target are read only when
178
+ a link's fragment needs checking.
179
+
180
+ The directories that the CLI does not visit, and the targets of symbolic links
181
+ and junctions, are still link targets: a link that names a path in or through
182
+ them is checked on disk. A path whose real location lies outside the workspace
183
+ root is not read and is reported as not checkable. A Git-ignored directory stays
184
+ out unless `resolve.gitIgnored` asks for it, whatever its name, and an Obsidian
185
+ search by note name does not look in these directories.
232
186
 
233
187
  ## Plugins
234
188
 
235
- `plugins` lists the plugins to load, each as a string:
189
+ `plugins` lists plugins to load, as strings:
236
190
 
237
191
  ```jsonc
238
192
  {
@@ -243,136 +197,17 @@ Problem rules report findings and never become formatting edits.
243
197
  ```
244
198
 
245
199
  An entry that starts with `.` or is an absolute path names a file, relative to
246
- the configuration file's directory. Any other entry names an installed package,
247
- looked up from the configuration file as Node.js looks up an `import`. A plugin
248
- adds rules and presets, which are used with the name that the plugin declares as
249
- a prefix. Above, the plugin in the file calls itself `local` and the one in the
250
- package `sample`. Only a configuration file can name plugins; there is no
251
- command-line option for them.
252
-
253
- A plugin is code. It runs with the caller's permissions when the configuration
254
- is loaded, so name only plugins you trust. A plugin that cannot be found or
255
- loaded stops the command with exit code `2` and a message that names the entry
256
- and the configuration file.
257
-
258
- [Rule plugins](plugins.md#rule-plugins) describes how a package is looked up in
259
- detail, what a plugin can contain, and how to write one.
260
-
261
- ## Document structure
262
-
263
- Enable `structure/initial-heading` explicitly with severity `warn` or `error`;
264
- no preset enables it. It accepts no options and reports problems without
265
- changing content. Use path `overrides` to exempt generated documents or trees
266
- with different conventions.
267
-
268
- `structure/initial-heading` skips YAML/TOML front matter and requires a level-1
269
- heading as the first content block. Both ATX and Setext headings count; use
270
- `style/heading` separately to require ATX spelling. Empty documents are
271
- reported. Comments and other content before the title are not skipped.
272
-
273
- Filename/title matching, metadata placement, and guesses about accidental
274
- Markdown markers are consumer policies. Implement those checks through
275
- [rule plugins](plugins.md#rule-plugins), which receive the document source,
276
- parsed tree, and path. Modified-date updates belong in a consumer wrapper or
277
- library workflow; formatting rules must preserve document meaning.
278
-
279
- ## Wrapping and inline code
280
-
281
- `style/wrap` accepts widths from 20 through 500 and defaults to `width: 80` and
282
- `measure: "columns"` (display columns). Set `measure: "codepoints"` to count
283
- Unicode scalar values instead; astral characters count once and combining marks
284
- count separately. Container prefixes count toward the width in either mode.
285
-
286
- `reportUnreflowed: true` reports breakable over-width lines in paragraphs
287
- protected by hard breaks, block IDs, inline HTML, callout headers, Forgejo or
288
- Gitea definition lists, display math, line-spanning inline syntax, or the Gitea
289
- `_.py` emphasis exception, or unsupported multiline syntax or containers.
290
- `reportUnbreakable: true` reports overflow from an atom that cannot be split,
291
- including in protected paragraphs. Whitespace inside a link, code span, or other
292
- protected inline node is not a wrapping opportunity, nor is whitespace inside a
293
- Forgejo or Gitea `\(...\)` expression or `[[...]]` shortlink written on one
294
- line. A protected paragraph with both breakable prose and an over-width atom can
295
- report both causes; an atom-only overflow does not trigger `reportUnreflowed`.
296
- Hard-break markers are excluded from protected-line width. An Obsidian callout
297
- title is one indivisible physical line: an over-width title follows
298
- `reportUnbreakable`, even when it contains spaces. Body prose is classified
299
- separately. A block ID does not make its whole paragraph indivisible. Both
300
- options default to false, propose no edits, and use the rule's severity. Use
301
- severity `error` or `--max-warnings 0` to make these diagnostics fail a check;
302
- `--check` does not change rule settings.
303
-
304
- Enable `style/inline-code` to join multiline code spans before paragraph reflow.
305
- This opt-in rule handles single and multiple backticks, including list and quote
306
- containers. It preserves the parsed code value, converting each line ending to
307
- one space under CommonMark's code-span rules. Other code whitespace remains
308
- significant. It is useful for Obsidian editors that display split spans
309
- differently from Reading view.
310
-
311
- ## Link policies
200
+ the configuration file's directory. Any other entry names a package, found from
201
+ the configuration file as Node.js finds an `import`. A plugin adds rules and
202
+ presets, named with the plugin's name as a prefix: above, the file's plugin
203
+ calls itself `local` and the package's plugin `sample`. Only a configuration
204
+ file can name plugins; the CLI has no option for them.
312
205
 
313
- ```jsonc
314
- {
315
- "extends": ["recommended", "obsidian"],
316
- "rules": {
317
- "links/path": ["warn", { "style": "root", "brackets": "angle", "extension": "preserve" }],
318
- },
319
- }
320
- ```
206
+ A plugin runs with your permissions when the configuration is loaded. A plugin
207
+ that cannot be loaded stops the command with exit code `2`.
208
+ [Plugins](plugins.md) describes how to write one and how a package is found.
321
209
 
322
- `style` accepts `preserve` (default), `relative`, `root`, or `shortest`. `root`
323
- means vault-relative, without a leading slash, and is available only for
324
- Obsidian. `shortest` uses the basename if it resolves uniquely, otherwise the
325
- vault-relative path. `leadingDot: true` prefixes ordinary relative paths with
326
- `./`. Filesystem absolute paths are not generated.
327
-
328
- `brackets` accepts `preserve` (default), `angle`, or `bare`. Spaces in bare
329
- Markdown destinations are percent-encoded. `extension` accepts `preserve`
330
- (default), `include`, or `omit`; omission is Obsidian specific. Fragments are
331
- resolved according to the dialect, separately from percent-decoded filenames. On
332
- GitHub, Forgejo, and Gitea, a fragment written with the `user-content-` prefix
333
- that the renderers add to every anchor reaches the same anchor as one without,
334
- and is validated the same way. Anchors written as HTML, the `id` of any element
335
- and the `name` of an `<a>`, such as `<a name="install"></a>` before a heading,
336
- are valid fragments on those three dialects. GitHub matches every fragment
337
- without regard to case, so `#Sponsors` reaches `## Sponsors` there; Forgejo and
338
- Gitea match exactly as written. A heading written as HTML
339
- (`<h2 align="center">Title</h2>`) gets a generated anchor on GitHub, numbered
340
- together with the Markdown headings, and on Gitea 1.26 and later, where it is
341
- not numbered and a heading with its own `id` gets none; Forgejo and earlier
342
- Gitea versions give it no anchor. HTML inside comments, code, and `<script>`,
343
- `<textarea>`, and the other elements whose content is text is not an anchor.
344
- Already compliant destinations retain their percent-encoding spelling, including
345
- literal `?` and `%` in resolving Obsidian paths. Newly generated paths encode
346
- literal percent signs and hashes to preserve target identity.
347
-
348
- Path edits require a resolved target and valid fragment. Unaliased ordinary
349
- wikilinks retain their target spelling because changing it may change the
350
- visible label. Reference definitions are validated but their source spelling is
351
- currently preserved.
352
-
353
- `links/notation` converts simple explicit-label wikilinks to Markdown links and
354
- plain-text Markdown links to wikilinks. Titles, rich labels, unaliased
355
- wikilinks, and embeds are preserved. A note embed is not interchangeable with a
356
- Markdown image. Do not enable notation conversion and path rewriting for the
357
- same link in one pass: overlapping edits are reported; run those policies in
358
- separate passes.
359
-
360
- Network URLs are recognized but never fetched. Website-root paths and
361
- query-bearing destinations in CommonMark, GitHub, Forgejo, and Gitea are outside
362
- local resolution. Missing or ambiguous targets are reported, never guessed. A
363
- target that could not be checked, because it lies in a directory the tool was
364
- not permitted to read, is reported as such and not as missing; see
365
- [paths that cannot be read](#paths-that-cannot-be-read). Obsidian resolution
366
- uses explicit `./` or `../` paths as source-relative; otherwise an exact
367
- vault-root match precedes an exact source-relative match, followed by unique
368
- suffix matching. Multiple candidates within the chosen tier remain ambiguous.
369
- Relative rewrites use `./` when needed to avoid a vault-root collision. Existing
370
- directories get a distinct diagnostic in Obsidian; they are not rewritten to
371
- README or index notes. CommonMark, GitHub, Forgejo, and Gitea directory links
372
- are accepted because a hosting site can serve them. Shortest-name/alias
373
- resolution is not a complete clone of Obsidian.
374
-
375
- ## Suppressions
210
+ ## Suppression comments
376
211
 
377
212
  ```markdown
378
213
  <!-- mdtools-disable style/wrap -->
@@ -385,146 +220,24 @@ Keep the wrapping in this region.
385
220
  Keep *this* delimiter.
386
221
  ```
387
222
 
388
- Omit rule names to disable all rules. A disabled region suppresses edits
389
- intersecting it; a paragraph spanning a disabled line is therefore preserved as
390
- a whole. An enable directive closes matching disabled regions; `enable` without
391
- names closes all. Directives inside code are ordinary code. `disable-next-line`
392
- targets the immediately following physical line, including a blank line; place
393
- it directly above the content to suppress.
223
+ `mdtools-disable` turns rules off from the comment on, and `mdtools-enable`
224
+ turns them on again; without rule names, they apply to every rule.
225
+ `mdtools-disable-next-line` applies to the next line only, even when it is
226
+ blank, so place it directly above the line. A suppressed rule reports nothing
227
+ there and changes nothing that touches the region, so a paragraph that crosses
228
+ into it is left as written. A directive inside code is code, not a directive.
394
229
 
395
- ## Selection, output, and exit codes
230
+ ## Inspect the result
396
231
 
397
- Commands accept explicit files/directories, or no paths to select the workspace.
398
- The CLI honors `.gitignore` files and configured ignore patterns. Config-ignored
399
- documents remain available for link resolution. Git-ignored content and nested
400
- repositories are not indexed by default. To make their files available only as
401
- link targets, set:
232
+ `config explain` prints, as JSON, the configuration in effect for one document:
233
+ the workspace `root`, the configuration file as `config` (or `null`), the
234
+ document's `path` from the root with forward slashes, and the `effective`
235
+ settings, with the dialect assumed for that document and the overrides that
236
+ match it. The document need not exist, but like an input of `lint` it must lie
237
+ inside the workspace.
402
238
 
403
- ```jsonc
404
- {
405
- "resolve": { "gitIgnored": true, "nestedRepositories": true },
406
- }
239
+ ```sh
240
+ npx mdtools config explain notes/example.md
407
241
  ```
408
242
 
409
- These options are independent and default to false. They never select those
410
- files for linting or formatting, including when a path is supplied explicitly.
411
- Markdown targets are read and parsed only for fragment checks; attachments need
412
- only a discovered file entry. Built-in traversal exclusions still apply. `.git`,
413
- `.obsidian`, `node_modules`, `.npm-cache`, `dist`, and `coverage` are excluded
414
- from traversal, as are symlinks. Nested Git repositories are traversed for
415
- targets only when opted in. Explicit symlink inputs are rejected. Shell-expanded
416
- globs work; the CLI does not expand path globs.
417
-
418
- `lint --json` and `format --json` print a single object with `version`, `mode`,
419
- `files`, and `written`, and with `skipped` when the run left out something it
420
- was not permitted to read. Each file carries diagnostics with rule ID, severity,
421
- message, offsets, and one-based line/column locations. Source offsets and
422
- columns use JavaScript UTF-16 units; wrapping widths use display columns.
423
- Formatting diagnostics refer to the resulting source. `written` records whether
424
- the requested write phase was allowed, not how many files changed; inspect each
425
- file's `changed` field.
426
-
427
- Without `--json`, a command that processed files ends with one summary line on
428
- stderr. `lint` counts the files and the findings it reported, as in
429
- `3 file(s) linted; 1 error(s), 2 warning(s).` `format` counts the files and
430
- those it changed or would change, as in `3 file(s) processed; 1 would change.`
431
- Both add `; 1 could not be read` when a selected document
432
- [could not be read](#paths-that-cannot-be-read). Versions up to `0.2.0-rc.1`
433
- ended `lint` with `0 would change`, whatever it found. The line is meant for
434
- people; a script should read the JSON report or the exit code.
435
-
436
- `format --diff` is the default for file input. `--check`, `--diff`, and
437
- `--write` are mutually exclusive. `format - --stdin-filepath notes/example.md`
438
- reads stdin with configuration and resolution context; it cannot be used with
439
- `--write`.
440
-
441
- | Exit | Meaning |
442
- | ---- | -------------------------------------------------------------------------------------------- |
443
- | `0` | Completed; no errors or failed formatting check |
444
- | `1` | Lint errors, exceeded `--max-warnings`, or changes required by `format --check` |
445
- | `2` | Configuration, execution, or formatting safety failure, or a document that could not be read |
446
-
447
- Warnings do not fail a command unless `--max-warnings` is supplied. A successful
448
- formatting command can still report remaining lint problems. Inspect its exit
449
- code and diagnostics. An unsafe-format diagnostic blocks the complete batch's
450
- write phase, and so does a selected document that
451
- [cannot be read](#paths-that-cannot-be-read). An I/O error during writing can
452
- leave earlier files written; replacement is atomic per file, not a
453
- workspace-wide transaction.
454
-
455
- ## Paths that cannot be read
456
-
457
- A directory that the system does not permit the tool to read is skipped. The
458
- tool reports it once as a warning that names it, indexes nothing below it, and
459
- processes the rest of the workspace. The warning alone does not change the exit
460
- code and is not counted by `--max-warnings`. It is printed on stderr as
461
- `mdtools: warning: ...`; with `--json` it is an entry of the report's `skipped`
462
- list instead, with the directory's `path`, `type`, the system's error `code`,
463
- and a `message`. This applies to a directory that can be neither listed nor
464
- entered, to one that can only be listed, and to one that can only be entered. It
465
- also applies to a directory whose `.gitignore` cannot be read, because without
466
- the ignore rules nothing tells which of its files may be processed. Versions up
467
- to `0.2.0-rc.1` stopped every command, with exit code `2`, when the workspace
468
- contained such a directory.
469
-
470
- A link that leads into a skipped directory cannot be checked. `links/valid`
471
- reports it as `Local target could not be checked`, naming the directory, with
472
- the rule's configured severity. The target is not called missing, and the link
473
- is never rewritten. An Obsidian link that names a note without a path is
474
- searched for in every directory, so when no readable file matches, it is
475
- reported the same way, naming the skipped directories. A note that such a search
476
- does find is accepted, and so is a note found beside the linking note when the
477
- vault-root path of the same spelling leads into a skipped directory.
478
- `links/path` and `links/notation` leave such a link as written, and an edit by a
479
- plugin that changes its destination is refused, because a skipped directory may
480
- hold a second note of that name, or the one that Obsidian would choose.
481
-
482
- A path named on the command line that cannot be used stops the command with exit
483
- code `2`. The message says what the path was named as and what is wrong with it:
484
- a workspace root, a configuration file, or an input that `does not exist` or
485
- `cannot be read`, a workspace root that `is not a directory`, a configuration
486
- file that `is not a file`, as in `Input does not exist: notes/absent.md`. An
487
- input and a file given with `--config` are named as they were given, the
488
- workspace root with its full path. Versions up to `0.2.0-rc.1` printed the
489
- system's own message, which named an internal step. An `--exclude` path that
490
- does not exist excludes nothing; one that the tool is not permitted to examine
491
- is an error, because it cannot tell which file the path stands for.
492
-
493
- Naming a skipped directory, or a path inside it, as an input is an error with
494
- exit code `2`, and so is a workspace root that cannot be read. Configuration
495
- `ignore` patterns and `--exclude` choose which documents are processed; they do
496
- not keep the tool out of a directory, because an ignored document remains a link
497
- target. To leave a directory out without a warning, list it in a `.gitignore`:
498
- Git-ignored content is not indexed unless `resolve.gitIgnored` asks for it.
499
-
500
- If a vault's `.obsidian/app.json` cannot be read, the tool warns in the same way
501
- and treats `strictLineBreaks` as unverified for that vault, so its documents are
502
- not reflowed. The same holds when the file's content cannot be used, because it
503
- is not valid JSON or is not a file; the entry's `code` is then `INVALID`.
504
- Versions up to `0.2.0-rc.1` stopped every command in that case. A file named
505
- `.obsidian` is not a vault's settings folder and is ignored. When the system
506
- does not let the tool find out whether a folder is a vault, which takes a
507
- `.obsidian` that is a link into a place it may not read, or a document named for
508
- `config explain` or `--stdin-filepath` in a directory it may not enter, the
509
- command stops with exit code `2`: reading a vault's notes as another dialect is
510
- not a safe fallback. A configuration file that cannot be read still stops the
511
- run, as does a directory that refuses the search for one: default rules must not
512
- take the place of a configuration that exists. An error about a configuration
513
- file names the file, also when its content is not valid JSON, does not fit the
514
- configuration format, or is a script that fails to load.
515
-
516
- A selected document that cannot be read is reported for that file, as an error
517
- diagnostic with the rule ID `engine/unreadable-file` at line 1, in the text
518
- output and in the file's entry of the JSON report. The other files are processed
519
- and reported, and the run ends with exit code `2`. As with an unsafe-format
520
- diagnostic, `format --write` then writes none of the batch; pass `--exclude` for
521
- the file to format the rest. A link to a heading or block in a file that cannot
522
- be read is reported by `links/valid` as `Fragment could not be checked`, naming
523
- the file, with the rule's configured severity, and is never rewritten. A link to
524
- the file without a fragment is valid, because the file is known to exist.
525
- Versions up to `0.2.0-rc.1` stopped at the first such file or link without
526
- reporting anything else.
527
-
528
- Only the system errors `EACCES` and `EPERM` count as not permitted. Any other
529
- read error, such as a path that disappeared during the run or a failing device,
530
- stops the run with exit code `2`.
243
+ It reads `--config`, `--root`, and `--dialect`, before or after `explain`.