@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,99 @@
1
+ import { findStdlibIncludes } from '../stdlibShared.js';
2
+ import { loadClassicScript, onRuntimeReset } from './assetLoader.js';
3
+ import { PlantUmlError } from './errors.js';
4
+ /** One in-flight or settled load per bundle URL, shared by every diagram on the page. */
5
+ const loaded = new Map();
6
+ onRuntimeReset(() => loaded.clear());
7
+ function bundleFileName(namespace) {
8
+ return `${namespace}.min.js`;
9
+ }
10
+ function joinUrl(base, file) {
11
+ return `${base.replace(/\/+$/, '')}/${file}`;
12
+ }
13
+ /**
14
+ * Tells the engine the namespace bundle is already in the document.
15
+ *
16
+ * The key is the bare file name because that is exactly the `src` the engine would have set,
17
+ * and its bookkeeping is keyed by that string rather than by the resolved URL.
18
+ */
19
+ function markLoadedForEngine(namespace) {
20
+ const engineWindow = window;
21
+ const state = (engineWindow.__pl_script_state ??= Object.create(null));
22
+ state[bundleFileName(namespace)] = { state: 'loaded', ok: [], err: [] };
23
+ }
24
+ /**
25
+ * The namespaces a source needs, in load order, including what those namespaces need in turn.
26
+ *
27
+ * Dependencies come from the manifest rather than from parsing the library sources in the
28
+ * browser: `k8s` includes `<c4/…>` from its own `Common.puml`, and the engine would ask for
29
+ * it mid-render, far too late to fetch anything. Example-only dependencies are pulled in
30
+ * solely when the diagram includes something from `_examples_/` — C4's examples reference
31
+ * `office`, and charging every C4 diagram 160 KB for that would be absurd.
32
+ *
33
+ * A dependency the site does not provide is skipped rather than raised: it may well sit
34
+ * behind an `!if` the diagram never takes, and failing here would break diagrams that work.
35
+ */
36
+ export function collectStdlibNamespaces(source, manifest) {
37
+ const references = findStdlibIncludes(source);
38
+ const ordered = [];
39
+ const seen = new Set();
40
+ const visit = (namespace, withExamples, direct) => {
41
+ const entry = manifest.namespaces[namespace];
42
+ if (!entry) {
43
+ if (!direct)
44
+ return;
45
+ throw new PlantUmlError('stdlib', `This diagram includes <${namespace}/…>, but the '${namespace}' standard library ` +
46
+ "namespace is not available on this site. Add it to the plugin's " +
47
+ `\`stdlib.include\` option and point \`stdlib.source\` at a plantuml-stdlib ` +
48
+ 'checkout that contains it.');
49
+ }
50
+ if (seen.has(namespace))
51
+ return;
52
+ seen.add(namespace);
53
+ for (const dependency of entry.dependencies)
54
+ visit(dependency, false, false);
55
+ if (withExamples) {
56
+ for (const dependency of entry.exampleDependencies)
57
+ visit(dependency, false, false);
58
+ }
59
+ ordered.push(namespace);
60
+ };
61
+ for (const reference of references) {
62
+ visit(reference.namespace, reference.fromExamples, true);
63
+ }
64
+ return ordered;
65
+ }
66
+ /**
67
+ * Loads whatever the source needs from the standard library. Resolves immediately when it
68
+ * needs nothing, which is the common case and must stay free.
69
+ */
70
+ export async function loadStdlibForSource({ source, stdlib, timeoutMs, }) {
71
+ const references = findStdlibIncludes(source);
72
+ if (references.length === 0)
73
+ return;
74
+ if (!stdlib) {
75
+ const names = [...new Set(references.map((reference) => reference.namespace))];
76
+ throw new PlantUmlError('stdlib', `This diagram includes ${names.map((name) => `<${name}/…>`).join(', ')} from the ` +
77
+ 'PlantUML standard library, which is switched off for this site (`stdlib: false`).');
78
+ }
79
+ const namespaces = collectStdlibNamespaces(source, stdlib.manifest);
80
+ await Promise.all(namespaces.map(async (namespace) => {
81
+ const url = joinUrl(stdlib.baseUrl, bundleFileName(namespace));
82
+ let pending = loaded.get(url);
83
+ if (!pending) {
84
+ pending = loadClassicScript(url, timeoutMs, `standard library namespace '${namespace}'`)
85
+ .then(() => markLoadedForEngine(namespace))
86
+ .catch((error) => {
87
+ // Clearing the entry lets a later diagram retry rather than inherit the failure.
88
+ loaded.delete(url);
89
+ throw error;
90
+ });
91
+ loaded.set(url, pending);
92
+ }
93
+ await pending;
94
+ }));
95
+ }
96
+ /** Whether a namespace bundle has been requested on this page. For tests and diagnostics. */
97
+ export function isStdlibNamespaceRequested(url) {
98
+ return loaded.has(url);
99
+ }
@@ -0,0 +1,35 @@
1
+ import type { ResolvedStdlibOptions } from './options.js';
2
+ import type { StdlibRuntimeManifest } from './stdlibShared.js';
3
+ /**
4
+ * Decides which standard library bundles a site build emits, and where each one comes from.
5
+ *
6
+ * The vendored bundles in `assets/stdlib` cover the namespaces this package ships (see
7
+ * `assets/stdlib/LICENSES.md`). Anything named in `stdlib.include` is generated on demand
8
+ * from a `plantuml-stdlib` checkout the site points at, and cached, because `aws` alone is
9
+ * 114 MB of source that nobody wants to re-read on every rebuild.
10
+ */
11
+ export type { StdlibNamespaceEntry, StdlibRuntimeManifest } from './stdlibShared.js';
12
+ export interface StdlibAssets {
13
+ /** Absolute paths of the bundles to copy into the site's assets directory. */
14
+ files: string[];
15
+ manifest: StdlibRuntimeManifest;
16
+ }
17
+ export interface ResolveStdlibInput {
18
+ options: ResolvedStdlibOptions;
19
+ /** Directory of the calling module, used to locate the vendored bundles. */
20
+ currentDir: string;
21
+ /** Site directory, which relative `stdlib.source` entries are resolved against. */
22
+ siteDir: string;
23
+ /**
24
+ * Where bundles generated for `stdlib.include` are cached. Docusaurus passes its
25
+ * `generatedFilesDir`; when it is absent the site's `.docusaurus` directory is assumed.
26
+ */
27
+ cacheDir?: string;
28
+ }
29
+ /**
30
+ * Resolves the standard library for one site build.
31
+ *
32
+ * Returns `null` when the feature is switched off, in which case nothing is emitted and the
33
+ * browser side never looks for a bundle.
34
+ */
35
+ export declare function resolveStdlibAssets({ options, currentDir, siteDir, cacheDir, }: ResolveStdlibInput): StdlibAssets | null;
package/dist/stdlib.js ADDED
@@ -0,0 +1,163 @@
1
+ import crypto from 'node:crypto';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import { PLUGIN_NAME } from './constants.js';
5
+ import { buildStdlibNamespace, stdlibBundleFileName } from './stdlibBundle.js';
6
+ /**
7
+ * `assets/` sits beside `dist/` in the published package and beside `src/` in the repository,
8
+ * so one relative step out of the compiled module's directory finds it either way.
9
+ */
10
+ function vendoredDirectory(currentDir) {
11
+ return path.join(currentDir, '..', 'assets', 'stdlib');
12
+ }
13
+ function readVendoredManifest(directory) {
14
+ const manifestPath = path.join(directory, 'manifest.json');
15
+ if (!fs.existsSync(manifestPath)) {
16
+ throw new Error(`[${PLUGIN_NAME}] The vendored standard library manifest is missing from ` +
17
+ `'${manifestPath}'. This is a packaging fault: reinstall the plugin, or run ` +
18
+ "'npm run stdlib:update' if you are working in a checkout of it.");
19
+ }
20
+ return JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
21
+ }
22
+ /** Short, stable content identity for a generated bundle, used to name its cache file. */
23
+ function digest(...parts) {
24
+ const hash = crypto.createHash('sha256');
25
+ for (const part of parts)
26
+ hash.update(part).update('\0');
27
+ return hash.digest('hex').slice(0, 12);
28
+ }
29
+ /**
30
+ * Cheap identity for a namespace directory: every file's path, size and mtime.
31
+ *
32
+ * Reading 114 MB of `aws` sources to hash them would defeat the point of the cache, and this
33
+ * is the same trade every bundler makes for its own file watching.
34
+ */
35
+ function directoryStamp(directory) {
36
+ const parts = [];
37
+ const walk = (current) => {
38
+ for (const entry of fs
39
+ .readdirSync(current, { withFileTypes: true })
40
+ .sort((a, b) => (a.name < b.name ? -1 : 1))) {
41
+ const absolute = path.join(current, entry.name);
42
+ if (entry.isDirectory()) {
43
+ walk(absolute);
44
+ continue;
45
+ }
46
+ const stats = fs.statSync(absolute);
47
+ parts.push(`${absolute}:${stats.size}:${stats.mtimeMs}`);
48
+ }
49
+ };
50
+ walk(directory);
51
+ return digest(...parts);
52
+ }
53
+ /**
54
+ * Finds a namespace directory in one of the configured source trees.
55
+ *
56
+ * Matching is case-insensitive because the option is written in the same lower case the
57
+ * engine uses (`'aws'`), while upstream spells some directories differently (`'C4'`).
58
+ */
59
+ function findNamespaceDirectory(sourceRoots, namespace) {
60
+ for (const root of sourceRoots) {
61
+ if (!fs.existsSync(root))
62
+ continue;
63
+ for (const entry of fs.readdirSync(root, { withFileTypes: true })) {
64
+ if (entry.isDirectory() && entry.name.toLowerCase() === namespace) {
65
+ return path.join(root, entry.name);
66
+ }
67
+ }
68
+ }
69
+ return null;
70
+ }
71
+ /**
72
+ * Resolves the standard library for one site build.
73
+ *
74
+ * Returns `null` when the feature is switched off, in which case nothing is emitted and the
75
+ * browser side never looks for a bundle.
76
+ */
77
+ export function resolveStdlibAssets({ options, currentDir, siteDir, cacheDir, }) {
78
+ if (!options.enabled)
79
+ return null;
80
+ const directory = vendoredDirectory(currentDir);
81
+ const vendored = readVendoredManifest(directory);
82
+ const vendoredNamespaces = vendored.namespaces ?? {};
83
+ const files = [];
84
+ const namespaces = {};
85
+ const addVendored = (namespace) => {
86
+ if (namespace in namespaces)
87
+ return;
88
+ const entry = vendoredNamespaces[namespace];
89
+ if (!entry)
90
+ return;
91
+ const bundlePath = path.join(directory, entry.file);
92
+ if (!fs.existsSync(bundlePath)) {
93
+ throw new Error(`[${PLUGIN_NAME}] The vendored standard library manifest lists '${entry.file}', but ` +
94
+ `'${bundlePath}' does not exist. Reinstall the plugin.`);
95
+ }
96
+ files.push(bundlePath);
97
+ namespaces[namespace] = {
98
+ dependencies: entry.dependencies ?? [],
99
+ exampleDependencies: entry.exampleDependencies ?? [],
100
+ };
101
+ // A namespace is useless without the ones its own files include, so `stdlib.namespaces`
102
+ // narrows the selection but can never narrow it into something that cannot render.
103
+ for (const dependency of entry.dependencies ?? [])
104
+ addVendored(dependency);
105
+ };
106
+ const selected = options.namespaces ?? Object.keys(vendoredNamespaces);
107
+ if (options.namespaces) {
108
+ const unknown = options.namespaces.filter((namespace) => !(namespace in vendoredNamespaces) && !options.include.includes(namespace));
109
+ if (unknown.length > 0) {
110
+ throw new Error(`[${PLUGIN_NAME}] options.stdlib.namespaces names ${unknown
111
+ .map((name) => `'${name}'`)
112
+ .join(', ')}, which ${unknown.length > 1 ? 'are' : 'is'} not vendored with this ` +
113
+ `plugin. Vendored namespaces: ${Object.keys(vendoredNamespaces).join(', ')}. To use ` +
114
+ 'another one, add it to options.stdlib.include and point options.stdlib.source at a ' +
115
+ 'plantuml-stdlib checkout.');
116
+ }
117
+ }
118
+ for (const namespace of selected)
119
+ addVendored(namespace);
120
+ // Extra namespaces come last so that naming a vendored one in `include` replaces it with
121
+ // the copy from the site's own checkout.
122
+ const sourceRoots = options.source.map((entry) => path.resolve(siteDir, entry));
123
+ const generatedDir = path.join(cacheDir ?? path.join(siteDir, '.docusaurus'), 'plantuml-stdlib');
124
+ const stamps = [];
125
+ for (const namespace of options.include) {
126
+ const namespaceDir = findNamespaceDirectory(sourceRoots, namespace);
127
+ if (!namespaceDir) {
128
+ throw new Error(`[${PLUGIN_NAME}] options.stdlib.include names '${namespace}', which was not found in ` +
129
+ `${sourceRoots.map((root) => `'${root}'`).join(', ')}. Clone the standard library ` +
130
+ '(git clone --depth 1 https://github.com/plantuml/plantuml-stdlib) and point ' +
131
+ "options.stdlib.source at its 'stdlib' directory.");
132
+ }
133
+ const stamp = directoryStamp(namespaceDir);
134
+ stamps.push(`${namespace}:${stamp}`);
135
+ const cached = path.join(generatedDir, stamp, stdlibBundleFileName(namespace));
136
+ if (!fs.existsSync(cached)) {
137
+ const bundle = buildStdlibNamespace(path.dirname(namespaceDir), path.basename(namespaceDir));
138
+ fs.mkdirSync(path.dirname(cached), { recursive: true });
139
+ fs.writeFileSync(cached, bundle.script, 'utf8');
140
+ fs.writeFileSync(`${cached}.json`, JSON.stringify({
141
+ dependencies: bundle.dependencies,
142
+ exampleDependencies: bundle.exampleDependencies,
143
+ }), 'utf8');
144
+ }
145
+ const meta = JSON.parse(fs.readFileSync(`${cached}.json`, 'utf8'));
146
+ // Replacing a vendored bundle means dropping its file, not shipping both under one name.
147
+ const previous = files.findIndex((file) => path.basename(file) === stdlibBundleFileName(namespace));
148
+ if (previous !== -1)
149
+ files.splice(previous, 1);
150
+ files.push(cached);
151
+ namespaces[namespace] = {
152
+ dependencies: meta.dependencies,
153
+ exampleDependencies: meta.exampleDependencies,
154
+ };
155
+ }
156
+ return {
157
+ files,
158
+ manifest: {
159
+ revision: digest(vendored.upstream?.commit ?? 'unknown', ...stamps.sort()),
160
+ namespaces,
161
+ },
162
+ };
163
+ }
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Turns a `plantuml-stdlib` namespace directory into the JavaScript bundle that
3
+ * `@plantuml/core` resolves `!include <namespace/file>` against.
4
+ *
5
+ * The engine reads three globals, all keyed by a *lower-cased* namespace and a *lower-cased*
6
+ * path with the extension stripped:
7
+ *
8
+ * ```js
9
+ * window.PLANTUML_STDLIB['c4']['c4_container'] = ['line', 'line', ...]
10
+ * window.PLANTUML_STDLIB_JSON['awslib10']['awslib10'] = { ... }
11
+ * window.PLANTUML_STDLIB_INFO['c4'] = { name: 'C4', license: 'MIT', ... }
12
+ * ```
13
+ *
14
+ * That layout is not documented anywhere; it was read out of the installed `plantuml.js`
15
+ * (helpers `CCf`, `DJm` and `DID`) and reproduced from upstream's own generator,
16
+ * `src/main/java/com/plantuml/stdlibencoder/js/JsBuilder.java` in plantuml/plantuml-stdlib.
17
+ * See `docs/adr/0005-stdlib-bundles.md`.
18
+ *
19
+ * This port deviates from upstream in exactly one way, and deliberately: every entry is also
20
+ * registered under its name *with* the `.puml` (or `.json`) suffix. Upstream strips the
21
+ * suffix while the engine looks the name up as written, so `!include <C4/C4_Container.puml>`
22
+ * — a spelling that appears throughout C4-PlantUML's own documentation — cannot resolve
23
+ * against an upstream bundle. Both spellings share one array, so the alias costs no memory.
24
+ */
25
+ /** Bumped when the emitted bundle layout changes in a way a stale manifest cannot describe. */
26
+ export declare const STDLIB_BUNDLE_FORMAT = 1;
27
+ export interface StdlibNamespaceBundle {
28
+ /** Lower-cased namespace key, as the engine looks it up: `c4`, `awslib10`. */
29
+ namespace: string;
30
+ /** Directory name as it appears upstream, which is not always lower case: `C4`. */
31
+ directoryName: string;
32
+ /** The bundle source, ready to be written as `<namespace>.min.js`. */
33
+ script: string;
34
+ /** The namespace's README front matter: `display_name`, `version`, `license`, `source`. */
35
+ info: Record<string, string>;
36
+ /**
37
+ * Other namespaces this one includes from its *library* files. Loading the namespace
38
+ * without these leaves the engine unable to resolve those includes.
39
+ */
40
+ dependencies: string[];
41
+ /**
42
+ * Other namespaces included only from `_examples_/`. Kept apart because they are dead
43
+ * weight for everyone who includes the library itself: C4's examples pull in `office`,
44
+ * 1.4 MB that a C4 diagram has no use for.
45
+ */
46
+ exampleDependencies: string[];
47
+ /** Number of `.puml` and `.json` files registered. */
48
+ fileCount: number;
49
+ }
50
+ /** The file name a namespace's bundle is served under. The engine's own loader assumes it. */
51
+ export declare function stdlibBundleFileName(namespace: string): string;
52
+ /**
53
+ * Builds the bundle for one namespace directory of a `plantuml-stdlib` checkout.
54
+ *
55
+ * @param stdlibRoot the checkout's `stdlib/` directory
56
+ * @param directoryName the namespace directory inside it, spelled as it is on disk
57
+ */
58
+ export declare function buildStdlibNamespace(stdlibRoot: string, directoryName: string): StdlibNamespaceBundle;
59
+ /** Namespace directories of a `plantuml-stdlib` checkout, in on-disk spelling. */
60
+ export declare function listStdlibNamespaceDirectories(stdlibRoot: string): string[];
@@ -0,0 +1,218 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { findStdlibIncludes, STDLIB_EXAMPLES_DIRECTORY } from './stdlibShared.js';
4
+ /**
5
+ * Turns a `plantuml-stdlib` namespace directory into the JavaScript bundle that
6
+ * `@plantuml/core` resolves `!include <namespace/file>` against.
7
+ *
8
+ * The engine reads three globals, all keyed by a *lower-cased* namespace and a *lower-cased*
9
+ * path with the extension stripped:
10
+ *
11
+ * ```js
12
+ * window.PLANTUML_STDLIB['c4']['c4_container'] = ['line', 'line', ...]
13
+ * window.PLANTUML_STDLIB_JSON['awslib10']['awslib10'] = { ... }
14
+ * window.PLANTUML_STDLIB_INFO['c4'] = { name: 'C4', license: 'MIT', ... }
15
+ * ```
16
+ *
17
+ * That layout is not documented anywhere; it was read out of the installed `plantuml.js`
18
+ * (helpers `CCf`, `DJm` and `DID`) and reproduced from upstream's own generator,
19
+ * `src/main/java/com/plantuml/stdlibencoder/js/JsBuilder.java` in plantuml/plantuml-stdlib.
20
+ * See `docs/adr/0005-stdlib-bundles.md`.
21
+ *
22
+ * This port deviates from upstream in exactly one way, and deliberately: every entry is also
23
+ * registered under its name *with* the `.puml` (or `.json`) suffix. Upstream strips the
24
+ * suffix while the engine looks the name up as written, so `!include <C4/C4_Container.puml>`
25
+ * — a spelling that appears throughout C4-PlantUML's own documentation — cannot resolve
26
+ * against an upstream bundle. Both spellings share one array, so the alias costs no memory.
27
+ */
28
+ /** Bumped when the emitted bundle layout changes in a way a stale manifest cannot describe. */
29
+ export const STDLIB_BUNDLE_FORMAT = 1;
30
+ /**
31
+ * Upstream's rule: `_foo_` directories hold metadata and are skipped, except `_examples_`,
32
+ * which holds includable examples. Mirrored so a bundle carries the same files as the
33
+ * `.repx` the PlantUML jar would use.
34
+ */
35
+ function isSkippedDirectory(name) {
36
+ if (name === STDLIB_EXAMPLES_DIRECTORY)
37
+ return false;
38
+ return name.startsWith('_') && name.endsWith('_');
39
+ }
40
+ function collectFiles(directory, namespaceRoot) {
41
+ const entries = fs
42
+ .readdirSync(directory, { withFileTypes: true })
43
+ // Sorted so a regenerated bundle is byte-identical on every platform.
44
+ .sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
45
+ const files = [];
46
+ for (const entry of entries) {
47
+ const absolutePath = path.join(directory, entry.name);
48
+ if (entry.isDirectory()) {
49
+ if (!isSkippedDirectory(entry.name))
50
+ files.push(...collectFiles(absolutePath, namespaceRoot));
51
+ continue;
52
+ }
53
+ const extension = path.extname(entry.name).toLowerCase();
54
+ if (extension !== '.puml' && extension !== '.json')
55
+ continue;
56
+ const key = path.relative(namespaceRoot, absolutePath).split(path.sep).join('/').toLowerCase();
57
+ files.push({
58
+ key,
59
+ absolutePath,
60
+ extension,
61
+ fromExamples: key.startsWith(`${STDLIB_EXAMPLES_DIRECTORY}/`),
62
+ });
63
+ }
64
+ return files;
65
+ }
66
+ /**
67
+ * Reads the YAML front matter every stdlib namespace README carries.
68
+ *
69
+ * Deliberately not a YAML parser: upstream's own reader accepts flat `key: value` lines and
70
+ * nothing else, and matching it keeps the emitted `PLANTUML_STDLIB_INFO` identical.
71
+ */
72
+ function readNamespaceInfo(namespaceRoot) {
73
+ const readme = path.join(namespaceRoot, 'README.md');
74
+ if (!fs.existsSync(readme))
75
+ return {};
76
+ const lines = fs.readFileSync(readme, 'utf8').split(/\r\n|\r|\n/);
77
+ if (lines[0]?.trim() !== '---')
78
+ return {};
79
+ const info = {};
80
+ for (const line of lines.slice(1)) {
81
+ const trimmed = line.trim();
82
+ if (trimmed === '---')
83
+ break;
84
+ const colon = trimmed.indexOf(':');
85
+ if (colon === -1)
86
+ continue;
87
+ info[trimmed.slice(0, colon).trim()] = trimmed.slice(colon + 1).trim();
88
+ }
89
+ return info;
90
+ }
91
+ /**
92
+ * `JSON.stringify` output is valid JavaScript except for U+2028/U+2029, which are legal in
93
+ * JSON strings and were illegal in JavaScript ones before ES2019. Escaping them keeps the
94
+ * bundle parseable by anything that can run the site.
95
+ */
96
+ function toJsLiteral(value) {
97
+ return JSON.stringify(value)
98
+ .replace(/\u2028/g, '\\u2028')
99
+ .replace(/\u2029/g, '\\u2029');
100
+ }
101
+ /**
102
+ * Re-serializes a `.json` file so the bundle registers an object rather than a string.
103
+ *
104
+ * Parsing first is what makes that safe: a malformed file becomes a build error naming the
105
+ * file, instead of a syntax error inside a 500 KB generated bundle in a reader's browser.
106
+ */
107
+ function toJsonLiteral(text, file) {
108
+ try {
109
+ return toJsLiteral(JSON.parse(text));
110
+ }
111
+ catch (cause) {
112
+ throw new Error(`'${file ?? 'a standard library JSON file'}' is not valid JSON.`, { cause });
113
+ }
114
+ }
115
+ /**
116
+ * Unwraps a library file that is itself a complete `@startuml … @enduml` document.
117
+ *
118
+ * PlantUML's file-based `!include` takes the *contents* of such a file rather than the
119
+ * document markers, but the standard library lookup this bundle feeds does not: the markers
120
+ * come through verbatim and the diagram fails on a nested `@startuml`. That is not a corner
121
+ * case — 81 of `cloudinsight`'s 83 sprite files are written this way, which would leave the
122
+ * whole namespace unusable.
123
+ *
124
+ * Only a single-block file is unwrapped. A file with more than one `@startuml` is left alone,
125
+ * because there the markers are what separates one block from the next.
126
+ *
127
+ * (`!include <ns/file!0>`, which selects a block by index, does not work through the standard
128
+ * library lookup either way — verified against the engine — so nothing is lost by unwrapping.)
129
+ */
130
+ function unwrapDocument(lines) {
131
+ if (lines.filter((line) => /^@startuml\b/.test(line.trim())).length !== 1)
132
+ return lines;
133
+ let first = 0;
134
+ while (first < lines.length && lines[first]?.trim() === '')
135
+ first += 1;
136
+ if (!/^@startuml\b/.test(lines[first]?.trim() ?? ''))
137
+ return lines;
138
+ let last = lines.length - 1;
139
+ while (last > first && lines[last]?.trim() === '')
140
+ last -= 1;
141
+ if (lines[last]?.trim() !== '@enduml')
142
+ return lines;
143
+ return lines.slice(first + 1, last);
144
+ }
145
+ /** Strips the extension the way the engine does before looking a name up. */
146
+ function withoutExtension(key) {
147
+ return key.replace(/\.(puml|json)$/, '');
148
+ }
149
+ /** The file name a namespace's bundle is served under. The engine's own loader assumes it. */
150
+ export function stdlibBundleFileName(namespace) {
151
+ return `${namespace.toLowerCase()}.min.js`;
152
+ }
153
+ /**
154
+ * Builds the bundle for one namespace directory of a `plantuml-stdlib` checkout.
155
+ *
156
+ * @param stdlibRoot the checkout's `stdlib/` directory
157
+ * @param directoryName the namespace directory inside it, spelled as it is on disk
158
+ */
159
+ export function buildStdlibNamespace(stdlibRoot, directoryName) {
160
+ const namespaceRoot = path.join(stdlibRoot, directoryName);
161
+ if (!fs.existsSync(namespaceRoot) || !fs.statSync(namespaceRoot).isDirectory()) {
162
+ throw new Error(`'${namespaceRoot}' is not a plantuml-stdlib namespace directory.`);
163
+ }
164
+ const namespace = directoryName.toLowerCase();
165
+ const info = readNamespaceInfo(namespaceRoot);
166
+ const files = collectFiles(namespaceRoot, namespaceRoot);
167
+ const dependencies = new Set();
168
+ const exampleDependencies = new Set();
169
+ const quoted = toJsLiteral(namespace);
170
+ const lines = [
171
+ `// ${namespace}.min.js - generated from plantuml-stdlib by @matfsw/docusaurus-plantuml-plugin.`,
172
+ '(function () {',
173
+ ' var w = window;',
174
+ ' w.PLANTUML_STDLIB = w.PLANTUML_STDLIB || {};',
175
+ ' w.PLANTUML_STDLIB_JSON = w.PLANTUML_STDLIB_JSON || {};',
176
+ ' w.PLANTUML_STDLIB_INFO = w.PLANTUML_STDLIB_INFO || {};',
177
+ ` w.PLANTUML_STDLIB_INFO[${quoted}] = ${toJsLiteral(info)};`,
178
+ ` var P = (w.PLANTUML_STDLIB[${quoted}] = w.PLANTUML_STDLIB[${quoted}] || {});`,
179
+ ` var J = (w.PLANTUML_STDLIB_JSON[${quoted}] = w.PLANTUML_STDLIB_JSON[${quoted}] || {});`,
180
+ ];
181
+ for (const file of files) {
182
+ const text = fs.readFileSync(file.absolutePath, 'utf8');
183
+ const bare = withoutExtension(file.key);
184
+ const target = file.extension === '.puml' ? 'P' : 'J';
185
+ // `.puml` becomes an array of lines and `.json` an object: the engine reads the two
186
+ // globals differently, joining the first with newlines and indexing into the second.
187
+ const value = file.extension === '.puml'
188
+ ? toJsLiteral(unwrapDocument(text.split(/\r\n|\r|\n/)))
189
+ : toJsonLiteral(text, file.absolutePath);
190
+ lines.push(` ${target}[${toJsLiteral(bare)}] = ${target}[${toJsLiteral(file.key)}] = ${value};`);
191
+ if (file.extension !== '.puml')
192
+ continue;
193
+ for (const reference of findStdlibIncludes(text)) {
194
+ if (reference.namespace === namespace)
195
+ continue;
196
+ (file.fromExamples ? exampleDependencies : dependencies).add(reference.namespace);
197
+ }
198
+ }
199
+ lines.push('})();', '');
200
+ return {
201
+ namespace,
202
+ directoryName,
203
+ script: lines.join('\n'),
204
+ info,
205
+ dependencies: [...dependencies].sort(),
206
+ // An example-only dependency that the library already needs is not worth recording twice.
207
+ exampleDependencies: [...exampleDependencies].filter((name) => !dependencies.has(name)).sort(),
208
+ fileCount: files.length,
209
+ };
210
+ }
211
+ /** Namespace directories of a `plantuml-stdlib` checkout, in on-disk spelling. */
212
+ export function listStdlibNamespaceDirectories(stdlibRoot) {
213
+ return fs
214
+ .readdirSync(stdlibRoot, { withFileTypes: true })
215
+ .filter((entry) => entry.isDirectory() && !entry.name.startsWith('.'))
216
+ .map((entry) => entry.name)
217
+ .sort();
218
+ }
@@ -0,0 +1,51 @@
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
+ /** One namespace as the browser needs to know about it. Kept small: this ships in global data. */
10
+ export interface StdlibNamespaceEntry {
11
+ /** Namespaces the engine will need as soon as this one is included. */
12
+ dependencies: string[];
13
+ /** Namespaces needed only by this one's `_examples_/` files. */
14
+ exampleDependencies: string[];
15
+ }
16
+ export interface StdlibRuntimeManifest {
17
+ /**
18
+ * Changes whenever the emitted bundles change. Part of the asset directory name, so a
19
+ * refreshed standard library can never be served from a stale HTTP cache, and part of the
20
+ * render cache key, so it can never be served from a stale SVG either.
21
+ */
22
+ revision: string;
23
+ namespaces: Record<string, StdlibNamespaceEntry>;
24
+ }
25
+ /** Directory holding a namespace's includable examples, which upstream keeps in the bundle. */
26
+ export declare const STDLIB_EXAMPLES_DIRECTORY = "_examples_";
27
+ /**
28
+ * `<namespace/path>` in an `!include` and its variants.
29
+ *
30
+ * Anchored to the start of a line because `!include` is only a directive there. Applied to
31
+ * stdlib sources when bundling and to author sources when rendering — one syntax, one place
32
+ * to correct it.
33
+ */
34
+ export declare const STDLIB_INCLUDE_PATTERN: RegExp;
35
+ export interface StdlibIncludeReference {
36
+ /** Lower-cased namespace. */
37
+ namespace: string;
38
+ /** The rest of the path, lower-cased, as written minus the namespace. */
39
+ target: string;
40
+ /** Whether the target lives under `_examples_/`. */
41
+ fromExamples: boolean;
42
+ }
43
+ /**
44
+ * Extracts the `<namespace/…>` includes of a PlantUML source.
45
+ *
46
+ * Includes whose path is computed (`!include <material2.1.19/$icon>`, as DomainStory does
47
+ * inside an `!if`) are skipped: the namespace is knowable but the file is not, so treating it
48
+ * as a hard dependency would make every DomainStory diagram drag in 6.8 MB of icons for a
49
+ * branch it will probably not take.
50
+ */
51
+ export declare function findStdlibIncludes(source: string): StdlibIncludeReference[];