@matfsw/docusaurus-plantuml-plugin 1.1.2 → 1.3.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.
Files changed (43) hide show
  1. package/CHANGELOG.md +103 -1
  2. package/README.md +211 -32
  3. package/assets/stdlib/LICENSES.md +21 -0
  4. package/assets/stdlib/archimate.min.js +22 -0
  5. package/assets/stdlib/azure.min.js +305 -0
  6. package/assets/stdlib/c4.min.js +47 -0
  7. package/assets/stdlib/cloudinsight.min.js +93 -0
  8. package/assets/stdlib/eip.min.js +14 -0
  9. package/assets/stdlib/k8s.min.js +52 -0
  10. package/assets/stdlib/kubernetes.min.js +17 -0
  11. package/assets/stdlib/manifest.json +112 -0
  12. package/assets/stdlib/office.min.js +557 -0
  13. package/dist/constants.d.ts +2 -0
  14. package/dist/constants.js +2 -0
  15. package/dist/index.cjs +384 -8
  16. package/dist/index.d.cts +91 -3
  17. package/dist/index.d.ts +12 -2
  18. package/dist/index.js +20 -1
  19. package/dist/options.d.ts +57 -1
  20. package/dist/options.js +84 -0
  21. package/dist/runtime/assetLoader.d.ts +13 -1
  22. package/dist/runtime/assetLoader.js +36 -13
  23. package/dist/runtime/cache.d.ts +6 -1
  24. package/dist/runtime/cache.js +2 -2
  25. package/dist/runtime/errors.d.ts +3 -1
  26. package/dist/runtime/errors.js +27 -1
  27. package/dist/runtime/renderer.d.ts +3 -0
  28. package/dist/runtime/renderer.js +21 -7
  29. package/dist/runtime/stdlibLoader.d.ts +31 -0
  30. package/dist/runtime/stdlibLoader.js +99 -0
  31. package/dist/stdlib.d.ts +35 -0
  32. package/dist/stdlib.js +163 -0
  33. package/dist/stdlibBundle.d.ts +60 -0
  34. package/dist/stdlibBundle.js +218 -0
  35. package/dist/stdlibShared.d.ts +51 -0
  36. package/dist/stdlibShared.js +54 -0
  37. package/dist/theme/MDXComponents/Code/index.js +1 -1
  38. package/dist/theme/PlantUmlDiagram/index.d.ts +6 -1
  39. package/dist/theme/PlantUmlDiagram/index.js +53 -5
  40. package/dist/theme/PlantUmlDiagram/styles.module.css +117 -0
  41. package/dist/theme/usePlantUmlConfig.d.ts +8 -0
  42. package/dist/theme/usePlantUmlConfig.js +14 -8
  43. package/package.json +3 -1
package/dist/index.js CHANGED
@@ -4,6 +4,7 @@ import CopyWebpackPlugin from 'copy-webpack-plugin';
4
4
  import { locatePlantUmlCore } from './assets.js';
5
5
  import { assetsDirForVersion, DEFAULT_PLUGIN_ID, PLUGIN_NAME } from './constants.js';
6
6
  import { resolveOptions, } from './options.js';
7
+ import { resolveStdlibAssets } from './stdlib.js';
7
8
  export { DEFAULT_OPTIONS, PlantUmlOptionsError, resolveOptions } from './options.js';
8
9
  export { PLUGIN_ID, PLUGIN_NAME } from './constants.js';
9
10
  const currentDir = path.dirname(fileURLToPath(import.meta.url));
@@ -25,12 +26,22 @@ export function createCopyPlugin(currentBundler, patterns) {
25
26
  }
26
27
  return new CopyWebpackPlugin({ patterns });
27
28
  }
