@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
@@ -0,0 +1,54 @@
1
+ /**
2
+ * What the Node side and the browser side of standard library support both need: the shape
3
+ * of the manifest, and the one definition of what a `<namespace/…>` include looks like.
4
+ *
5
+ * Deliberately free of `node:` imports. `stdlibBundle.ts` reads the file system and
6
+ * `stdlib.ts` resolves paths; neither may be pulled into a theme component's bundle, and
7
+ * keeping the shared half here is what stops that from happening by accident.
8
+ */
9
+ /** Directory holding a namespace's includable examples, which upstream keeps in the bundle. */
10
+ export const STDLIB_EXAMPLES_DIRECTORY = '_examples_';
11
+ /**
12
+ * `<namespace/path>` in an `!include` and its variants.
13
+ *
14
+ * Anchored to the start of a line because `!include` is only a directive there. Applied to
15
+ * stdlib sources when bundling and to author sources when rendering — one syntax, one place
16
+ * to correct it.
17
+ */
18
+ export const STDLIB_INCLUDE_PATTERN = /^[^\S\r\n]*!include(?:sub|url|_many)?[^\S\r\n]+<([^>\r\n]+)>/gim;
19
+ /**
20
+ * Extracts the `<namespace/…>` includes of a PlantUML source.
21
+ *
22
+ * Includes whose path is computed (`!include <material2.1.19/$icon>`, as DomainStory does
23
+ * inside an `!if`) are skipped: the namespace is knowable but the file is not, so treating it
24
+ * as a hard dependency would make every DomainStory diagram drag in 6.8 MB of icons for a
25
+ * branch it will probably not take.
26
+ */
27
+ export function findStdlibIncludes(source) {
28
+ const found = [];
29
+ const seen = new Set();
30
+ // `matchAll` on a shared global regex is safe: it clones the regex rather than advancing
31
+ // `lastIndex` on the original.
32
+ for (const match of source.matchAll(STDLIB_INCLUDE_PATTERN)) {
33
+ const raw = match[1]?.trim() ?? '';
34
+ if (raw.includes('$') || raw.includes('%'))
35
+ continue;
36
+ const slash = raw.indexOf('/');
37
+ if (slash <= 0)
38
+ continue;
39
+ const namespace = raw.slice(0, slash).toLowerCase();
40
+ const target = raw.slice(slash + 1).toLowerCase();
41
+ if (target === '')
42
+ continue;
43
+ const key = `${namespace}/${target}`;
44
+ if (seen.has(key))
45
+ continue;
46
+ seen.add(key);
47
+ found.push({
48
+ namespace,
49
+ target,
50
+ fromExamples: target.startsWith(`${STDLIB_EXAMPLES_DIRECTORY}/`),
51
+ });
52
+ }
53
+ return found;
54
+ }
@@ -29,7 +29,7 @@ export default function Code(props) {
29
29
  if (engine === null || source === null) {
30
30
  return _jsx(OriginalCode, { ...props });
31
31
  }
32
- return (_jsx(PlantUmlDiagram, { source: source, title: parseTitle(props), language: language, engine: engine, layout: engine === 'graphviz' ? parseStringMeta(props, 'engine') : undefined, zoom: parseBooleanMeta(props, 'zoom') }));
32
+ return (_jsx(PlantUmlDiagram, { source: source, title: parseTitle(props), language: language, engine: engine, layout: engine === 'graphviz' ? parseStringMeta(props, 'engine') : undefined, zoom: parseBooleanMeta(props, 'zoom'), showSource: parseBooleanMeta(props, 'showSource') }));
33
33
  }
34
34
  /**
35
35
  * Decides which engine — if either — owns this fence.
@@ -19,6 +19,11 @@ export interface PlantUmlDiagramProps {
19
19
  * Set from the fence metastring: `zoom` or `zoom=false`.
20
20
  */
21
21
  zoom?: boolean;
22
+ /**
23
+ * Overrides the `showSource` plugin option for this diagram. `undefined` follows the option.
24
+ * Set from the fence metastring: `showSource` or `showSource=false`.
25
+ */
26
+ showSource?: boolean;
22
27
  }
