@jpeakai/mermaidjs-lint 0.6.0 → 0.6.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/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.0",
12
+ version: "0.6.1",
13
13
  type: "module",
14
14
  description: "Deterministic lint gates and verified fixers for Mermaid diagrams in markdown",
15
15
  license: "MIT",
@@ -181,8 +181,9 @@ class RuleOptionError extends UsageError {
181
181
 
182
182
  class CanvasProfileError extends UsageError {
183
183
  file;
184
- constructor(file) {
185
- super(`${file}: a canvas applies under the github profile only, and this file is scored under mkdocs-material, whose page is Material's own; drop the canvas for it, or pass --profile github`, "canvas-profile");
184
+ constructor(file, cause = "flag") {
185
+ const hint = cause === "flag" ? "run --canvas-light and --canvas-dark over the GitHub-rendered paths only, or set the canvas in the config, which a Material file sets aside" : "remove the mkdocs-material profile setting for this run, or remove the canvas";
186
+ super(`${file}: a canvas applies under the github profile only, and this file is scored under mkdocs-material, whose page is Material's own; ${hint}`, "canvas-profile");
186
187
  this.file = file;
187
188
  }
188
189
  }
@@ -1609,8 +1610,9 @@ var settingsOf = (ctx) => {
1609
1610
  if (problem)
1610
1611
  throw new Error(`canvas.${side} ${problem}: ${JSON.stringify(setting[side])}`);
1611
1612
  }
1612
- if (profile === "mkdocs-material")
1613
- throw new CanvasProfileError(ctx.file);
1613
+ if (profile === "mkdocs-material") {
1614
+ throw new CanvasProfileError(ctx.file, ctx.options.profile === "mkdocs-material" ? "profile" : "flag");
1615
+ }
1614
1616
  return { profile, canvas: resolveCanvas(setting) };
1615
1617
  };
1616
1618
  var scored = new WeakMap;
@@ -2048,7 +2050,10 @@ var checkRunFlags = (flags, prefix = "") => {
2048
2050
  throw new FlagError(`${prefix}profile`, `${prefix}profile must be one of ${PROFILES.join(", ")}, not "${profile}"`);
2049
2051
  }
2050
2052
  };
