@jpeakai/mermaidjs-lint 0.6.3 → 0.7.1
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/README.md +39 -105
- package/dist/bin.js +57 -9
- package/dist/index.js +57 -9
- package/dist/testing.js +29 -2
- package/dist/types/index.d.ts +1 -1
- package/dist/types/project.d.ts +3 -2
- package/dist/types/rules/registry.d.ts +6 -0
- package/package.json +1 -1
- package/src/cli.ts +31 -2
- package/src/index.ts +1 -0
- package/src/project.ts +5 -1
- package/src/rules/registry.ts +60 -4
package/README.md
CHANGED
|
@@ -1,30 +1,37 @@
|
|
|
1
|
-
#
|
|
1
|
+
# mermaidjs-lint
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
Deterministic checks and verified fixes for Mermaid diagrams in markdown.
|
|
3
|
+
Catch Mermaid diagrams in your markdown that fail to parse, turn unreadable in light or dark themes, or grow too big to follow.
|
|
5
4
|
|
|
6
|
-
**[Docs](https://jpeakai.github.io/jpai-mermaidjs-lint/)** | **[Rules](https://jpeakai.github.io/jpai-mermaidjs-lint/reference/rules/)** | **[Changelog](CHANGELOG.md)** | **[
|
|
5
|
+
**[Docs](https://jpeakai.github.io/jpai-mermaidjs-lint/)** | **[Rules](https://jpeakai.github.io/jpai-mermaidjs-lint/reference/rules/)** | **[Changelog](CHANGELOG.md)** | **[npm](https://www.npmjs.com/package/@jpeakai/mermaidjs-lint)**
|
|
7
6
|
|
|
8
|
-
|
|
9
|
-
This README, read on GitHub, describes the `main` branch.
|
|
10
|
-
A change on `main` that is not yet released is listed under `Unreleased` in the [changelog](CHANGELOG.md).
|
|
11
|
-
|
|
12
|
-
## Usage
|
|
7
|
+
## Lint your docs with one command
|
|
13
8
|
|
|
14
9
|
```sh
|
|
15
|
-
|
|
16
|
-
|
|
10
|
+
npx -y @jpeakai/mermaidjs-lint docs/
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
```text
|
|
14
|
+
docs/architecture.md:6 contrast-text-below-aa classDef warn: text 1.43:1 < 4.5 (#ffffff on #ffd400)
|
|
15
|
+
docs/login.md:3-7 parse-diagram-unparseable sequenceDiagram did not parse (parser: mermaid-core) (its line numbers count within the diagram, not the file): Parse error on line 3: Expecting 'TXT', got 'NEWLINE'
|
|
16
|
+
docs/pipeline.md:3-41 complexity-node-count-acceptable-exceeded 37 nodes > 35 acceptable threshold
|
|
17
|
+
3 finding(s) in 3 file(s)
|
|
17
18
|
```
|
|
18
19
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
20
|
+
The run exits 1 while any finding remains, so it works as a CI gate.
|
|
21
|
+
|
|
22
|
+
## Fix unreadable label text and unclosed fences
|
|
23
|
+
|
|
24
|
+
Add `--fix` to apply the fixes that pass verification:
|
|
25
|
+
|
|
26
|
+
```diff
|
|
27
|
+
- classDef warn fill:#ffd400,color:#ffffff
|
|
28
|
+
+ classDef warn fill:#ffd400,color:#000000
|
|
29
|
+
```
|
|
24
30
|
|
|
25
|
-
|
|
31
|
+
A fix is kept only if the markdown around the diagrams is unchanged and each diagram reparses with the same node ids, subgraphs and counts.
|
|
32
|
+
Edge endpoints, edge labels and node text are not compared, so review the diff.
|
|
26
33
|
|
|
27
|
-
##
|
|
34
|
+
## Four tenets define the tool
|
|
28
35
|
|
|
29
36
|
Three tenets are guarantees about every diagram the tool passes, and the fourth is a commitment about the tool.
|
|
30
37
|
|
|
@@ -33,101 +40,28 @@ Three tenets are guarantees about every diagram the tool passes, and the fourth
|
|
|
33
40
|
- **Simple enough to follow.** Node count and visual complexity are scored against a density preset, so an oversized diagram is flagged to be split.
|
|
34
41
|
- **Extensible.** A plugin adds rules of your own, which run through the same config, fix engine and verification as the built-ins.
|
|
35
42
|
|
|
36
|
-
|
|
37
|
-
|---|---|
|
|
38
|
-
| Readable in light and dark | The contrast rules |
|
|
39
|
-
| Renders | The parse rules |
|
|
40
|
-
| Simple enough to follow | The complexity rules |
|
|
41
|
-
| Extensible | Local rules and plugin packages |
|
|
43
|
+
## Go further in the docs site
|
|
42
44
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
Every finding fails the run, and a rule you do not want is switched off in the config.
|
|
48
|
-
Node, edge and subgraph counts come from mermaid's own parsers.
|
|
49
|
-
Style directives are read line by line, so a style joined to other statements by a semicolon is not seen.
|
|
50
|
-
Every fix is verified by parsing the document again, and a fix that fails the check is refused, so the finding stays for a human.
|
|
51
|
-
The check holds the markdown outside the diagrams unchanged, and each diagram's type, node ids, subgraph names and counts, and whether it parses.
|
|
52
|
-
It does not compare edge endpoints, edge labels or node text, so review the diff a fix writes.
|
|
53
|
-
|
|
54
|
-
## Requirements and compatibility
|
|
55
|
-
|
|
56
|
-
- **Runtime:** Bun, or Node `>=20` as the `engines` field of `package.json` states.
|
|
57
|
-
- **Mermaid:** the parse and contrast rules use the `mermaid` package at exactly `11.17.2`, and `@mermaid-js/parser`, both installed with this package. The pin holds sequence diagram colours, which change between mermaid releases.
|
|
58
|
-
- **Your host's mermaid may differ.** GitHub and other hosts draw diagrams with their own mermaid version. A diagram type newer than the host's may parse here and still fail to draw there.
|
|
59
|
-
|
|
60
|
-
`npm ls mermaid` in your project prints the mermaid version a run uses.
|
|
61
|
-
[Reference](https://jpeakai.github.io/jpai-mermaidjs-lint/reference/#requirements-and-compatibility) lists which parser reads each diagram type.
|
|
62
|
-
|
|
63
|
-
## Configure and extend
|
|
64
|
-
|
|
65
|
-
- [Configure the rules](https://jpeakai.github.io/jpai-mermaidjs-lint/guides/configure/): switch a rule off, change the complexity preset, set options.
|
|
66
|
-
- [Lint a MkDocs Material site](https://jpeakai.github.io/jpai-mermaidjs-lint/guides/mkdocs/): score diagrams the way the theme renders them.
|
|
67
|
-
- [Write a local rule](https://jpeakai.github.io/jpai-mermaidjs-lint/guides/local-rule/) or [publish a plugin package](https://jpeakai.github.io/jpai-mermaidjs-lint/guides/plugin-package/): add rules of your own, which load with no config.
|
|
68
|
-
- [Troubleshooting](https://jpeakai.github.io/jpai-mermaidjs-lint/guides/troubleshooting/): a README that fails after adding `mkdocs.yml`, skipping a diagram, and other common questions.
|
|
45
|
+
- [Get started](https://jpeakai.github.io/jpai-mermaidjs-lint/get-started/): run it, fix a diagram, and add the gate to your project.
|
|
46
|
+
- [Gate diagrams in CI](https://jpeakai.github.io/jpai-mermaidjs-lint/guides/ci/) or [before each commit](https://jpeakai.github.io/jpai-mermaidjs-lint/guides/pre-commit/).
|
|
47
|
+
- [Configure the rules](https://jpeakai.github.io/jpai-mermaidjs-lint/guides/configure/), or [write a local rule](https://jpeakai.github.io/jpai-mermaidjs-lint/guides/local-rule/).
|
|
69
48
|
|
|
70
49
|
## Use it from an AI agent
|
|
71
50
|
|
|
72
|
-
[`skills/mermaidjs-lint
|
|
73
|
-
|
|
74
|
-
The skill is the one file `SKILL.md`.
|
|
75
|
-
Its `evals/` folder holds deliberately failing fixtures for this repository's own tests, so never copy it.
|
|
76
|
-
For Claude Code, run these in your project:
|
|
51
|
+
The agent skill in [`skills/mermaidjs-lint/SKILL.md`](skills/mermaidjs-lint/SKILL.md) runs the tool, applies the safe fixes, and fixes the rest by judgement.
|
|
52
|
+
Copy it into a Claude Code project:
|
|
77
53
|
|
|
78
54
|
```sh
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
-o .claude/skills/mermaidjs-lint/SKILL.md
|
|
82
|
-
```
|
|
83
|
-
|
|
84
|
-
Use `~/.claude/skills/mermaidjs-lint` instead to use it in every project.
|
|
85
|
-
Another agent can follow `SKILL.md` as plain instructions.
|
|
86
|
-
[Agent evals](https://jpeakai.github.io/jpai-mermaidjs-lint/gallery/agent-evals/) shows how well agents fixed the failing fixtures it lists, and which skill revision they used.
|
|
87
|
-
|
|
88
|
-
## Library use
|
|
89
|
-
|
|
90
|
-
```ts
|
|
91
|
-
import { checkMarkdown, fixSource } from "@jpeakai/mermaidjs-lint";
|
|
92
|
-
|
|
93
|
-
const findings = await checkMarkdown(src, "README.md");
|
|
94
|
-
const fixed = await fixSource(src, "README.md");
|
|
55
|
+
curl -fsSL --create-dirs -o .claude/skills/mermaidjs-lint/SKILL.md \
|
|
56
|
+
https://raw.githubusercontent.com/jpeakai/jpai-mermaidjs-lint/main/skills/mermaidjs-lint/SKILL.md
|
|
95
57
|
```
|
|
96
58
|
|
|
97
|
-
|
|
98
|
-
The [Library API](https://jpeakai.github.io/jpai-mermaidjs-lint/reference/library-api/) page lists every export, and shows how to run with the config and plugins as the command does.
|
|
99
|
-
|
|
100
|
-
## Versioning
|
|
101
|
-
|
|
102
|
-
The package follows semantic versioning, and while it is at `0.x` a minor release may break the public surface.
|
|
103
|
-
[Versioning](https://jpeakai.github.io/jpai-mermaidjs-lint/reference/#versioning) lists what that surface is.
|
|
104
|
-
[CHANGELOG.md](CHANGELOG.md) lists what changed in each release, and what is on `main` but not yet released.
|
|
105
|
-
|
|
106
|
-
## Security
|
|
107
|
-
|
|
108
|
-
A plugin is code that runs on your machine, and plugins load on their own once a project depends on them.
|
|
109
|
-
[SECURITY.md](SECURITY.md) covers the supported versions, the trust model and how to report a vulnerability privately.
|
|
110
|
-
|
|
111
|
-
## Support
|
|
112
|
-
|
|
113
|
-
Report a bug or ask a question in [GitHub issues](https://github.com/jpeakai/jpai-mermaidjs-lint/issues).
|
|
114
|
-
A bug report needs:
|
|
115
|
-
|
|
116
|
-
- The version, from `mermaidjs-lint --version`, or from `npm ls @jpeakai/mermaidjs-lint` on 0.1.1 and earlier.
|
|
117
|
-
- The command you ran.
|
|
118
|
-
- The diagram, or the smallest file that shows the problem.
|
|
119
|
-
- The output of the same command with `--json`.
|
|
120
|
-
|
|
121
|
-
## Origin
|
|
122
|
-
|
|
123
|
-
The checks port an earlier set of diagram-checking scripts, which measured complexity and contrast for Mermaid diagrams.
|
|
124
|
-
The package shape, the plugin mechanism and the fix verification follow [jpai-prose-gates](https://github.com/jpeakai/jpai-prose-gates).
|
|
125
|
-
The reasoning behind each choice is recorded in [`adrs/`](adrs/index.md).
|
|
126
|
-
|
|
127
|
-
## Contributing
|
|
59
|
+
## Runs on Bun or Node
|
|
128
60
|
|
|
129
|
-
|
|
61
|
+
It needs Bun, or Node `>=20`.
|
|
62
|
+
The package installs mermaid `11.17.2` to parse diagrams, and your host may draw with another version.
|
|
130
63
|
|
|
131
|
-
##
|
|
64
|
+
## Contribute
|
|
132
65
|
|
|
133
|
-
|
|
66
|
+
[CONTRIBUTING.md](CONTRIBUTING.md) covers setup.
|
|
67
|
+
Licensed under [MIT](LICENSE).
|
package/dist/bin.js
CHANGED
|
@@ -9,7 +9,7 @@ import { parseArgs } from "node:util";
|
|
|
9
9
|
// package.json
|
|
10
10
|
var package_default = {
|
|
11
11
|
name: "@jpeakai/mermaidjs-lint",
|
|
12
|
-
version: "0.
|
|
12
|
+
version: "0.7.1",
|
|
13
13
|
type: "module",
|
|
14
14
|
description: "Deterministic lint gates and verified fixers for Mermaid diagrams in markdown",
|
|
15
15
|
license: "MIT",
|
|
@@ -1936,6 +1936,22 @@ var BUILTIN_CATEGORIES = new Map([
|
|
|
1936
1936
|
["contrast", "Whether labels and borders can be read against their fills"]
|
|
1937
1937
|
]);
|
|
1938
1938
|
var HELPERS = { detectKeyword, wcagRatio, compositeOver };
|
|
1939
|
+
var selectionNames = (prefix) => prefix ? { enable: `${prefix}category`, disable: `${prefix}disable-category` } : { enable: "selection.enable", disable: "selection.disable" };
|
|
1940
|
+
var checkSelection = (selection, categories) => {
|
|
1941
|
+
const names = selectionNames(selection.prefix ?? "");
|
|
1942
|
+
const known = [...categories.keys()];
|
|
1943
|
+
for (const side of ["enable", "disable"]) {
|
|
1944
|
+
for (const word of selection[side] ?? []) {
|
|
1945
|
+
if (!categories.has(word)) {
|
|
1946
|
+
throw new FlagError(names[side], `${names[side]} names no loaded category "${word}"; the categories are ${known.join(", ")}`);
|
|
1947
|
+
}
|
|
1948
|
+
}
|
|
1949
|
+
}
|
|
1950
|
+
const both = (selection.enable ?? []).filter((word) => selection.disable?.includes(word));
|
|
1951
|
+
if (both.length > 0) {
|
|
1952
|
+
throw new FlagError(names.disable, `${names.enable} and ${names.disable} both name ${[...new Set(both)].join(", ")}; name each category once`);
|
|
1953
|
+
}
|
|
1954
|
+
};
|
|
1939
1955
|
var typeOf = (value) => {
|
|
1940
1956
|
if (typeof value === "number" && Number.isFinite(value))
|
|
1941
1957
|
return "number";
|
|
@@ -1992,6 +2008,8 @@ var checkOptions = (where, rule, options) => {
|
|
|
1992
2008
|
};
|
|
1993
2009
|
var buildRegistry = (input) => {
|
|
1994
2010
|
const { rules, fixOrder, categories, sources = [], config, ignoreUnknownPluginIds = false } = input;
|
|
2011
|
+
const selection = input.selection ?? {};
|
|
2012
|
+
checkSelection(selection, categories);
|
|
1995
2013
|
const off = new Set;
|
|
1996
2014
|
const options = new Map;
|
|
1997
2015
|
const ignored = [];
|
|
@@ -2006,12 +2024,21 @@ var buildRegistry = (input) => {
|
|
|
2006
2024
|
throw new UnknownRuleError(where, id, rules.map((r) => r.id));
|
|
2007
2025
|
}
|
|
2008
2026
|
checkOptions(where, rule, setting.options);
|
|
2027
|
+
options.set(rule.id, setting.options);
|
|
2009
2028
|
if (setting.severity === "off")
|
|
2010
2029
|
off.add(id);
|
|
2011
|
-
|
|
2012
|
-
|
|
2030
|
+
}
|
|
2031
|
+
for (const rule of rules) {
|
|
2032
|
+
if (selection.enable?.includes(rule.category))
|
|
2033
|
+
off.delete(rule.id);
|
|
2034
|
+
if (selection.disable?.includes(rule.category))
|
|
2035
|
+
off.add(rule.id);
|
|
2013
2036
|
}
|
|
2014
2037
|
const active = rules.filter((r) => !off.has(r.id));
|
|
2038
|
+
if (active.length === 0 && (selection.disable?.length ?? 0) > 0) {
|
|
2039
|
+
const name = selectionNames(selection.prefix ?? "").disable;
|
|
2040
|
+
throw new FlagError(name, `${name} leaves no rule to run; disable fewer categories`);
|
|
2041
|
+
}
|
|
2015
2042
|
return {
|
|
2016
2043
|
rules: active,
|
|
2017
2044
|
fixOrder: fixOrder.filter((r) => !off.has(r.id)),
|
|
@@ -2792,7 +2819,8 @@ var setUp = async ({
|
|
|
2792
2819
|
configPath,
|
|
2793
2820
|
noPlugins = false,
|
|
2794
2821
|
lenient = false,
|
|
2795
|
-
collect = false
|
|
2822
|
+
collect = false,
|
|
2823
|
+
selection
|
|
2796
2824
|
}) => {
|
|
2797
2825
|
const root = await findProjectRoot(cwd);
|
|
2798
2826
|
let file;
|
|
@@ -2820,7 +2848,8 @@ var setUp = async ({
|
|
|
2820
2848
|
categories: plugins.categories,
|
|
2821
2849
|
sources: plugins.sources,
|
|
2822
2850
|
config,
|
|
2823
|
-
ignoreUnknownPluginIds: mode !== "strict"
|
|
2851
|
+
ignoreUnknownPluginIds: mode !== "strict",
|
|
2852
|
+
...selection && { selection }
|
|
2824
2853
|
});
|
|
2825
2854
|
return { root, registry, skipped: plugins.skipped, failures: plugins.failures };
|
|
2826
2855
|
};
|
|
@@ -2828,7 +2857,8 @@ var setUp = async ({
|
|
|
2828
2857
|
// src/cli.ts
|
|
2829
2858
|
var SYNOPSIS = `usage: mermaidjs-lint <path...> [--fix] [--json] [--preset NAME] [--profile NAME] [--config FILE]
|
|
2830
2859
|
[--canvas-light COLOUR] [--canvas-dark COLOUR] [--no-plugins | --lenient-plugins]
|
|
2831
|
-
|
|
2860
|
+
[--category NAME]... [--disable-category NAME]...
|
|
2861
|
+
mermaidjs-lint --list-rules [--config FILE] [--no-plugins | --lenient-plugins] [category flags]
|
|
2832
2862
|
mermaidjs-lint --validate-plugins [--json] [--config FILE]
|
|
2833
2863
|
mermaidjs-lint --version | -h, --help`;
|
|
2834
2864
|
var ruleRow = (r, width) => `${`${r.id}${r.fix ? "*" : ""}`.padEnd(width)} ${r.summary}`;
|
|
@@ -2858,6 +2888,10 @@ Examples of every rule, with fixes: https://jpeakai.github.io/jpai-mermaidjs-lin
|
|
|
2858
2888
|
on it. Each is an opaque CSS colour such as #faf8f9, and a side not given keeps the config's, or GitHub's
|
|
2859
2889
|
(#ffffff light, #0d1117 dark). A file scored under mkdocs-material refuses a canvas flag with a usage
|
|
2860
2890
|
error, and sets a config canvas aside when auto picked the profile.
|
|
2891
|
+
--disable-category NAME turns off every rule in that category for this run, after the config is applied.
|
|
2892
|
+
--category NAME turns on every rule in that category for this run, even one the config turns off.
|
|
2893
|
+
Each may be repeated or take a comma-separated list, such as --disable-category parse,complexity.
|
|
2894
|
+
A category that is not loaded, one named by both flags, or a run left with no rule is a usage error.
|
|
2861
2895
|
--json prints one JSON object instead of text lines.
|
|
2862
2896
|
--fix applies every fix it can prove safe, then reports what remains.
|
|
2863
2897
|
A fixer that is unsure leaves the text alone and the finding stays.
|
|
@@ -2869,7 +2903,7 @@ Plugins load on their own from .mermaidjs-lint/rules, from dependencies named me
|
|
|
2869
2903
|
--lenient-plugins skips a plugin that fails to load and says so, instead of stopping the run.
|
|
2870
2904
|
--validate-plugins imports and checks every plugin, reports every failure, and runs no check or fix.
|
|
2871
2905
|
--list-rules prints every active rule by category, including plugin rules, and where each plugin came from.
|
|
2872
|
-
It imports the plugins to list them, and
|
|
2906
|
+
It imports the plugins to list them. It takes the category flags, to preview a run, and no path or other flag.
|
|
2873
2907
|
--version prints the version, to quote in a bug report.
|
|
2874
2908
|
Exits 0 when no findings remain, 1 when findings remain, a plugin fails to load or the run fails (a lenient run
|
|
2875
2909
|
skips a broken plugin, so it alone does not exit 1), and 2 on a usage error.`;
|
|
@@ -2888,11 +2922,19 @@ var parse2 = (argv) => parseArgs({
|
|
|
2888
2922
|
"lenient-plugins": { type: "boolean", default: false },
|
|
2889
2923
|
"validate-plugins": { type: "boolean", default: false },
|
|
2890
2924
|
"list-rules": { type: "boolean", default: false },
|
|
2891
|
-
version: { type: "boolean", default: false }
|
|
2925
|
+
version: { type: "boolean", default: false },
|
|
2926
|
+
category: { type: "string", multiple: true },
|
|
2927
|
+
"disable-category": { type: "string", multiple: true }
|
|
2892
2928
|
},
|
|
2893
2929
|
allowPositionals: true,
|
|
2894
2930
|
strict: true
|
|
2895
2931
|
});
|
|
2932
|
+
var categoryWords = (flag, values) => (values ?? []).flatMap((value) => value.split(",").map((word) => {
|
|
2933
|
+
const trimmed = word.trim();
|
|
2934
|
+
if (!trimmed)
|
|
2935
|
+
throw new FlagError(flag, `${flag} needs a category name, not "${value}"`);
|
|
2936
|
+
return trimmed;
|
|
2937
|
+
}));
|
|
2896
2938
|
var plural = (n, word) => `${n} ${word}${n === 1 ? "" : "s"}`;
|
|
2897
2939
|
var namesOf = (sources) => sources.map((s) => `${s.namespace} (${plural(s.rules.length, "rule")})`).join(", ");
|
|
2898
2940
|
var loadedLine = (sources, skipped) => {
|
|
@@ -3004,6 +3046,11 @@ var run = async (argv, cwd) => {
|
|
|
3004
3046
|
if (validating && (values["no-plugins"] || values.fix || listing)) {
|
|
3005
3047
|
throw new FlagError("--validate-plugins", "--validate-plugins takes no --no-plugins, --fix or --list-rules");
|
|
3006
3048
|
}
|
|
3049
|
+
const enable = categoryWords("--category", values.category);
|
|
3050
|
+
const disable = categoryWords("--disable-category", values["disable-category"]);
|
|
3051
|
+
if (validating && enable.length + disable.length > 0) {
|
|
3052
|
+
throw new FlagError("--validate-plugins", "--validate-plugins checks every plugin, so it takes no category flag");
|
|
3053
|
+
}
|
|
3007
3054
|
if (listing && !validating) {
|
|
3008
3055
|
const ignored = [
|
|
3009
3056
|
...["fix", "json", "preset", "profile", "canvas-light", "canvas-dark"].filter((name) => values[name] !== undefined && values[name] !== false).map((name) => `--${name}`),
|
|
@@ -3023,7 +3070,8 @@ var run = async (argv, cwd) => {
|
|
|
3023
3070
|
configPath: values.config,
|
|
3024
3071
|
noPlugins: values["no-plugins"],
|
|
3025
3072
|
lenient: values["lenient-plugins"],
|
|
3026
|
-
collect: validating
|
|
3073
|
+
collect: validating,
|
|
3074
|
+
selection: { enable, disable, prefix: "--" }
|
|
3027
3075
|
});
|
|
3028
3076
|
if (validating) {
|
|
3029
3077
|
const result = validate(registry, failures, Boolean(values.json));
|
package/dist/index.js
CHANGED
|
@@ -1858,6 +1858,22 @@ var BUILTIN_CATEGORIES = new Map([
|
|
|
1858
1858
|
["contrast", "Whether labels and borders can be read against their fills"]
|
|
1859
1859
|
]);
|
|
1860
1860
|
var HELPERS = { detectKeyword, wcagRatio, compositeOver };
|
|
1861
|
+
var selectionNames = (prefix) => prefix ? { enable: `${prefix}category`, disable: `${prefix}disable-category` } : { enable: "selection.enable", disable: "selection.disable" };
|
|
1862
|
+
var checkSelection = (selection, categories) => {
|
|
1863
|
+
const names = selectionNames(selection.prefix ?? "");
|
|
1864
|
+
const known = [...categories.keys()];
|
|
1865
|
+
for (const side of ["enable", "disable"]) {
|
|
1866
|
+
for (const word of selection[side] ?? []) {
|
|
1867
|
+
if (!categories.has(word)) {
|
|
1868
|
+
throw new FlagError(names[side], `${names[side]} names no loaded category "${word}"; the categories are ${known.join(", ")}`);
|
|
1869
|
+
}
|
|
1870
|
+
}
|
|
1871
|
+
}
|
|
1872
|
+
const both = (selection.enable ?? []).filter((word) => selection.disable?.includes(word));
|
|
1873
|
+
if (both.length > 0) {
|
|
1874
|
+
throw new FlagError(names.disable, `${names.enable} and ${names.disable} both name ${[...new Set(both)].join(", ")}; name each category once`);
|
|
1875
|
+
}
|
|
1876
|
+
};
|
|
1861
1877
|
var typeOf = (value) => {
|
|
1862
1878
|
if (typeof value === "number" && Number.isFinite(value))
|
|
1863
1879
|
return "number";
|
|
@@ -1914,6 +1930,8 @@ var checkOptions = (where, rule, options) => {
|
|
|
1914
1930
|
};
|
|
1915
1931
|
var buildRegistry = (input) => {
|
|
1916
1932
|
const { rules, fixOrder, categories, sources = [], config, ignoreUnknownPluginIds = false } = input;
|
|
1933
|
+
const selection = input.selection ?? {};
|
|
1934
|
+
checkSelection(selection, categories);
|
|
1917
1935
|
const off = new Set;
|
|
1918
1936
|
const options = new Map;
|
|
1919
1937
|
const ignored = [];
|
|
@@ -1928,12 +1946,21 @@ var buildRegistry = (input) => {
|
|
|
1928
1946
|
throw new UnknownRuleError(where, id, rules.map((r) => r.id));
|
|
1929
1947
|
}
|
|
1930
1948
|
checkOptions(where, rule, setting.options);
|
|
1949
|
+
options.set(rule.id, setting.options);
|
|
1931
1950
|
if (setting.severity === "off")
|
|
1932
1951
|
off.add(id);
|
|
1933
|
-
|
|
1934
|
-
|
|
1952
|
+
}
|
|
1953
|
+
for (const rule of rules) {
|
|
1954
|
+
if (selection.enable?.includes(rule.category))
|
|
1955
|
+
off.delete(rule.id);
|
|
1956
|
+
if (selection.disable?.includes(rule.category))
|
|
1957
|
+
off.add(rule.id);
|
|
1935
1958
|
}
|
|
1936
1959
|
const active = rules.filter((r) => !off.has(r.id));
|
|
1960
|
+
if (active.length === 0 && (selection.disable?.length ?? 0) > 0) {
|
|
1961
|
+
const name = selectionNames(selection.prefix ?? "").disable;
|
|
1962
|
+
throw new FlagError(name, `${name} leaves no rule to run; disable fewer categories`);
|
|
1963
|
+
}
|
|
1937
1964
|
return {
|
|
1938
1965
|
rules: active,
|
|
1939
1966
|
fixOrder: fixOrder.filter((r) => !off.has(r.id)),
|
|
@@ -2023,7 +2050,7 @@ import { parseArgs } from "node:util";
|
|
|
2023
2050
|
// package.json
|
|
2024
2051
|
var package_default = {
|
|
2025
2052
|
name: "@jpeakai/mermaidjs-lint",
|
|
2026
|
-
version: "0.
|
|
2053
|
+
version: "0.7.1",
|
|
2027
2054
|
type: "module",
|
|
2028
2055
|
description: "Deterministic lint gates and verified fixers for Mermaid diagrams in markdown",
|
|
2029
2056
|
license: "MIT",
|
|
@@ -2792,7 +2819,8 @@ var setUp = async ({
|
|
|
2792
2819
|
configPath,
|
|
2793
2820
|
noPlugins = false,
|
|
2794
2821
|
lenient = false,
|
|
2795
|
-
collect = false
|
|
2822
|
+
collect = false,
|
|
2823
|
+
selection
|
|
2796
2824
|
}) => {
|
|
2797
2825
|
const root = await findProjectRoot(cwd);
|
|
2798
2826
|
let file;
|
|
@@ -2820,7 +2848,8 @@ var setUp = async ({
|
|
|
2820
2848
|
categories: plugins.categories,
|
|
2821
2849
|
sources: plugins.sources,
|
|
2822
2850
|
config,
|
|
2823
|
-
ignoreUnknownPluginIds: mode !== "strict"
|
|
2851
|
+
ignoreUnknownPluginIds: mode !== "strict",
|
|
2852
|
+
...selection && { selection }
|
|
2824
2853
|
});
|
|
2825
2854
|
return { root, registry, skipped: plugins.skipped, failures: plugins.failures };
|
|
2826
2855
|
};
|
|
@@ -2828,7 +2857,8 @@ var setUp = async ({
|
|
|
2828
2857
|
// src/cli.ts
|
|
2829
2858
|
var SYNOPSIS = `usage: mermaidjs-lint <path...> [--fix] [--json] [--preset NAME] [--profile NAME] [--config FILE]
|
|
2830
2859
|
[--canvas-light COLOUR] [--canvas-dark COLOUR] [--no-plugins | --lenient-plugins]
|
|
2831
|
-
|
|
2860
|
+
[--category NAME]... [--disable-category NAME]...
|
|
2861
|
+
mermaidjs-lint --list-rules [--config FILE] [--no-plugins | --lenient-plugins] [category flags]
|
|
2832
2862
|
mermaidjs-lint --validate-plugins [--json] [--config FILE]
|
|
2833
2863
|
mermaidjs-lint --version | -h, --help`;
|
|
2834
2864
|
var ruleRow = (r, width) => `${`${r.id}${r.fix ? "*" : ""}`.padEnd(width)} ${r.summary}`;
|
|
@@ -2858,6 +2888,10 @@ Examples of every rule, with fixes: https://jpeakai.github.io/jpai-mermaidjs-lin
|
|
|
2858
2888
|
on it. Each is an opaque CSS colour such as #faf8f9, and a side not given keeps the config's, or GitHub's
|
|
2859
2889
|
(#ffffff light, #0d1117 dark). A file scored under mkdocs-material refuses a canvas flag with a usage
|
|
2860
2890
|
error, and sets a config canvas aside when auto picked the profile.
|
|
2891
|
+
--disable-category NAME turns off every rule in that category for this run, after the config is applied.
|
|
2892
|
+
--category NAME turns on every rule in that category for this run, even one the config turns off.
|
|
2893
|
+
Each may be repeated or take a comma-separated list, such as --disable-category parse,complexity.
|
|
2894
|
+
A category that is not loaded, one named by both flags, or a run left with no rule is a usage error.
|
|
2861
2895
|
--json prints one JSON object instead of text lines.
|
|
2862
2896
|
--fix applies every fix it can prove safe, then reports what remains.
|
|
2863
2897
|
A fixer that is unsure leaves the text alone and the finding stays.
|
|
@@ -2869,7 +2903,7 @@ Plugins load on their own from .mermaidjs-lint/rules, from dependencies named me
|
|
|
2869
2903
|
--lenient-plugins skips a plugin that fails to load and says so, instead of stopping the run.
|
|
2870
2904
|
--validate-plugins imports and checks every plugin, reports every failure, and runs no check or fix.
|
|
2871
2905
|
--list-rules prints every active rule by category, including plugin rules, and where each plugin came from.
|
|
2872
|
-
It imports the plugins to list them, and
|
|
2906
|
+
It imports the plugins to list them. It takes the category flags, to preview a run, and no path or other flag.
|
|
2873
2907
|
--version prints the version, to quote in a bug report.
|
|
2874
2908
|
Exits 0 when no findings remain, 1 when findings remain, a plugin fails to load or the run fails (a lenient run
|
|
2875
2909
|
skips a broken plugin, so it alone does not exit 1), and 2 on a usage error.`;
|
|
@@ -2888,11 +2922,19 @@ var parse2 = (argv) => parseArgs({
|
|
|
2888
2922
|
"lenient-plugins": { type: "boolean", default: false },
|
|
2889
2923
|
"validate-plugins": { type: "boolean", default: false },
|
|
2890
2924
|
"list-rules": { type: "boolean", default: false },
|
|
2891
|
-
version: { type: "boolean", default: false }
|
|
2925
|
+
version: { type: "boolean", default: false },
|
|
2926
|
+
category: { type: "string", multiple: true },
|
|
2927
|
+
"disable-category": { type: "string", multiple: true }
|
|
2892
2928
|
},
|
|
2893
2929
|
allowPositionals: true,
|
|
2894
2930
|
strict: true
|
|
2895
2931
|
});
|
|
2932
|
+
var categoryWords = (flag, values) => (values ?? []).flatMap((value) => value.split(",").map((word) => {
|
|
2933
|
+
const trimmed = word.trim();
|
|
2934
|
+
if (!trimmed)
|
|
2935
|
+
throw new FlagError(flag, `${flag} needs a category name, not "${value}"`);
|
|
2936
|
+
return trimmed;
|
|
2937
|
+
}));
|
|
2896
2938
|
var plural = (n, word) => `${n} ${word}${n === 1 ? "" : "s"}`;
|
|
2897
2939
|
var namesOf = (sources) => sources.map((s) => `${s.namespace} (${plural(s.rules.length, "rule")})`).join(", ");
|
|
2898
2940
|
var loadedLine = (sources, skipped) => {
|
|
@@ -3004,6 +3046,11 @@ var run = async (argv, cwd) => {
|
|
|
3004
3046
|
if (validating && (values["no-plugins"] || values.fix || listing)) {
|
|
3005
3047
|
throw new FlagError("--validate-plugins", "--validate-plugins takes no --no-plugins, --fix or --list-rules");
|
|
3006
3048
|
}
|
|
3049
|
+
const enable = categoryWords("--category", values.category);
|
|
3050
|
+
const disable = categoryWords("--disable-category", values["disable-category"]);
|
|
3051
|
+
if (validating && enable.length + disable.length > 0) {
|
|
3052
|
+
throw new FlagError("--validate-plugins", "--validate-plugins checks every plugin, so it takes no category flag");
|
|
3053
|
+
}
|
|
3007
3054
|
if (listing && !validating) {
|
|
3008
3055
|
const ignored = [
|
|
3009
3056
|
...["fix", "json", "preset", "profile", "canvas-light", "canvas-dark"].filter((name) => values[name] !== undefined && values[name] !== false).map((name) => `--${name}`),
|
|
@@ -3023,7 +3070,8 @@ var run = async (argv, cwd) => {
|
|
|
3023
3070
|
configPath: values.config,
|
|
3024
3071
|
noPlugins: values["no-plugins"],
|
|
3025
3072
|
lenient: values["lenient-plugins"],
|
|
3026
|
-
collect: validating
|
|
3073
|
+
collect: validating,
|
|
3074
|
+
selection: { enable, disable, prefix: "--" }
|
|
3027
3075
|
});
|
|
3028
3076
|
if (validating) {
|
|
3029
3077
|
const result = validate(registry, failures, Boolean(values.json));
|
package/dist/testing.js
CHANGED
|
@@ -1755,6 +1755,22 @@ var BUILTIN_CATEGORIES = new Map([
|
|
|
1755
1755
|
["contrast", "Whether labels and borders can be read against their fills"]
|
|
1756
1756
|
]);
|
|
1757
1757
|
var HELPERS = { detectKeyword, wcagRatio, compositeOver };
|
|
1758
|
+
var selectionNames = (prefix) => prefix ? { enable: `${prefix}category`, disable: `${prefix}disable-category` } : { enable: "selection.enable", disable: "selection.disable" };
|
|
1759
|
+
var checkSelection = (selection, categories) => {
|
|
1760
|
+
const names = selectionNames(selection.prefix ?? "");
|
|
1761
|
+
const known = [...categories.keys()];
|
|
1762
|
+
for (const side of ["enable", "disable"]) {
|
|
1763
|
+
for (const word of selection[side] ?? []) {
|
|
1764
|
+
if (!categories.has(word)) {
|
|
1765
|
+
throw new FlagError(names[side], `${names[side]} names no loaded category "${word}"; the categories are ${known.join(", ")}`);
|
|
1766
|
+
}
|
|
1767
|
+
}
|
|
1768
|
+
}
|
|
1769
|
+
const both = (selection.enable ?? []).filter((word) => selection.disable?.includes(word));
|
|
1770
|
+
if (both.length > 0) {
|
|
1771
|
+
throw new FlagError(names.disable, `${names.enable} and ${names.disable} both name ${[...new Set(both)].join(", ")}; name each category once`);
|
|
1772
|
+
}
|
|
1773
|
+
};
|
|
1758
1774
|
var typeOf = (value) => {
|
|
1759
1775
|
if (typeof value === "number" && Number.isFinite(value))
|
|
1760
1776
|
return "number";
|
|
@@ -1811,6 +1827,8 @@ var checkOptions = (where, rule, options) => {
|
|
|
1811
1827
|
};
|
|
1812
1828
|
var buildRegistry = (input) => {
|
|
1813
1829
|
const { rules, fixOrder, categories, sources = [], config, ignoreUnknownPluginIds = false } = input;
|
|
1830
|
+
const selection = input.selection ?? {};
|
|
1831
|
+
checkSelection(selection, categories);
|
|
1814
1832
|
const off = new Set;
|
|
1815
1833
|
const options = new Map;
|
|
1816
1834
|
const ignored = [];
|
|
@@ -1825,12 +1843,21 @@ var buildRegistry = (input) => {
|
|
|
1825
1843
|
throw new UnknownRuleError(where, id, rules.map((r) => r.id));
|
|
1826
1844
|
}
|
|
1827
1845
|
checkOptions(where, rule, setting.options);
|
|
1846
|
+
options.set(rule.id, setting.options);
|
|
1828
1847
|
if (setting.severity === "off")
|
|
1829
1848
|
off.add(id);
|
|
1830
|
-
|
|
1831
|
-
|
|
1849
|
+
}
|
|
1850
|
+
for (const rule of rules) {
|
|
1851
|
+
if (selection.enable?.includes(rule.category))
|
|
1852
|
+
off.delete(rule.id);
|
|
1853
|
+
if (selection.disable?.includes(rule.category))
|
|
1854
|
+
off.add(rule.id);
|
|
1832
1855
|
}
|
|
1833
1856
|
const active = rules.filter((r) => !off.has(r.id));
|
|
1857
|
+
if (active.length === 0 && (selection.disable?.length ?? 0) > 0) {
|
|
1858
|
+
const name = selectionNames(selection.prefix ?? "").disable;
|
|
1859
|
+
throw new FlagError(name, `${name} leaves no rule to run; disable fewer categories`);
|
|
1860
|
+
}
|
|
1834
1861
|
return {
|
|
1835
1862
|
rules: active,
|
|
1836
1863
|
fixOrder: fixOrder.filter((r) => !off.has(r.id)),
|
package/dist/types/index.d.ts
CHANGED
|
@@ -14,6 +14,6 @@ export type { SourceKind } from "./plugins/discover.ts";
|
|
|
14
14
|
export type { PluginFailure } from "./plugins/index.ts";
|
|
15
15
|
export { findProjectRoot, type Setup, type SetupOptions, setUp } from "./project.ts";
|
|
16
16
|
export { FIX_ORDER, RULES } from "./rules/index.ts";
|
|
17
|
-
export { BUILTIN, BUILTIN_CATEGORIES, buildRegistry, type LoadedSource, type Registry, type RegistryInput, } from "./rules/registry.ts";
|
|
17
|
+
export { BUILTIN, BUILTIN_CATEGORIES, buildRegistry, type CategorySelection, type LoadedSource, type Registry, type RegistryInput, } from "./rules/registry.ts";
|
|
18
18
|
export type { Category, CheckContext, CoreRuleId, Edit, Expectation, Finding, FixContext, Helpers, OptionSpec, OptionType, PluginRuleId, Rule, RuleContext, RuleId, RuleOptions, } from "./rules/types.ts";
|
|
19
19
|
export { CATEGORIES, RULE } from "./rules/types.ts";
|
package/dist/types/project.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type PluginFailure } from "./plugins/index.ts";
|
|
2
|
-
import { type Registry } from "./rules/registry.ts";
|
|
2
|
+
import { type CategorySelection, type Registry } from "./rules/registry.ts";
|
|
3
3
|
export declare const LOCAL_DIR = ".mermaidjs-lint";
|
|
4
4
|
export declare const findProjectRoot: (cwd: string) => Promise<string>;
|
|
5
5
|
export interface SetupOptions {
|
|
@@ -8,6 +8,7 @@ export interface SetupOptions {
|
|
|
8
8
|
noPlugins?: boolean;
|
|
9
9
|
lenient?: boolean;
|
|
10
10
|
collect?: boolean;
|
|
11
|
+
selection?: CategorySelection;
|
|
11
12
|
}
|
|
12
13
|
export interface Setup {
|
|
13
14
|
root: string;
|
|
@@ -15,4 +16,4 @@ export interface Setup {
|
|
|
15
16
|
skipped: string[];
|
|
16
17
|
failures: PluginFailure[];
|
|
17
18
|
}
|
|
18
|
-
export declare const setUp: ({ cwd, configPath, noPlugins, lenient, collect, }: SetupOptions) => Promise<Setup>;
|
|
19
|
+
export declare const setUp: ({ cwd, configPath, noPlugins, lenient, collect, selection, }: SetupOptions) => Promise<Setup>;
|
|
@@ -24,6 +24,12 @@ export interface RegistryInput {
|
|
|
24
24
|
sources?: LoadedSource[];
|
|
25
25
|
config?: Config;
|
|
26
26
|
ignoreUnknownPluginIds?: boolean;
|
|
27
|
+
selection?: CategorySelection;
|
|
28
|
+
}
|
|
29
|
+
export interface CategorySelection {
|
|
30
|
+
enable?: readonly string[];
|
|
31
|
+
disable?: readonly string[];
|
|
32
|
+
prefix?: string;
|
|
27
33
|
}
|
|
28
34
|
export declare const buildRegistry: (input: RegistryInput) => Registry;
|
|
29
35
|
export declare const BUILTIN: Registry;
|
package/package.json
CHANGED
package/src/cli.ts
CHANGED
|
@@ -22,7 +22,8 @@ import { CATEGORIES, type Finding, type Rule } from "./rules/types.ts";
|
|
|
22
22
|
// buried, and first in --help.
|
|
23
23
|
const SYNOPSIS = `usage: mermaidjs-lint <path...> [--fix] [--json] [--preset NAME] [--profile NAME] [--config FILE]
|
|
24
24
|
[--canvas-light COLOUR] [--canvas-dark COLOUR] [--no-plugins | --lenient-plugins]
|
|
25
|
-
|
|
25
|
+
[--category NAME]... [--disable-category NAME]...
|
|
26
|
+
mermaidjs-lint --list-rules [--config FILE] [--no-plugins | --lenient-plugins] [category flags]
|
|
26
27
|
mermaidjs-lint --validate-plugins [--json] [--config FILE]
|
|
27
28
|
mermaidjs-lint --version | -h, --help`;
|
|
28
29
|
|
|
@@ -57,6 +58,10 @@ Examples of every rule, with fixes: https://jpeakai.github.io/jpai-mermaidjs-lin
|
|
|
57
58
|
on it. Each is an opaque CSS colour such as #faf8f9, and a side not given keeps the config's, or GitHub's
|
|
58
59
|
(#ffffff light, #0d1117 dark). A file scored under mkdocs-material refuses a canvas flag with a usage
|
|
59
60
|
error, and sets a config canvas aside when auto picked the profile.
|
|
61
|
+
--disable-category NAME turns off every rule in that category for this run, after the config is applied.
|
|
62
|
+
--category NAME turns on every rule in that category for this run, even one the config turns off.
|
|
63
|
+
Each may be repeated or take a comma-separated list, such as --disable-category parse,complexity.
|
|
64
|
+
A category that is not loaded, one named by both flags, or a run left with no rule is a usage error.
|
|
60
65
|
--json prints one JSON object instead of text lines.
|
|
61
66
|
--fix applies every fix it can prove safe, then reports what remains.
|
|
62
67
|
A fixer that is unsure leaves the text alone and the finding stays.
|
|
@@ -68,7 +73,7 @@ Plugins load on their own from .mermaidjs-lint/rules, from dependencies named me
|
|
|
68
73
|
--lenient-plugins skips a plugin that fails to load and says so, instead of stopping the run.
|
|
69
74
|
--validate-plugins imports and checks every plugin, reports every failure, and runs no check or fix.
|
|
70
75
|
--list-rules prints every active rule by category, including plugin rules, and where each plugin came from.
|
|
71
|
-
It imports the plugins to list them, and
|
|
76
|
+
It imports the plugins to list them. It takes the category flags, to preview a run, and no path or other flag.
|
|
72
77
|
--version prints the version, to quote in a bug report.
|
|
73
78
|
Exits 0 when no findings remain, 1 when findings remain, a plugin fails to load or the run fails (a lenient run
|
|
74
79
|
skips a broken plugin, so it alone does not exit 1), and 2 on a usage error.`;
|
|
@@ -88,6 +93,8 @@ interface Parsed {
|
|
|
88
93
|
"validate-plugins"?: boolean;
|
|
89
94
|
"list-rules"?: boolean;
|
|
90
95
|
version?: boolean;
|
|
96
|
+
category?: string[];
|
|
97
|
+
"disable-category"?: string[];
|
|
91
98
|
};
|
|
92
99
|
positionals: string[];
|
|
93
100
|
}
|
|
@@ -109,11 +116,25 @@ const parse = (argv: string[]): Parsed =>
|
|
|
109
116
|
"validate-plugins": { type: "boolean", default: false },
|
|
110
117
|
"list-rules": { type: "boolean", default: false },
|
|
111
118
|
version: { type: "boolean", default: false },
|
|
119
|
+
category: { type: "string", multiple: true },
|
|
120
|
+
"disable-category": { type: "string", multiple: true },
|
|
112
121
|
},
|
|
113
122
|
allowPositionals: true,
|
|
114
123
|
strict: true,
|
|
115
124
|
});
|
|
116
125
|
|
|
126
|
+
// A category flag may be repeated or take a comma-separated list. A category
|
|
127
|
+
// word never holds a comma, so the split cannot change a name. An empty item,
|
|
128
|
+
// as in "parse,", is refused rather than dropped.
|
|
129
|
+
const categoryWords = (flag: string, values: string[] | undefined): string[] =>
|
|
130
|
+
(values ?? []).flatMap((value) =>
|
|
131
|
+
value.split(",").map((word) => {
|
|
132
|
+
const trimmed = word.trim();
|
|
133
|
+
if (!trimmed) throw new FlagError(flag, `${flag} needs a category name, not "${value}"`);
|
|
134
|
+
return trimmed;
|
|
135
|
+
}),
|
|
136
|
+
);
|
|
137
|
+
|
|
117
138
|
const plural = (n: number, word: string): string => `${n} ${word}${n === 1 ? "" : "s"}`;
|
|
118
139
|
|
|
119
140
|
const namesOf = (sources: LoadedSource[]): string =>
|
|
@@ -246,6 +267,13 @@ const run = async (argv: string[], cwd: string): Promise<number> => {
|
|
|
246
267
|
if (validating && (values["no-plugins"] || values.fix || listing)) {
|
|
247
268
|
throw new FlagError("--validate-plugins", "--validate-plugins takes no --no-plugins, --fix or --list-rules");
|
|
248
269
|
}
|
|
270
|
+
const enable = categoryWords("--category", values.category);
|
|
271
|
+
const disable = categoryWords("--disable-category", values["disable-category"]);
|
|
272
|
+
// Validation loads every plugin whatever the category flags say, so it
|
|
273
|
+
// refuses them rather than ignore them.
|
|
274
|
+
if (validating && enable.length + disable.length > 0) {
|
|
275
|
+
throw new FlagError("--validate-plugins", "--validate-plugins checks every plugin, so it takes no category flag");
|
|
276
|
+
}
|
|
249
277
|
// --list-rules prints the rules the config and the plugin flags leave
|
|
250
278
|
// active. Any other flag or a path would be silently ignored, so it is refused.
|
|
251
279
|
if (listing && !validating) {
|
|
@@ -273,6 +301,7 @@ const run = async (argv: string[], cwd: string): Promise<number> => {
|
|
|
273
301
|
noPlugins: values["no-plugins"],
|
|
274
302
|
lenient: values["lenient-plugins"],
|
|
275
303
|
collect: validating,
|
|
304
|
+
selection: { enable, disable, prefix: "--" },
|
|
276
305
|
});
|
|
277
306
|
|
|
278
307
|
if (validating) {
|
package/src/index.ts
CHANGED
package/src/project.ts
CHANGED
|
@@ -9,7 +9,7 @@ import { CONFIG_FILES, findConfig, loadConfig, NO_CONFIG } from "./config.ts";
|
|
|
9
9
|
import { ConfigCodeError, ConfigNotFoundError } from "./errors.ts";
|
|
10
10
|
import { loadPlugins, type PluginFailure, type PluginMode } from "./plugins/index.ts";
|
|
11
11
|
import { FIX_ORDER, RULES } from "./rules/index.ts";
|
|
12
|
-
import { buildRegistry, type Registry } from "./rules/registry.ts";
|
|
12
|
+
import { buildRegistry, type CategorySelection, type Registry } from "./rules/registry.ts";
|
|
13
13
|
|
|
14
14
|
export const LOCAL_DIR = ".mermaidjs-lint";
|
|
15
15
|
|
|
@@ -40,6 +40,8 @@ export interface SetupOptions {
|
|
|
40
40
|
lenient?: boolean;
|
|
41
41
|
// Load every plugin and report every failure, without stopping at the first.
|
|
42
42
|
collect?: boolean;
|
|
43
|
+
// Categories switched on or off for this run, over the config.
|
|
44
|
+
selection?: CategorySelection;
|
|
43
45
|
}
|
|
44
46
|
|
|
45
47
|
export interface Setup {
|
|
@@ -57,6 +59,7 @@ export const setUp = async ({
|
|
|
57
59
|
noPlugins = false,
|
|
58
60
|
lenient = false,
|
|
59
61
|
collect = false,
|
|
62
|
+
selection,
|
|
60
63
|
}: SetupOptions): Promise<Setup> => {
|
|
61
64
|
const root = await findProjectRoot(cwd);
|
|
62
65
|
let file: string | null;
|
|
@@ -85,6 +88,7 @@ export const setUp = async ({
|
|
|
85
88
|
// by choice (off) or by failure (lenient, collect), so it is set aside
|
|
86
89
|
// rather than failing the run. A built-in id never is.
|
|
87
90
|
ignoreUnknownPluginIds: mode !== "strict",
|
|
91
|
+
...(selection && { selection }),
|
|
88
92
|
});
|
|
89
93
|
return { root, registry, skipped: plugins.skipped, failures: plugins.failures };
|
|
90
94
|
};
|
package/src/rules/registry.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
// The registry: the rules a run actually uses. It starts from the built-in
|
|
2
|
-
// catalogue, adds whatever plugins loaded,
|
|
3
|
-
// that is off loses its check and its fixer together. Every check and fix reads
|
|
2
|
+
// catalogue, adds whatever plugins loaded, applies the config, then the run's
|
|
3
|
+
// category choice, so a rule that is off loses its check and its fixer together. Every check and fix reads
|
|
4
4
|
// the registry, which is what lets a plugin rule behave as a built-in. The CLI
|
|
5
5
|
// help reads the built-in catalogue directly.
|
|
6
6
|
|
|
7
7
|
import type { Config } from "../config.ts";
|
|
8
|
-
import { RuleOptionError, UnknownRuleError } from "../errors.ts";
|
|
8
|
+
import { FlagError, RuleOptionError, UnknownRuleError } from "../errors.ts";
|
|
9
9
|
import { compositeOver, wcagRatio } from "../parse/color.ts";
|
|
10
10
|
import { canvasColourProblem, PROFILES, THEMES, type Theme } from "../parse/contrast.ts";
|
|
11
11
|
import { canonicalPreset } from "../parse/metrics.ts";
|
|
@@ -49,8 +49,49 @@ export interface RegistryInput {
|
|
|
49
49
|
// A lenient or collect (validating) run drops a setting for an unloaded
|
|
50
50
|
// plugin rule instead of failing. A built-in id always fails.
|
|
51
51
|
ignoreUnknownPluginIds?: boolean;
|
|
52
|
+
// Categories switched on or off for this run, over the config.
|
|
53
|
+
selection?: CategorySelection;
|
|
52
54
|
}
|
|
53
55
|
|
|
56
|
+
// The run's category choice. `enable` turns a category's rules on even when
|
|
57
|
+
// the config turns them off, and `disable` turns them off. `prefix` is "--"
|
|
58
|
+
// when the command passes its flags, so a message names the flag the user typed.
|
|
59
|
+
export interface CategorySelection {
|
|
60
|
+
enable?: readonly string[];
|
|
61
|
+
disable?: readonly string[];
|
|
62
|
+
prefix?: string;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const selectionNames = (prefix: string): { enable: string; disable: string } =>
|
|
66
|
+
prefix
|
|
67
|
+
? { enable: `${prefix}category`, disable: `${prefix}disable-category` }
|
|
68
|
+
: { enable: "selection.enable", disable: "selection.disable" };
|
|
69
|
+
|
|
70
|
+
// A category the run does not know, or one named both on and off, is a usage
|
|
71
|
+
// error, because either would leave the run checking something other than
|
|
72
|
+
// what was asked.
|
|
73
|
+
const checkSelection = (selection: CategorySelection, categories: ReadonlyMap<string, string>): void => {
|
|
74
|
+
const names = selectionNames(selection.prefix ?? "");
|
|
75
|
+
const known = [...categories.keys()];
|
|
76
|
+
for (const side of ["enable", "disable"] as const) {
|
|
77
|
+
for (const word of selection[side] ?? []) {
|
|
78
|
+
if (!categories.has(word)) {
|
|
79
|
+
throw new FlagError(
|
|
80
|
+
names[side],
|
|
81
|
+
`${names[side]} names no loaded category "${word}"; the categories are ${known.join(", ")}`,
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
const both = (selection.enable ?? []).filter((word) => selection.disable?.includes(word));
|
|
87
|
+
if (both.length > 0) {
|
|
88
|
+
throw new FlagError(
|
|
89
|
+
names.disable,
|
|
90
|
+
`${names.enable} and ${names.disable} both name ${[...new Set(both)].join(", ")}; name each category once`,
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
|
|
54
95
|
const typeOf = (value: unknown): OptionType | null => {
|
|
55
96
|
if (typeof value === "number" && Number.isFinite(value)) return "number";
|
|
56
97
|
if (typeof value === "string") return "string";
|
|
@@ -123,6 +164,8 @@ const checkOptions = (where: string, rule: Rule, options: RuleOptions): void =>
|
|
|
123
164
|
|
|
124
165
|
export const buildRegistry = (input: RegistryInput): Registry => {
|
|
125
166
|
const { rules, fixOrder, categories, sources = [], config, ignoreUnknownPluginIds = false } = input;
|
|
167
|
+
const selection = input.selection ?? {};
|
|
168
|
+
checkSelection(selection, categories);
|
|
126
169
|
const off = new Set<string>();
|
|
127
170
|
const options = new Map<RuleId, RuleOptions>();
|
|
128
171
|
const ignored: string[] = [];
|
|
@@ -144,10 +187,23 @@ export const buildRegistry = (input: RegistryInput): Registry => {
|
|
|
144
187
|
);
|
|
145
188
|
}
|
|
146
189
|
checkOptions(where, rule, setting.options);
|
|
190
|
+
// A rule that is off keeps its options, so a category flag that turns it
|
|
191
|
+
// back on runs it as the config describes it.
|
|
192
|
+
options.set(rule.id, setting.options);
|
|
147
193
|
if (setting.severity === "off") off.add(id);
|
|
148
|
-
|
|
194
|
+
}
|
|
195
|
+
// Every rule starts on, the config turns some off, and then the category
|
|
196
|
+
// flags have the last word for this run (MJL-0020).
|
|
197
|
+
for (const rule of rules) {
|
|
198
|
+
if (selection.enable?.includes(rule.category)) off.delete(rule.id);
|
|
199
|
+
if (selection.disable?.includes(rule.category)) off.add(rule.id);
|
|
149
200
|
}
|
|
150
201
|
const active = rules.filter((r) => !off.has(r.id));
|
|
202
|
+
// Category flags that leave nothing to run would pass every file unchecked.
|
|
203
|
+
if (active.length === 0 && (selection.disable?.length ?? 0) > 0) {
|
|
204
|
+
const name = selectionNames(selection.prefix ?? "").disable;
|
|
205
|
+
throw new FlagError(name, `${name} leaves no rule to run; disable fewer categories`);
|
|
206
|
+
}
|
|
151
207
|
return {
|
|
152
208
|
rules: active,
|
|
153
209
|
fixOrder: fixOrder.filter((r) => !off.has(r.id)),
|