23
28
  /**
24
29
  * Renders one diagram in the browser, with either engine.
@@ -30,4 +35,4 @@ export interface PlantUmlDiagramProps {
30
35
  * Nothing happens during server-side rendering: the markup emitted on the server is the
31
36
  * same deferred placeholder the client renders first, so hydration never mismatches.
32
37
  */
33
- export default function PlantUmlDiagram({ source, title, language, engine, layout, zoom: zoomProp, }: PlantUmlDiagramProps): ReactNode;
38
+ export default function PlantUmlDiagram({ source, title, language, engine, layout, zoom: zoomProp, showSource: showSourceProp, }: PlantUmlDiagramProps): ReactNode;
@@ -1,5 +1,5 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useEffect, useId, useRef, useState } from 'react';
2
+ import { useCallback, useEffect, useId, useRef, useState } from 'react';
3
3
  import { useColorMode } from '@docusaurus/theme-common';
4
4
  import { DATA_ATTR } from '../../constants.js';
5
5
  import { getSharedCache } from '../../runtime/cache.js';
@@ -19,6 +19,8 @@ const ENGINE_LABEL = {
19
19
  };
20
20
  /** Margin large enough that a diagram is usually ready by the time it is scrolled into view. */
21
21
  const LAZY_ROOT_MARGIN = '300px';
22
+ /** How long the copy result stays on screen before the control goes quiet again. */
23
+ const COPY_FEEDBACK_MS = 2_000;
22
24
  const INITIAL_STATE = { status: 'idle', svg: null, error: null };
