@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 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.6.3",
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
- else
2012
- options.set(rule.id, setting.options);
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
- mermaidjs-lint --list-rules [--config FILE] [--no-plugins | --lenient-plugins]
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 takes no path or other flag.
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
- else
1934
- options.set(rule.id, setting.options);
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.6.3",
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
- mermaidjs-lint --list-rules [--config FILE] [--no-plugins | --lenient-plugins]
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 takes no path or other flag.
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
- else
1831
- options.set(rule.id, setting.options);
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)),
@@ -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";
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jpeakai/mermaidjs-lint",
3
- "version": "0.6.3",
3
+ "version": "0.7.0",
4
4
  "type": "module",
5
5
  "description": "Deterministic lint gates and verified fixers for Mermaid diagrams in markdown",
6
6
  "license": "MIT",
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
- mermaidjs-lint --list-rules [--config FILE] [--no-plugins | --lenient-plugins]
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 takes no path or other flag.
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
@@ -20,6 +20,7 @@ export {
20
20
  BUILTIN,
21
21
  BUILTIN_CATEGORIES,
22
22
  buildRegistry,
23
+ type CategorySelection,
23
24
  type LoadedSource,
24
25
  type Registry,
25
26
  type RegistryInput,
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
  };
@@ -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, then applies the config, so a rule
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
- else options.set(rule.id, setting.options);
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)),