@matfsw/docusaurus-plantuml-plugin 1.2.0 → 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.
- package/CHANGELOG.md +65 -1
- package/README.md +144 -18
- package/assets/stdlib/LICENSES.md +21 -0
- package/assets/stdlib/archimate.min.js +22 -0
- package/assets/stdlib/azure.min.js +305 -0
- package/assets/stdlib/c4.min.js +47 -0
- package/assets/stdlib/cloudinsight.min.js +93 -0
- package/assets/stdlib/eip.min.js +14 -0
- package/assets/stdlib/k8s.min.js +52 -0
- package/assets/stdlib/kubernetes.min.js +17 -0
- package/assets/stdlib/manifest.json +112 -0
- package/assets/stdlib/office.min.js +557 -0
- package/dist/index.cjs +380 -7
- package/dist/index.d.cts +86 -3
- package/dist/index.d.ts +12 -2
- package/dist/index.js +20 -1
- package/dist/options.d.ts +52 -1
- package/dist/options.js +81 -0
- package/dist/runtime/assetLoader.d.ts +13 -1
- package/dist/runtime/assetLoader.js +36 -13
- package/dist/runtime/cache.d.ts +6 -1
- package/dist/runtime/cache.js +2 -2
- package/dist/runtime/errors.d.ts +3 -1
- package/dist/runtime/errors.js +27 -1
- package/dist/runtime/renderer.d.ts +3 -0
- package/dist/runtime/renderer.js +21 -7
- package/dist/runtime/stdlibLoader.d.ts +31 -0
- package/dist/runtime/stdlibLoader.js +99 -0
- package/dist/stdlib.d.ts +35 -0
- package/dist/stdlib.js +163 -0
- package/dist/stdlibBundle.d.ts +60 -0
- package/dist/stdlibBundle.js +218 -0
- package/dist/stdlibShared.d.ts +51 -0
- package/dist/stdlibShared.js +54 -0
- package/dist/theme/PlantUmlDiagram/index.js +2 -1
- package/dist/theme/usePlantUmlConfig.d.ts +8 -0
- package/dist/theme/usePlantUmlConfig.js +14 -8
- package/package.json +3 -1
package/dist/stdlib.d.ts
ADDED
|
@@ -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[];
|
|
@@ -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
|
+
}
|
|
@@ -170,7 +170,7 @@ export default function PlantUmlDiagram({ source, title, language, engine = 'pla
|
|
|
170
170
|
transparentBackground: options.graphviz.transparentBackground,
|
|
171
171
|
maxSourceBytes: options.graphviz.maxSourceBytes,
|
|
172
172
|
})
|
|
173
|
-
: renderDiagram({ ...shared, dark: renderDark });
|
|
173
|
+
: renderDiagram({ ...shared, dark: renderDark, stdlib: config.stdlib });
|
|
174
174
|
void pending
|
|
175
175
|
.then((svg) => commit({ status: 'ready', svg, error: null }))
|
|
176
176
|
.catch((error) => {
|
|
@@ -190,6 +190,7 @@ export default function PlantUmlDiagram({ source, title, language, engine = 'pla
|
|
|
190
190
|
config,
|
|
191
191
|
config?.assetsBaseUrl,
|
|
192
192
|
config?.coreVersion,
|
|
193
|
+
config?.stdlib,
|
|
193
194
|
config?.options.sanitizeSvg,
|
|
194
195
|
config?.options.renderTimeoutMs,
|
|
195
196
|
config?.options.cache,
|
|
@@ -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:
|
|
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
|
|
40
|
+
* Fills in the nested option groups when they are missing from global data.
|
|
39
41
|
*
|
|
40
|
-
* `resolveOptions` always produces
|
|
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
|
|
45
|
-
if (options.graphviz)
|
|
46
|
+
function withDefaults(options) {
|
|
47
|
+
if (options.graphviz && options.stdlib)
|
|
46
48
|
return options;
|
|
47
|
-
return {
|
|
49
|
+
return {
|
|
50
|
+
...options,
|
|
51
|
+
graphviz: options.graphviz ?? DEFAULT_GRAPHVIZ_OPTIONS,
|
|
52
|
+
stdlib: options.stdlib ?? DEFAULT_STDLIB_OPTIONS,
|
|
53
|
+
};
|
|
48
54
|
}
|