@matfsw/docusaurus-plantuml-plugin 1.0.2 → 1.1.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/dist/constants.js CHANGED
@@ -16,9 +16,20 @@ export function assetsDirForVersion(coreVersion) {
16
16
  }
17
17
  export const VIZ_SCRIPT_FILENAME = 'viz-global.js';
18
18
  export const PLANTUML_MODULE_FILENAME = 'plantuml.js';
19
- /** `data-*` attribute names, kept in one place because tests and docs depend on them. */
19
+ /**
20
+ * `data-*` attribute names, kept in one place because tests and docs depend on them.
21
+ *
22
+ * The `data-plantuml-*` prefix is historical: it predates Graphviz support and is applied to
23
+ * every diagram this plugin renders, whatever the engine. Renaming it would break author CSS
24
+ * for no functional gain, so {@link DATA_ATTR.engine} was added alongside it instead —
25
+ * that is the attribute to select on when the two engines need different styling.
26
+ */
20
27
  export const DATA_ATTR = {
21
28
  diagram: 'data-plantuml-diagram',
29
+ /** `"plantuml"` or `"graphviz"` — which engine produced this figure. */
30
+ engine: 'data-diagram-engine',
31
+ /** Graphviz layout engine (`dot`, `neato`, …). Absent on PlantUML figures. */
32
+ layout: 'data-diagram-layout',
22
33
  status: 'data-plantuml-status',
23
34
  theme: 'data-plantuml-theme',
24
35
  /** `"true"` on the figure when the diagram is zoomable. */
package/dist/index.cjs CHANGED
@@ -80,17 +80,42 @@ function locatePlantUmlCore() {
80
80
  const packageDir = import_node_path.default.dirname(packageJsonPath);
81
81
  const packageJson = JSON.parse(import_node_fs.default.readFileSync(packageJsonPath, "utf8"));
82
82
  const version = typeof packageJson.version === "string" ? packageJson.version : "0.0.0";
83
- return {
84
- packageDir,
85
- version,
86
- files: [
87
- import_node_path.default.join(packageDir, VIZ_SCRIPT_FILENAME),
88
- import_node_path.default.join(packageDir, PLANTUML_MODULE_FILENAME)
89
- ]
90
- };
83
+ const files = [
84
+ import_node_path.default.join(packageDir, VIZ_SCRIPT_FILENAME),
85
+ import_node_path.default.join(packageDir, PLANTUML_MODULE_FILENAME)
86
+ ];
87
+ files.forEach((file) => assertRuntimeFile(file, version));
88
+ return { packageDir, version, files };
89
+ }
90
+ function assertRuntimeFile(file, version) {
91
+ if (import_node_fs.default.existsSync(file)) return;
92
+ throw new Error(
93
+ `[${PLUGIN_NAME}] '@plantuml/core@${version}' does not contain '${import_node_path.default.basename(file)}', which this plugin serves as a runtime asset. This means the installed version of @plantuml/core is not one this plugin supports. Pin a supported version, or open an issue if the package has genuinely changed its layout.`
94
+ );
91
95
  }
92
96
 
93
97
  // src/options.ts
98
+ var GRAPHVIZ_ENGINES = [
99
+ "circo",
100
+ "dot",
101
+ "fdp",
102
+ "neato",
103
+ "nop",
104
+ "nop1",
105
+ "nop2",
106
+ "osage",
107
+ "patchwork",
108
+ "sfdp",
109
+ "twopi"
110
+ ];
111
+ var DEFAULT_GRAPHVIZ_OPTIONS = {
112
+ enabled: true,
113
+ languages: ["dot", "graphviz", "gv"],
114
+ engine: "dot",
115
+ allowEngineOverride: true,
116
+ maxSourceBytes: 1e5,
117
+ transparentBackground: true
118
+ };
94
119
  var DEFAULT_OPTIONS = {
95
120
  languages: ["plantuml", "puml"],
96
121
  theme: "auto",
@@ -100,7 +125,8 @@ var DEFAULT_OPTIONS = {
100
125
  showSourceOnError: true,
101
126
  renderTimeoutMs: 2e4,
102
127
  cacheMaxEntries: 50,
103
- zoom: true
128
+ zoom: true,
129
+ graphviz: DEFAULT_GRAPHVIZ_OPTIONS
104
130
  };
105
131
  var CACHE_MODES = ["none", "memory", "session"];
106
132
  var THEMES = ["auto", "light", "dark"];
@@ -128,28 +154,85 @@ function validateBoolean(value, key, fallback) {
128
154
  }
129
155
  return value;
130
156
  }
131
- function validateLanguages(value) {
132
- if (value === void 0) return [...DEFAULT_OPTIONS.languages];
157
+ function validateLanguages(value, key, fallback) {
158
+ if (value === void 0) return [...fallback];
133
159
  if (!Array.isArray(value)) {
134
- fail(`options.languages must be an array of strings, received ${JSON.stringify(value)}.`);
160
+ fail(`options.${key} must be an array of strings, received ${JSON.stringify(value)}.`);
135
161
  }
136
162
  if (value.length === 0) {
137
- fail("options.languages must contain at least one language.");
163
+ fail(`options.${key} must contain at least one language.`);
138
164
  }
139
165
  const normalized = value.map((entry, index) => {
140
166
  if (typeof entry !== "string" || entry.trim() === "") {
141
167
  fail(
142
- `options.languages[${index}] must be a non-empty string, received ${JSON.stringify(entry)}.`
168
+ `options.${key}[${index}] must be a non-empty string, received ${JSON.stringify(entry)}.`
143
169
  );
144
170
  }
145
171
  return entry.trim().toLowerCase();
146
172
  });
147
173
  const duplicates = normalized.filter((entry, index) => normalized.indexOf(entry) !== index);
148
174
  if (duplicates.length > 0) {
149
- fail(`options.languages contains duplicate entries: ${quoteList([...new Set(duplicates)])}.`);
175
+ fail(`options.${key} contains duplicate entries: ${quoteList([...new Set(duplicates)])}.`);
150
176
  }
151
177
  return normalized;
152
178
  }
179
+ function validateMaxSourceBytes(value) {
180
+ if (value === void 0) return DEFAULT_GRAPHVIZ_OPTIONS.maxSourceBytes;
181
+ if (typeof value !== "number" || !Number.isInteger(value) || value < 1) {
182
+ fail(
183
+ `options.graphviz.maxSourceBytes must be a positive integer, received ${JSON.stringify(value)}.`
184
+ );
185
+ }
186
+ return value;
187
+ }
188
+ function validateGraphviz(value) {
189
+ if (value === void 0) {
190
+ return { ...DEFAULT_GRAPHVIZ_OPTIONS, languages: [...DEFAULT_GRAPHVIZ_OPTIONS.languages] };
191
+ }
192
+ if (!isPlainObject(value)) {
193
+ fail(`options.graphviz must be an object, received ${JSON.stringify(value)}.`);
194
+ }
195
+ const allowedKeys = [
196
+ "enabled",
197
+ "languages",
198
+ "engine",
199
+ "allowEngineOverride",
200
+ "maxSourceBytes",
201
+ "transparentBackground"
202
+ ];
203
+ const unknownKeys = Object.keys(value).filter((key) => !allowedKeys.includes(key));
204
+ if (unknownKeys.length > 0) {
205
+ fail(
206
+ `Unknown option${unknownKeys.length > 1 ? "s" : ""} ${quoteList(unknownKeys.map((key) => `graphviz.${key}`))}. Supported options: ${quoteList(allowedKeys.map((key) => `graphviz.${key}`))}.`
207
+ );
208
+ }
209
+ const { engine } = value;
210
+ if (engine !== void 0 && !GRAPHVIZ_ENGINES.includes(engine)) {
211
+ fail(
212
+ `options.graphviz.engine must be one of ${quoteList(GRAPHVIZ_ENGINES)}, received ${JSON.stringify(engine)}.`
213
+ );
214
+ }
215
+ return {
216
+ enabled: validateBoolean(value.enabled, "graphviz.enabled", DEFAULT_GRAPHVIZ_OPTIONS.enabled),
217
+ languages: validateLanguages(
218
+ value.languages,
219
+ "graphviz.languages",
220
+ DEFAULT_GRAPHVIZ_OPTIONS.languages
221
+ ),
222
+ engine: engine ?? DEFAULT_GRAPHVIZ_OPTIONS.engine,
223
+ allowEngineOverride: validateBoolean(
224
+ value.allowEngineOverride,
225
+ "graphviz.allowEngineOverride",
226
+ DEFAULT_GRAPHVIZ_OPTIONS.allowEngineOverride
227
+ ),
228
+ maxSourceBytes: validateMaxSourceBytes(value.maxSourceBytes),
229
+ transparentBackground: validateBoolean(
230
+ value.transparentBackground,
231
+ "graphviz.transparentBackground",
232
+ DEFAULT_GRAPHVIZ_OPTIONS.transparentBackground
233
+ )
234
+ };
235
+ }
153
236
  function validateRenderTimeout(value) {
154
237
  if (value === void 0) return DEFAULT_OPTIONS.renderTimeoutMs;
155
238
  if (typeof value !== "number" || !Number.isFinite(value)) {
@@ -174,7 +257,11 @@ function validateCacheMaxEntries(value) {
174
257
  }
175
258
  function resolveOptions(rawOptions) {
176
259
  if (rawOptions === void 0 || rawOptions === null) {
177
- return { ...DEFAULT_OPTIONS, languages: [...DEFAULT_OPTIONS.languages] };
260
+ return {
261
+ ...DEFAULT_OPTIONS,
262
+ languages: [...DEFAULT_OPTIONS.languages],
263
+ graphviz: { ...DEFAULT_GRAPHVIZ_OPTIONS, languages: [...DEFAULT_GRAPHVIZ_OPTIONS.languages] }
264
+ };
178
265
  }
179
266
  if (!isPlainObject(rawOptions)) {
180
267
  fail(`options must be an object, received ${JSON.stringify(rawOptions)}.`);
@@ -189,6 +276,7 @@ function resolveOptions(rawOptions) {
189
276
  "renderTimeoutMs",
190
277
  "cacheMaxEntries",
191
278
  "zoom",
279
+ "graphviz",
192
280
  // Docusaurus injects `id` for multi-instance plugins.
193
281
  "id"
194
282
  ]);
@@ -207,8 +295,19 @@ function resolveOptions(rawOptions) {
207
295
  `options.cache must be one of ${quoteList(CACHE_MODES)}, received ${JSON.stringify(cache)}.`
208
296
  );
209
297
  }
298
+ const languages = validateLanguages(rawOptions.languages, "languages", DEFAULT_OPTIONS.languages);
299
+ const graphviz = validateGraphviz(rawOptions.graphviz);
300
+ if (graphviz.enabled) {
301
+ const claimedTwice = graphviz.languages.filter((entry) => languages.includes(entry));
302
+ if (claimedTwice.length > 0) {
303
+ fail(
304
+ `options.graphviz.languages and options.languages both claim ${quoteList(claimedTwice)}. A fence language can only belong to one engine.`
305
+ );
306
+ }
307
+ }
210
308
  return {
211
- languages: validateLanguages(rawOptions.languages),
309
+ languages,
310
+ graphviz,
212
311
  theme: theme ?? DEFAULT_OPTIONS.theme,
213
312
  lazy: validateBoolean(rawOptions.lazy, "lazy", DEFAULT_OPTIONS.lazy),
214
313
  cache: cache ?? DEFAULT_OPTIONS.cache,
package/dist/index.d.cts CHANGED
@@ -5,6 +5,43 @@ import { WebpackPluginInstance } from 'webpack';
5
5
  type CacheMode = 'none' | 'memory' | 'session';
6
6
  /** Which PlantUML colour scheme to render. `auto` follows the Docusaurus colour mode. */
7
7
  type DiagramTheme = 'auto' | 'light' | 'dark';
8
+ /**
9
+ * Graphviz layout engines available in the bundled build.
10
+ *
11
+ * Taken from `Viz.engines` of the `viz-global.js` that `@plantuml/core` ships. The loader
12
+ * re-checks the requested engine against the live list at render time, so this array only has
13
+ * to be good enough to reject typos in `docusaurus.config.*` at build time.
14
+ */
15
+ declare const GRAPHVIZ_ENGINES: readonly ["circo", "dot", "fdp", "neato", "nop", "nop1", "nop2", "osage", "patchwork", "sfdp", "twopi"];
16
+ type GraphvizEngine = (typeof GRAPHVIZ_ENGINES)[number];
17
+ /** Graphviz/DOT options, nested so the two engines cannot collide in the option namespace. */
18
+ interface GraphvizOptions {
19
+ /** Intercept DOT fences at all. Set to `false` to leave `dot` code blocks as code blocks. */
20
+ enabled?: boolean;
21
+ /** Fenced-code languages treated as Graphviz DOT. Matched case-insensitively. */
22
+ languages?: string[];
23
+ /** Default layout engine used when a fence does not name one. */
24
+ engine?: GraphvizEngine;
25
+ /** Allow a fence to select a different layout engine with `engine=neato`. */
26
+ allowEngineOverride?: boolean;
27
+ /**
28
+ * Refuse to render sources larger than this many UTF-8 bytes.
29
+ *
30
+ * Graphviz renders synchronously, so an enormous graph blocks the main thread; the guard
31
+ * turns a frozen tab into an explanatory panel. See `docs/adr/0004-graphviz-engine-reuse.md`.
32
+ */
33
+ maxSourceBytes?: number;
34
+ /**
35
+ * Render on a transparent background instead of Graphviz's opaque white, so diagrams sit on
36
+ * the page background in both colour modes.
37
+ */
38
+ transparentBackground?: boolean;
39
+ }
40
+ /** Graphviz options after validation, with every default applied. */
41
+ type ResolvedGraphvizOptions = Required<Omit<GraphvizOptions, 'languages'>> & {
42
+ /** Normalized to lower case; matching is case-insensitive. */
43
+ languages: string[];
44
+ };
8
45
  /**
9
46
  * Options accepted by the plugin in `docusaurus.config.*`.
10
47
  *
@@ -32,13 +69,16 @@ interface PlantUmlPluginOptions {
32
69
  * Override for a single fence with `zoom` or `zoom=false` in its metastring.
33
70
  */
34
71
  zoom?: boolean;
72
+ /** Graphviz/DOT diagram support. See {@link GraphvizOptions}. */
73
+ graphviz?: GraphvizOptions;
35
74
  /** Docusaurus plugin instance id, used when the plugin is registered more than once. */
36
75
  id?: string;
37
76
  }
38
77
  /** Plugin options after validation, with every default applied. */
39
- type ResolvedPlantUmlOptions = Required<Omit<PlantUmlPluginOptions, 'id' | 'languages'>> & {
78
+ type ResolvedPlantUmlOptions = Required<Omit<PlantUmlPluginOptions, 'id' | 'languages' | 'graphviz'>> & {
40
79
  /** Normalized to lower case; matching is case-insensitive. */
41
80
  languages: string[];
81
+ graphviz: ResolvedGraphvizOptions;
42
82
  };
43
83
  declare const DEFAULT_OPTIONS: ResolvedPlantUmlOptions;
44
84
  /** Thrown for invalid plugin options so Docusaurus fails the build with a useful message. */
package/dist/options.d.ts CHANGED
@@ -2,6 +2,43 @@
2
2
  export type CacheMode = 'none' | 'memory' | 'session';
3
3
  /** Which PlantUML colour scheme to render. `auto` follows the Docusaurus colour mode. */
4
4
  export type DiagramTheme = 'auto' | 'light' | 'dark';
5
+ /**
6
+ * Graphviz layout engines available in the bundled build.
7
+ *
8
+ * Taken from `Viz.engines` of the `viz-global.js` that `@plantuml/core` ships. The loader
9
+ * re-checks the requested engine against the live list at render time, so this array only has
10
+ * to be good enough to reject typos in `docusaurus.config.*` at build time.
11
+ */
12
+ export declare const GRAPHVIZ_ENGINES: readonly ["circo", "dot", "fdp", "neato", "nop", "nop1", "nop2", "osage", "patchwork", "sfdp", "twopi"];
13
+ export type GraphvizEngine = (typeof GRAPHVIZ_ENGINES)[number];
14
+ /** Graphviz/DOT options, nested so the two engines cannot collide in the option namespace. */
15
+ export interface GraphvizOptions {
16
+ /** Intercept DOT fences at all. Set to `false` to leave `dot` code blocks as code blocks. */
17
+ enabled?: boolean;
18
+ /** Fenced-code languages treated as Graphviz DOT. Matched case-insensitively. */
19
+ languages?: string[];
20
+ /** Default layout engine used when a fence does not name one. */
21
+ engine?: GraphvizEngine;
22
+ /** Allow a fence to select a different layout engine with `engine=neato`. */
23
+ allowEngineOverride?: boolean;
24
+ /**
25
+ * Refuse to render sources larger than this many UTF-8 bytes.
26
+ *
27
+ * Graphviz renders synchronously, so an enormous graph blocks the main thread; the guard
28
+ * turns a frozen tab into an explanatory panel. See `docs/adr/0004-graphviz-engine-reuse.md`.
29
+ */
30
+ maxSourceBytes?: number;
31
+ /**
32
+ * Render on a transparent background instead of Graphviz's opaque white, so diagrams sit on
33
+ * the page background in both colour modes.
34
+ */
35
+ transparentBackground?: boolean;
36
+ }
37
+ /** Graphviz options after validation, with every default applied. */
38
+ export type ResolvedGraphvizOptions = Required<Omit<GraphvizOptions, 'languages'>> & {
39
+ /** Normalized to lower case; matching is case-insensitive. */
40
+ languages: string[];
41
+ };
5
42
  /**
6
43
  * Options accepted by the plugin in `docusaurus.config.*`.
7
44
  *
@@ -29,14 +66,18 @@ export interface PlantUmlPluginOptions {
29
66
  * Override for a single fence with `zoom` or `zoom=false` in its metastring.
30
67
  */
31
68
  zoom?: boolean;
69
+ /** Graphviz/DOT diagram support. See {@link GraphvizOptions}. */
70
+ graphviz?: GraphvizOptions;
32
71
  /** Docusaurus plugin instance id, used when the plugin is registered more than once. */
33
72
  id?: string;
34
73
  }
35
74
  /** Plugin options after validation, with every default applied. */
36
- export type ResolvedPlantUmlOptions = Required<Omit<PlantUmlPluginOptions, 'id' | 'languages'>> & {
75
+ export type ResolvedPlantUmlOptions = Required<Omit<PlantUmlPluginOptions, 'id' | 'languages' | 'graphviz'>> & {
37
76
  /** Normalized to lower case; matching is case-insensitive. */
38
77
  languages: string[];
78
+ graphviz: ResolvedGraphvizOptions;
39
79
  };
80
+ export declare const DEFAULT_GRAPHVIZ_OPTIONS: ResolvedGraphvizOptions;
40
81
  export declare const DEFAULT_OPTIONS: ResolvedPlantUmlOptions;
41
82
  /** Thrown for invalid plugin options so Docusaurus fails the build with a useful message. */
42
83
  export declare class PlantUmlOptionsError extends Error {
package/dist/options.js CHANGED
@@ -1,4 +1,32 @@
1
1
  import { PLUGIN_NAME } from './constants.js';
2
+ /**
3
+ * Graphviz layout engines available in the bundled build.
4
+ *
5
+ * Taken from `Viz.engines` of the `viz-global.js` that `@plantuml/core` ships. The loader
6
+ * re-checks the requested engine against the live list at render time, so this array only has
7
+ * to be good enough to reject typos in `docusaurus.config.*` at build time.
8
+ */
9
+ export const GRAPHVIZ_ENGINES = [
10
+ 'circo',
11
+ 'dot',
12
+ 'fdp',
13
+ 'neato',
14
+ 'nop',
15
+ 'nop1',
16
+ 'nop2',
17
+ 'osage',
18
+ 'patchwork',
19
+ 'sfdp',
20
+ 'twopi',
21
+ ];
22
+ export const DEFAULT_GRAPHVIZ_OPTIONS = {
23
+ enabled: true,
24
+ languages: ['dot', 'graphviz', 'gv'],
25
+ engine: 'dot',
26
+ allowEngineOverride: true,
27
+ maxSourceBytes: 100_000,
28
+ transparentBackground: true,
29
+ };
2
30
  export const DEFAULT_OPTIONS = {
3
31
  languages: ['plantuml', 'puml'],
4
32
  theme: 'auto',
@@ -9,6 +37,7 @@ export const DEFAULT_OPTIONS = {
9
37
  renderTimeoutMs: 20_000,
10
38
  cacheMaxEntries: 50,
11
39
  zoom: true,
40
+ graphviz: DEFAULT_GRAPHVIZ_OPTIONS,
12
41
  };
13
42
  const CACHE_MODES = ['none', 'memory', 'session'];
14
43
  const THEMES = ['auto', 'light', 'dark'];
@@ -38,27 +67,78 @@ function validateBoolean(value, key, fallback) {
38
67
  }
39
68
  return value;
40
69
  }
41
- function validateLanguages(value) {
70
+ /** Shared by `options.languages` and `options.graphviz.languages`; `key` names the one in play. */
71
+ function validateLanguages(value, key, fallback) {
42
72
  if (value === undefined)
43
- return [...DEFAULT_OPTIONS.languages];
73
+ return [...fallback];
44
74
  if (!Array.isArray(value)) {
45
- fail(`options.languages must be an array of strings, received ${JSON.stringify(value)}.`);
75
+ fail(`options.${key} must be an array of strings, received ${JSON.stringify(value)}.`);
46
76
  }
47
77
  if (value.length === 0) {
48
- fail('options.languages must contain at least one language.');
78
+ fail(`options.${key} must contain at least one language.`);
49
79
  }
50
80
  const normalized = value.map((entry, index) => {
51
81
  if (typeof entry !== 'string' || entry.trim() === '') {
52
- fail(`options.languages[${index}] must be a non-empty string, received ${JSON.stringify(entry)}.`);
82
+ fail(`options.${key}[${index}] must be a non-empty string, received ${JSON.stringify(entry)}.`);
53
83
  }
54
84
  return entry.trim().toLowerCase();
55
85
  });
56
86
  const duplicates = normalized.filter((entry, index) => normalized.indexOf(entry) !== index);
57
87
  if (duplicates.length > 0) {
58
- fail(`options.languages contains duplicate entries: ${quoteList([...new Set(duplicates)])}.`);
88
+ fail(`options.${key} contains duplicate entries: ${quoteList([...new Set(duplicates)])}.`);
59
89
  }
60
90
  return normalized;
61
91
  }
92
+ function validateMaxSourceBytes(value) {
93
+ if (value === undefined)
94
+ return DEFAULT_GRAPHVIZ_OPTIONS.maxSourceBytes;
95
+ if (typeof value !== 'number' || !Number.isInteger(value) || value < 1) {
96
+ fail('options.graphviz.maxSourceBytes must be a positive integer, ' +
97
+ `received ${JSON.stringify(value)}.`);
98
+ }
99
+ return value;
100
+ }
101
+ /**
102
+ * Validates the nested `graphviz` group.
103
+ *
104
+ * Unknown keys are rejected here too: a typo nested one level deep is exactly as silent — and
105
+ * exactly as confusing — as a typo at the top level.
106
+ */
107
+ function validateGraphviz(value) {
108
+ if (value === undefined) {
109
+ return { ...DEFAULT_GRAPHVIZ_OPTIONS, languages: [...DEFAULT_GRAPHVIZ_OPTIONS.languages] };
110
+ }
111
+ if (!isPlainObject(value)) {
112
+ fail(`options.graphviz must be an object, received ${JSON.stringify(value)}.`);
113
+ }
114
+ const allowedKeys = [
115
+ 'enabled',
116
+ 'languages',
117
+ 'engine',
118
+ 'allowEngineOverride',
119
+ 'maxSourceBytes',
120
+ 'transparentBackground',
121
+ ];
122
+ const unknownKeys = Object.keys(value).filter((key) => !allowedKeys.includes(key));
123
+ if (unknownKeys.length > 0) {
124
+ fail(`Unknown option${unknownKeys.length > 1 ? 's' : ''} ` +
125
+ `${quoteList(unknownKeys.map((key) => `graphviz.${key}`))}. ` +
126
+ `Supported options: ${quoteList(allowedKeys.map((key) => `graphviz.${key}`))}.`);
127
+ }
128
+ const { engine } = value;
129
+ if (engine !== undefined && !GRAPHVIZ_ENGINES.includes(engine)) {
130
+ fail(`options.graphviz.engine must be one of ${quoteList(GRAPHVIZ_ENGINES)}, ` +
131
+ `received ${JSON.stringify(engine)}.`);
132
+ }
133
+ return {
134
+ enabled: validateBoolean(value.enabled, 'graphviz.enabled', DEFAULT_GRAPHVIZ_OPTIONS.enabled),
135
+ languages: validateLanguages(value.languages, 'graphviz.languages', DEFAULT_GRAPHVIZ_OPTIONS.languages),
136
+ engine: engine ?? DEFAULT_GRAPHVIZ_OPTIONS.engine,
137
+ allowEngineOverride: validateBoolean(value.allowEngineOverride, 'graphviz.allowEngineOverride', DEFAULT_GRAPHVIZ_OPTIONS.allowEngineOverride),
138
+ maxSourceBytes: validateMaxSourceBytes(value.maxSourceBytes),
139
+ transparentBackground: validateBoolean(value.transparentBackground, 'graphviz.transparentBackground', DEFAULT_GRAPHVIZ_OPTIONS.transparentBackground),
140
+ };
141
+ }
62
142
  function validateRenderTimeout(value) {
63
143
  if (value === undefined)
64
144
  return DEFAULT_OPTIONS.renderTimeoutMs;
@@ -90,7 +170,11 @@ function validateCacheMaxEntries(value) {
90
170
  */
91
171
  export function resolveOptions(rawOptions) {
92
172
  if (rawOptions === undefined || rawOptions === null) {
93
- return { ...DEFAULT_OPTIONS, languages: [...DEFAULT_OPTIONS.languages] };
173
+ return {
174
+ ...DEFAULT_OPTIONS,
175
+ languages: [...DEFAULT_OPTIONS.languages],
176
+ graphviz: { ...DEFAULT_GRAPHVIZ_OPTIONS, languages: [...DEFAULT_GRAPHVIZ_OPTIONS.languages] },
177
+ };
94
178
  }
95
179
  if (!isPlainObject(rawOptions)) {
96
180
  fail(`options must be an object, received ${JSON.stringify(rawOptions)}.`);
@@ -105,6 +189,7 @@ export function resolveOptions(rawOptions) {
105
189
  'renderTimeoutMs',
106
190
  'cacheMaxEntries',
107
191
  'zoom',
192
+ 'graphviz',
108
193
  // Docusaurus injects `id` for multi-instance plugins.
109
194
  'id',
110
195
  ]);
@@ -120,8 +205,20 @@ export function resolveOptions(rawOptions) {
120
205
  if (cache !== undefined && !CACHE_MODES.includes(cache)) {
121
206
  fail(`options.cache must be one of ${quoteList(CACHE_MODES)}, received ${JSON.stringify(cache)}.`);
122
207
  }
208
+ const languages = validateLanguages(rawOptions.languages, 'languages', DEFAULT_OPTIONS.languages);
209
+ const graphviz = validateGraphviz(rawOptions.graphviz);
210
+ // A fence has exactly one language, so a language claimed by both engines would make the
211
+ // rendered output depend on the order the wrapper happens to check them in. Reject it.
212
+ if (graphviz.enabled) {
213
+ const claimedTwice = graphviz.languages.filter((entry) => languages.includes(entry));
214
+ if (claimedTwice.length > 0) {
215
+ fail(`options.graphviz.languages and options.languages both claim ` +
216
+ `${quoteList(claimedTwice)}. A fence language can only belong to one engine.`);
217
+ }
218
+ }
123
219
  return {
124
- languages: validateLanguages(rawOptions.languages),
220
+ languages,
221
+ graphviz,
125
222
  theme: theme ?? DEFAULT_OPTIONS.theme,
126
223
  lazy: validateBoolean(rawOptions.lazy, 'lazy', DEFAULT_OPTIONS.lazy),
127
224
  cache: cache ?? DEFAULT_OPTIONS.cache,
@@ -1,4 +1,4 @@
1
- import type { PlantUmlCoreModule } from './types.js';
1
+ import type { PlantUmlCoreModule, VizInstance } from './types.js';
2
2
  export interface LoadRuntimeOptions {
3
3
  /** Absolute, `baseUrl`-prefixed directory URL containing the two runtime files. */
4
4
  assetsBaseUrl: string;
@@ -10,6 +10,14 @@ export interface LoadRuntimeOptions {
10
10
  */
11
11
  importModule?: (url: string) => Promise<unknown>;
12
12
  }
13
+ /**
14
+ * Resolves a ready-to-use Graphviz instance, loading `viz-global.js` on first use.
15
+ *
16
+ * One instance serves every DOT diagram on the page: Viz.js documents that an instance renders
17
+ * multiple graphs, and a 200-render soak with syntax errors interleaved left it fully healthy,
18
+ * so there is no reason to pay the instantiation cost per diagram.
19
+ */
20
+ export declare function loadVizRuntime(options: LoadRuntimeOptions): Promise<VizInstance>;
13
21
  /**
14
22
  * Resolves the PlantUML engine, loading it on first use.
15
23
  *
@@ -17,7 +25,9 @@ export interface LoadRuntimeOptions {
17
25
  * diagram can retry rather than inheriting a permanent failure.
18
26
  */
19
27
  export declare function loadPlantUmlRuntime(options: LoadRuntimeOptions): Promise<PlantUmlCoreModule>;
20
- /** Whether the runtime has been requested on this page. Used by tests and diagnostics. */
28
+ /** Whether the PlantUML runtime has been requested on this page. For tests and diagnostics. */
21
29
  export declare function isRuntimeRequested(): boolean;
22
- /** Test-only: forgets the cached runtime and removes the injected script tag. */
30
+ /** Whether the Graphviz runtime has been requested on this page. For tests and diagnostics. */
31
+ export declare function isVizRuntimeRequested(): boolean;
32
+ /** Test-only: forgets both cached runtimes and removes the injected script tag. */
23
33
  export declare function resetRuntimeLoader(): void;
@@ -1,14 +1,21 @@
1
1
  import { PLANTUML_MODULE_FILENAME, VIZ_SCRIPT_FILENAME } from '../constants.js';
2
2
  import { PlantUmlError } from './errors.js';
3
3
  /**
4
- * Loads the PlantUML browser runtime exactly once per page session.
4
+ * Loads the browser runtimes, each at most once per page session.
5
5
  *
6
- * `viz-global.js` is a classic script that installs the Graphviz layout engine on `window`,
7
- * and it must have finished executing before `plantuml.js` is imported. Both files are
8
- * served from the Docusaurus origin under `baseUrl`, never from a CDN.
6
+ * There are two, and they are loaded independently:
7
+ *
8
+ * - `viz-global.js` (~1.4 MB) is a classic script that installs Graphviz on `window.Viz`. It
9
+ * is what renders DOT fences, and PlantUML also needs it for its own layout.
10
+ * - `plantuml.js` (~6.8 MB) is an ES module that renders PlantUML fences. It requires
11
+ * `window.Viz` to already exist, so it builds on the first.
12
+ *
13
+ * The split matters: a page with only DOT diagrams must never pay for the 6.8 MB PlantUML
14
+ * engine. Both files are served from the Docusaurus origin under `baseUrl`, never from a CDN.
9
15
  */
10
16
  const SCRIPT_MARKER = 'data-plantuml-runtime';
11
17
  let cachedRuntime = null;
18
+ let cachedViz = null;
12
19
  function isBrowser() {
13
20
  return typeof window !== 'undefined' && typeof document !== 'undefined';
14
21
  }
@@ -32,7 +39,7 @@ function loadVizGlobal(url, timeoutMs) {
32
39
  if (existing.dataset.plantumlRuntimeState === 'loaded')
33
40
  return Promise.resolve();
34
41
  if (existing.dataset.plantumlRuntimeState === 'error') {
35
- return Promise.reject(new PlantUmlError('load', `Failed to load the PlantUML Graphviz runtime from ${url}.`));
42
+ return Promise.reject(new PlantUmlError('load', `Failed to load the Graphviz runtime from ${url}.`));
36
43
  }
37
44
  return waitForScript(existing, url, timeoutMs);
38
45
  }
@@ -63,7 +70,7 @@ function waitForScript(script, url, timeoutMs) {
63
70
  };
64
71
  const onError = () => {
65
72
  script.dataset.plantumlRuntimeState = 'error';
66
- finish(() => reject(new PlantUmlError('load', `Failed to load the PlantUML Graphviz runtime from ${url}. ` +
73
+ finish(() => reject(new PlantUmlError('load', `Failed to load the Graphviz runtime from ${url}. ` +
67
74
  'Check that the asset is served under your Docusaurus baseUrl with a JavaScript MIME type.')));
68
75
  };
69
76
  const timer = setTimeout(() => {
@@ -83,6 +90,54 @@ function assertCoreModule(value, url) {
83
90
  }
84
91
  return candidate;
85
92
  }
93
+ /**
94
+ * Checks that the loaded script really installed the Viz.js API this plugin renders DOT with.
95
+ *
96
+ * `viz-global.js` reaches the browser via `@plantuml/core`, which ships it for PlantUML's own
97
+ * layout rather than for this plugin's benefit. Asserting the shape here means a future
98
+ * `@plantuml/core` that changed engines fails with a message naming the cause, instead of a
99
+ * `TypeError` on `Viz.instance` several frames away. See `docs/adr/0004-graphviz-engine-reuse.md`.
100
+ */
101
+ function assertVizGlobal(url) {
102
+ const candidate = globalThis.Viz;
103
+ if (!candidate ||
104
+ typeof candidate.instance !== 'function' ||
105
+ !Array.isArray(candidate.engines) ||
106
+ typeof candidate.graphvizVersion !== 'string') {
107
+ throw new PlantUmlError('load', `The script at ${url} did not install the expected Graphviz (Viz.js) API on window.Viz. ` +
108
+ 'This usually means a proxy or service worker returned the wrong file, or that the ' +
109
+ 'installed @plantuml/core no longer bundles Viz.js.');
110
+ }
111
+ return candidate;
112
+ }
113
+ /**
114
+ * Resolves a ready-to-use Graphviz instance, loading `viz-global.js` on first use.
115
+ *
116
+ * One instance serves every DOT diagram on the page: Viz.js documents that an instance renders
117
+ * multiple graphs, and a 200-render soak with syntax errors interleaved left it fully healthy,
118
+ * so there is no reason to pay the instantiation cost per diagram.
119
+ */
120
+ export function loadVizRuntime(options) {
121
+ if (!isBrowser()) {
122
+ return Promise.reject(new PlantUmlError('load', 'The Graphviz runtime cannot be loaded during server-side rendering.'));
123
+ }
124
+ if (cachedViz)
125
+ return cachedViz;
126
+ const vizUrl = joinUrl(options.assetsBaseUrl, VIZ_SCRIPT_FILENAME);
127
+ const viz = loadVizGlobal(vizUrl, options.timeoutMs)
128
+ .then(() => assertVizGlobal(vizUrl).instance())
129
+ .catch((error) => {
130
+ // Clearing the cache lets a later diagram retry rather than inherit a permanent failure.
131
+ cachedViz = null;
132
+ throw error instanceof PlantUmlError
133
+ ? error
134
+ : new PlantUmlError('load', `Failed to initialize the Graphviz engine from ${vizUrl}.`, {
135
+ cause: error,
136
+ });
137
+ });
138
+ cachedViz = viz;
139
+ return viz;
140
+ }
86
141
  /**
87
142
  * Resolves the PlantUML engine, loading it on first use.
88
143
  *
@@ -112,13 +167,18 @@ export function loadPlantUmlRuntime(options) {
112
167
  cachedRuntime = runtime;
113
168
  return runtime;
114
169
  }
115
- /** Whether the runtime has been requested on this page. Used by tests and diagnostics. */
170
+ /** Whether the PlantUML runtime has been requested on this page. For tests and diagnostics. */
116
171
  export function isRuntimeRequested() {
117
172
  return cachedRuntime !== null;
118
173
  }
119
- /** Test-only: forgets the cached runtime and removes the injected script tag. */
174
+ /** Whether the Graphviz runtime has been requested on this page. For tests and diagnostics. */
175
+ export function isVizRuntimeRequested() {
176
+ return cachedViz !== null;
177
+ }
178
+ /** Test-only: forgets both cached runtimes and removes the injected script tag. */
120
179
  export function resetRuntimeLoader() {
121
180
  cachedRuntime = null;
181
+ cachedViz = null;
122
182
  if (typeof document !== 'undefined') {
123
183
  document.querySelectorAll(`script[${SCRIPT_MARKER}]`).forEach((node) => node.remove());
124
184
  }