mdrefine 0.2.0-rc.2 → 0.2.0-rc.3
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.
- package/CHANGELOG.md +509 -0
- package/CODE_OF_CONDUCT.md +30 -0
- package/CONTRIBUTING.md +188 -16
- package/README.md +100 -154
- package/SECURITY.md +15 -13
- package/config.schema.json +91 -0
- package/dist/cli/main.js +261 -148
- package/dist/cli/main.js.map +1 -1
- package/dist/config/load.js +67 -6
- package/dist/config/load.js.map +1 -1
- package/dist/config/problems.d.ts +7 -0
- package/dist/config/problems.js +208 -0
- package/dist/config/problems.js.map +1 -0
- package/dist/config/resolve.d.ts +42 -109
- package/dist/config/resolve.js +193 -52
- package/dist/config/resolve.js.map +1 -1
- package/dist/core/directives.d.ts +7 -0
- package/dist/core/directives.js +25 -13
- package/dist/core/directives.js.map +1 -1
- package/dist/core/engine.d.ts +42 -4
- package/dist/core/engine.js +518 -79
- package/dist/core/engine.js.map +1 -1
- package/dist/core/frozen.d.ts +6 -0
- package/dist/core/frozen.js +15 -0
- package/dist/core/frozen.js.map +1 -0
- package/dist/core/scope.d.ts +30 -0
- package/dist/core/scope.js +122 -0
- package/dist/core/scope.js.map +1 -0
- package/dist/core/types.d.ts +202 -23
- package/dist/core/types.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js.map +1 -1
- package/dist/node.d.ts +3 -0
- package/dist/node.js +4 -0
- package/dist/node.js.map +1 -0
- package/dist/rules/blocks.d.ts +34 -0
- package/dist/rules/blocks.js +538 -0
- package/dist/rules/blocks.js.map +1 -0
- package/dist/rules/content.d.ts +2 -0
- package/dist/rules/content.js +85 -0
- package/dist/rules/content.js.map +1 -0
- package/dist/rules/dialects.js +10 -6
- package/dist/rules/dialects.js.map +1 -1
- package/dist/rules/links.js +302 -6
- package/dist/rules/links.js.map +1 -1
- package/dist/rules/lists.d.ts +2 -0
- package/dist/rules/lists.js +924 -0
- package/dist/rules/lists.js.map +1 -0
- package/dist/rules/structure.js +127 -1
- package/dist/rules/structure.js.map +1 -1
- package/dist/rules/style.d.ts +8 -2
- package/dist/rules/style.js +1118 -165
- package/dist/rules/style.js.map +1 -1
- package/dist/syntax/block-identifier.d.ts +18 -0
- package/dist/syntax/block-identifier.js +24 -0
- package/dist/syntax/block-identifier.js.map +1 -0
- package/dist/syntax/obsidian.d.ts +29 -11
- package/dist/syntax/obsidian.js +9 -10
- package/dist/syntax/obsidian.js.map +1 -1
- package/dist/syntax/parse.d.ts +74 -3
- package/dist/syntax/parse.js +227 -19
- package/dist/syntax/parse.js.map +1 -1
- package/dist/workspace/access.d.ts +12 -3
- package/dist/workspace/access.js +33 -5
- package/dist/workspace/access.js.map +1 -1
- package/dist/workspace/discovery.d.ts +80 -0
- package/dist/workspace/discovery.js +143 -0
- package/dist/workspace/discovery.js.map +1 -0
- package/dist/workspace/encoding.d.ts +24 -0
- package/dist/workspace/encoding.js +113 -0
- package/dist/workspace/encoding.js.map +1 -0
- package/dist/workspace/files.d.ts +76 -6
- package/dist/workspace/files.js +639 -59
- package/dist/workspace/files.js.map +1 -1
- package/dist/workspace/git.d.ts +54 -0
- package/dist/workspace/git.js +198 -0
- package/dist/workspace/git.js.map +1 -0
- package/dist/workspace/index.d.ts +58 -3
- package/dist/workspace/index.js +136 -37
- package/dist/workspace/index.js.map +1 -1
- package/dist/workspace/selection.d.ts +2 -2
- package/dist/workspace/selection.js +3 -3
- package/dist/workspace/selection.js.map +1 -1
- package/dist/workspace/slug.js +8 -1
- package/dist/workspace/slug.js.map +1 -1
- package/dist/workspace/vault.js +3 -1
- package/dist/workspace/vault.js.map +1 -1
- package/docs/api.md +237 -0
- package/docs/benchmarks.md +139 -199
- package/docs/changelog.md +1 -0
- package/docs/cli.md +271 -89
- package/docs/configuration.md +193 -480
- package/docs/design.md +158 -284
- package/docs/dialects.md +169 -0
- package/docs/examples.md +151 -109
- package/docs/index.md +21 -37
- package/docs/integrations.md +138 -0
- package/docs/obsidian.md +91 -115
- package/docs/plugins.md +147 -212
- package/docs/quick-start.md +68 -74
- package/docs/rules.md +1230 -0
- package/docs/versions.md +56 -0
- package/examples/README.md +17 -14
- package/examples/automation/summarize-report.mjs +5 -2
- package/examples/library/lint-folder.mjs +5 -4
- package/examples/library/lint-workspace.mjs +32 -0
- package/examples/plugin/notes/release-checklist.md +2 -2
- package/examples/plugin/rules.mjs +17 -23
- package/examples/workspaces/obsidian-vault/after/mdtools.config.jsonc +2 -2
- package/examples/workspaces/obsidian-vault/before/mdtools.config.jsonc +2 -2
- package/examples/workspaces/repository-docs/after/README.md +2 -2
- package/examples/workspaces/repository-docs/after/mdtools.config.jsonc +0 -2
- package/examples/workspaces/repository-docs/before/README.md +2 -2
- package/examples/workspaces/repository-docs/before/docs/guide.md +7 -7
- package/examples/workspaces/repository-docs/before/docs/reference.md +3 -1
- package/examples/workspaces/repository-docs/before/mdtools.config.jsonc +0 -2
- package/package.json +37 -11
- package/docs/contributing.md +0 -218
- package/docs/releases/0.1.0-beta.1.md +0 -39
- package/docs/releases/0.1.0-beta.2.md +0 -83
- package/docs/releases/0.1.0-beta.3.md +0 -61
- package/docs/releases/0.1.0-beta.4.md +0 -50
- package/docs/releases/0.1.0-rc.1.md +0 -99
- package/docs/releases/0.2.0-rc.1.md +0 -141
- package/docs/releases/0.2.0-rc.2.md +0 -225
- package/docs/releases.md +0 -144
- package/docs/testing.md +0 -93
package/dist/cli/main.js
CHANGED
|
@@ -1,151 +1,181 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { Command } from "commander";
|
|
2
|
+
import { Command, CommanderError, InvalidArgumentError, Option } from "commander";
|
|
3
3
|
import { createTwoFilesPatch } from "diff";
|
|
4
|
-
import {
|
|
4
|
+
import { readFile, realpath } from "node:fs/promises";
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
7
|
import { format, lint, ruleRegistry } from "../core/engine.js";
|
|
8
8
|
import { loadConfig } from "../config/load.js";
|
|
9
|
-
import { resolveConfig } from "../config/resolve.js";
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
9
|
+
import { dialectAliases, dialects, resolveConfig } from "../config/resolve.js";
|
|
10
|
+
import { replacementFailure, replacementProblem, skippedMessage, writeFormatted, } from "../workspace/files.js";
|
|
11
|
+
import { assumedDialect, checkRoot, discoverFrom } from "../workspace/discovery.js";
|
|
12
|
+
import { enclosingVault } from "../workspace/vault.js";
|
|
13
|
+
import { refusal, refusalText } from "../workspace/access.js";
|
|
14
|
+
import { decodeText, NotUtf8Error } from "../workspace/encoding.js";
|
|
15
15
|
const manifest = JSON.parse(await readFile(fileURLToPath(new URL("../../package.json", import.meta.url)), "utf8"));
|
|
16
|
+
// Subcommands inherit these settings, so they come before the commands.
|
|
16
17
|
const program = new Command()
|
|
17
18
|
.name("mdtools")
|
|
18
19
|
.description("Lint and format Markdown with explicit dialects and configurable rules.")
|
|
19
|
-
.version(manifest.version)
|
|
20
|
-
|
|
20
|
+
.version(manifest.version, "-V, --version", "Print the version")
|
|
21
|
+
.helpOption("-h, --help", "Print help for the command")
|
|
22
|
+
.helpCommand("help [command]", "Print help for a command")
|
|
23
|
+
// Commander starts its messages with "error: ", the tool with its name.
|
|
24
|
+
.configureOutput({
|
|
25
|
+
outputError: (text, write) => write(`mdtools: ${text.replace(/^error: /, "")}`),
|
|
26
|
+
})
|
|
27
|
+
// Throw instead of exiting, so that a usage error can end with code 2.
|
|
28
|
+
.exitOverride();
|
|
29
|
+
const configOption = [
|
|
30
|
+
"--config <file>",
|
|
31
|
+
"Use this JSON, JSONC, or JavaScript configuration file instead of searching for one",
|
|
32
|
+
];
|
|
33
|
+
/** The dialects, each with its other names, as in "forgejo (alias: codeberg)". */
|
|
34
|
+
const dialectList = dialects
|
|
35
|
+
.map((dialect) => {
|
|
36
|
+
const aliases = Object.entries(dialectAliases).flatMap(([name, of]) => of === dialect ? [name] : []);
|
|
37
|
+
return aliases.length ? `${dialect} (alias: ${aliases.join(", ")})` : dialect;
|
|
38
|
+
})
|
|
39
|
+
.join(", ");
|
|
40
|
+
/** The options that choose the configuration, the workspace, and the dialect. */
|
|
41
|
+
function workspaceOptions(command) {
|
|
21
42
|
return command
|
|
22
|
-
.option(
|
|
23
|
-
.option("--root <directory>", "Workspace root (default: configuration directory or
|
|
24
|
-
.option("--dialect <dialect>",
|
|
25
|
-
.
|
|
43
|
+
.option(...configOption)
|
|
44
|
+
.option("--root <directory>", "Workspace root (default: the configuration file's directory; without one, the Git repository or vault where the search stopped, else the current directory)")
|
|
45
|
+
.option("--dialect <dialect>", `${dialectList} (when none is named: github, or obsidian inside a vault)`, (value) => {
|
|
46
|
+
if (!Object.hasOwn(dialectAliases, value) && !dialects.includes(value))
|
|
47
|
+
throw new InvalidArgumentError(`The dialects are ${dialectList}.`);
|
|
48
|
+
return value;
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
function common(command) {
|
|
52
|
+
return (workspaceOptions(command)
|
|
53
|
+
.option("--exclude <path>", "Exclude an exact file or directory (repeatable)", (value, previous = []) => [...previous, value])
|
|
26
54
|
.option("--json", "Machine-readable output")
|
|
55
|
+
.option("--quiet", "Report errors only: leave warnings out of the report and its counts")
|
|
27
56
|
.option("--stdin-filepath <file>", "Filename context for stdin")
|
|
28
57
|
.option("--max-warnings <count>", "Fail when warning count exceeds this nonnegative integer", (value) => {
|
|
29
58
|
const number = Number(value);
|
|
30
59
|
if (!Number.isInteger(number) || number < 0)
|
|
31
60
|
throw new Error("--max-warnings must be a nonnegative integer.");
|
|
32
61
|
return number;
|
|
33
|
-
})
|
|
62
|
+
})
|
|
63
|
+
// Not an option: `run` answers it with the option that does what it asks for.
|
|
64
|
+
.addOption(new Option("--fix").hideHelp()));
|
|
34
65
|
}
|
|
35
66
|
/**
|
|
36
|
-
* The
|
|
37
|
-
*
|
|
38
|
-
* line break and holds wikilinks, both of which any other dialect lets reflow
|
|
39
|
-
* change, so a document that a vault holds is read as a note.
|
|
67
|
+
* The configuration and workspace root that the options name. `--dialect` is
|
|
68
|
+
* applied to a copy, because an .mjs configuration may export a frozen object.
|
|
40
69
|
*/
|
|
41
|
-
function
|
|
42
|
-
|
|
70
|
+
async function workspace(flags) {
|
|
71
|
+
// Without this check the first thing to fail would be the search for a
|
|
72
|
+
// configuration file in the root, with a message about a file the user never named.
|
|
73
|
+
if (flags.root !== undefined)
|
|
74
|
+
await checkRoot(flags.root);
|
|
75
|
+
const loaded = await loadConfig(path.resolve(flags.root ?? "."), flags.config);
|
|
76
|
+
return {
|
|
77
|
+
...loaded,
|
|
78
|
+
root: path.resolve(flags.root ?? loaded.root),
|
|
79
|
+
config: flags.dialect ? { ...loaded.config, dialect: flags.dialect } : loaded.config,
|
|
80
|
+
};
|
|
43
81
|
}
|
|
44
82
|
/**
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
* configuration file in it, with a message about a file the user never named.
|
|
83
|
+
* The real path of `file`, which need not exist: the real path of the nearest
|
|
84
|
+
* directory above it that does, with the rest of `file` appended.
|
|
48
85
|
*/
|
|
49
|
-
async function
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
86
|
+
async function located(file) {
|
|
87
|
+
const rest = [];
|
|
88
|
+
for (let current = path.resolve(file);;) {
|
|
89
|
+
try {
|
|
90
|
+
return path.join(await realpath(current), ...rest);
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
const parent = path.dirname(current);
|
|
94
|
+
if (parent === current)
|
|
95
|
+
return path.resolve(file);
|
|
96
|
+
rest.unshift(path.basename(current));
|
|
97
|
+
current = parent;
|
|
98
|
+
}
|
|
60
99
|
}
|
|
61
100
|
}
|
|
101
|
+
/**
|
|
102
|
+
* The path of `file`, which need not exist, from the workspace root, with
|
|
103
|
+
* forward slashes, or undefined when it lies outside. Both are located through
|
|
104
|
+
* their real paths, as discovery locates files, so that a root reached through
|
|
105
|
+
* a link still holds the files below it.
|
|
106
|
+
*/
|
|
107
|
+
async function workspacePath(root, file) {
|
|
108
|
+
const actual = await realpath(root).catch(() => root);
|
|
109
|
+
const name = path
|
|
110
|
+
.relative(actual, await located(file))
|
|
111
|
+
.split(path.sep)
|
|
112
|
+
.join("/");
|
|
113
|
+
return name === ".." || name.startsWith("../") || path.isAbsolute(name) ? undefined : name;
|
|
114
|
+
}
|
|
115
|
+
/** The library's diagnostics for a document that it could not process as configured. */
|
|
116
|
+
const failures = new Set(["engine/unsafe-format", "engine/rule-error"]);
|
|
62
117
|
async function run(mode, inputs, flags) {
|
|
118
|
+
// ESLint and markdownlint-cli2 fix with --fix, Prettier with --write.
|
|
119
|
+
if (flags.fix)
|
|
120
|
+
throw new Error(mode === "lint"
|
|
121
|
+
? "unknown option '--fix': lint never changes files; 'mdtools format --write' applies the fixes."
|
|
122
|
+
: "unknown option '--fix': format writes its changes with --write.");
|
|
63
123
|
if ([flags.check, flags.diff, flags.write].filter(Boolean).length > 1)
|
|
64
124
|
throw new Error("Choose only one of --check, --diff, and --write.");
|
|
65
125
|
if (inputs.includes("-") && flags.exclude?.length)
|
|
66
126
|
throw new Error("--exclude cannot be combined with stdin.");
|
|
67
|
-
await
|
|
68
|
-
const loaded = await loadConfig(path.resolve(flags.root ?? "."), flags.config);
|
|
69
|
-
const root = path.resolve(flags.root ?? loaded.root);
|
|
70
|
-
if (flags.dialect)
|
|
71
|
-
loaded.config.dialect = flags.dialect;
|
|
72
|
-
// Which files to leave out does not depend on a document's dialect.
|
|
73
|
-
const resolved = resolveConfig(loaded.config, "document.md", loaded.plugins);
|
|
127
|
+
const { root, ...loaded } = await workspace(flags);
|
|
74
128
|
const stdin = inputs.includes("-");
|
|
75
129
|
if (stdin && (inputs.length !== 1 || flags.write))
|
|
76
130
|
throw new Error("stdin must be the only input and cannot be combined with --write.");
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
/** The vault of the document read from stdin, found by a search of its own. */
|
|
80
|
-
let named;
|
|
131
|
+
/** The text read from stdin, by the name it is given in the workspace. */
|
|
132
|
+
let documents;
|
|
81
133
|
if (stdin) {
|
|
82
|
-
const name = path
|
|
83
|
-
|
|
84
|
-
.split(path.sep)
|
|
85
|
-
.join("/");
|
|
86
|
-
if (name.startsWith("../") || path.isAbsolute(name))
|
|
134
|
+
const name = await workspacePath(root, flags.stdinFilepath ?? path.join(root, "stdin.md"));
|
|
135
|
+
if (name === undefined)
|
|
87
136
|
throw new Error("--stdin-filepath must be within the workspace.");
|
|
88
|
-
|
|
89
|
-
process.stdin.setEncoding("utf8");
|
|
137
|
+
const chunks = [];
|
|
90
138
|
for await (const chunk of process.stdin)
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
139
|
+
chunks.push(chunk);
|
|
140
|
+
try {
|
|
141
|
+
documents = { [name]: decodeText(Buffer.concat(chunks), "stdin") };
|
|
142
|
+
}
|
|
143
|
+
catch (error) {
|
|
144
|
+
// There is no file to report it for, and nothing else to process.
|
|
145
|
+
if (error instanceof NotUtf8Error)
|
|
146
|
+
throw new Error(`stdin is not UTF-8 text (${error.reason})`, { cause: error });
|
|
147
|
+
throw error;
|
|
148
|
+
}
|
|
96
149
|
}
|
|
97
|
-
//
|
|
98
|
-
|
|
150
|
+
// Paths on the command line are relative to the working directory, and no
|
|
151
|
+
// path selects the whole workspace; stdin is the only document then.
|
|
152
|
+
const discovery = await discoverFrom({
|
|
153
|
+
root,
|
|
154
|
+
...(stdin ? { inputs: [] } : inputs.length ? { inputs } : {}),
|
|
155
|
+
exclude: flags.exclude ?? [],
|
|
156
|
+
config: loaded.config,
|
|
157
|
+
plugins: loaded.plugins,
|
|
158
|
+
...(documents ? { documents } : {}),
|
|
159
|
+
}, process.cwd());
|
|
99
160
|
const reports = [];
|
|
100
161
|
const changes = [];
|
|
101
|
-
// Selected files that the system refused to let the tool read.
|
|
162
|
+
// Selected files that the system refused to let the tool read, or that are not UTF-8 text.
|
|
102
163
|
const unread = [];
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
// One index per active dialect; build lazily for mixed documentation workspaces.
|
|
106
|
-
const indexes = new Map();
|
|
107
|
-
// The same index with the line-break setting of each vault that has documents in it.
|
|
108
|
-
const views = new Map();
|
|
109
|
-
for (const name of set.selected) {
|
|
110
|
-
const vault = stdin ? named : vaultOf(set.vaults, name);
|
|
111
|
-
const assumed = assumedDialect(vault);
|
|
112
|
-
const config = resolveConfig(loaded.config, name, loaded.plugins, assumed);
|
|
113
|
-
let workspace = indexes.get(config.dialect);
|
|
114
|
-
if (!workspace)
|
|
115
|
-
indexes.set(config.dialect, (workspace = createWorkspace(set.files, {
|
|
116
|
-
dialect: config.dialect,
|
|
117
|
-
directories: set.directories,
|
|
118
|
-
unreadable,
|
|
119
|
-
// The vault at or above the root holds the whole workspace and needs no entry.
|
|
120
|
-
vaults: set.vaults.flatMap((item) => (item.path ? [item.path] : [])),
|
|
121
|
-
...(enclosing?.rootInVault ? { rootInVault: enclosing.rootInVault } : {}),
|
|
122
|
-
})));
|
|
123
|
-
// Whether a note may be reflowed is a setting of its own vault.
|
|
124
|
-
if (vault?.strictLineBreaks !== undefined) {
|
|
125
|
-
let ofDialect = views.get(config.dialect);
|
|
126
|
-
if (!ofDialect)
|
|
127
|
-
views.set(config.dialect, (ofDialect = new Map()));
|
|
128
|
-
const index = workspace;
|
|
129
|
-
workspace = ofDialect.get(vault);
|
|
130
|
-
if (!workspace)
|
|
131
|
-
ofDialect.set(vault, (workspace = { ...index, strictLineBreaks: vault.strictLineBreaks }));
|
|
132
|
-
}
|
|
133
|
-
const options = {
|
|
134
|
-
path: name,
|
|
135
|
-
config: loaded.config,
|
|
136
|
-
plugins: loaded.plugins,
|
|
137
|
-
workspace,
|
|
138
|
-
...(assumed ? { defaultDialect: assumed } : {}),
|
|
139
|
-
};
|
|
140
|
-
const value = set.files[name];
|
|
164
|
+
for (const name of discovery.selected) {
|
|
165
|
+
const options = discovery.options(name);
|
|
141
166
|
let source;
|
|
142
167
|
try {
|
|
143
|
-
source =
|
|
168
|
+
source = discovery.read(name);
|
|
144
169
|
}
|
|
145
170
|
catch (error) {
|
|
146
171
|
// Report the file and carry on with the others; any other error is a fault.
|
|
147
172
|
const code = refusal(error);
|
|
148
|
-
|
|
173
|
+
const problem = code
|
|
174
|
+
? `could not be read (${refusalText(code)})`
|
|
175
|
+
: error instanceof NotUtf8Error
|
|
176
|
+
? `is not UTF-8 text (${error.reason})`
|
|
177
|
+
: undefined;
|
|
178
|
+
if (!problem)
|
|
149
179
|
throw error;
|
|
150
180
|
unread.push(name);
|
|
151
181
|
reports.push({
|
|
@@ -154,7 +184,7 @@ async function run(mode, inputs, flags) {
|
|
|
154
184
|
diagnostics: [
|
|
155
185
|
{
|
|
156
186
|
start: 0,
|
|
157
|
-
message: `The file
|
|
187
|
+
message: `The file ${problem} and was not ${mode === "lint" ? "linted" : "formatted"}.`,
|
|
158
188
|
rule: "engine/unreadable-file",
|
|
159
189
|
severity: "error",
|
|
160
190
|
line: 1,
|
|
@@ -164,8 +194,6 @@ async function run(mode, inputs, flags) {
|
|
|
164
194
|
});
|
|
165
195
|
continue;
|
|
166
196
|
}
|
|
167
|
-
if (source === null)
|
|
168
|
-
continue;
|
|
169
197
|
if (mode === "lint")
|
|
170
198
|
reports.push({ path: name, diagnostics: lint(source, options) });
|
|
171
199
|
else {
|
|
@@ -180,89 +208,174 @@ async function run(mode, inputs, flags) {
|
|
|
180
208
|
changes.push({ file: name, before: source, after: result.output });
|
|
181
209
|
}
|
|
182
210
|
}
|
|
183
|
-
// A
|
|
184
|
-
//
|
|
211
|
+
// A document that could not be read, had a rule refused or failing on it, or
|
|
212
|
+
// could not be written is a failure to operate, reported for its own file,
|
|
213
|
+
// and the run ends with status 2. Files do not depend on one another's
|
|
214
|
+
// content, so every other changed document is still written, and so is the
|
|
215
|
+
// result of one with a refused or failing rule, which the engine checked.
|
|
216
|
+
const failed = unread.length > 0 ||
|
|
217
|
+
reports.some((report) => report.diagnostics.some((item) => failures.has(item.rule)));
|
|
218
|
+
// Changed files that could not be written, each reported for its file.
|
|
219
|
+
const unwritten = [];
|
|
220
|
+
const refuse = (file, message) => {
|
|
221
|
+
unwritten.push(file);
|
|
222
|
+
reports
|
|
223
|
+
.find((report) => report.path === file)
|
|
224
|
+
.diagnostics.unshift({
|
|
225
|
+
start: 0,
|
|
226
|
+
message: `${message}.`,
|
|
227
|
+
rule: "engine/unwritable-file",
|
|
228
|
+
severity: "error",
|
|
229
|
+
line: 1,
|
|
230
|
+
column: 1,
|
|
231
|
+
});
|
|
232
|
+
};
|
|
233
|
+
// The files that were written, in the order of the report.
|
|
234
|
+
const written = [];
|
|
235
|
+
if (flags.write)
|
|
236
|
+
for (const change of changes) {
|
|
237
|
+
const file = path.join(discovery.root, change.file);
|
|
238
|
+
// A file that may not be written, such as a read-only one, is left alone.
|
|
239
|
+
const problem = await replacementProblem(file);
|
|
240
|
+
if (problem) {
|
|
241
|
+
refuse(change.file, `${problem}; the file is unchanged`);
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
try {
|
|
245
|
+
await writeFormatted(file, change.before, change.after);
|
|
246
|
+
written.push(change.file);
|
|
247
|
+
}
|
|
248
|
+
catch (error) {
|
|
249
|
+
const failure = replacementFailure(error);
|
|
250
|
+
if (!failure)
|
|
251
|
+
throw error;
|
|
252
|
+
refuse(change.file, failure);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
// As in ESLint, --quiet leaves warnings out of the report and of what counts them.
|
|
256
|
+
if (flags.quiet)
|
|
257
|
+
for (const report of reports)
|
|
258
|
+
report.diagnostics = report.diagnostics.filter((item) => item.severity !== "warn");
|
|
185
259
|
const diagnostics = reports.flatMap((report) => report.diagnostics);
|
|
186
260
|
const count = (severity) => diagnostics.filter((item) => item.severity === severity).length;
|
|
187
|
-
const unsafe = unread.length > 0 ||
|
|
188
|
-
reports.some((report) => report.diagnostics.some((item) => item.rule === "engine/unsafe-format"));
|
|
189
|
-
if (flags.write && !unsafe)
|
|
190
|
-
for (const change of changes)
|
|
191
|
-
await writeAtomic(path.join(set.root, change.file), change.before, change.after);
|
|
192
261
|
if (flags.json)
|
|
193
262
|
process.stdout.write(JSON.stringify({
|
|
194
263
|
version: manifest.version,
|
|
195
264
|
mode,
|
|
265
|
+
// The files' paths are relative to it, for programs to join.
|
|
266
|
+
root,
|
|
196
267
|
files: reports,
|
|
197
|
-
written
|
|
198
|
-
...(
|
|
268
|
+
written,
|
|
269
|
+
...(discovery.skipped.length ? { skipped: discovery.skipped } : {}),
|
|
199
270
|
}, null, 2) + "\n");
|
|
200
271
|
else {
|
|
272
|
+
// Text names documents from the current directory, as Prettier does, so
|
|
273
|
+
// that a terminal or an editor opens them from where the command ran.
|
|
274
|
+
const here = await realpath(process.cwd()).catch(() => process.cwd());
|
|
275
|
+
const shown = (name) => path.relative(here, path.join(discovery.root, name)).split(path.sep).join("/");
|
|
201
276
|
// Warnings about the run, not about a document: they do not change the exit status.
|
|
202
|
-
for (const item of
|
|
203
|
-
process.stderr.write(`mdtools: warning: ${item
|
|
277
|
+
for (const item of discovery.skipped)
|
|
278
|
+
process.stderr.write(`mdtools: warning: ${skippedMessage(item, shown)}\n`);
|
|
204
279
|
if (stdin && mode === "format" && !flags.check && !flags.diff)
|
|
205
280
|
process.stdout.write(reports[0]?.output ?? "");
|
|
206
281
|
else if (mode === "format" && !flags.check && !flags.write)
|
|
207
282
|
for (const change of changes)
|
|
208
|
-
process.stdout.write(createTwoFilesPatch(`a/${change.file}`, `b/${change.file}`, change.before, change.after));
|
|
283
|
+
process.stdout.write(createTwoFilesPatch(`a/${shown(change.file)}`, `b/${shown(change.file)}`, change.before, change.after));
|
|
209
284
|
for (const report of reports)
|
|
210
285
|
for (const item of report.diagnostics)
|
|
211
|
-
process.stderr.write(`${report.path}:${item.line}:${item.column}: ${item.severity} ${item.rule}: ${item.message}\n`);
|
|
286
|
+
process.stderr.write(`${shown(report.path)}:${item.line}:${item.column}: ${item.severity} ${item.rule}: ${item.message}\n`);
|
|
287
|
+
// The files to fix when a check fails, as Prettier's --check lists them.
|
|
288
|
+
if (mode === "format" && flags.check && !stdin)
|
|
289
|
+
for (const change of changes)
|
|
290
|
+
process.stderr.write(`${shown(change.file)}\n`);
|
|
212
291
|
if (!stdin) {
|
|
213
292
|
// Lint changes nothing, so its summary counts what it reported instead.
|
|
214
293
|
const outcome = mode === "lint"
|
|
215
294
|
? `${count("error")} error(s), ${count("warn")} warning(s)`
|
|
216
|
-
:
|
|
217
|
-
|
|
295
|
+
: flags.write
|
|
296
|
+
? `${written.length} written`
|
|
297
|
+
: `${changes.length} would change`;
|
|
298
|
+
process.stderr.write(`${discovery.selected.length - unread.length} file(s) ${mode === "lint" ? "linted" : "processed"}; ${outcome}${unread.length ? `; ${unread.length} could not be read` : ""}${unwritten.length ? `; ${unwritten.length} could not be written` : ""}.\n`);
|
|
218
299
|
}
|
|
219
300
|
}
|
|
220
301
|
const errors = count("error") > 0;
|
|
221
302
|
const tooManyWarnings = flags.maxWarnings !== undefined && count("warn") > flags.maxWarnings;
|
|
222
|
-
process.exitCode =
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
303
|
+
process.exitCode =
|
|
304
|
+
failed || unwritten.length > 0
|
|
305
|
+
? 2
|
|
306
|
+
: errors || tooManyWarnings || (flags.check && changes.length > 0)
|
|
307
|
+
? 1
|
|
308
|
+
: 0;
|
|
227
309
|
}
|
|
228
|
-
common(program
|
|
229
|
-
|
|
230
|
-
.
|
|
310
|
+
common(program
|
|
311
|
+
.command("lint")
|
|
312
|
+
.argument("[paths...]", "Files, directories, or glob patterns to select, or - for stdin"))
|
|
313
|
+
.description("Report the findings of the enabled rules; never writes")
|
|
314
|
+
.action((inputs, flags) => run("lint", inputs, flags));
|
|
315
|
+
common(program
|
|
316
|
+
.command("format")
|
|
317
|
+
.argument("[paths...]", "Files, directories, or glob patterns to select, or - for stdin"))
|
|
318
|
+
.description("Format documents: print a diff (the default), check, or write")
|
|
319
|
+
.option("--check", "Fail if formatting changes would be needed, and name the files")
|
|
231
320
|
.option("--diff", "Preview the complete diff (default for file inputs)")
|
|
232
321
|
.option("--write", "Write validated formatting changes")
|
|
233
322
|
.action((inputs, flags) => run("format", inputs, flags));
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
const
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
//
|
|
246
|
-
// upward from where it really is.
|
|
323
|
+
// The options belong to `config`, which reads them before or after `explain`;
|
|
324
|
+
// `explain` repeats them so that its help lists them.
|
|
325
|
+
workspaceOptions(workspaceOptions(program.command("config").description("Inspect the configuration"))
|
|
326
|
+
.command("explain")
|
|
327
|
+
.description("Print, as JSON, the configuration in effect for one document")
|
|
328
|
+
.argument("<file>", "The document, which need not exist")).action(async (file, _flags, command) => {
|
|
329
|
+
const flags = command.optsWithGlobals();
|
|
330
|
+
const { root, config, plugins, file: source } = await workspace(flags);
|
|
331
|
+
const name = await workspacePath(root, file);
|
|
332
|
+
if (name === undefined)
|
|
333
|
+
throw new Error(`Input is outside the workspace: ${file}`);
|
|
334
|
+
// The search for the document's vault starts from the root's real path too.
|
|
247
335
|
const actual = await realpath(root).catch(() => root);
|
|
248
336
|
process.stdout.write(JSON.stringify({
|
|
249
337
|
root,
|
|
250
|
-
|
|
251
|
-
|
|
338
|
+
config: source ?? null,
|
|
339
|
+
path: name,
|
|
340
|
+
effective: resolveConfig({
|
|
341
|
+
config,
|
|
342
|
+
path: name,
|
|
343
|
+
plugins,
|
|
344
|
+
defaultDialect: assumedDialect(await enclosingVault(path.join(actual, name))),
|
|
345
|
+
}),
|
|
252
346
|
}, null, 2) + "\n");
|
|
253
347
|
});
|
|
254
348
|
program
|
|
255
349
|
.command("rules")
|
|
256
|
-
.description("List built-in rules")
|
|
257
|
-
.
|
|
258
|
-
|
|
259
|
-
|
|
350
|
+
.description("List the built-in rules and those of the configuration's plugins")
|
|
351
|
+
.option(...configOption)
|
|
352
|
+
.option("--json", "Print the list as JSON")
|
|
353
|
+
.action(async (flags) => {
|
|
354
|
+
const { plugins } = await loadConfig(path.resolve("."), flags.config);
|
|
355
|
+
const rules = Object.entries(ruleRegistry(plugins)).map(([id, rule]) => ({
|
|
356
|
+
id,
|
|
357
|
+
kind: rule.kind,
|
|
358
|
+
description: rule.description,
|
|
359
|
+
}));
|
|
360
|
+
if (flags.json)
|
|
361
|
+
process.stdout.write(JSON.stringify({ rules }, null, 2) + "\n");
|
|
362
|
+
else
|
|
363
|
+
for (const rule of rules)
|
|
364
|
+
// One line per rule, even when a plugin's description has line breaks or tabs.
|
|
365
|
+
process.stdout.write(`${rule.id}\t${rule.kind}\t${rule.description.replace(/\s+/g, " ")}\n`);
|
|
260
366
|
});
|
|
261
367
|
try {
|
|
262
368
|
await program.parseAsync();
|
|
263
369
|
}
|
|
264
370
|
catch (error) {
|
|
265
|
-
|
|
266
|
-
|
|
371
|
+
// Commander has printed the help, the version, or what is wrong with the
|
|
372
|
+
// command line. Help and the version succeed; a usage error ends with code 2,
|
|
373
|
+
// as in ESLint and Prettier, so that it does not pass for findings.
|
|
374
|
+
if (error instanceof CommanderError)
|
|
375
|
+
process.exitCode = error.exitCode === 0 ? 0 : 2;
|
|
376
|
+
else {
|
|
377
|
+
process.stderr.write(`mdtools: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
378
|
+
process.exitCode = 2;
|
|
379
|
+
}
|
|
267
380
|
}
|
|
268
381
|
//# sourceMappingURL=main.js.map
|