28
- export default function plantumlPlugin(_context, options = {}) {
29
+ export default function plantumlPlugin(context, options = {}) {
29
30
  // Docusaurus already ran `validateOptions`, but the plugin function is also callable
30
31
  // directly (and from JS configs that skip validation), so normalize defensively.
31
32
  const resolved = resolveOptions(options);
32
33
  const core = locatePlantUmlCore();
33
34
  const assetsDir = assetsDirForVersion(core.version);
35
+ const stdlib = resolveStdlibAssets({
36
+ options: resolved.stdlib,
37
+ currentDir,
38
+ siteDir: context.siteDir,
39
+ cacheDir: context.generatedFilesDir,
40
+ });
41
+ // The revision is part of the directory name, not just of the cache key: refreshing the
42
+ // standard library without moving the URL would leave readers on a cached bundle from
43
+ // before the refresh, and the mismatch would surface as an unresolved include.
44
+ const stdlibDir = stdlib ? `${assetsDir}/stdlib-${stdlib.manifest.revision}` : null;
34
45
  return {
35
46
  name: PLUGIN_NAME,
36
47
  getThemePath() {
@@ -41,6 +52,7 @@ export default function plantumlPlugin(_context, options = {}) {
41
52
  options: resolved,
42
53
  assetsDir,
43
54
  coreVersion: core.version,
55
+ stdlib: stdlib && stdlibDir ? { dir: stdlibDir, manifest: stdlib.manifest } : null,
44
56
  };
45
57
  actions.setGlobalData(globalData);
46
58
  },
@@ -56,6 +68,13 @@ export default function plantumlPlugin(_context, options = {}) {
56
68
  // These files are already minified upstream; re-processing 8 MB is wasted work.
57
69
  info: { minimized: true },
58
70
  }));
71
+ if (stdlib && stdlibDir) {
72
+ patterns.push(...stdlib.files.map((absolutePath) => ({
73
+ from: absolutePath,
74
+ to: `${stdlibDir}/[name][ext]`,
75
+ info: { minimized: true },
76
+ })));
77
+ }
59
78
  // Optional chaining, not laziness: `currentBundler` is absent on Docusaurus 3.5.x.
60
79
  return { plugins: [createCopyPlugin(utils?.currentBundler, patterns)] };
61
80
  },
package/dist/options.d.ts CHANGED
@@ -39,6 +39,50 @@ export type ResolvedGraphvizOptions = Required<Omit<GraphvizOptions, 'languages'
39
39
  /** Normalized to lower case; matching is case-insensitive. */
40
40
  languages: string[];
41
41
  };
42
+ /**
43
+ * PlantUML standard library support: `!include <C4/C4_Container>` and friends.
44
+ *
45
+ * On by default and normally not worth configuring. The two lists exist for the namespaces
46
+ * this package does not vendor — the standard library is 265 MB in full, and several of its
47
+ * namespaces carry no upstream licence — which a site can still opt into from its own
48
+ * checkout. See `assets/stdlib/LICENSES.md` for what is vendored and under what terms.
49
+ */
50
+ export interface StdlibOptions {
51
+ /**
52
+ * Resolve `!include <namespace/…>` at all. `false` (or `stdlib: false`) leaves such
53
+ * diagrams to fail in the engine, exactly as they did before this feature existed.
54
+ */
55
+ enabled?: boolean;
56
+ /**
57
+ * Extra namespaces to make available, beyond the vendored ones — `['aws', 'tupadr3']`.
58
+ * Each must be found in one of {@link StdlibOptions.source}, and the build fails naming
59
+ * the namespace if it is not.
60
+ */
61
+ include?: string[];
62
+ /**
63
+ * Directories to resolve {@link StdlibOptions.include} from: the `stdlib/` directory of a
64
+ * `plantuml-stdlib` checkout, relative to the site directory or absolute. Searched in
65
+ * order, before the vendored bundles, so a namespace can also be replaced with a newer one
66
+ * by naming it in `include`.
67
+ */
68
+ source?: string | string[];
69
+ /**
70
+ * Narrow the vendored namespaces emitted into the build, for sites that care more about
71
+ * build output size than about which diagrams keep working. Dependencies of the named
72
+ * namespaces are kept regardless. Defaults to all of them; a reader only downloads the
73
+ * ones a page uses either way.
74
+ */
75
+ namespaces?: string[];
76
+ }
77
+ /** Standard library options after validation, with every default applied. */
78
+ export interface ResolvedStdlibOptions {
79
+ enabled: boolean;
80
+ /** Normalized to lower case. */
81
+ include: string[];
82
+ source: string[];
83
+ /** Normalized to lower case. `null` means "every vendored namespace". */
84
+ namespaces: string[] | null;
85
+ }
42
86
  /**
43
87
  * Options accepted by the plugin in `docusaurus.config.*`.
44
88
  *
@@ -66,18 +110,30 @@ export interface PlantUmlPluginOptions {
66
110
  * Override for a single fence with `zoom` or `zoom=false` in its metastring.
67
111
  */
68
112
  zoom?: boolean;
113
+ /**
114
+ * Offer a control that reveals the diagram's source and copies it to the clipboard.
115
+ * Override for a single fence with `showSource` or `showSource=false` in its metastring.
116
+ */
117
+ showSource?: boolean;
69
118
  /** Graphviz/DOT diagram support. See {@link GraphvizOptions}. */
70
119
  graphviz?: GraphvizOptions;
