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/api.md ADDED
@@ -0,0 +1,239 @@
1
+ # Library API
2
+
3
+ The package's main entry, `mdrefine`, lints and formats strings and never
4
+ touches the filesystem. A second entry, `mdrefine/node`, finds and reads files
5
+ the way the CLI does. Both are TypeScript-typed ES modules, and their exports
6
+ are part of the interface that [version numbers](versions.md) describe.
7
+
8
+ ## Lint and format
9
+
10
+ ```ts
11
+ import { createWorkspace, format, lint } from "mdrefine";
12
+
13
+ const files = {
14
+ "note.md": "A *short* note with [a link](target.md).\n",
15
+ "target.md": "# Target\n",
16
+ };
17
+ const options = {
18
+ path: "note.md",
19
+ config: { extends: ["recommended", "github"] },
20
+ workspace: createWorkspace(files, { dialect: "github" }),
21
+ };
22
+
23
+ const diagnostics = lint(files["note.md"], options);
24
+ const result = format(files["note.md"], options);
25
+ // result: { output, changed, diagnostics }
26
+ ```
27
+
28
+ `lint(source, options)` returns the diagnostics, with the fields of the [JSON
29
+ report](cli.md#json-report). `format(source, options)` returns `output`, the
30
+ formatted text; `changed`, whether it differs; `diagnostics`, the findings that
31
+ remain in the output; and `scope` with [`ranges`](#part-of-a-document). The
32
+ edits of a rule that would change the document's meaning are left out and
33
+ reported as `engine/unsafe-format`, as the [safety
34
+ model](design.md#safety-model) describes, and a rule that fails is reported as
35
+ [`engine/rule-error`](plugins.md#rules). An invalid configuration, an unknown
36
+ rule, invalid rule options, or a plugin rule of the wrong shape throw an error.
37
+
38
+ | Option | Meaning |
39
+ | ---------------- | ------------------------------------------------------------------------------------------------------------------ |
40
+ | `path` | The document's path relative to the workspace root, with forward slashes. It selects overrides and resolves links. |
41
+ | `config` | The object a configuration file would hold. `plugins` there are not loaded; pass plugin objects instead. |
42
+ | `plugins` | [Plugin](plugins.md) objects. |
43
+ | `workspace` | The files that links can reach; see [workspaces](#workspaces). |
44
+ | `defaultDialect` | The dialect to assume when the configuration names none; see [the dialect](#the-dialect). |
45
+ | `ranges` | Limit the work to [part of a document](#part-of-a-document). |
46
+
47
+ Without `config`, the `recommended` preset applies. Positions count UTF-16 code
48
+ units in the text as given. A byte order mark at its start (U+FEFF, which some
49
+ editors write) is not part of what rules see, but it counts in the offsets that
50
+ `lint` and `format` report, so that their edits apply to the text as given, and
51
+ `format` keeps it in the output.
52
+
53
+ ## The dialect
54
+
55
+ A document is read in the dialect that its configuration names, through
56
+ `dialect`, a preset, or a matching override. When none does, `lint`, `format`,
57
+ and `resolveConfig` assume `defaultDialect`, or `github` without it;
58
+ `defaultDialect` accepts a dialect or an alias and never overrides a dialect
59
+ that the configuration names. The library reads no files, so it cannot recognize
60
+ a vault by itself. The CLI passes `obsidian` for a document with a `.obsidian`
61
+ directory in its own directory or one above it, and gives the workspace that
62
+ vault's `strictLineBreaks` setting; a host for vaults should do the same:
63
+
64
+ ```ts
65
+ // The host found `.obsidian/` at the root of the directory it read `files` from.
66
+ const options: ProcessOptions = { path: "Note.md", config, defaultDialect: "obsidian" };
67
+ const { dialect } = resolveConfig(options);
68
+ const workspace = createWorkspace(files, { dialect, strictLineBreaks });
69
+ const result = format(files["Note.md"], { ...options, workspace });
70
+ ```
71
+
72
+ `resolveConfig(options)` returns the settings in effect for a document:
73
+ `dialect`, `rules`, `ignore`, and `resolve`. It takes the `path`, `config`,
74
+ `plugins`, and `defaultDialect` of `lint` and `format`, so their options serve
75
+ as they are. The prereleases took these four as separate arguments; such a call
76
+ throws an error that says so.
77
+
78
+ ## Workspaces
79
+
80
+ `createWorkspace(files, options)` builds the workspace that link checks, link
81
+ rewriting, and Obsidian wrapping need; without one, none of them happen. `files`
82
+ maps each path, relative to the workspace root with forward slashes, to one of:
83
+
84
+ - the text of a Markdown document, whose headings are link targets when its name
85
+ ends in `.md` or `.markdown`, or `.md` for Obsidian links;
86
+ - `null` for a file that is not Markdown, such as an image, so that links to it
87
+ resolve;
88
+ - a synchronous loader, `() => string`, which is called only when a link's
89
+ fragment needs the document. The workspace then keeps the document's anchors,
90
+ not its text. The anchors of Obsidian, of GitHub and CommonMark, and of
91
+ Forgejo and Gitea differ, so a loader is called once more when the same target
92
+ is checked for another of these groups. A loader must return the same text
93
+ each time, so create a new workspace when files change. A loader that throws
94
+ an error whose `code` is `EACCES`, `EPERM`, or `EBUSY`, or
95
+ `ERR_ENCODING_INVALID_ENCODED_DATA`, which a `TextDecoder` created with
96
+ `fatal: true` throws for a file that is not valid in its encoding, makes a
97
+ link to a heading in that file resolve as `unreadable`, while a link to the
98
+ file itself still resolves; any other error propagates.
99
+
100
+ | `WorkspaceOptions` | Meaning |
101
+ | ------------------ | --------------------------------------------------------------------------------------------------------------------------- |
102
+ | `dialect` | The dialect that link targets are parsed in; `github` when omitted. Give the dialect of the documents that link into them. |
103
+ | `strictLineBreaks` | `true` when the vault's "Strict line breaks" setting is verified on. Obsidian notes are wrapped only then. |
104
+ | `directories` | Existing directories that hold no files; the others are known from `files`. A link to a directory resolves as `directory`. |
105
+ | `unreadable` | Directories whose contents the host could not read. A link that may lead into one resolves as `unreadable`. |
106
+ | `vaults` | Directories below the root that are Obsidian vaults. An Obsidian link in one of their notes reaches only the vault's files. |
107
+ | `rootInVault` | When the root is a directory inside a vault: its path from the vault's root, such as `Projects/Notes`. |
108
+ | `lookup` | `(path, folded) => result`: what is at a path that `files` and `directories` do not hold; see below. |
109
+
110
+ `lookup` lets a host leave files out of `files` and still have links to them
111
+ checked. It is called once per path, with `folded` true for an Obsidian link,
112
+ which matches without regard to case, and returns
113
+ `{ type: "file", path, source }`, `{ type: "directory" }`,
114
+ `{ type: "outside", through }` for a path that leads out of the workspace root
115
+ through the link `through`, `{ type: "unreadable", at }`, or `undefined` when
116
+ nothing is there. An Obsidian search by note name does not call it.
117
+
118
+ The CLI passes the directories it skipped as `unreadable`, the vaults it found
119
+ as `vaults`, and `rootInVault` when it finds the vault above the root; [Obsidian
120
+ vaults](obsidian.md#vaults-inside-a-larger-workspace) describes the effects. A
121
+ rule asks the workspace where a link leads; [resolve links in a
122
+ rule](plugins.md#resolve-links-in-a-rule) describes the result. A host can also
123
+ implement the `Workspace` interface itself.
124
+
125
+ ## Part of a document
126
+
127
+ An editor's "format selection", or an agent that changed one paragraph, can
128
+ limit `lint` and `format` to parts of a document with `ranges`: a list of
129
+ `{ start, end }` offsets into the source as given, with `end` exclusive. Each
130
+ range is widened to the whole blocks that it touches, such as a paragraph, a
131
+ heading, a table, or a code block, also inside lists and block quotes, where a
132
+ range inside one item selects only that item's paragraph. A selected block takes
133
+ in its whole lines and the line breaks around it, so that the blank lines next
134
+ to it can change. An empty range, a cursor position, selects the block on whose
135
+ lines it is; a range in a run of blank lines selects nothing.
136
+
137
+ ```ts
138
+ const source = "Legacy *word* stays.\n\nChange *this paragraph*.\n";
139
+ const start = source.indexOf("Change");
140
+ const result = format(source, { config, ranges: [{ start, end: start }] });
141
+ // result.output: "Legacy *word* stays.\n\nChange _this paragraph_.\n"
142
+ // result.scope: [{ start: 21, end: 47 }]: the paragraph and its line breaks
143
+ ```
144
+
145
+ The whole document is still parsed and checked, so link definitions, suppression
146
+ comments, and the semantic check work as without ranges. `lint` returns the
147
+ diagnostics that start in the selected parts. `format` applies only the edits
148
+ that lie wholly inside them and leaves the rest byte for byte as it was; an edit
149
+ that reaches outside is not applied, but its diagnostic is reported, and so is
150
+ every `engine/` diagnostic. `scope` gives the selected parts in the output's
151
+ positions, so it can be passed as `ranges` again. Without `ranges` the whole
152
+ document is processed, and an empty list processes nothing. A range outside the
153
+ source, with `start` after `end`, or with offsets that are not integers is an
154
+ error. The CLI has no option for ranges.
155
+
156
+ ## Files on disk
157
+
158
+ The `mdrefine/node` entry exports `discoverWorkspace`, the CLI's own discovery:
159
+
160
+ ```ts
161
+ import { lint } from "mdrefine";
162
+ import { discoverWorkspace } from "mdrefine/node";
163
+
164
+ const discovery = await discoverWorkspace({ root: "/path/to/documents", config });
165
+ for (const name of discovery.selected)
166
+ console.log(name, lint(discovery.read(name), discovery.options(name)));
167
+ ```
168
+
169
+ It finds the files below `root` and selects documents as the CLI does: it honors
170
+ Git's ignore rules and the configuration's `ignore` and `resolve` settings,
171
+ leaves out nested repositories, symbolic links, and what the system does not let
172
+ it read, and recognizes each document's vault. It reads no configuration file
173
+ and writes nothing: pass the configuration as `config`, plugin objects as
174
+ `plugins`, and write results yourself.
175
+
176
+ - `inputs` lists files, directories, and patterns to select, absolute or
177
+ relative to `root`, not to the current directory, as the CLI [selects
178
+ them](cli.md#which-files-are-processed): a file is selected whatever its name,
179
+ and an ignored file or directory is an entry of `skipped` with the code
180
+ `IGNORED`. Omitted, every document is selected; an empty list selects none.
181
+ - `exclude` lists files and directories to leave out, as paths, not patterns.
182
+ - `documents` gives text for paths relative to `root`, such as an editor's
183
+ unsaved buffer. Those documents are selected, and the workspace reads that
184
+ text in place of a file, also where no file exists. One whose path ignore
185
+ rules leave out is not selected and is an entry of `skipped`, as a named file
186
+ is.
187
+
188
+ The result has `root`, the root's real path; `selected`, the documents as sorted
189
+ forward-slash paths relative to it; and `skipped`, with the entries of the JSON
190
+ report's `skipped` list. `read(name)` returns a selected document's text, read
191
+ when first needed and then kept, so one result is a snapshot for one run. An
192
+ error with the code `EACCES`, `EPERM`, or `EBUSY` means the file could not be
193
+ read, and one with the code `ERR_ENCODING_INVALID_ENCODED_DATA` that it is not
194
+ [UTF-8 text](cli.md#files-that-cannot-be-read-or-written), as its message says,
195
+ such as `Not UTF-8 text: notes/old.md (byte 0xE9 on line 1 is not valid UTF-8)`.
196
+ `options(name)` returns what the CLI passes to `lint` and `format` for the
197
+ document. Both throw for a name that is not selected. A root or input that
198
+ cannot be used is an error in the CLI's words, such as
199
+ `Input does not exist: notes/absent.md`.
200
+
201
+ ## Other exports
202
+
203
+ - `parse(source, dialect, path, plugins)` returns a `Document`: `source`,
204
+ `path`, `dialect`, and `tree`, an
205
+ [mdast](https://github.com/syntax-tree/mdast) syntax tree. `document.source`
206
+ starts after any byte order mark, and `range(node)` gives a node's
207
+ `[start, end]` offsets into it. `textContent(node)` gives a node's text. An
208
+ alias such as `codeberg` reads the document in its dialect, which
209
+ `document.dialect` holds, and an unknown dialect is an error. A document whose
210
+ front matter would end inside a code block, an HTML block, or display math is
211
+ read without front matter, as the [safety model](design.md#safety-model)
212
+ describes. In the Obsidian dialect, the tree holds `wikiLink`,
213
+ `obsidianComment`, and `obsidianHighlight` nodes, typed as `WikiLink`,
214
+ `ObsidianComment`, and `ObsidianHighlight` and registered with mdast's node
215
+ types.
216
+ - `resolveConfig`, described above, and `validateConfig(value)`, which throws
217
+ when a value is not a valid configuration. `configSchema` is the
218
+ configuration's [JSON Schema](configuration.md#json-schema), which the package
219
+ also ships as `mdrefine/config.schema.json`. `presets` holds the built-in
220
+ presets.
221
+ - `dialects` lists the dialect names, `dialectAliases` maps aliases such as
222
+ `codeberg` to them, and `canonicalDialect(name)` resolves an alias.
223
+ - `builtInRules` holds the built-in rule objects, and `ruleRegistry(plugins)`
224
+ returns a new table that adds the rules of plugins under their IDs.
225
+ - `applyEdits(source, edits)` applies `{ start, end, text }` edits, which must
226
+ not overlap. `semanticFingerprint(document, workspace)` is the comparison that
227
+ `format` uses to refuse a change of meaning.
228
+
229
+ `presets`, `builtInRules`, `dialects`, `dialectAliases`, and `configSchema` are
230
+ shared by every caller in the process, so they are frozen and typed read-only;
231
+ copy what you want to change. Each export has a doc comment, and the TypeScript
232
+ types of these values are exported too, among them `Config`, `ProcessOptions`,
233
+ `FormatResult`, `Diagnostic`, `Plugin`, `Rule`, `JsonSchema`, `Workspace`,
234
+ `WorkspaceOptions`, and `LinkResolution`; `mdrefine/node` exports
235
+ `DiscoverOptions`, `WorkspaceDiscovery`, and `Skipped`. The type declarations
236
+ depend on `@types/mdast`, which the package installs.
237
+
238
+ Both entries are ES modules. A CommonJS module can load them with `require()`,
239
+ which the supported Node.js versions allow for ES modules.