@matfsw/docusaurus-plantuml-plugin 1.7.1 → 1.8.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 +61 -1
- package/README.md +99 -66
- package/dist/assets.js +38 -1
- package/dist/constants.d.ts +8 -13
- package/dist/constants.js +9 -14
- package/dist/index.cjs +68 -92
- package/dist/index.d.cts +14 -0
- package/dist/index.js +2 -13
- package/dist/options.d.ts +14 -0
- package/dist/options.js +21 -0
- package/dist/runtime/cache.d.ts +9 -1
- package/dist/runtime/cache.js +3 -2
- package/dist/runtime/renderer.d.ts +2 -0
- package/dist/runtime/renderer.js +9 -4
- package/dist/runtime/types.d.ts +12 -3
- package/dist/theme/PlantUmlDiagram/index.js +7 -1
- package/dist/theme/PlantUmlDiagram/routeMatching.d.ts +42 -0
- package/dist/theme/PlantUmlDiagram/routeMatching.js +38 -0
- package/dist/theme/PlantUmlDiagram/useDiagramLinks.d.ts +5 -2
- package/dist/theme/PlantUmlDiagram/useDiagramLinks.js +6 -0
- package/package.json +4 -4
- package/dist/enginePatch.d.ts +0 -41
- package/dist/enginePatch.js +0 -120
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether a path belongs to the Docusaurus build that is currently running.
|
|
3
|
+
*
|
|
4
|
+
* In-diagram links are navigated through the router, which is only correct for a path this
|
|
5
|
+
* build actually routes. A site published as **several** builds — one per product area, a
|
|
6
|
+
* docs build beside a marketing build, two builds stitched together by a reverse proxy —
|
|
7
|
+
* shares an origin across bundles that each route only their own subtree. Pushing a sibling
|
|
8
|
+
* build's path into this build's router matches no route, renders this build's own "Page not
|
|
9
|
+
* found", and never reaches the server that could have served it. Reloading that URL works,
|
|
10
|
+
* which is the tell.
|
|
11
|
+
*
|
|
12
|
+
* So the rule is: push what this build routes, and hand everything else to the browser.
|
|
13
|
+
*/
|
|
14
|
+
/**
|
|
15
|
+
* One entry of the generated route table, narrowed to what matching needs.
|
|
16
|
+
*
|
|
17
|
+
* `path` widens to an array because react-router allows one route to answer to several
|
|
18
|
+
* paths. Docusaurus' own codegen never emits that, but the generated table is typed as
|
|
19
|
+
* react-router's `RouteConfig`, and quietly ignoring the array form would silently treat
|
|
20
|
+
* such a route as unrouted.
|
|
21
|
+
*/
|
|
22
|
+
export interface RouteLike {
|
|
23
|
+
path?: string | string[];
|
|
24
|
+
exact?: boolean;
|
|
25
|
+
routes?: RouteLike[];
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Whether `pathname` matches any real route of `routes`.
|
|
29
|
+
*
|
|
30
|
+
* A flat walk rather than `react-router-config`'s `matchRoutes`: it gives the same answer for
|
|
31
|
+
* every shape a Docusaurus table takes — verified against a generated `routes.js`, including
|
|
32
|
+
* nested docs trees, non-exact parents and segment-boundary near-misses like `/docsomething` —
|
|
33
|
+
* and it needs only `matchPath`, which `@docusaurus/router` exports for theme code. Reaching
|
|
34
|
+
* past that export into `react-router-config` would mean depending on a transitive package of
|
|
35
|
+
* `@docusaurus/core` that a strict installer need not hoist.
|
|
36
|
+
*
|
|
37
|
+
* Non-exact parent routes match their whole subtree, so an unknown page *inside* this build's
|
|
38
|
+
* own docs tree still counts as routed. That is deliberate: this build owns that prefix, and
|
|
39
|
+
* its own `NotFound` is the right answer there. Only a path no route claims at all is handed
|
|
40
|
+
* back to the browser.
|
|
41
|
+
*/
|
|
42
|
+
export declare function isRoutedPathname(routes: readonly RouteLike[], pathname: string): boolean;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { matchPath } from '@docusaurus/router';
|
|
2
|
+
/**
|
|
3
|
+
* The catch-all every generated route table ends with.
|
|
4
|
+
*
|
|
5
|
+
* `codegenRoutes` appends `{path: '*', component: ComponentCreator('*')}` to every build,
|
|
6
|
+
* and that route is what renders `NotFound`. It therefore matches *every* path, which would
|
|
7
|
+
* make the check below vacuous — excluding it is the whole point.
|
|
8
|
+
*/
|
|
9
|
+
const CATCH_ALL_PATH = '*';
|
|
10
|
+
/** Depth-first flattening; nested routes are how Docusaurus expresses a docs plugin's tree. */
|
|
11
|
+
function flattenRoutes(routes) {
|
|
12
|
+
return routes.flatMap((route) => [route, ...flattenRoutes(route.routes ?? [])]);
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Whether `pathname` matches any real route of `routes`.
|
|
16
|
+
*
|
|
17
|
+
* A flat walk rather than `react-router-config`'s `matchRoutes`: it gives the same answer for
|
|
18
|
+
* every shape a Docusaurus table takes — verified against a generated `routes.js`, including
|
|
19
|
+
* nested docs trees, non-exact parents and segment-boundary near-misses like `/docsomething` —
|
|
20
|
+
* and it needs only `matchPath`, which `@docusaurus/router` exports for theme code. Reaching
|
|
21
|
+
* past that export into `react-router-config` would mean depending on a transitive package of
|
|
22
|
+
* `@docusaurus/core` that a strict installer need not hoist.
|
|
23
|
+
*
|
|
24
|
+
* Non-exact parent routes match their whole subtree, so an unknown page *inside* this build's
|
|
25
|
+
* own docs tree still counts as routed. That is deliberate: this build owns that prefix, and
|
|
26
|
+
* its own `NotFound` is the right answer there. Only a path no route claims at all is handed
|
|
27
|
+
* back to the browser.
|
|
28
|
+
*/
|
|
29
|
+
export function isRoutedPathname(routes, pathname) {
|
|
30
|
+
return flattenRoutes(routes).some((route) => {
|
|
31
|
+
if (route.path === undefined)
|
|
32
|
+
return false;
|
|
33
|
+
const paths = (Array.isArray(route.path) ? route.path : [route.path]).filter((path) => path !== CATCH_ALL_PATH);
|
|
34
|
+
if (paths.length === 0)
|
|
35
|
+
return false;
|
|
36
|
+
return matchPath(pathname, { path: paths, exact: route.exact === true }) !== null;
|
|
37
|
+
});
|
|
38
|
+
}
|
|
@@ -8,13 +8,16 @@ import type { DiagramEngine } from '../../runtime/types.js';
|
|
|
8
8
|
* `diagramLinks.ts` for the correlation. Graphviz needs none of this; `URL=` already emits
|
|
9
9
|
* real anchors.
|
|
10
10
|
*
|
|
11
|
-
* **Navigation** (both engines): a click on any in-diagram link to a
|
|
12
|
-
* through the router instead of a full page load, so a node can link to a diagram on
|
|
11
|
+
* **Navigation** (both engines): a click on any in-diagram link to a URL *this build routes*
|
|
12
|
+
* goes through the router instead of a full page load, so a node can link to a diagram on
|
|
13
13
|
* another page and arrive instantly — with the target page's diagrams reacting to the
|
|
14
14
|
* hash through the same router-driven deep-link tracking. Site-absolute paths get the
|
|
15
15
|
* site's `baseUrl`, exactly as markdown links do, so authors write the same `/docs/…`
|
|
16
16
|
* everywhere. External links and pure `#…` anchors stay native: the browser handles both
|
|
17
17
|
* correctly on its own, and the router observes hash navigations anyway.
|
|
18
|
+
*
|
|
19
|
+
* A same-origin path that this build does *not* route stays native too. See
|
|
20
|
+
* `routeMatching.ts` for why that matters on a site published as several builds.
|
|
18
21
|
*/
|
|
19
22
|
export interface UseDiagramLinksParams {
|
|
20
23
|
/** Whether the diagram is rendered and its SVG is in the DOM. */
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { useEffect } from 'react';
|
|
2
|
+
import generatedRoutes from '@generated/routes';
|
|
2
3
|
import { useHistory } from '@docusaurus/router';
|
|
3
4
|
import { useBaseUrlUtils } from '@docusaurus/useBaseUrl';
|
|
4
5
|
import { attachDiagramLinks, detachDiagramLinks, extractSourceLinks } from './diagramLinks.js';
|
|
6
|
+
import { isRoutedPathname } from './routeMatching.js';
|
|
5
7
|
export function useDiagramLinks({ ready, svg, engine, source, containerRef, }) {
|
|
6
8
|
const history = useHistory();
|
|
7
9
|
const { withBaseUrl } = useBaseUrlUtils();
|
|
@@ -53,6 +55,10 @@ export function useDiagramLinks({ ready, svg, engine, source, containerRef, }) {
|
|
|
53
55
|
}
|
|
54
56
|
if (url.origin !== window.location.origin)
|
|
55
57
|
return;
|
|
58
|
+
// Same origin is not the same bundle. A path this build has no route for must reach
|
|
59
|
+
// the server, which can serve it, rather than this build's own "Page not found".
|
|
60
|
+
if (!isRoutedPathname(generatedRoutes, url.pathname))
|
|
61
|
+
return;
|
|
56
62
|
event.preventDefault();
|
|
57
63
|
history.push(url.pathname + url.search + url.hash);
|
|
58
64
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@matfsw/docusaurus-plantuml-plugin",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.8.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",
|
|
@@ -71,7 +71,7 @@
|
|
|
71
71
|
"prepublishOnly": "node scripts/assert-built.mjs"
|
|
72
72
|
},
|
|
73
73
|
"dependencies": {
|
|
74
|
-
"@plantuml/core": "^1.2026.
|
|
74
|
+
"@plantuml/core": "^1.2026.8",
|
|
75
75
|
"copy-webpack-plugin": "^14.0.0",
|
|
76
76
|
"dompurify": "^3.2.7"
|
|
77
77
|
},
|
|
@@ -96,7 +96,7 @@
|
|
|
96
96
|
"@types/node": "^26.2.0",
|
|
97
97
|
"@types/react": "^19.2.18",
|
|
98
98
|
"@types/react-dom": "^19.2.4",
|
|
99
|
-
"@vitest/coverage-v8": "^
|
|
99
|
+
"@vitest/coverage-v8": "^5.0.0",
|
|
100
100
|
"eslint": "^9.39.5",
|
|
101
101
|
"eslint-config-prettier": "^10.1.8",
|
|
102
102
|
"eslint-plugin-react": "^7.37.5",
|
|
@@ -109,7 +109,7 @@
|
|
|
109
109
|
"tsup": "^8.5.0",
|
|
110
110
|
"typescript": "~5.9.3",
|
|
111
111
|
"typescript-eslint": "^8.67.0",
|
|
112
|
-
"vitest": "^
|
|
112
|
+
"vitest": "^5.0.0"
|
|
113
113
|
},
|
|
114
114
|
"overrides": {
|
|
115
115
|
"serialize-javascript": "^7.1.1",
|
package/dist/enginePatch.d.ts
DELETED
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* How many bytes the patch adds. Every replacement is ASCII, so the byte delta is the
|
|
3
|
-
* character delta, and it is fixed for a given {@link MAX_DIAGRAM_SIZE}. This is what makes
|
|
4
|
-
* an already-generated file cheap to validate: its size is an exact fingerprint.
|
|
5
|
-
*/
|
|
6
|
-
export declare const PATCH_SIZE_DELTA: number;
|
|
7
|
-
/**
|
|
8
|
-
* Rewrites the engine source so it accepts diagrams up to {@link MAX_DIAGRAM_SIZE} points.
|
|
9
|
-
*
|
|
10
|
-
* Both literals are counted *before* anything is replaced, so an engine that changed shape
|
|
11
|
-
* fails the build with a name attached rather than being silently half-patched. Patching the
|
|
12
|
-
* message as well as the comparison is not cosmetic — an engine that still reported
|
|
13
|
-
* `(max 4096)` while refusing at 32768 would send every future reader of that error down the
|
|
14
|
-
* wrong path. It is also safe: TeaVM's string pool is a plain list of JavaScript string
|
|
15
|
-
* literals, so a literal that changes length does not disturb its neighbours.
|
|
16
|
-
*/
|
|
17
|
-
export declare function patchEngineSource(source: string, coreVersion: string): string;
|
|
18
|
-
export interface ResolvePatchedEngineOptions {
|
|
19
|
-
/** Absolute path of the vendored `plantuml.js` inside the installed `@plantuml/core`. */
|
|
20
|
-
vendoredPath: string;
|
|
21
|
-
/** Installed `@plantuml/core` version, which names the cache directory. */
|
|
22
|
-
coreVersion: string;
|
|
23
|
-
/** Site directory, used when `cacheDir` is absent. */
|
|
24
|
-
siteDir: string;
|
|
25
|
-
/**
|
|
26
|
-
* Where the patched engine is cached. Docusaurus passes its `generatedFilesDir`; when it
|
|
27
|
-
* is absent the site's `.docusaurus` directory is assumed, as in `src/stdlib.ts`.
|
|
28
|
-
*/
|
|
29
|
-
cacheDir?: string;
|
|
30
|
-
}
|
|
31
|
-
/**
|
|
32
|
-
* Returns the path of the `plantuml.js` this build should serve, generating it if needed.
|
|
33
|
-
*
|
|
34
|
-
* The basename stays `plantuml.js`: the copy pattern emits `[name][ext]`, and the browser
|
|
35
|
-
* loader joins {@link PLANTUML_MODULE_FILENAME} onto the assets directory.
|
|
36
|
-
*
|
|
37
|
-
* Generation is skipped when the cached file is already the right size. That check is exact
|
|
38
|
-
* rather than approximate — {@link PATCH_SIZE_DELTA} is fixed — and it is O(1) where reading
|
|
39
|
-
* 7 MB back to inspect its contents would not be.
|
|
40
|
-
*/
|
|
41
|
-
export declare function resolvePatchedEngine(options: ResolvePatchedEngineOptions): string;
|
package/dist/enginePatch.js
DELETED
|
@@ -1,120 +0,0 @@
|
|
|
1
|
-
import fs from 'node:fs';
|
|
2
|
-
import path from 'node:path';
|
|
3
|
-
import { MAX_DIAGRAM_SIZE, PLANTUML_MODULE_FILENAME, PLUGIN_NAME, UPSTREAM_MAX_DIAGRAM_SIZE, } from './constants.js';
|
|
4
|
-
/**
|
|
5
|
-
* Raises the diagram size ceiling compiled into `@plantuml/core`.
|
|
6
|
-
*
|
|
7
|
-
* The engine measures the laid-out diagram and refuses to serialize anything wider or taller
|
|
8
|
-
* than 4096 points, reporting `Diagram too large for browser rendering: 78x12916 (max 4096)`
|
|
9
|
-
* through its error callback. `renderToString` takes only `{dark}`, so there is no option to
|
|
10
|
-
* turn this off, and no way to work around it from diagram source: the check runs *before* a
|
|
11
|
-
* `scale` directive is applied, `skinparam dpi` does not move it, and `left to right
|
|
12
|
-
* direction` only transposes the two numbers. Shrinking the Graphviz layout by hand —
|
|
13
|
-
* `skinparam ranksep`, fewer nodes, smaller fonts — is the only lever an author has.
|
|
14
|
-
*
|
|
15
|
-
* The ceiling is arbitrary. It is not an SVG or a browser constraint; patched to 65536 the
|
|
16
|
-
* same engine produced correct SVG for a 79x43157 diagram. So this plugin patches it, and
|
|
17
|
-
* serves the patched file rather than the vendored one.
|
|
18
|
-
*
|
|
19
|
-
* See `docs/adr/0007-engine-size-ceiling-patch.md` for the evidence and the rejected
|
|
20
|
-
* alternatives.
|
|
21
|
-
*/
|
|
22
|
-
/**
|
|
23
|
-
* The comparison the engine guards on, as it appears in the minified bundle:
|
|
24
|
-
*
|
|
25
|
-
* ```js
|
|
26
|
-
* p=o.bBY;if(!(p>4096.0)){q=o.bB0;if(!(q>4096.0)){ …serialize… }}
|
|
27
|
-
* ```
|
|
28
|
-
*
|
|
29
|
-
* Deliberately anchored on `>4096.0)` rather than on the surrounding code: `p`, `q` and `o`
|
|
30
|
-
* are TeaVM-mangled locals that are renamed by every `@plantuml/core` build, so a pattern
|
|
31
|
-
* that named them would break on a release that changed nothing meaningful.
|
|
32
|
-
*/
|
|
33
|
-
const COMPARISON = `>${UPSTREAM_MAX_DIAGRAM_SIZE}.0)`;
|
|
34
|
-
/**
|
|
35
|
-
* `4096.0` occurs exactly twice in the whole 7 MB bundle, and both occurrences are the width
|
|
36
|
-
* and height halves of this one guard. That makes the count the safety property: two matches
|
|
37
|
-
* means the engine still has the shape this patch was written against, and any other number
|
|
38
|
-
* means it does not.
|
|
39
|
-
*/
|
|
40
|
-
const COMPARISON_OCCURRENCES = 2;
|
|
41
|
-
/** The suffix of the engine's own error message, in TeaVM's string pool. */
|
|
42
|
-
const MESSAGE_SUFFIX = ` (max ${UPSTREAM_MAX_DIAGRAM_SIZE})`;
|
|
43
|
-
const MESSAGE_SUFFIX_OCCURRENCES = 1;
|
|
44
|
-
const PATCHED_COMPARISON = `>${MAX_DIAGRAM_SIZE}.0)`;
|
|
45
|
-
const PATCHED_MESSAGE_SUFFIX = ` (max ${MAX_DIAGRAM_SIZE})`;
|
|
46
|
-
/**
|
|
47
|
-
* How many bytes the patch adds. Every replacement is ASCII, so the byte delta is the
|
|
48
|
-
* character delta, and it is fixed for a given {@link MAX_DIAGRAM_SIZE}. This is what makes
|
|
49
|
-
* an already-generated file cheap to validate: its size is an exact fingerprint.
|
|
50
|
-
*/
|
|
51
|
-
export const PATCH_SIZE_DELTA = COMPARISON_OCCURRENCES * (PATCHED_COMPARISON.length - COMPARISON.length) +
|
|
52
|
-
MESSAGE_SUFFIX_OCCURRENCES * (PATCHED_MESSAGE_SUFFIX.length - MESSAGE_SUFFIX.length);
|
|
53
|
-
/** Directory under the Docusaurus cache that generated engine builds live in. */
|
|
54
|
-
const CACHE_SUBDIRECTORY = 'plantuml-engine';
|
|
55
|
-
function countOccurrences(haystack, needle) {
|
|
56
|
-
let count = 0;
|
|
57
|
-
let index = haystack.indexOf(needle);
|
|
58
|
-
while (index !== -1) {
|
|
59
|
-
count += 1;
|
|
60
|
-
index = haystack.indexOf(needle, index + needle.length);
|
|
61
|
-
}
|
|
62
|
-
return count;
|
|
63
|
-
}
|
|
64
|
-
function assertOccurrences(source, needle, expected, coreVersion) {
|
|
65
|
-
const found = countOccurrences(source, needle);
|
|
66
|
-
if (found === expected)
|
|
67
|
-
return;
|
|
68
|
-
throw new Error(`[${PLUGIN_NAME}] Cannot raise the diagram size ceiling of ` +
|
|
69
|
-
`'@plantuml/core@${coreVersion}': expected ${expected} occurrence(s) of ` +
|
|
70
|
-
`${JSON.stringify(needle)} in '${PLANTUML_MODULE_FILENAME}', found ${found}. ` +
|
|
71
|
-
'The engine no longer has the shape this plugin patches, which means the installed ' +
|
|
72
|
-
'@plantuml/core is not one this plugin supports. Pin @plantuml/core to the last ' +
|
|
73
|
-
'version that worked, or upgrade this plugin.');
|
|
74
|
-
}
|
|
75
|
-
/**
|
|
76
|
-
* Rewrites the engine source so it accepts diagrams up to {@link MAX_DIAGRAM_SIZE} points.
|
|
77
|
-
*
|
|
78
|
-
* Both literals are counted *before* anything is replaced, so an engine that changed shape
|
|
79
|
-
* fails the build with a name attached rather than being silently half-patched. Patching the
|
|
80
|
-
* message as well as the comparison is not cosmetic — an engine that still reported
|
|
81
|
-
* `(max 4096)` while refusing at 32768 would send every future reader of that error down the
|
|
82
|
-
* wrong path. It is also safe: TeaVM's string pool is a plain list of JavaScript string
|
|
83
|
-
* literals, so a literal that changes length does not disturb its neighbours.
|
|
84
|
-
*/
|
|
85
|
-
export function patchEngineSource(source, coreVersion) {
|
|
86
|
-
assertOccurrences(source, COMPARISON, COMPARISON_OCCURRENCES, coreVersion);
|
|
87
|
-
assertOccurrences(source, MESSAGE_SUFFIX, MESSAGE_SUFFIX_OCCURRENCES, coreVersion);
|
|
88
|
-
return source
|
|
89
|
-
.split(COMPARISON)
|
|
90
|
-
.join(PATCHED_COMPARISON)
|
|
91
|
-
.split(MESSAGE_SUFFIX)
|
|
92
|
-
.join(PATCHED_MESSAGE_SUFFIX);
|
|
93
|
-
}
|
|
94
|
-
/**
|
|
95
|
-
* Returns the path of the `plantuml.js` this build should serve, generating it if needed.
|
|
96
|
-
*
|
|
97
|
-
* The basename stays `plantuml.js`: the copy pattern emits `[name][ext]`, and the browser
|
|
98
|
-
* loader joins {@link PLANTUML_MODULE_FILENAME} onto the assets directory.
|
|
99
|
-
*
|
|
100
|
-
* Generation is skipped when the cached file is already the right size. That check is exact
|
|
101
|
-
* rather than approximate — {@link PATCH_SIZE_DELTA} is fixed — and it is O(1) where reading
|
|
102
|
-
* 7 MB back to inspect its contents would not be.
|
|
103
|
-
*/
|
|
104
|
-
export function resolvePatchedEngine(options) {
|
|
105
|
-
const { vendoredPath, coreVersion, siteDir, cacheDir } = options;
|
|
106
|
-
const generatedDir = cacheDir ?? path.join(siteDir, '.docusaurus');
|
|
107
|
-
const target = path.join(generatedDir, CACHE_SUBDIRECTORY, coreVersion, PLANTUML_MODULE_FILENAME);
|
|
108
|
-
const expectedSize = fs.statSync(vendoredPath).size + PATCH_SIZE_DELTA;
|
|
109
|
-
if (fs.existsSync(target) && fs.statSync(target).size === expectedSize)
|
|
110
|
-
return target;
|
|
111
|
-
const patched = patchEngineSource(fs.readFileSync(vendoredPath, 'utf8'), coreVersion);
|
|
112
|
-
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
113
|
-
// `.docusaurus` is shared by every locale, and Docusaurus builds locales in separate
|
|
114
|
-
// processes. Writing 7 MB in place would let a concurrent build read a torn file — and a
|
|
115
|
-
// torn file of the right length would pass the size check above. Rename is atomic.
|
|
116
|
-
const temporary = `${target}.${process.pid}.tmp`;
|
|
117
|
-
fs.writeFileSync(temporary, patched, 'utf8');
|
|
118
|
-
fs.renameSync(temporary, target);
|
|
119
|
-
return target;
|
|
120
|
-
}
|