23
25
  function escapeHtml(value) {
24
26
  return value
@@ -44,11 +46,12 @@ function messageFor(error) {
44
46
  * Nothing happens during server-side rendering: the markup emitted on the server is the
45
47
  * same deferred placeholder the client renders first, so hydration never mismatches.
46
48
  */
47
- export default function PlantUmlDiagram({ source, title, language, engine = 'plantuml', layout, zoom: zoomProp, }) {
49
+ export default function PlantUmlDiagram({ source, title, language, engine = 'plantuml', layout, zoom: zoomProp, showSource: showSourceProp, }) {
48
50
  const config = usePlantUmlConfig();
49
51
  const { colorMode } = useColorMode();
50
52
  const containerRef = useRef(null);
51
53
  const hintId = useId();
54
+ const sourcePanelId = useId();
52
55
  const themeOption = config?.options.theme ?? 'auto';
53
56
  const dark = themeOption === 'auto' ? colorMode === 'dark' : themeOption === 'dark';
54
57
  const lazy = config?.options.lazy ?? true;
@@ -62,8 +65,39 @@ export default function PlantUmlDiagram({ source, title, language, engine = 'pla
62
65
  const renderDark = engine === 'graphviz' ? false : dark;
63
66
  // A fence flag wins over the plugin option, which in turn wins over the built-in default.
64
67
  const interactive = zoomProp ?? config?.options.zoom ?? true;
68
+ const sourceAvailable = showSourceProp ?? config?.options.showSource ?? true;
65
69
  const [state, setState] = useState(INITIAL_STATE);
66
70
  const [inView, setInView] = useState(!lazy);
71
+ const [sourceOpen, setSourceOpen] = useState(false);
72
+ const [copyState, setCopyState] = useState('idle');
73
+ const copyTimer = useRef(undefined);
74
+ // A pending "Copied" message must not fire into an unmounted component.
75
+ useEffect(() => () => clearTimeout(copyTimer.current), []);
76
+ /**
77
+ * Copies the source, and says whether it worked.
78
+ *
79
+ * `navigator.clipboard` is undefined outside a secure context, which a documentation site
80
+ * served over plain HTTP genuinely is. Reporting that plainly is better than a control that
81
+ * silently does nothing — the panel is open, so the reader can still select the text.
82
+ */
83
+ const copySource = useCallback(() => {
84
+ const settle = (next) => {
85
+ setCopyState(next);
86
+ clearTimeout(copyTimer.current);
87
+ copyTimer.current = setTimeout(() => setCopyState('idle'), COPY_FEEDBACK_MS);
88
+ };
89
+ const clipboard = typeof navigator === 'undefined' ? undefined : navigator.clipboard;
90
+ if (typeof clipboard?.writeText !== 'function') {
91
+ settle('failed');
92
+ return;
93
+ }
94
+ void clipboard.writeText(source).then(() => settle('copied'), () => settle('failed'));
95
+ }, [source]);
96
+ // A diagram whose source changed is a different diagram: an open panel would otherwise keep
97
+ // showing the old text's copy result, and the reader would have no idea it was stale.
98
+ useEffect(() => {
99
+ setCopyState('idle');
100
+ }, [source]);
67
101
  // Called unconditionally, as hooks must be; it attaches nothing when not interactive.
68
102
  const zoom = useZoomPan({
69
103
  enabled: interactive && state.status === 'ready',
@@ -136,7 +170,7 @@ export default function PlantUmlDiagram({ source, title, language, engine = 'pla
136
170
  transparentBackground: options.graphviz.transparentBackground,
137
171
  maxSourceBytes: options.graphviz.maxSourceBytes,
138
172
  })
139
- : renderDiagram({ ...shared, dark: renderDark });
173
+ : renderDiagram({ ...shared, dark: renderDark, stdlib: config.stdlib });
140
174
  void pending
141
175
  .then((svg) => commit({ status: 'ready', svg, error: null }))
142
176
  .catch((error) => {
@@ -156,6 +190,7 @@ export default function PlantUmlDiagram({ source, title, language, engine = 'pla
156
190
  config,
157
191
  config?.assetsBaseUrl,
158
192
  config?.coreVersion,
193
+ config?.stdlib,
159
194
  config?.options.sanitizeSvg,
160
195
  config?.options.renderTimeoutMs,
161
196
  config?.options.cache,
@@ -167,16 +202,29 @@ export default function PlantUmlDiagram({ source, title, language, engine = 'pla
167
202
  const busy = state.status === 'loading' || state.status === 'rendering';
168
203
  // The sanitized SVG always lives in this element, zoomable or not, so `role="img" > svg`
169
204
  // stays a stable contract for tests and for author CSS.
170
- const canvas = state.svg !== null && (_jsx("div", { className: styles.canvas, role: "img", "aria-label": label,
205
+ const canvas = state.svg !== null && (_jsx("div", { className: !interactive && sourceOpen ? `${styles.canvas} ${styles.hiddenView}` : styles.canvas, role: "img", "aria-label": label,
171
206
  // Sanitized above unless `sanitizeSvg: false` was explicitly opted into.
172
207
  dangerouslySetInnerHTML: { __html: state.svg } }));
208
+ const copyControl = sourceAvailable && sourceOpen && state.status === 'ready' && (_jsx("button", { type: "button", className: `${styles.toolbarButton} ${styles.toolbarTextButton}`, "aria-label": `Copy ${engineName} source to clipboard`, onClick: copySource, children: "Copy" }));
209
+ const sourceToggle = sourceAvailable && (_jsx("button", { type: "button", className: styles.toolbarButton, "aria-label": sourceOpen ? 'Hide diagram source' : 'Show diagram source', "aria-expanded": sourceOpen, "aria-controls": sourcePanelId, onClick: () => setSourceOpen((open) => !open), children: _jsx("span", { "aria-hidden": "true", children: '</>' }) }));
210
+ /**
211
+ * The source *replaces the diagram in its own frame* rather than appearing beneath it.
212
+ *
213
+ * Below the diagram it had two failure modes that the frame removes by construction: a
214
+ * diagram taller than the window pushed the panel off-screen, so the control looked broken;
215
+ * and while maximized the panel was painted behind the `position: fixed` overlay, so it was
216
+ * invisible however far you scrolled. Living in the same box as the picture, it is visible
217
+ * exactly when and where the picture was.
218
+ */
219
+ const sourceView = sourceAvailable && sourceOpen && state.status === 'ready' && (_jsxs("div", { className: styles.sourceView, id: sourcePanelId, children: [_jsxs("div", { className: styles.sourceViewBar, children: [_jsxs("span", { className: styles.sourceViewTitle, children: [engineName, " source"] }), _jsxs("span", { className: styles.copyStatus, role: "status", children: [copyState === 'copied' ? 'Copied to clipboard' : null, copyState === 'failed' ? 'Could not copy — select the text instead' : null] })] }), _jsx("pre", { className: styles.sourceCode, children: source })] }));
173
220
  return (_jsxs("figure", { ref: containerRef, className: styles.figure, "aria-busy": busy || undefined, [DATA_ATTR.diagram]: language ?? engine,
174
221
  [DATA_ATTR.engine]: engine,
175
222
  ...(engine === 'graphviz' ? { [DATA_ATTR.layout]: resolvedLayout } : {}),
176
223
  [DATA_ATTR.status]: state.status,
177
224
  [DATA_ATTR.theme]: dark ? 'dark' : 'light',
178
225
  ...(interactive ? { [DATA_ATTR.interactive]: 'true' } : {}),
179
- ...(zoom.maximized ? { [DATA_ATTR.maximized]: 'true' } : {}), children: [state.status === 'ready' && state.svg !== null && !interactive && canvas, state.status === 'ready' && state.svg !== null && interactive && (_jsxs("div", { ref: zoom.stageRef, className: zoom.maximized ? `${styles.stage} ${styles.maximized}` : styles.stage, children: [_jsx("div", { ref: zoom.viewportRef, className: styles.viewport, tabIndex: 0, "aria-describedby": hintId, "aria-keyshortcuts": "Plus Minus 0 ArrowUp ArrowDown ArrowLeft ArrowRight", onKeyDown: zoom.onKeyDown, [DATA_ATTR.zoom]: '1', children: _jsx("div", { ref: zoom.layerRef, className: styles.layer, children: canvas }) }), _jsxs("div", { className: styles.toolbar, role: "group", "aria-label": `${label} zoom controls`, children: [_jsx("button", { type: "button", className: styles.toolbarButton, "aria-label": "Zoom out", onClick: zoom.zoomOut, children: _jsx("span", { "aria-hidden": "true", children: "\u2212" }) }), _jsx("button", { type: "button", className: styles.toolbarButton, "aria-label": "Zoom in", onClick: zoom.zoomIn, children: _jsx("span", { "aria-hidden": "true", children: "+" }) }), _jsx("button", { type: "button", className: styles.toolbarButton, "aria-label": "Reset zoom", onClick: zoom.reset, children: _jsx("span", { "aria-hidden": "true", children: "\u27F2" }) }), _jsx("button", { type: "button", className: styles.toolbarButton, "aria-label": "Maximize diagram", "aria-pressed": zoom.maximized, onClick: zoom.toggleMaximize, children: _jsx("span", { "aria-hidden": "true", children: zoom.maximized ? '✕' : '⛶' }) }), _jsx("span", { ref: zoom.readoutRef, className: styles.readout, "aria-hidden": "true", children: "100%" })] })] })), state.status === 'ready' && state.svg !== null && interactive && (_jsx("p", { id: hintId, className: styles.visuallyHidden, children: "Zoomable diagram. Use the zoom controls, or press plus and minus to zoom, the arrow keys to pan, and zero to reset." })), state.status !== 'ready' && state.status !== 'error' && (
226
+ ...(zoom.maximized ? { [DATA_ATTR.maximized]: 'true' } : {}),
227
+ ...(sourceOpen ? { [DATA_ATTR.sourceOpen]: 'true' } : {}), children: [state.status === 'ready' && state.svg !== null && !interactive && canvas, state.status === 'ready' && state.svg !== null && !interactive && sourceView, state.status === 'ready' && state.svg !== null && !interactive && sourceAvailable && (_jsxs("div", { className: styles.plainToolbar, role: "group", "aria-label": `${label} source controls`, children: [sourceToggle, copyControl] })), state.status === 'ready' && state.svg !== null && interactive && (_jsxs("div", { ref: zoom.stageRef, className: zoom.maximized ? `${styles.stage} ${styles.maximized}` : styles.stage, children: [_jsxs("div", { className: styles.stageBody, children: [_jsx("div", { ref: zoom.viewportRef, className: sourceOpen ? `${styles.viewport} ${styles.invisibleView}` : styles.viewport, tabIndex: sourceOpen ? -1 : 0, "aria-describedby": hintId, "aria-keyshortcuts": "Plus Minus 0 ArrowUp ArrowDown ArrowLeft ArrowRight", onKeyDown: zoom.onKeyDown, [DATA_ATTR.zoom]: '1', children: _jsx("div", { ref: zoom.layerRef, className: styles.layer, children: canvas }) }), sourceView] }), _jsxs("div", { className: styles.toolbar, role: "group", "aria-label": `${label} zoom controls`, children: [!sourceOpen && (_jsx("button", { type: "button", className: styles.toolbarButton, "aria-label": "Zoom out", onClick: zoom.zoomOut, children: _jsx("span", { "aria-hidden": "true", children: "\u2212" }) })), !sourceOpen && (_jsx("button", { type: "button", className: styles.toolbarButton, "aria-label": "Zoom in", onClick: zoom.zoomIn, children: _jsx("span", { "aria-hidden": "true", children: "+" }) })), !sourceOpen && (_jsx("button", { type: "button", className: styles.toolbarButton, "aria-label": "Reset zoom", onClick: zoom.reset, children: _jsx("span", { "aria-hidden": "true", children: "\u27F2" }) })), _jsx("button", { type: "button", className: styles.toolbarButton, "aria-label": "Maximize diagram", "aria-pressed": zoom.maximized, onClick: zoom.toggleMaximize, children: _jsx("span", { "aria-hidden": "true", children: zoom.maximized ? '✕' : '⛶' }) }), sourceToggle, copyControl, _jsx("span", { ref: zoom.readoutRef, className: sourceOpen ? `${styles.readout} ${styles.hiddenView}` : styles.readout, "aria-hidden": "true", children: "100%" })] })] })), state.status === 'ready' && state.svg !== null && interactive && (_jsx("p", { id: hintId, className: styles.visuallyHidden, children: "Zoomable diagram. Use the zoom controls, or press plus and minus to zoom, the arrow keys to pan, and zero to reset." })), state.status !== 'ready' && state.status !== 'error' && (
180
228
  // `aria-busy` on the figure already conveys progress; announcing every phase change
181
229
  // would make a page with several diagrams unusable with a screen reader.
182
230
  _jsxs("div", { className: styles.placeholder, children: [_jsx("span", { className: styles.spinner, "aria-hidden": "true" }), _jsxs("span", { children: [state.status === 'loading' ? `Loading ${engineName} runtime…` : null, state.status === 'rendering' ? `Rendering ${label}…` : null, state.status === 'idle' ? `${label} (waiting to render)` : null] })] })), state.status === 'error' && (_jsxs("div", { className: styles.error, role: "alert", children: [_jsxs("p", { className: styles.errorHeading, children: [_jsx("span", { className: styles.errorIcon, "aria-hidden": "true", children: "\u26A0" }), _jsxs("span", { children: ["Error: ", engineName, " diagram could not be rendered"] })] }), _jsx("pre", { className: styles.errorMessage, children: state.error }), config?.options.showSourceOnError !== false && (_jsxs("details", { className: styles.errorDetails, children: [_jsx("summary", { children: "Show diagram source" }), _jsx("pre", { className: styles.source, children: source })] }))] })), title !== undefined && _jsx("figcaption", { className: styles.caption, children: title }), _jsx("noscript", { dangerouslySetInnerHTML: {
@@ -291,3 +291,120 @@
291
291
  overflow-x: auto;
292
292
  font-size: 0.8125rem;
293
293
  }
294
+
295
+ /*
296
+ * Source view.
297
+ *
298
+ * The source takes the diagram's place inside the same frame rather than appearing beneath it.
299
+ * Below the diagram it had two failure modes this removes by construction: a diagram taller
300
+ * than the window pushed it off-screen, and while maximized it was painted behind the
301
+ * `position: fixed` overlay. In the frame it is visible exactly where the picture was.
302
+ */
303
+
304
+ /* One grid cell shared by the viewport and the source view, so they occupy the same box. */
305
+ .stageBody {
306
+ position: relative;
307
+ display: grid;
308
+ }
309
+
310
+ .stageBody > * {
311
+ grid-area: 1 / 1;
312
+ /* Without this a wide <pre> would stretch the grid column instead of scrolling. */
313
+ min-width: 0;
314
+ }
315
+
316
+ /*
317
+ * `visibility`, not `display`: the hidden viewport keeps contributing its height, so the frame
318
+ * does not resize when flipping, and the zoom hook's layout measurements stay valid for the
319
+ * flip back. A `visibility: hidden` subtree is also not focusable and takes no pointer events,
320
+ * so the invisible diagram cannot be tabbed into or dragged.
321
+ */
322
+ .invisibleView {
323
+ visibility: hidden;
324
+ }
325
+
326
+ /* Collapses entirely — used where there is no frame height worth preserving. */
327
+ .hiddenView {
328
+ display: none;
329
+ }
330
+
331
+ /*
332
+ * Inside a zoom frame the source is taken out of flow so that it cannot size the grid row: the
333
+ * row stays exactly as tall as the (hidden but still laid-out) viewport, and a long source
334
+ * scrolls within it. Left in flow it grew the frame from the diagram's height to the source's —
335
+ * on a long diagram that pushed the text back off the bottom of the window, which is the very
336
+ * problem moving it into the frame was meant to solve.
337
+ */
338
+ .stageBody .sourceView {
339
+ position: absolute;
340
+ inset: 0;
341
+ }
342
+
343
+ .sourceView {
344
+ display: flex;
345
+ flex-direction: column;
346
+ min-height: 0;
347
+ border: 1px solid var(--ifm-color-emphasis-300);
348
+ border-radius: var(--ifm-global-radius);
349
+ background: var(--ifm-background-surface-color);
350
+ overflow: hidden;
351
+ }
352
+
353
+ .sourceViewBar {
354
+ display: flex;
355
+ align-items: center;
356
+ flex-wrap: wrap;
357
+ gap: 0.5rem;
358
+ /* Clears the toolbar, which floats over the top-right of the same frame. */
359
+ padding: 0.5rem 12rem 0.25rem 0.75rem;
360
+ }
361
+
362
+ .sourceViewTitle {
363
+ color: var(--ifm-color-emphasis-700);
364
+ font-size: 0.8125rem;
365
+ font-weight: 600;
366
+ }
367
+
368
+ .copyStatus {
369
+ flex: 1;
370
+ color: var(--ifm-color-emphasis-700);
371
+ font-size: 0.75rem;
372
+ text-align: right;
373
+ }
374
+
375
+ /*
376
+ * The copy control is a toolbar button like the rest, just carrying a word instead of a glyph,
377
+ * so it needs width where the icon buttons are square.
378
+ */
379
+ .toolbarTextButton {
380
+ width: auto;
381
+ padding: 0 0.5rem;
382
+ font-size: 0.8125rem;
383
+ }
384
+
385
+ .sourceCode {
386
+ flex: 1;
387
+ min-height: 0;
388
+ margin: 0;
389
+ padding: 0.75rem;
390
+ overflow: auto;
391
+ background: transparent;
392
+ font-size: 0.8125rem;
393
+ /* Wraps rather than scrolls sideways: diagram source is read, not just copied. */
394
+ white-space: pre-wrap;
395
+ word-break: break-word;
396
+ }
397
+
398
+ /*
399
+ * Without a zoom frame the source has no box to fill, so it needs a floor of its own — and a
400
+ * ceiling, so a long diagram cannot produce an endless wall of text.
401
+ */
402
+ .figure:not([data-plantuml-interactive]) .sourceView {
403
+ max-height: 30rem;
404
+ }
405
+
406
+ .plainToolbar {
407
+ display: flex;
408
+ justify-content: flex-end;
409
+ margin-top: 0.25rem;
410
+ }
@@ -1,9 +1,17 @@
1
1
  import { type ResolvedPlantUmlOptions } from '../options.js';
2
+ import type { StdlibRuntimeManifest } from '../stdlibShared.js';
3
+ /** The standard library as the browser sees it: a URL to fetch bundles from, and an index. */
4
+ export interface StdlibRuntimeConfig {
5
+ baseUrl: string;
6
+ manifest: StdlibRuntimeManifest;
7
+ }
2
8
  export interface PlantUmlRuntimeConfig {
3
9
  options: ResolvedPlantUmlOptions;
4
10
  /** Absolute, `baseUrl`-prefixed URL of the directory holding the runtime assets. */
5
11
  assetsBaseUrl: string;
6
12
  coreVersion: string;
13
+ /** `null` when the standard library is switched off for this site. */
14
+ stdlib: StdlibRuntimeConfig | null;
7
15
  }
8
16
  /**
9
17
  * Reads the plugin's configuration from Docusaurus global data.
@@ -2,7 +2,7 @@ import { useMemo } from 'react';
2
2
  import useBaseUrl from '@docusaurus/useBaseUrl';
3
3
  import { useAllPluginInstancesData } from '@docusaurus/useGlobalData';
4
4
  import { PLUGIN_NAME } from '../constants.js';
5
- import { DEFAULT_GRAPHVIZ_OPTIONS } from '../options.js';
5
+ import { DEFAULT_GRAPHVIZ_OPTIONS, DEFAULT_STDLIB_OPTIONS, } from '../options.js';
6
6
  function isGlobalData(value) {
7
7
  if (typeof value !== 'object' || value === null)
8
8
  return false;
@@ -23,26 +23,32 @@ export function usePlantUmlConfig() {
23
23
  const first = instances ? Object.values(instances).find(isGlobalData) : undefined;
24
24
  // `useBaseUrl` must be called unconditionally; the placeholder path is discarded below.
25
25
  const assetsBaseUrl = useBaseUrl(first ? first.assetsDir : '/');
26
+ const stdlibBaseUrl = useBaseUrl(first?.stdlib ? first.stdlib.dir : '/');
26
27
  // Memoized because consumers use this object as an effect dependency. Returning a fresh
27
28
  // object on every render would restart the render effect on every state update, which
28
29
  // re-enters rendering forever.
29
30
  return useMemo(() => first
30
31
  ? {
31
- options: withGraphvizDefaults(first.options),
32
+ options: withDefaults(first.options),
32
33
  assetsBaseUrl,
33
34
  coreVersion: first.coreVersion,
35
+ stdlib: first.stdlib ? { baseUrl: stdlibBaseUrl, manifest: first.stdlib.manifest } : null,
34
36
  }
35
- : null, [first, assetsBaseUrl]);
37
+ : null, [first, assetsBaseUrl, stdlibBaseUrl]);
36
38
  }
37
39
  /**
38
- * Fills in the `graphviz` group when it is missing from global data.
40
+ * Fills in the nested option groups when they are missing from global data.
39
41
  *
40
- * `resolveOptions` always produces it, so this only matters for data written by an older
42
+ * `resolveOptions` always produces them, so this only matters for data written by an older
41
43
  * version of the plugin — a stale `.docusaurus` cache surviving an upgrade, most plausibly.
42
44
  * Substituting the defaults keeps such a site rendering instead of failing on a missing key.
43
45
  */
44
- function withGraphvizDefaults(options) {
45
- if (options.graphviz)
46
+ function withDefaults(options) {
47
+ if (options.graphviz && options.stdlib)
46
48
  return options;
47
- return { ...options, graphviz: DEFAULT_GRAPHVIZ_OPTIONS };
49
+ return {
50
+ ...options,
51
+ graphviz: options.graphviz ?? DEFAULT_GRAPHVIZ_OPTIONS,
52
+ stdlib: options.stdlib ?? DEFAULT_STDLIB_OPTIONS,
53
+ };
48
54
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@matfsw/docusaurus-plantuml-plugin",
3
- "version": "1.1.2",
3
+ "version": "1.3.0",
4
4
  "description": "Render PlantUML diagrams in Docusaurus 3 entirely in the browser - no Java, no PlantUML server, no CDN.",
5
5
  "keywords": [
6
6
  "docusaurus",
@@ -35,6 +35,7 @@
35
35
  "./package.json": "./package.json"
36
36
  },
37
37
  "files": [
38
+ "assets",
38
39
  "dist",
39
40
  "README.md",
40
41
  "LICENSE",
@@ -62,6 +63,7 @@
62
63
  "verify:tag": "node scripts/verify-tag-version.mjs",
63
64
  "sync:meta": "node scripts/sync-project-metadata.mjs",
64
65
  "sync:check": "node scripts/sync-project-metadata.mjs --check",
66
+ "stdlib:update": "node scripts/update-stdlib.mjs",
65
67
  "example:install": "npm --prefix examples/docusaurus install",
66
68
  "example:build": "npm --prefix examples/docusaurus run build",
67
69
  "example:serve": "npm --prefix examples/docusaurus run serve",