2051
- var optionsFor = (rule, configured, flags) => {
2053
+ var canvasOptions = (rule) => Object.entries(rule.options ?? {}).filter(([, type]) => type === "canvas").map(([name]) => name);
2054
+ var hasFlagCanvas = (flags) => THEMES.some((side) => flags.canvas?.[side] !== undefined);
2055
+ var setsAside = (rule, options, flags, file) => !hasFlagCanvas(flags) && rule.options?.profile !== undefined && (options.profile ?? "auto") === "auto" && canvasOptions(rule).some((name) => options[name] !== undefined) && detectProfile(file) === "mkdocs-material";
2056
+ var optionsFor = (rule, configured, flags, file) => {
2052
2057
  const options = { ...configured };
2053
2058
  for (const name of ["preset", "profile"]) {
2054
2059
  const value = flags[name];
@@ -2058,21 +2063,33 @@ var optionsFor = (rule, configured, flags) => {
2058
2063
  if (typeof options.preset === "string")
2059
2064
  options.preset = canonicalPreset(options.preset) ?? options.preset;
2060
2065
  const canvas = flags.canvas;
2061
- if (canvas && THEMES.some((side) => canvas[side] !== undefined) && rule.options?.canvas === "canvas") {
2062
- const configured2 = typeof options.canvas === "object" && options.canvas !== null ? options.canvas : {};
2063
- const merged = { ...configured2 };
2064
- for (const side of THEMES) {
2065
- const value = canvas[side];
2066
- if (value !== undefined)
2067
- merged[side] = value;
2066
+ if (canvas && hasFlagCanvas(flags)) {
2067
+ for (const name of canvasOptions(rule)) {
2068
+ const current = options[name];
2069
+ const merged = {
2070
+ ...typeof current === "object" && current !== null ? current : {}
2071
+ };
2072
+ for (const side of THEMES) {
2073
+ const value = canvas[side];
2074
+ if (value !== undefined)
2075
+ merged[side] = value;
2076
+ }
2077
+ options[name] = merged;
2068
2078
  }
2069
- options.canvas = merged;
2079
+ }
2080
+ if (file !== undefined && setsAside(rule, options, flags, file)) {
2081
+ for (const name of canvasOptions(rule))
2082
+ delete options[name];
2070
2083
  }
2071
2084
  return options;
2072
2085
  };
2086
+ var canvasSetAside = (file, flags = {}, registry = BUILTIN) => registry.rules.some((rule) => {
2087
+ const options = optionsFor(rule, registry.options.get(rule.id), flags);
2088
+ return setsAside(rule, options, flags, file);
2089
+ });
2073
2090
  var checkModel = async (doc, file, flags = {}, registry = BUILTIN) => {
2074
2091
  checkRunFlags(flags);
2075
- const perRule = registry.rules.map((rule) => rule.check({ doc, file, options: optionsFor(rule, registry.options.get(rule.id), flags), helpers: HELPERS }));
2092
+ const perRule = registry.rules.map((rule) => rule.check({ doc, file, options: optionsFor(rule, registry.options.get(rule.id), flags, file), helpers: HELPERS }));
2076
2093
  const findings = (await Promise.all(perRule)).flat();
2077
2094
  return findings.sort((a, b) => a.line - b.line || a.rule.localeCompare(b.rule));
2078
2095
  };
@@ -2244,7 +2261,7 @@ var fixSourceReport = async (src, file, flags = {}, registry = BUILTIN) => {
2244
2261
  };
2245
2262
  let progressed = false;
2246
2263
  for (const rule of registry.fixOrder) {
2247
- const options = optionsFor(rule, registry.options.get(rule.id), flags);
2264
+ const options = optionsFor(rule, registry.options.get(rule.id), flags, file);
2248
2265
  const proposed = await rule.fix?.({ doc, file, options, helpers: HELPERS }) ?? [];
2249
2266
  const sound = proposed.filter((e) => {
2250
2267
  if (wellFormed(e, current.length))
@@ -2839,7 +2856,8 @@ Examples of every rule, with fixes: https://jpeakai.github.io/jpai-mermaidjs-lin
2839
2856
  --canvas-light COLOUR and --canvas-dark COLOUR set the page a host draws in each theme, under the github
2840
2857
  profile only. A translucent fill, an edge label box and a sequence diagram's page-backed text are scored
2841
2858
  on it. Each is an opaque CSS colour such as #faf8f9, and a side not given keeps the config's, or GitHub's
2842
- (#ffffff light, #0d1117 dark). A file scored under mkdocs-material refuses a canvas with a usage error.
2859
+ (#ffffff light, #0d1117 dark). A file scored under mkdocs-material refuses a canvas flag with a usage
2860
+ error, and sets a config canvas aside when auto picked the profile.
2843
2861
  --json prints one JSON object instead of text lines.
2844
2862
  --fix applies every fix it can prove safe, then reports what remains.
2845
2863
  A fixer that is unsure leaves the text alone and the finding stays.
@@ -3025,6 +3043,10 @@ var run = async (argv, cwd) => {
3025
3043
  throw new FlagError("files", "no matching files");
3026
3044
  if (values.fix)
3027
3045
  await refuseOutsideLinks(files, root, cwd);
3046
+ const setAside = files.filter((file) => canvasSetAside(file, flags, registry));
3047
+ if (setAside.length > 0) {
3048
+ console.error(`mermaidjs-lint: config canvas set aside for ${plural(setAside.length, "file")} scored under mkdocs-material: ${setAside.map((f) => display(f, cwd)).join(", ")}`);
3049
+ }
3028
3050
  const perFile = await Promise.all(files.map(async (file) => {
3029
3051
  let src = await readFile3(file, "utf8");
3030
3052
  if (values.fix) {
package/dist/index.js CHANGED
@@ -103,8 +103,9 @@ class RuleOptionError extends UsageError {
103
103
 
104
104
  class CanvasProfileError extends UsageError {
105
105
  file;
106
- constructor(file) {
107
- super(`${file}: a canvas applies under the github profile only, and this file is scored under mkdocs-material, whose page is Material's own; drop the canvas for it, or pass --profile github`, "canvas-profile");
106
+ constructor(file, cause = "flag") {
107
+ const hint = cause === "flag" ? "run --canvas-light and --canvas-dark over the GitHub-rendered paths only, or set the canvas in the config, which a Material file sets aside" : "remove the mkdocs-material profile setting for this run, or remove the canvas";
108
+ super(`${file}: a canvas applies under the github profile only, and this file is scored under mkdocs-material, whose page is Material's own; ${hint}`, "canvas-profile");
108
109
  this.file = file;
109
110
  }
110
111
  }
@@ -1531,8 +1532,9 @@ var settingsOf = (ctx) => {
1531
1532
  if (problem)
1532
1533
  throw new Error(`canvas.${side} ${problem}: ${JSON.stringify(setting[side])}`);
1533
1534
  }
1534
- if (profile === "mkdocs-material")
1535
- throw new CanvasProfileError(ctx.file);
1535
+ if (profile === "mkdocs-material") {
1536
+ throw new CanvasProfileError(ctx.file, ctx.options.profile === "mkdocs-material" ? "profile" : "flag");
1537
+ }
1536
1538
  return { profile, canvas: resolveCanvas(setting) };
1537
1539
  };
1538
1540
  var scored = new WeakMap;
@@ -1970,7 +1972,10 @@ var checkRunFlags = (flags, prefix = "") => {
1970
1972
  throw new FlagError(`${prefix}profile`, `${prefix}profile must be one of ${PROFILES.join(", ")}, not "${profile}"`);
1971
1973
  }
1972
1974
  };
1973
- var optionsFor = (rule, configured, flags) => {
1975
+ var canvasOptions = (rule) => Object.entries(rule.options ?? {}).filter(([, type]) => type === "canvas").map(([name]) => name);
1976
+ var hasFlagCanvas = (flags) => THEMES.some((side) => flags.canvas?.[side] !== undefined);
1977
+ var setsAside = (rule, options, flags, file) => !hasFlagCanvas(flags) && rule.options?.profile !== undefined && (options.profile ?? "auto") === "auto" && canvasOptions(rule).some((name) => options[name] !== undefined) && detectProfile(file) === "mkdocs-material";
1978
+ var optionsFor = (rule, configured, flags, file) => {
1974
1979
  const options = { ...configured };
1975
1980
  for (const name of ["preset", "profile"]) {
1976
1981
  const value = flags[name];
@@ -1980,21 +1985,33 @@ var optionsFor = (rule, configured, flags) => {
1980
1985
  if (typeof options.preset === "string")
1981
1986
  options.preset = canonicalPreset(options.preset) ?? options.preset;
1982
1987
  const canvas = flags.canvas;
1983
- if (canvas && THEMES.some((side) => canvas[side] !== undefined) && rule.options?.canvas === "canvas") {
1984
- const configured2 = typeof options.canvas === "object" && options.canvas !== null ? options.canvas : {};
1985
- const merged = { ...configured2 };
1986
- for (const side of THEMES) {
1987
- const value = canvas[side];
1988
- if (value !== undefined)
1989
- merged[side] = value;
1988
+ if (canvas && hasFlagCanvas(flags)) {
1989
+ for (const name of canvasOptions(rule)) {
1990
+ const current = options[name];
1991
+ const merged = {
1992
+ ...typeof current === "object" && current !== null ? current : {}
1993
+ };
1994
+ for (const side of THEMES) {
1995
+ const value = canvas[side];
1996
+ if (value !== undefined)
1997
+ merged[side] = value;
1998
+ }
1999
+ options[name] = merged;
1990
2000
  }
1991
- options.canvas = merged;
2001
+ }
2002
+ if (file !== undefined && setsAside(rule, options, flags, file)) {
2003
+ for (const name of canvasOptions(rule))
2004
+ delete options[name];
1992
2005
  }
1993
2006
  return options;
1994
2007
  };
2008
+ var canvasSetAside = (file, flags = {}, registry = BUILTIN) => registry.rules.some((rule) => {
2009
+ const options = optionsFor(rule, registry.options.get(rule.id), flags);
2010
+ return setsAside(rule, options, flags, file);
2011
+ });
1995
2012
  var checkModel = async (doc, file, flags = {}, registry = BUILTIN) => {
1996
2013
  checkRunFlags(flags);
1997
- const perRule = registry.rules.map((rule) => rule.check({ doc, file, options: optionsFor(rule, registry.options.get(rule.id), flags), helpers: HELPERS }));
2014
+ const perRule = registry.rules.map((rule) => rule.check({ doc, file, options: optionsFor(rule, registry.options.get(rule.id), flags, file), helpers: HELPERS }));
1998
2015
  const findings = (await Promise.all(perRule)).flat();
1999
2016
  return findings.sort((a, b) => a.line - b.line || a.rule.localeCompare(b.rule));
2000
2017
  };
@@ -2006,7 +2023,7 @@ import { parseArgs } from "node:util";
2006
2023
  // package.json
2007
2024
  var package_default = {
2008
2025
  name: "@jpeakai/mermaidjs-lint",
2009
- version: "0.6.0",
2026
+ version: "0.6.1",
2010
2027
  type: "module",
2011
2028
  description: "Deterministic lint gates and verified fixers for Mermaid diagrams in markdown",
2012
2029
  license: "MIT",
@@ -2243,7 +2260,7 @@ var fixSourceReport = async (src, file, flags = {}, registry = BUILTIN) => {
2243
2260
  };
2244
2261
  let progressed = false;
2245
2262
  for (const rule of registry.fixOrder) {
2246
- const options = optionsFor(rule, registry.options.get(rule.id), flags);
2263
+ const options = optionsFor(rule, registry.options.get(rule.id), flags, file);
2247
2264
  const proposed = await rule.fix?.({ doc, file, options, helpers: HELPERS }) ?? [];
2248
2265
  const sound = proposed.filter((e) => {
2249
2266
  if (wellFormed(e, current.length))
@@ -2839,7 +2856,8 @@ Examples of every rule, with fixes: https://jpeakai.github.io/jpai-mermaidjs-lin
2839
2856
  --canvas-light COLOUR and --canvas-dark COLOUR set the page a host draws in each theme, under the github
2840
2857
  profile only. A translucent fill, an edge label box and a sequence diagram's page-backed text are scored
2841
2858
  on it. Each is an opaque CSS colour such as #faf8f9, and a side not given keeps the config's, or GitHub's
2842
- (#ffffff light, #0d1117 dark). A file scored under mkdocs-material refuses a canvas with a usage error.
2859
+ (#ffffff light, #0d1117 dark). A file scored under mkdocs-material refuses a canvas flag with a usage
2860
+ error, and sets a config canvas aside when auto picked the profile.
2843
2861
  --json prints one JSON object instead of text lines.
2844
2862
  --fix applies every fix it can prove safe, then reports what remains.
2845
2863
  A fixer that is unsure leaves the text alone and the finding stays.
@@ -3025,6 +3043,10 @@ var run = async (argv, cwd) => {
3025
3043
  throw new FlagError("files", "no matching files");
3026
3044
  if (values.fix)
3027
3045
  await refuseOutsideLinks(files, root, cwd);
3046
+ const setAside = files.filter((file) => canvasSetAside(file, flags, registry));
3047
+ if (setAside.length > 0) {
3048
+ console.error(`mermaidjs-lint: config canvas set aside for ${plural(setAside.length, "file")} scored under mkdocs-material: ${setAside.map((f) => display(f, cwd)).join(", ")}`);
3049
+ }
3028
3050
  const perFile = await Promise.all(files.map(async (file) => {
3029
3051
  let src = await readFile3(file, "utf8");
3030
3052
  if (values.fix) {
@@ -3082,6 +3104,7 @@ export {
3082
3104
  collectFiles,
3083
3105
  checkModel,
3084
3106
  checkMarkdown,
3107
+ canvasSetAside,
3085
3108
  buildRegistry,
3086
3109
  buildDocModel,
3087
3110
  UsageError,
package/dist/testing.js CHANGED
@@ -50,8 +50,9 @@ class RuleOptionError extends UsageError {
50
50
 
51
51
  class CanvasProfileError extends UsageError {
52
52
  file;
53
- constructor(file) {
54
- super(`${file}: a canvas applies under the github profile only, and this file is scored under mkdocs-material, whose page is Material's own; drop the canvas for it, or pass --profile github`, "canvas-profile");
53
+ constructor(file, cause = "flag") {
54
+ const hint = cause === "flag" ? "run --canvas-light and --canvas-dark over the GitHub-rendered paths only, or set the canvas in the config, which a Material file sets aside" : "remove the mkdocs-material profile setting for this run, or remove the canvas";
55
+ super(`${file}: a canvas applies under the github profile only, and this file is scored under mkdocs-material, whose page is Material's own; ${hint}`, "canvas-profile");
55
56
  this.file = file;
56
57
  }
57
58
  }
@@ -1429,8 +1430,9 @@ var settingsOf = (ctx) => {
1429
1430
  if (problem)
1430
1431
  throw new Error(`canvas.${side} ${problem}: ${JSON.stringify(setting[side])}`);
1431
1432
  }
1432
- if (profile === "mkdocs-material")
1433
- throw new CanvasProfileError(ctx.file);
1433
+ if (profile === "mkdocs-material") {
1434
+ throw new CanvasProfileError(ctx.file, ctx.options.profile === "mkdocs-material" ? "profile" : "flag");
1435
+ }
1434
1436
  return { profile, canvas: resolveCanvas(setting) };
1435
1437
  };
1436
1438
  var scored = new WeakMap;
@@ -1867,7 +1869,10 @@ var checkRunFlags = (flags, prefix = "") => {
1867
1869
  throw new FlagError(`${prefix}profile`, `${prefix}profile must be one of ${PROFILES.join(", ")}, not "${profile}"`);
1868
1870
  }
1869
1871
  };
1870
- var optionsFor = (rule, configured, flags) => {
1872
+ var canvasOptions = (rule) => Object.entries(rule.options ?? {}).filter(([, type]) => type === "canvas").map(([name]) => name);
1873
+ var hasFlagCanvas = (flags) => THEMES.some((side) => flags.canvas?.[side] !== undefined);
1874
+ var setsAside = (rule, options, flags, file) => !hasFlagCanvas(flags) && rule.options?.profile !== undefined && (options.profile ?? "auto") === "auto" && canvasOptions(rule).some((name) => options[name] !== undefined) && detectProfile(file) === "mkdocs-material";
1875
+ var optionsFor = (rule, configured, flags, file) => {
1871
1876
  const options = { ...configured };
1872
1877
  for (const name of ["preset", "profile"]) {
1873
1878
  const value = flags[name];
@@ -1877,21 +1882,29 @@ var optionsFor = (rule, configured, flags) => {
1877
1882
  if (typeof options.preset === "string")
1878
1883
  options.preset = canonicalPreset(options.preset) ?? options.preset;
1879
1884
  const canvas = flags.canvas;
1880
- if (canvas && THEMES.some((side) => canvas[side] !== undefined) && rule.options?.canvas === "canvas") {
1881
- const configured2 = typeof options.canvas === "object" && options.canvas !== null ? options.canvas : {};
1882
- const merged = { ...configured2 };
1883
- for (const side of THEMES) {
1884
- const value = canvas[side];
1885
- if (value !== undefined)
1886
- merged[side] = value;
1885
+ if (canvas && hasFlagCanvas(flags)) {
1886
+ for (const name of canvasOptions(rule)) {
1887
+ const current = options[name];
1888
+ const merged = {
1889
+ ...typeof current === "object" && current !== null ? current : {}
1890
+ };
1891
+ for (const side of THEMES) {
1892
+ const value = canvas[side];
1893
+ if (value !== undefined)
1894
+ merged[side] = value;
1895
+ }
1896
+ options[name] = merged;
1887
1897
  }
1888
- options.canvas = merged;
1898
+ }
1899
+ if (file !== undefined && setsAside(rule, options, flags, file)) {
1900
+ for (const name of canvasOptions(rule))
1901
+ delete options[name];
1889
1902
  }
1890
1903
  return options;
1891
1904
  };
1892
1905
  var checkModel = async (doc, file, flags = {}, registry = BUILTIN) => {
1893
1906
  checkRunFlags(flags);
1894
- const perRule = registry.rules.map((rule) => rule.check({ doc, file, options: optionsFor(rule, registry.options.get(rule.id), flags), helpers: HELPERS }));
1907
+ const perRule = registry.rules.map((rule) => rule.check({ doc, file, options: optionsFor(rule, registry.options.get(rule.id), flags, file), helpers: HELPERS }));
1895
1908
  const findings = (await Promise.all(perRule)).flat();
1896
1909
  return findings.sort((a, b) => a.line - b.line || a.rule.localeCompare(b.rule));
1897
1910
  };
@@ -2004,7 +2017,7 @@ var fixSourceReport = async (src, file, flags = {}, registry = BUILTIN) => {
2004
2017
  };
2005
2018
  let progressed = false;
2006
2019
  for (const rule of registry.fixOrder) {
2007
- const options = optionsFor(rule, registry.options.get(rule.id), flags);
2020
+ const options = optionsFor(rule, registry.options.get(rule.id), flags, file);
2008
2021
  const proposed = await rule.fix?.({ doc, file, options, helpers: HELPERS }) ?? [];
2009
2022
  const sound = proposed.filter((e) => {
2010
2023
  if (wellFormed(e, current.length))
@@ -8,6 +8,7 @@ export interface RunFlags {
8
8
  canvas?: CanvasSetting;
9
9
  }
10
10
  export declare const checkRunFlags: (flags: RunFlags, prefix?: string) => void;
11
- export declare const optionsFor: (rule: Rule, configured: RuleOptions | undefined, flags: RunFlags) => RuleOptions;
11
+ export declare const optionsFor: (rule: Rule, configured: RuleOptions | undefined, flags: RunFlags, file?: string) => RuleOptions;
12
+ export declare const canvasSetAside: (file: string, flags?: RunFlags, registry?: Registry) => boolean;
12
13
  export declare const checkModel: (doc: DocModel, file: string, flags?: RunFlags, registry?: Registry) => Promise<Finding[]>;
13
14
  export declare const checkMarkdown: (src: string, file: string, flags?: RunFlags, registry?: Registry) => Promise<Finding[]>;
@@ -49,7 +49,7 @@ export declare class RuleOptionError extends UsageError {
49
49
  }
50
50
  export declare class CanvasProfileError extends UsageError {
51
51
  readonly file: string;
52
- constructor(file: string);
52
+ constructor(file: string, cause?: "flag" | "profile");
53
53
  }
54
54
  export declare class FixOutsideRootError extends UsageError {
55
55
  readonly file: string;
@@ -1,4 +1,4 @@
1
- export { checkMarkdown, checkModel, type RunFlags } from "./check.ts";
1
+ export { canvasSetAside, checkMarkdown, checkModel, type RunFlags } from "./check.ts";
2
2
  export { main } from "./cli.ts";
3
3
  export { type Config, loadConfig, type PluginLoading, parseConfig, type RuleSetting, type Severity } from "./config.ts";
4
4
  export * from "./errors.ts";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jpeakai/mermaidjs-lint",
3
- "version": "0.6.0",
3
+ "version": "0.6.1",
4
4
  "type": "module",
5
5
  "description": "Deterministic lint gates and verified fixers for Mermaid diagrams in markdown",
6
6
  "license": "MIT",
package/src/check.ts CHANGED
@@ -6,7 +6,14 @@
6
6
 
7
7
  import { FlagError } from "./errors.ts";
8
8
  import { buildDocModel, type DocModel, documentKind } from "./model.ts";
9
- import { type CanvasSetting, canvasColourProblem, PROFILES, THEMES, type Theme } from "./parse/contrast.ts";
9
+ import {
10
+ type CanvasSetting,
11
+ canvasColourProblem,
12
+ detectProfile,
13
+ PROFILES,
14
+ THEMES,
15
+ type Theme,
16
+ } from "./parse/contrast.ts";
10
17
  import { canonicalPreset, PRESET_NAMES } from "./parse/metrics.ts";
11
18
  import { BUILTIN, HELPERS, type Registry } from "./rules/registry.ts";
12
19
  import type { Finding, Rule, RuleOptions } from "./rules/types.ts";
@@ -57,10 +64,36 @@ export const checkRunFlags = (flags: RunFlags, prefix = ""): void => {
57
64
  }
58
65
  };
59
66
 
67
+ // Every option the rule declares with the canvas type, whatever its name.
68
+ const canvasOptions = (rule: Rule): string[] =>
69
+ Object.entries(rule.options ?? {})
70
+ .filter(([, type]) => type === "canvas")
71
+ .map(([name]) => name);
72
+
73
+ const hasFlagCanvas = (flags: RunFlags): boolean => THEMES.some((side) => flags.canvas?.[side] !== undefined);
74
+
75
+ // A canvas from the config is set aside, rather than refused, for a file the
76
+ // auto profile reads as mkdocs-material. The config covers the whole tree and
77
+ // has no per-path setting, so refusing it would stop every run over a repo
78
+ // that holds a Material site beside its GitHub-rendered markdown. A canvas
79
+ // flag, or an explicit mkdocs-material profile, still refuses (MJL-0019).
80
+ const setsAside = (rule: Rule, options: Record<string, unknown>, flags: RunFlags, file: string): boolean =>
81
+ !hasFlagCanvas(flags) &&
82
+ rule.options?.profile !== undefined &&
83
+ (options.profile ?? "auto") === "auto" &&
84
+ canvasOptions(rule).some((name) => options[name] !== undefined) &&
85
+ detectProfile(file) === "mkdocs-material";
86
+
60
87
  // The options a rule runs with: its configured options, then a flag for any
61
88
  // option the rule declares. A preset reaches the rule by its canonical name,
62
- // so a plugin rule never has to resolve an alias such as `high` or `H`.
63
- export const optionsFor = (rule: Rule, configured: RuleOptions | undefined, flags: RunFlags): RuleOptions => {
89
+ // so a plugin rule never has to resolve an alias such as `high` or `H`. Given
90
+ // the file, a config canvas the file sets aside is dropped.
91
+ export const optionsFor = (
92
+ rule: Rule,
93
+ configured: RuleOptions | undefined,
94
+ flags: RunFlags,
95
+ file?: string,
96
+ ): RuleOptions => {
64
97
  const options: Record<string, unknown> = { ...configured };
65
98
  for (const name of ["preset", "profile"] as const) {
66
99
  const value = flags[name];
@@ -68,20 +101,36 @@ export const optionsFor = (rule: Rule, configured: RuleOptions | undefined, flag
68
101
  }
69
102
  if (typeof options.preset === "string") options.preset = canonicalPreset(options.preset) ?? options.preset;
70
103
  // A canvas flag sets one side over the configured canvas, so --canvas-dark
71
- // alone keeps a configured light side.
104
+ // alone keeps a configured light side. It reaches every option of the
105
+ // canvas type, not only one named canvas.
72
106
  const canvas = flags.canvas;
73
- if (canvas && THEMES.some((side) => canvas[side] !== undefined) && rule.options?.canvas === "canvas") {
74
- const configured = typeof options.canvas === "object" && options.canvas !== null ? options.canvas : {};
75
- const merged: Record<string, string> = { ...(configured as Record<string, string>) };
76
- for (const side of THEMES) {
77
- const value = canvas[side];
78
- if (value !== undefined) merged[side] = value;
107
+ if (canvas && hasFlagCanvas(flags)) {
108
+ for (const name of canvasOptions(rule)) {
109
+ const current = options[name];
110
+ const merged: Record<string, string> = {
111
+ ...(typeof current === "object" && current !== null ? (current as Record<string, string>) : {}),
112
+ };
113
+ for (const side of THEMES) {
114
+ const value = canvas[side];
115
+ if (value !== undefined) merged[side] = value;
116
+ }
117
+ options[name] = merged;
79
118
  }
80
- options.canvas = merged;
119
+ }
120
+ if (file !== undefined && setsAside(rule, options, flags, file)) {
121
+ for (const name of canvasOptions(rule)) delete options[name];
81
122
  }
82
123
  return options;
83
124
  };
84
125
 
126
+ // Whether any rule sets its config canvas aside for this file, so the command
127
+ // can say which files were scored without it.
128
+ export const canvasSetAside = (file: string, flags: RunFlags = {}, registry: Registry = BUILTIN): boolean =>
129
+ registry.rules.some((rule) => {
130
+ const options = optionsFor(rule, registry.options.get(rule.id), flags);
131
+ return setsAside(rule, options as Record<string, unknown>, flags, file);
132
+ });
133
+
85
134
  export const checkModel = async (
86
135
  doc: DocModel,
87
136
  file: string,
@@ -90,7 +139,7 @@ export const checkModel = async (
90
139
  ): Promise<Finding[]> => {
91
140
  checkRunFlags(flags);
92
141
  const perRule = registry.rules.map((rule) =>
93
- rule.check({ doc, file, options: optionsFor(rule, registry.options.get(rule.id), flags), helpers: HELPERS }),
142
+ rule.check({ doc, file, options: optionsFor(rule, registry.options.get(rule.id), flags, file), helpers: HELPERS }),
94
143
  );
95
144
  const findings = (await Promise.all(perRule)).flat();
96
145
  return findings.sort((a, b) => a.line - b.line || a.rule.localeCompare(b.rule));
package/src/cli.ts CHANGED
@@ -5,7 +5,7 @@ import { lstat, readFile, realpath, writeFile } from "node:fs/promises";
5
5
  import { isAbsolute, relative, resolve } from "node:path";
6
6
  import { parseArgs } from "node:util";
7
7
  import pkg from "../package.json" with { type: "json" };
8
- import { checkModel, checkRunFlags, type RunFlags } from "./check.ts";
8
+ import { canvasSetAside, checkModel, checkRunFlags, type RunFlags } from "./check.ts";
9
9
  import { FixOutsideRootError, FlagError, MermaidLintError, UsageError } from "./errors.ts";
10
10
  import { collectFiles, DIAGRAM_FILE_EXTS, SKIP_DIRS } from "./files.ts";
11
11
  import { fixSourceReport } from "./fix/engine.ts";
@@ -55,7 +55,8 @@ Examples of every rule, with fixes: https://jpeakai.github.io/jpai-mermaidjs-lin
55
55
  --canvas-light COLOUR and --canvas-dark COLOUR set the page a host draws in each theme, under the github
56
56
  profile only. A translucent fill, an edge label box and a sequence diagram's page-backed text are scored
57
57
  on it. Each is an opaque CSS colour such as #faf8f9, and a side not given keeps the config's, or GitHub's
58
- (#ffffff light, #0d1117 dark). A file scored under mkdocs-material refuses a canvas with a usage error.
58
+ (#ffffff light, #0d1117 dark). A file scored under mkdocs-material refuses a canvas flag with a usage
59
+ error, and sets a config canvas aside when auto picked the profile.
59
60
  --json prints one JSON object instead of text lines.
60
61
  --fix applies every fix it can prove safe, then reports what remains.
61
62
  A fixer that is unsure leaves the text alone and the finding stays.
@@ -281,6 +282,16 @@ const run = async (argv: string[], cwd: string): Promise<number> => {
281
282
  if (files.length === 0) throw new FlagError("files", "no matching files");
282
283
  if (values.fix) await refuseOutsideLinks(files, root, cwd);
283
284
 
285
+ // A config canvas is set aside for a file the auto profile reads as
286
+ // mkdocs-material, and one line names every such file, so the run says what
287
+ // it scored without the canvas.
288
+ const setAside = files.filter((file) => canvasSetAside(file, flags, registry));
289
+ if (setAside.length > 0) {
290
+ console.error(
291
+ `mermaidjs-lint: config canvas set aside for ${plural(setAside.length, "file")} scored under mkdocs-material: ${setAside.map((f) => display(f, cwd)).join(", ")}`,
292
+ );
293
+ }
294
+
284
295
  // Every rule, in check and in fix, sees the file as an absolute path, so a
285
296
  // rule that reads the path sees one form, and the auto profile is found
286
297
  // from the file's own folder whatever the process's working directory. The
package/src/errors.ts CHANGED
@@ -132,13 +132,22 @@ export class RuleOptionError extends UsageError {
132
132
 
133
133
  // A canvas was set for a file scored under mkdocs-material, whose page is
134
134
  // Material's own, so the canvas would change nothing. Ignoring it would report
135
- // a pass the caller did not ask for, so the run stops instead.
135
+ // a pass the caller did not ask for, so the run stops instead. A config canvas
136
+ // on a file the auto profile reads as mkdocs-material is set aside, not
137
+ // refused (MJL-0019), so this error comes from one of two explicit settings,
138
+ // and its hint names the way out of each.
136
139
  export class CanvasProfileError extends UsageError {
137
140
  readonly file: string;
138
141
 
139
- constructor(file: string) {
142
+ // "flag": a canvas flag met a file the auto profile reads as mkdocs-material.
143
+ // "profile": a canvas met an explicitly set mkdocs-material profile.
144
+ constructor(file: string, cause: "flag" | "profile" = "flag") {
145
+ const hint =
146
+ cause === "flag"
147
+ ? "run --canvas-light and --canvas-dark over the GitHub-rendered paths only, or set the canvas in the config, which a Material file sets aside"
148
+ : "remove the mkdocs-material profile setting for this run, or remove the canvas";
140
149
  super(
141
- `${file}: a canvas applies under the github profile only, and this file is scored under mkdocs-material, whose page is Material's own; drop the canvas for it, or pass --profile github`,
150
+ `${file}: a canvas applies under the github profile only, and this file is scored under mkdocs-material, whose page is Material's own; ${hint}`,
142
151
  "canvas-profile",
143
152
  );
144
153
  this.file = file;
package/src/fix/engine.ts CHANGED
@@ -73,7 +73,7 @@ export const fixSourceReport = async (
73
73
 
74
74
  let progressed = false;
75
75
  for (const rule of registry.fixOrder) {
76
- const options = optionsFor(rule, registry.options.get(rule.id), flags);
76
+ const options = optionsFor(rule, registry.options.get(rule.id), flags, file);
77
77
  const proposed = (await rule.fix?.({ doc, file, options, helpers: HELPERS })) ?? [];
78
78
  const sound = proposed.filter((e) => {
79
79
  if (wellFormed(e, current.length)) return true;
package/src/index.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  // Public API.
2
2
 
3
- export { checkMarkdown, checkModel, type RunFlags } from "./check.ts";
3
+ export { canvasSetAside, checkMarkdown, checkModel, type RunFlags } from "./check.ts";
4
4
  export { main } from "./cli.ts";
5
5
  export { type Config, loadConfig, type PluginLoading, parseConfig, type RuleSetting, type Severity } from "./config.ts";
6
6
  export * from "./errors.ts";
@@ -109,7 +109,9 @@ export const settingsOf = (ctx: RuleContext): ContrastSettings => {
109
109
  const problem = canvasColourProblem(setting[side]);
110
110
  if (problem) throw new Error(`canvas.${side} ${problem}: ${JSON.stringify(setting[side])}`);
111
111
  }
112
- if (profile === "mkdocs-material") throw new CanvasProfileError(ctx.file);
112
+ if (profile === "mkdocs-material") {
113
+ throw new CanvasProfileError(ctx.file, ctx.options.profile === "mkdocs-material" ? "profile" : "flag");
114
+ }
113
115
  return { profile, canvas: resolveCanvas(setting) };
114
116
  };
115
117
 
@@ -45,8 +45,8 @@ export interface Finding {
45
45
  }
46
46
 
47
47
  // "canvas" is an object { light?, dark? } of opaque CSS colours: the page a
48
- // diagram is drawn on. A rule that declares it receives --canvas-light and
49
- // --canvas-dark, as the contrast rules do.
48
+ // diagram is drawn on. Every option of this type, whatever its name, receives
49
+ // --canvas-light and --canvas-dark, as the contrast rules do.
50
50
  export type OptionType = "number" | "string" | "boolean" | "string[]" | "canvas";
51
51
 
52
52
  export type OptionSpec = Readonly<Record<string, OptionType>>;