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