120
+ /**
121
+ * PlantUML standard library support. See {@link StdlibOptions}. `false` is shorthand for
122
+ * `{enabled: false}`.
123
+ */
124
+ stdlib?: StdlibOptions | false;
71
125
  /** Docusaurus plugin instance id, used when the plugin is registered more than once. */
72
126
  id?: string;
73
127
  }
74
128
  /** Plugin options after validation, with every default applied. */
75
- export type ResolvedPlantUmlOptions = Required<Omit<PlantUmlPluginOptions, 'id' | 'languages' | 'graphviz'>> & {
129
+ export type ResolvedPlantUmlOptions = Required<Omit<PlantUmlPluginOptions, 'id' | 'languages' | 'graphviz' | 'stdlib'>> & {
76
130
  /** Normalized to lower case; matching is case-insensitive. */
77
131
  languages: string[];
78
132
  graphviz: ResolvedGraphvizOptions;
133
+ stdlib: ResolvedStdlibOptions;
79
134
  };
80
135
  export declare const DEFAULT_GRAPHVIZ_OPTIONS: ResolvedGraphvizOptions;
136
+ export declare const DEFAULT_STDLIB_OPTIONS: ResolvedStdlibOptions;
81
137
  export declare const DEFAULT_OPTIONS: ResolvedPlantUmlOptions;
82
138
  /** Thrown for invalid plugin options so Docusaurus fails the build with a useful message. */
83
139
  export declare class PlantUmlOptionsError extends Error {
package/dist/options.js CHANGED
@@ -27,6 +27,12 @@ export const DEFAULT_GRAPHVIZ_OPTIONS = {
27
27
  maxSourceBytes: 100_000,
28
28
  transparentBackground: true,
29
29
  };
30
+ export const DEFAULT_STDLIB_OPTIONS = {
31
+ enabled: true,
32
+ include: [],
33
+ source: [],
34
+ namespaces: null,
35
+ };
30
36
  export const DEFAULT_OPTIONS = {
31
37
  languages: ['plantuml', 'puml'],
32
38
  theme: 'auto',
@@ -37,7 +43,9 @@ export const DEFAULT_OPTIONS = {
37
43
  renderTimeoutMs: 20_000,
38
44
  cacheMaxEntries: 50,
39
45
  zoom: true,
46
+ showSource: true,
40
47
  graphviz: DEFAULT_GRAPHVIZ_OPTIONS,
48
+ stdlib: DEFAULT_STDLIB_OPTIONS,
41
49
  };
42
50
  const CACHE_MODES = ['none', 'memory', 'session'];
43
51
  const THEMES = ['auto', 'light', 'dark'];
@@ -139,6 +147,76 @@ function validateGraphviz(value) {
139
147
  transparentBackground: validateBoolean(value.transparentBackground, 'graphviz.transparentBackground', DEFAULT_GRAPHVIZ_OPTIONS.transparentBackground),
140
148
  };
141
149
  }
150
+ /** Shared by `stdlib.include` and `stdlib.namespaces`, which are both lists of namespaces. */
151
+ function validateNamespaceList(value, key) {
152
+ if (!Array.isArray(value)) {
153
+ fail(`options.${key} must be an array of strings, received ${JSON.stringify(value)}.`);
154
+ }
155
+ const normalized = value.map((entry, index) => {
156
+ if (typeof entry !== 'string' || entry.trim() === '') {
157
+ fail(`options.${key}[${index}] must be a non-empty string, received ${JSON.stringify(entry)}.`);
158
+ }
159
+ // Namespaces are matched case-insensitively: the engine lower-cases `<C4/…>` before
160
+ // looking it up, so `'C4'` and `'c4'` have to mean the same thing here too.
161
+ return entry.trim().toLowerCase();
162
+ });
163
+ const duplicates = normalized.filter((entry, index) => normalized.indexOf(entry) !== index);
164
+ if (duplicates.length > 0) {
165
+ fail(`options.${key} contains duplicate entries: ${quoteList([...new Set(duplicates)])}.`);
166
+ }
167
+ return normalized;
168
+ }
169
+ function validateStdlibSource(value) {
170
+ if (value === undefined)
171
+ return [];
172
+ const entries = Array.isArray(value) ? value : [value];
173
+ return entries.map((entry, index) => {
174
+ if (typeof entry !== 'string' || entry.trim() === '') {
175
+ const where = Array.isArray(value) ? `stdlib.source[${index}]` : 'stdlib.source';
176
+ fail(`options.${where} must be a non-empty string, received ${JSON.stringify(entry)}.`);
177
+ }
178
+ return entry.trim();
179
+ });
180
+ }
181
+ /**
182
+ * Validates the nested `stdlib` group, accepting `false` as shorthand for `{enabled: false}`.
183
+ */
184
+ function validateStdlib(value) {
185
+ if (value === undefined)
186
+ return { ...DEFAULT_STDLIB_OPTIONS, include: [], source: [] };
187
+ if (value === false)
188
+ return { ...DEFAULT_STDLIB_OPTIONS, enabled: false, include: [], source: [] };
189
+ if (!isPlainObject(value)) {
190
+ fail(`options.stdlib must be an object or false, received ${JSON.stringify(value)}.`);
191
+ }
192
+ const allowedKeys = ['enabled', 'include', 'source', 'namespaces'];
193
+ const unknownKeys = Object.keys(value).filter((key) => !allowedKeys.includes(key));
194
+ if (unknownKeys.length > 0) {
195
+ fail(`Unknown option${unknownKeys.length > 1 ? 's' : ''} ` +
196
+ `${quoteList(unknownKeys.map((key) => `stdlib.${key}`))}. ` +
197
+ `Supported options: ${quoteList(allowedKeys.map((key) => `stdlib.${key}`))}.`);
198
+ }
199
+ const include = value.include === undefined ? [] : validateNamespaceList(value.include, 'stdlib.include');
200
+ const source = validateStdlibSource(value.source);
201
+ const namespaces = value.namespaces === undefined
202
+ ? null
203
+ : validateNamespaceList(value.namespaces, 'stdlib.namespaces');
204
+ if (namespaces !== null && namespaces.length === 0) {
205
+ fail('options.stdlib.namespaces must name at least one namespace. To turn the standard ' +
206
+ 'library off entirely, use `stdlib: false`.');
207
+ }
208
+ if (include.length > 0 && source.length === 0) {
209
+ fail(`options.stdlib.include names ${quoteList(include)}, but options.stdlib.source is not ` +
210
+ 'set, so there is nowhere to read them from. Point it at the `stdlib` directory of a ' +
211
+ 'plantuml-stdlib checkout.');
212
+ }
213
+ return {
214
+ enabled: validateBoolean(value.enabled, 'stdlib.enabled', DEFAULT_STDLIB_OPTIONS.enabled),
215
+ include,
216
+ source,
217
+ namespaces,
218
+ };
219
+ }
142
220
  function validateRenderTimeout(value) {
143
221
  if (value === undefined)
144
222
  return DEFAULT_OPTIONS.renderTimeoutMs;
@@ -174,6 +252,7 @@ export function resolveOptions(rawOptions) {
174
252
  ...DEFAULT_OPTIONS,
175
253
  languages: [...DEFAULT_OPTIONS.languages],
176
254
  graphviz: { ...DEFAULT_GRAPHVIZ_OPTIONS, languages: [...DEFAULT_GRAPHVIZ_OPTIONS.languages] },
255
+ stdlib: { ...DEFAULT_STDLIB_OPTIONS, include: [], source: [] },
177
256
  };
178
257
  }
179
258
  if (!isPlainObject(rawOptions)) {
@@ -189,7 +268,9 @@ export function resolveOptions(rawOptions) {
189
268
  'renderTimeoutMs',
190
269
  'cacheMaxEntries',
191
270
  'zoom',
271
+ 'showSource',
192
272
  'graphviz',
273
+ 'stdlib',
193
274
  // Docusaurus injects `id` for multi-instance plugins.
194
275
  'id',
195
276
  ]);
@@ -207,6 +288,7 @@ export function resolveOptions(rawOptions) {
207
288
  }
208
289
  const languages = validateLanguages(rawOptions.languages, 'languages', DEFAULT_OPTIONS.languages);
209
290
  const graphviz = validateGraphviz(rawOptions.graphviz);
291
+ const stdlib = validateStdlib(rawOptions.stdlib);
210
292
  // A fence has exactly one language, so a language claimed by both engines would make the
211
293
  // rendered output depend on the order the wrapper happens to check them in. Reject it.
212
294
  if (graphviz.enabled) {
@@ -219,6 +301,7 @@ export function resolveOptions(rawOptions) {
219
301
  return {
220
302
  languages,
221
303
  graphviz,
304
+ stdlib,
222
305
  theme: theme ?? DEFAULT_OPTIONS.theme,
223
306
  lazy: validateBoolean(rawOptions.lazy, 'lazy', DEFAULT_OPTIONS.lazy),
224
307
  cache: cache ?? DEFAULT_OPTIONS.cache,
@@ -227,5 +310,6 @@ export function resolveOptions(rawOptions) {
227
310
  renderTimeoutMs: validateRenderTimeout(rawOptions.renderTimeoutMs),
228
311
  cacheMaxEntries: validateCacheMaxEntries(rawOptions.cacheMaxEntries),
229
312
  zoom: validateBoolean(rawOptions.zoom, 'zoom', DEFAULT_OPTIONS.zoom),
313
+ showSource: validateBoolean(rawOptions.showSource, 'showSource', DEFAULT_OPTIONS.showSource),
230
314
  };
231
315
  }
@@ -10,6 +10,17 @@ export interface LoadRuntimeOptions {
10
10
  */
11
11
  importModule?: (url: string) => Promise<unknown>;
12
12
  }
13
+ /**
14
+ * Injects a classic script once, reusing an existing tag for the same URL.
15
+ *
16
+ * Client-side navigation re-mounts diagram components but does not reload the document, so
17
+ * the marker attribute is what keeps a second `<script>` from ever being appended. The URL is
18
+ * the marker's value, because the standard library loader adds a tag per namespace and a
19
+ * bare marker could not tell them apart.
20
+ *
21
+ * @param label how the asset is named in an error message, e.g. `Graphviz runtime`
22
+ */
23
+ export declare function loadClassicScript(url: string, timeoutMs: number, label: string): Promise<void>;
13
24
  /**
14
25
  * Resolves a ready-to-use Graphviz instance, loading `viz-global.js` on first use.
15
26
  *
@@ -29,5 +40,6 @@ export declare function loadPlantUmlRuntime(options: LoadRuntimeOptions): Promis
29
40
  export declare function isRuntimeRequested(): boolean;
30
41
  /** Whether the Graphviz runtime has been requested on this page. For tests and diagnostics. */
31
42
  export declare function isVizRuntimeRequested(): boolean;
32
- /** Test-only: forgets both cached runtimes and removes the injected script tag. */
43
+ export declare function onRuntimeReset(hook: () => void): void;
44
+ /** Test-only: forgets every cached runtime and removes the injected script tags. */
33
45
  export declare function resetRuntimeLoader(): void;
@@ -28,31 +28,41 @@ function defaultImportModule(url) {
28
28
  return import(/* webpackIgnore: true */ url);
29
29
  }
30
30
  /**
31
- * Injects `viz-global.js` as a classic script, reusing an existing tag.
31
+ * Injects a classic script once, reusing an existing tag for the same URL.
32
32
  *
33
33
  * Client-side navigation re-mounts diagram components but does not reload the document, so
34
- * the marker attribute is what keeps a second `<script>` from ever being appended.
34
+ * the marker attribute is what keeps a second `<script>` from ever being appended. The URL is
35
+ * the marker's value, because the standard library loader adds a tag per namespace and a
36
+ * bare marker could not tell them apart.
37
+ *
38
+ * @param label how the asset is named in an error message, e.g. `Graphviz runtime`
35
39
  */
36
- function loadVizGlobal(url, timeoutMs) {
37
- const existing = document.querySelector(`script[${SCRIPT_MARKER}]`);
40
+ export function loadClassicScript(url, timeoutMs, label) {
41
+ const existing = document.querySelector(`script[${SCRIPT_MARKER}=${JSON.stringify(url)}]`);
38
42
  if (existing) {
39
43
  if (existing.dataset.plantumlRuntimeState === 'loaded')
40
44
  return Promise.resolve();
41
- if (existing.dataset.plantumlRuntimeState === 'error') {
42
- return Promise.reject(new PlantUmlError('load', `Failed to load the Graphviz runtime from ${url}.`));
43
- }
44
- return waitForScript(existing, url, timeoutMs);
45
+ // A failed tag is discarded rather than reused. The caller that reaches this point has
46
+ // already dropped its cached promise in order to retry, and reusing the dead tag would
47
+ // make that retry impossible — a single flaky response would poison the page.
48
+ if (existing.dataset.plantumlRuntimeState === 'error')
49
+ existing.remove();
50
+ else
51
+ return waitForScript(existing, url, timeoutMs, label);
45
52
  }
46
53
  const script = document.createElement('script');
47
- script.setAttribute(SCRIPT_MARKER, '');
54
+ script.setAttribute(SCRIPT_MARKER, url);
48
55
  script.dataset.plantumlRuntimeState = 'loading';
49
56
  script.async = false;
50
57
  script.src = url;
51
- const promise = waitForScript(script, url, timeoutMs);
58
+ const promise = waitForScript(script, url, timeoutMs, label);
52
59
  document.head.appendChild(script);
53
60
  return promise;
54
61
  }
55
- function waitForScript(script, url, timeoutMs) {
62
+ function loadVizGlobal(url, timeoutMs) {
63
+ return loadClassicScript(url, timeoutMs, 'Graphviz runtime');
64
+ }
65
+ function waitForScript(script, url, timeoutMs, label) {
56
66
  return new Promise((resolve, reject) => {
57
67
  let done = false;
58
68
  const finish = (fn) => {
@@ -70,7 +80,7 @@ function waitForScript(script, url, timeoutMs) {
70
80
  };
71
81
  const onError = () => {
72
82
  script.dataset.plantumlRuntimeState = 'error';
73
- finish(() => reject(new PlantUmlError('load', `Failed to load the Graphviz runtime from ${url}. ` +
83
+ finish(() => reject(new PlantUmlError('load', `Failed to load the ${label} from ${url}. ` +
74
84
  'Check that the asset is served under your Docusaurus baseUrl with a JavaScript MIME type.')));
75
85
  };
76
86
  const timer = setTimeout(() => {
@@ -175,10 +185,23 @@ export function isRuntimeRequested() {
175
185
  export function isVizRuntimeRequested() {
176
186
  return cachedViz !== null;
177
187
  }
178
- /** Test-only: forgets both cached runtimes and removes the injected script tag. */
188
+ /**
189
+ * Registers a callback for {@link resetRuntimeLoader}.
190
+ *
191
+ * The standard library loader keeps its own per-namespace cache but shares the script tags
192
+ * this module injects, so a reset that removed the tags without clearing that cache would
193
+ * leave it claiming namespaces are loaded when their globals are gone. The dependency points
194
+ * this way — loader to registry — so that `assetLoader` needs no import of its callers.
195
+ */
196
+ const resetHooks = [];
197
+ export function onRuntimeReset(hook) {
198
+ resetHooks.push(hook);
199
+ }
200
+ /** Test-only: forgets every cached runtime and removes the injected script tags. */
179
201
  export function resetRuntimeLoader() {
180
202
  cachedRuntime = null;
181
203
  cachedViz = null;
204
+ resetHooks.forEach((hook) => hook());
182
205
  if (typeof document !== 'undefined') {
183
206
  document.querySelectorAll(`script[${SCRIPT_MARKER}]`).forEach((node) => node.remove());
184
207
  }
@@ -4,6 +4,11 @@ export interface CacheKeyInput {
4
4
  dark: boolean;
5
5
  sanitized: boolean;
6
6
  coreVersion: string;
7
+ /**
8
+ * Identifies the standard library the diagram was rendered against, so refreshing it
9
+ * invalidates the entries whose pictures it shaped. `null` when it is switched off.
10
+ */
11
+ stdlibRevision: string | null;
7
12
  }
8
13
  export interface DiagramCache {
9
14
  readonly mode: CacheMode;
@@ -13,7 +18,7 @@ export interface DiagramCache {
13
18
  /** Number of entries currently held. Exposed for tests and the documented size limit. */
14
19
  size(): number;
15
20
  }
16
- export declare function computeCacheKey({ source, dark, sanitized, coreVersion }: CacheKeyInput): string;
21
+ export declare function computeCacheKey({ source, dark, sanitized, coreVersion, stdlibRevision, }: CacheKeyInput): string;
17
22
  export interface GraphvizCacheKeyInput {
18
23
  source: string;
19
24
  /** Graphviz layout engine; `dot` and `neato` draw different pictures from one source. */
@@ -15,12 +15,12 @@ function hash(input) {
15
15
  }
16
16
  return value.toString(36).padStart(7, '0');
17
17
  }
18
- export function computeCacheKey({ source, dark, sanitized, coreVersion }) {
18
+ export function computeCacheKey({ source, dark, sanitized, coreVersion, stdlibRevision, }) {
19
19
  const mode = dark ? 'dark' : 'light';
20
20
  const clean = sanitized ? 'san' : 'raw';
21
21
  // The length guards against the (astronomically unlikely) 32-bit hash collision between
22
22
  // two different sources that happen to share every other key component.
23
- return `${coreVersion}|${mode}|${clean}|${source.length}|${hash(source)}`;
23
+ return `${coreVersion}|${stdlibRevision ?? 'nostd'}|${mode}|${clean}|${source.length}|${hash(source)}`;
24
24
  }
25
25
  /**
26
26
  * Cache key for a Graphviz diagram.
@@ -1,5 +1,7 @@
1
1
  import type { VizRenderError } from './types.js';
2
- export type PlantUmlErrorKind = 'load' | 'engine' | 'diagram' | 'syntax' | 'timeout' | 'config' | 'aborted' | 'too-large';
2
+ export type PlantUmlErrorKind = 'load' | 'engine' | 'diagram' | 'syntax' | 'timeout' | 'config' | 'aborted' | 'too-large'
3
+ /** A `!include <namespace/…>` the site cannot resolve. */
4
+ | 'stdlib';
3
5
  /** Every failure surfaced to the UI is one of these, so the panel can explain what broke. */
4
6
  export declare class PlantUmlError extends Error {
5
7
  readonly kind: PlantUmlErrorKind;
@@ -18,6 +18,24 @@ const BOILERPLATE_PATTERNS = [
18
18
  /^\s*$/,
19
19
  ];
20
20
  const NOT_SUPPORTED_HEADING = 'Diagram not supported by this release of PlantUML';
21
+ /**
22
+ * Failures raised by PlantUML's *preprocessor* rather than by its parser.
23
+ *
24
+ * These matter far more now that `!include <namespace/…>` is resolvable. Calling a macro the
25
+ * included version does not define, naming a file that is not in the namespace, and an
26
+ * include the preprocessor could not resolve at all are the three likeliest ways a standard
27
+ * library diagram goes wrong. *None* of them produces the `Syntax Error?` marker below — the
28
+ * picture says only `Function not found RelIndex`, `cannot include <…>` or `Fatal parsing
29
+ * error` — so without this signature the engine's error card was passed through as a
30
+ * successful render.
31
+ */
32
+ const PREPROCESSOR_FAILURE_PATTERN = /^\s*(?:function not found|cannot include|fatal parsing error)\b/i;
33
+ /**
34
+ * The header PlantUML puts above the source listing in every error picture. Required to
35
+ * co-occur with the line above, so that a diagram merely containing the words "cannot
36
+ * include" in a note is not mistaken for a failure.
37
+ */
38
+ const SOURCE_LISTING_PATTERN = /^\[From .+\(line \d+\)/;
21
39
  function textNodesOf(svg) {
22
40
  if (typeof DOMParser === 'undefined')
23
41
  return [];
@@ -49,8 +67,16 @@ export function detectDiagramError(svg) {
49
67
  const isEmptyDescription = texts.some((text) => /Empty description/.test(text) && text.includes('(Assumed diagram type:'));
50
68
  const isUnsupported = texts.some((text) => text.includes(NOT_SUPPORTED_HEADING)) &&
51
69
  texts.some((text) => text.includes('is not recognized.'));
52
- if (!isSyntaxError && !isEmptyDescription && !isUnsupported)
70
+ const preprocessorFailures = texts.filter((text) => PREPROCESSOR_FAILURE_PATTERN.test(text));
71
+ const isPreprocessorError = preprocessorFailures.length > 0 && texts.some((text) => SOURCE_LISTING_PATTERN.test(text));
72
+ if (!isSyntaxError && !isEmptyDescription && !isUnsupported && !isPreprocessorError)
53
73
  return null;
74
+ // A preprocessor failure lists the *expanded* source, which for a standard library macro
75
+ // runs to thousands of lines. The failure itself is the only part worth showing; the
76
+ // reader still has the original fence a click away in the source view.
77
+ if (isPreprocessorError && !isSyntaxError) {
78
+ return preprocessorFailures.map((text) => text.trim()).join('\n');
79
+ }
54
80
  const meaningful = texts
55
81
  .map((text) => text.replace(/\s+$/, ''))
56
82
  .filter((text) => !BOILERPLATE_PATTERNS.some((pattern) => pattern.test(text)));
@@ -1,5 +1,6 @@
1
1
  import { type LoadRuntimeOptions } from './assetLoader.js';
2
2
  import { type DiagramCache } from './cache.js';
3
+ import { type StdlibRuntime } from './stdlibLoader.js';
3
4
  export interface RenderDiagramRequest {
4
5
  source: string;
5
6
  dark: boolean;
@@ -7,6 +8,8 @@ export interface RenderDiagramRequest {
7
8
  timeoutMs: number;
8
9
  assetsBaseUrl: string;
9
10
  coreVersion: string;
11
+ /** Standard library assets for this site, or `null` when the feature is switched off. */
12
+ stdlib: StdlibRuntime | null;
10
13
  cache: DiagramCache;
11
14
  signal?: AbortSignal;
12
15
  /** Reports which long-running phase is in progress, so the UI can show an accurate state. */
@@ -3,6 +3,7 @@ import { computeCacheKey } from './cache.js';
3
3
  import { describeEngineError, detectDiagramError, PlantUmlError } from './errors.js';
4
4
  import { enqueueRender } from './queue.js';
5
5
  import { sanitizeSvgMarkup } from './sanitize.js';
6
+ import { loadStdlibForSource } from './stdlibLoader.js';
6
7
  /**
7
8
  * `renderToString` takes its options as a *fourth* argument, after the two callbacks.
8
9
  * This was verified against the installed `@plantuml/core` rather than assumed: the
@@ -41,19 +42,32 @@ function renderWithEngine(engine, source, dark) {
41
42
  * {@link enqueueRender} because the engine cannot render two diagrams at once.
42
43
  */
43
44
  export async function renderDiagram(request) {
44
- const { source, dark, sanitize, timeoutMs, cache, coreVersion, signal, onPhase } = request;
45
- const cacheKey = computeCacheKey({ source, dark, sanitized: sanitize, coreVersion });
45
+ const { source, dark, sanitize, timeoutMs, cache, coreVersion, stdlib, signal, onPhase } = request;
46
+ const cacheKey = computeCacheKey({
47
+ source,
48
+ dark,
49
+ sanitized: sanitize,
50
+ coreVersion,
51
+ stdlibRevision: stdlib?.manifest.revision ?? null,
52
+ });
46
53
  const cached = cache.get(cacheKey);
47
54
  if (cached !== undefined)
48
55
  return cached;
49
56
  // Loading happens outside the queue: it is shared, idempotent work, and holding the
50
57
  // single render slot for an 8 MB download would stall every other diagram on the page.
58
+ //
59
+ // The standard library loads alongside the engine rather than after it. The two are
60
+ // independent downloads, and the engine cannot be asked for a diagram whose includes are
61
+ // not resolvable yet — by the time it looks, the namespaces have to be in place already.
51
62
  onPhase?.('loading');
52
- const engine = await loadPlantUmlRuntime({
53
- assetsBaseUrl: request.assetsBaseUrl,
54
- timeoutMs,
55
- importModule: request.importModule,
56
- });
63
+ const [engine] = await Promise.all([
64
+ loadPlantUmlRuntime({
65
+ assetsBaseUrl: request.assetsBaseUrl,
66
+ timeoutMs,
67
+ importModule: request.importModule,
68
+ }),
69
+ loadStdlibForSource({ source, stdlib, timeoutMs }),
70
+ ]);
57
71
  onPhase?.('rendering');
58
72
  const svg = await enqueueRender(async () => {
59
73
  const raw = await renderWithEngine(engine, source, dark);
@@ -0,0 +1,31 @@
1
+ import { type StdlibRuntimeManifest } from '../stdlibShared.js';
2
+ export interface StdlibRuntime {
3
+ /** Absolute, `baseUrl`-prefixed URL of the directory holding the namespace bundles. */
4
+ baseUrl: string;
5
+ manifest: StdlibRuntimeManifest;
6
+ }
7
+ /**
8
+ * The namespaces a source needs, in load order, including what those namespaces need in turn.
9
+ *
10
+ * Dependencies come from the manifest rather than from parsing the library sources in the
11
+ * browser: `k8s` includes `<c4/…>` from its own `Common.puml`, and the engine would ask for
12
+ * it mid-render, far too late to fetch anything. Example-only dependencies are pulled in
13
+ * solely when the diagram includes something from `_examples_/` — C4's examples reference
14
+ * `office`, and charging every C4 diagram 160 KB for that would be absurd.
15
+ *
16
+ * A dependency the site does not provide is skipped rather than raised: it may well sit
17
+ * behind an `!if` the diagram never takes, and failing here would break diagrams that work.
18
+ */
19
+ export declare function collectStdlibNamespaces(source: string, manifest: StdlibRuntimeManifest): string[];
20
+ export interface LoadStdlibOptions {
21
+ source: string;
22
+ stdlib: StdlibRuntime | null;
23
+ timeoutMs: number;
24
+ }
25
+ /**
26
+ * Loads whatever the source needs from the standard library. Resolves immediately when it
27
+ * needs nothing, which is the common case and must stay free.
28
+ */
29
+ export declare function loadStdlibForSource({ source, stdlib, timeoutMs, }: LoadStdlibOptions): Promise<void>;
30
+ /** Whether a namespace bundle has been requested on this page. For tests and diagnostics. */
31
+ export declare function isStdlibNamespaceRequested(url: string): boolean;