@jpeakai/mermaidjs-lint 0.6.3 → 0.7.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/README.md +0 -1
- 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
|
@@ -19,7 +19,6 @@ npx -y @jpeakai/mermaidjs-lint --fix docs/ # apply every fix proven safe,
|
|
|
19
19
|
Pass your own paths: every path you name must exist, or the run stops with a usage error and exit 2.
|
|
20
20
|
A `.mmd` or `.mermaid` file is read as one diagram, and any other file you name is read as markdown.
|
|
21
21
|
A directory is walked for `.mmd`, `.mermaid`, `.md` and `.markdown` files.
|
|
22
|
-
The walk skips dot-folders and any folder named `node_modules`, `vendor`, `dist`, `build`, `tmp` or `__pycache__`, at any depth.
|
|
23
22
|
Installed as a dev dependency, the command is `mermaidjs-lint`, and `--help` lists every flag.
|
|
24
23
|
|
|
25
24
|
[Get started](https://jpeakai.github.io/jpai-mermaidjs-lint/get-started/) walks through a first run, a fix, and adding the check to a project.
|
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.0",
|
|
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.0",
|
|
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)),
|