@yalehwang/plantuml-js 0.2.2 → 0.3.1
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/README.md +20 -0
- package/dist/core.d.ts +106 -13
- package/dist/core.js +92 -52
- package/dist/foundation/labels.d.ts +44 -0
- package/dist/foundation/labels.js +231 -0
- package/dist/foundation/layers.d.ts +25 -0
- package/dist/foundation/layers.js +101 -0
- package/dist/foundation/layout.d.ts +116 -0
- package/dist/foundation/layout.js +298 -0
- package/dist/{model.d.ts → foundation/model.d.ts} +20 -0
- package/dist/foundation/relation-semantics.d.ts +49 -0
- package/dist/foundation/relation-semantics.js +112 -0
- package/dist/{text-measure.d.ts → foundation/text-measure.d.ts} +2 -0
- package/dist/{text-measure.js → foundation/text-measure.js} +20 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/layout/compound.d.ts +74 -0
- package/dist/layout/compound.js +1204 -0
- package/dist/layout/elk-graph.d.ts +36 -0
- package/dist/layout/elk-graph.js +220 -0
- package/dist/layout/elk-result.d.ts +18 -0
- package/dist/layout/elk-result.js +127 -0
- package/dist/layout/elk.d.ts +80 -33
- package/dist/layout/elk.js +229 -320
- package/dist/layout/fanout-trunk.d.ts +18 -0
- package/dist/layout/fanout-trunk.js +380 -0
- package/dist/layout/grid-router.d.ts +69 -0
- package/dist/layout/grid-router.js +615 -0
- package/dist/layout/lane-spread.d.ts +11 -0
- package/dist/layout/lane-spread.js +302 -0
- package/dist/layout/package-endpoints.d.ts +10 -0
- package/dist/layout/package-endpoints.js +54 -0
- package/dist/layout/placement-strategy.d.ts +98 -0
- package/dist/layout/placement-strategy.js +26 -0
- package/dist/layout/strategy.d.ts +27 -0
- package/dist/layout/strategy.js +19 -0
- package/dist/layout/trunk-lanes.d.ts +29 -0
- package/dist/layout/trunk-lanes.js +308 -0
- package/dist/layout/viz.d.ts +3 -3
- package/dist/layout/viz.js +13 -17
- package/dist/metrics/layout-metrics.d.ts +183 -0
- package/dist/metrics/layout-metrics.js +766 -0
- package/dist/parse/parser.d.ts +43 -0
- package/dist/{parser.js → parse/parser.js} +111 -5
- package/dist/parse/serialize.d.ts +32 -0
- package/dist/parse/serialize.js +218 -0
- package/dist/render/legend.d.ts +15 -0
- package/dist/render/legend.js +42 -0
- package/dist/render/svg.d.ts +60 -3
- package/dist/render/svg.js +326 -98
- package/dist/render/theme.d.ts +109 -0
- package/dist/render/theme.js +99 -0
- package/dist/{transform.d.ts → transform/transform.d.ts} +55 -1
- package/dist/{transform.js → transform/transform.js} +277 -29
- package/package.json +4 -3
- package/dist/layout-metrics.d.ts +0 -42
- package/dist/layout-metrics.js +0 -241
- package/dist/layout.d.ts +0 -75
- package/dist/layout.js +0 -51
- package/dist/parser.d.ts +0 -17
- /package/dist/{edge-router.d.ts → foundation/edge-router.d.ts} +0 -0
- /package/dist/{edge-router.js → foundation/edge-router.js} +0 -0
- /package/dist/{model.js → foundation/model.js} +0 -0
- /package/dist/{render-dot.d.ts → layout/render-dot.d.ts} +0 -0
- /package/dist/{render-dot.js → layout/render-dot.js} +0 -0
package/README.md
CHANGED
|
@@ -53,12 +53,32 @@ console.log(svg);
|
|
|
53
53
|
|---|---|
|
|
54
54
|
| `createRenderer({ layout })` | Build a renderer. `layout` is `'elk'` (default), `'viz'` (experimental), or a custom `LayoutEngine`. |
|
|
55
55
|
| `renderer.render(puml)` | Parse + layout + emit SVG. Returns `{ svg, diagramType, warnings }`. Throws `RenderError` on structurally invalid puml. |
|
|
56
|
+
| `renderDetailed(pumlOrModel, options?)` | Run the same pipeline as `renderer.render` and return every intermediate: `{ model, layout, svg, layoutMs, hiddenTransitive }`. Accepts a puml string (throws `RenderError` on invalid input) or an already-parsed `PlantUmlModel`. `DetailedRender.layout` carries the **stable** node/package/edge rectangles — see [Stable layout geometry](#stable-layout-geometry-renderdetailed) — so tooling never has to re-read bounding boxes from the SVG. Exported from both `.` and `./elk`. |
|
|
56
57
|
| `parsePlantUml(puml)` | Parse a puml string into a `PlantUmlModel`. |
|
|
57
58
|
| `checkSyntax(puml)` | Validate without throwing. Returns `{ ok, errors, warnings }` of line-accurate `SyntaxDiagnostic`s. |
|
|
58
59
|
| `explain(puml)` | Explain each line. Returns `LineExplanation[]` (line, kind, explanation). |
|
|
59
60
|
| `renderDot(dot, engine)` | Render a raw DOT string to SVG via `@viz-js/viz`. `engine` is `'dot'` (default), `'neato'`, `'fdp'`, `'sfdp'`, `'circo'`, `'twopi'`, `'osage'`, or `'patchwork'`. |
|
|
60
61
|
| `ElkLayoutEngine` / `VizLayoutEngine` | The two built-in `LayoutEngine` implementations, exported for direct use. |
|
|
61
62
|
|
|
63
|
+
### Stable layout geometry (`renderDetailed`)
|
|
64
|
+
|
|
65
|
+
`DetailedRender.layout` is a **stable public contract**: the rectangle fields
|
|
66
|
+
describe exactly what the renderer drew, so a thumbnail, a search jump or a
|
|
67
|
+
selection box can use them directly instead of parsing the SVG. Coordinates are
|
|
68
|
+
relative to the diagram origin (the SVG `viewBox` offset).
|
|
69
|
+
|
|
70
|
+
| `layout` field | Shape | Corresponding SVG hook |
|
|
71
|
+
|---|---|---|
|
|
72
|
+
| `nodes[]` | `{ id, x, y, width, height }` | `<g class="uml-node" data-entity-id="{id}">` |
|
|
73
|
+
| `packages[]` (engines with container support, e.g. `'elk'`) | `{ id, x, y, width, height }` | `<g class="uml-package" data-package-id="{id}">`; grouped nodes also carry `data-package-id` |
|
|
74
|
+
| `edges[]` | `{ id, source, target, points }` | `<g class="uml-edge" data-edge-source="{source}" data-edge-target="{target}" data-edge-type="…">`; aggregated package corridors add `data-weight`, bundled arrowheads carry `data-edge-target` on `uml-bundle-head` |
|
|
75
|
+
| `width`, `height` | `number` | The `viewBox` extent |
|
|
76
|
+
|
|
77
|
+
`points` is the same polyline the SVG draws, so an edge hit-test can reuse it
|
|
78
|
+
without re-routing. `model` is the diagram actually drawn (after view,
|
|
79
|
+
transforms and detail), and `hiddenTransitive` counts the edges dropped by
|
|
80
|
+
transitive reduction.
|
|
81
|
+
|
|
62
82
|
### Error semantics
|
|
63
83
|
|
|
64
84
|
`createRenderer` fails closed on an unknown layout name (`'elk'`, `'viz'`, or a
|
package/dist/core.d.ts
CHANGED
|
@@ -6,14 +6,18 @@
|
|
|
6
6
|
* `src/index.ts` is the full build: it re-exports this core and adds the viz
|
|
7
7
|
* engine + `renderDot`.
|
|
8
8
|
*/
|
|
9
|
-
import { PlantUmlModel } from './model.js';
|
|
10
|
-
import { parsePlantUml } from './parser.js';
|
|
11
|
-
import { LayoutEngine } from './layout.js';
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
|
|
16
|
-
export type {
|
|
9
|
+
import { PlantUmlModel } from './foundation/model.js';
|
|
10
|
+
import { parsePlantUml } from './parse/parser.js';
|
|
11
|
+
import { LayoutEngine, LayoutResult, DetailLevel } from './foundation/layout.js';
|
|
12
|
+
import { ThemeOverrides } from './render/theme.js';
|
|
13
|
+
import { ElkLayoutEngine, type ElkLayoutOptions } from './layout/elk.js';
|
|
14
|
+
import { renderSvg, RenderSvgOptions, RenderEngine, RenderFrameOptions, SvgRenderer, SvgRendererOptions } from './render/svg.js';
|
|
15
|
+
import { ViewSpec, TransformOptions } from './transform/transform.js';
|
|
16
|
+
export type { PlantUmlModel, PlantUmlEntity, PlantUmlMember, PlantUmlRelation, PlantUmlPackage, EntityKind, RelationType, } from './foundation/model.js';
|
|
17
|
+
export { defaultTheme, resolveTheme } from './render/theme.js';
|
|
18
|
+
export type { Theme, ThemeOverrides } from './render/theme.js';
|
|
19
|
+
export type { DetailLevel } from './foundation/layout.js';
|
|
20
|
+
export type { LayoutEngine, LayoutResult, NodeLayout, EdgeLayout, Point, } from './foundation/layout.js';
|
|
17
21
|
export interface RendererOptions {
|
|
18
22
|
/**
|
|
19
23
|
* `'elk'` (default) | a custom `LayoutEngine` instance. `'viz'` is only
|
|
@@ -21,16 +25,80 @@ export interface RendererOptions {
|
|
|
21
25
|
* fails closed on it.
|
|
22
26
|
*/
|
|
23
27
|
layout?: 'elk' | 'viz' | LayoutEngine;
|
|
28
|
+
/** ELK tuning (e.g. `aspectRatio`); only applies to the `'elk'` engine, ignored by `'viz'` and custom engines. */
|
|
29
|
+
layoutOptions?: ElkLayoutOptions;
|
|
30
|
+
/**
|
|
31
|
+
* Custom `RenderEngine` instance. Default: the built-in [[SvgRenderer]],
|
|
32
|
+
* configured with `theme` and `legend`. When set, this engine owns the
|
|
33
|
+
* rendering configuration, so `theme`/`legend` are not applied to it.
|
|
34
|
+
*/
|
|
35
|
+
renderer?: RenderEngine;
|
|
24
36
|
/** Collapse/expand the diagram before layout (overview, single package, …). */
|
|
25
37
|
view?: ViewSpec;
|
|
26
38
|
/** Extra model transforms applied on top of the view (aggregation, cycles, …). */
|
|
27
39
|
transform?: TransformOptions;
|
|
40
|
+
/** Draw a legend of the visual encodings. Default: on for diagrams with more than 30 entities. */
|
|
41
|
+
legend?: boolean;
|
|
42
|
+
/** Visual theme tokens; unspecified tokens fall back to `defaultTheme`. */
|
|
43
|
+
theme?: ThemeOverrides;
|
|
44
|
+
/**
|
|
45
|
+
* Member detail: `'full'`, `'compact'` (first rows + count) or `'names'`.
|
|
46
|
+
* Default: chosen by entity count (≤40 full, ≤150 compact, else names).
|
|
47
|
+
*/
|
|
48
|
+
detail?: DetailLevel;
|
|
49
|
+
/**
|
|
50
|
+
* `'aggregate'` folds the relations between two packages into one weighted
|
|
51
|
+
* box-to-box corridor edge (originals kept in its `<title>`); `'raw'` draws
|
|
52
|
+
* every edge. Default: `'aggregate'` for diagrams with more than 150 entities.
|
|
53
|
+
*/
|
|
54
|
+
interPackage?: 'aggregate' | 'raw';
|
|
55
|
+
/**
|
|
56
|
+
* Transitive edge reduction (A→B→C hides A→C). `'auto'` (default) reduces only
|
|
57
|
+
* when every relation is a dependency/association and edges outnumber nodes
|
|
58
|
+
* more than 3:1; `'transitive'` always reduces; `'none'` draws every edge.
|
|
59
|
+
* Hidden edges are counted in the legend and in `data-hidden-transitive`.
|
|
60
|
+
*/
|
|
61
|
+
edgeReduction?: 'auto' | 'transitive' | 'none';
|
|
28
62
|
}
|
|
29
63
|
export interface RenderResult {
|
|
30
64
|
svg: string;
|
|
31
65
|
diagramType: string;
|
|
32
66
|
warnings: string[];
|
|
33
67
|
}
|
|
68
|
+
/**
|
|
69
|
+
* Everything {@link renderDetailed} computed for one diagram: the model that was
|
|
70
|
+
* actually laid out and drawn, the geometry the renderer used, the SVG it
|
|
71
|
+
* emitted, and the reduction bookkeeping.
|
|
72
|
+
*
|
|
73
|
+
* This is a **public, stable API** (available from both the `.` and `./elk`
|
|
74
|
+
* entries). Consumers such as an architecture tab should read geometry from
|
|
75
|
+
* `layout` instead of parsing `svg` back into bounding boxes. In particular the
|
|
76
|
+
* rectangle fields below are a stable contract:
|
|
77
|
+
*
|
|
78
|
+
* - `layout.nodes[i]` — `{ id, x, y, width, height }`; `id` is the same string
|
|
79
|
+
* the SVG puts on `<g class="uml-node" data-entity-id="…">`.
|
|
80
|
+
* - `layout.packages[i]` (present for engines with container support, e.g.
|
|
81
|
+
* `'elk'`) — `{ id, x, y, width, height }`; `id` matches
|
|
82
|
+
* `<g class="uml-package" data-package-id="…">`.
|
|
83
|
+
* - `layout.edges[i]` — `{ id, source, target, points }`; `source`/`target`
|
|
84
|
+
* match the SVG's `data-edge-source`/`data-edge-target` (aggregated package
|
|
85
|
+
* corridors are emitted as `data-edge-source`/`data-edge-target` pairs with
|
|
86
|
+
* `data-weight`), and `points` is the same polyline the SVG draws.
|
|
87
|
+
*
|
|
88
|
+
* `x`/`y` are relative to the diagram origin (the SVG's `viewBox` offset), so
|
|
89
|
+
* they can be translated into a thumbnail directly.
|
|
90
|
+
*/
|
|
91
|
+
export interface DetailedRender {
|
|
92
|
+
/** The model actually laid out and drawn (after view, transforms and detail). */
|
|
93
|
+
model: PlantUmlModel;
|
|
94
|
+
/** Geometry actually used by the renderer; its node/package/edge rectangles are a stable contract (see above). */
|
|
95
|
+
layout: LayoutResult;
|
|
96
|
+
svg: string;
|
|
97
|
+
/** Layout engine wall time in milliseconds (excludes transforms, SVG and any rasterisation). */
|
|
98
|
+
layoutMs: number;
|
|
99
|
+
/** Edges hidden by transitive reduction (0 when it did not apply). */
|
|
100
|
+
hiddenTransitive: number;
|
|
101
|
+
}
|
|
34
102
|
export interface Renderer {
|
|
35
103
|
render(puml: string): Promise<RenderResult>;
|
|
36
104
|
}
|
|
@@ -54,6 +122,23 @@ export declare class RenderError extends Error {
|
|
|
54
122
|
constructor(diagnostics: SyntaxDiagnostic[]);
|
|
55
123
|
}
|
|
56
124
|
export declare function createRenderer(options?: RendererOptions): Renderer;
|
|
125
|
+
/**
|
|
126
|
+
* Render one diagram through the exact pipeline `createRenderer` uses
|
|
127
|
+
* (view/transforms → detail → layout → svg) and return every intermediate, so
|
|
128
|
+
* tooling can read real geometry instead of reverse-engineering the SVG.
|
|
129
|
+
*
|
|
130
|
+
* Accepts either a puml string (the counterpart of
|
|
131
|
+
* `createRenderer(options).render(puml)`, with the same fail-closed
|
|
132
|
+
* {@link RenderError} on invalid input) or an already-parsed
|
|
133
|
+
* {@link PlantUmlModel} (used by the sample harness, which parses first to also
|
|
134
|
+
* read parse diagnostics).
|
|
135
|
+
*
|
|
136
|
+
* Stable public API, exported from both the `.` and `./elk` package entries.
|
|
137
|
+
* The returned {@link DetailedRender.layout} rectangles are the stable contract
|
|
138
|
+
* documented on {@link DetailedRender}.
|
|
139
|
+
*/
|
|
140
|
+
export declare function renderDetailed(input: string, options?: RendererOptions): Promise<DetailedRender>;
|
|
141
|
+
export declare function renderDetailed(input: PlantUmlModel, options?: RendererOptions): Promise<DetailedRender>;
|
|
57
142
|
/**
|
|
58
143
|
* Entity count at or above which the default full-graph render enables hub
|
|
59
144
|
* summarization. Smaller models stay untouched so a hand-authored diagram is
|
|
@@ -76,6 +161,12 @@ export declare const FULL_GRAPH_HUB_IN_DEGREE_THRESHOLD = 6;
|
|
|
76
161
|
* drawing of the hub's incoming edges.
|
|
77
162
|
*/
|
|
78
163
|
export declare function transformFullGraph(model: PlantUmlModel, overrides?: TransformOptions): PlantUmlModel;
|
|
164
|
+
/**
|
|
165
|
+
* Apply a view, then the same default transforms as the full graph (computed on
|
|
166
|
+
* the *viewed* model, so a large single-package view summarizes its hubs too).
|
|
167
|
+
* `overrides` wins over the defaults.
|
|
168
|
+
*/
|
|
169
|
+
export declare function transformView(model: PlantUmlModel, view: ViewSpec, overrides?: TransformOptions): PlantUmlModel;
|
|
79
170
|
/**
|
|
80
171
|
* Validate a puml string without throwing. Returns structured diagnostics for
|
|
81
172
|
* structurally invalid input (content outside a diagram, unbalanced
|
|
@@ -99,10 +190,12 @@ export interface LineExplanation {
|
|
|
99
190
|
* human-readable description. Purely descriptive — it never mutates or throws.
|
|
100
191
|
*/
|
|
101
192
|
export declare function explain(puml: string): LineExplanation[];
|
|
102
|
-
export { parsePlantUml, ElkLayoutEngine, renderSvg };
|
|
103
|
-
export {
|
|
104
|
-
export type {
|
|
105
|
-
export {
|
|
106
|
-
export type {
|
|
193
|
+
export { parsePlantUml, ElkLayoutEngine, renderSvg, SvgRenderer };
|
|
194
|
+
export { toPlantUml } from './parse/serialize.js';
|
|
195
|
+
export type { RenderSvgOptions, RenderEngine, RenderFrameOptions, SvgRendererOptions };
|
|
196
|
+
export { computeLayoutMetrics } from './metrics/layout-metrics.js';
|
|
197
|
+
export type { LayoutMetrics } from './metrics/layout-metrics.js';
|
|
198
|
+
export { applyView, transformModel, aggregateRelations, transitiveReduce, summarizeHubs, markCycles, groupByPathPrefix, } from './transform/transform.js';
|
|
199
|
+
export type { ViewSpec, TransformOptions } from './transform/transform.js';
|
|
107
200
|
export { archguardToPlantUml, archguardAliasMap, pathName, displayName, ARCHGUARD_RELATION_ARROWS, } from './archguard.js';
|
|
108
201
|
export type { ArchguardNode, ArchguardEdge, ArchguardGraph, ArchguardBridgeOptions, ArchguardPlantUml, } from './archguard.js';
|
package/dist/core.js
CHANGED
|
@@ -6,10 +6,12 @@
|
|
|
6
6
|
* `src/index.ts` is the full build: it re-exports this core and adds the viz
|
|
7
7
|
* engine + `renderDot`.
|
|
8
8
|
*/
|
|
9
|
-
import { parsePlantUml } from './parser.js';
|
|
9
|
+
import { parsePlantUml, parseWithDiagnostics, ENTITY_LINE, ENTITY_KEYWORD, extractDeclaredName, stripComment } from './parse/parser.js';
|
|
10
|
+
import { applyDetail } from './foundation/layout.js';
|
|
10
11
|
import { ElkLayoutEngine } from './layout/elk.js';
|
|
11
|
-
import { renderSvg } from './render/svg.js';
|
|
12
|
-
import { applyView, transformModel } from './transform.js';
|
|
12
|
+
import { renderSvg, SvgRenderer, } from './render/svg.js';
|
|
13
|
+
import { applyView, transformModel, transformModelDetailed, shouldAutoReduce, INTER_PACKAGE_AGGREGATE_ENTITY_THRESHOLD } from './transform/transform.js';
|
|
14
|
+
export { defaultTheme, resolveTheme } from './render/theme.js';
|
|
13
15
|
/** Error thrown by {@link createRenderer}'s `render` on structurally invalid puml. */
|
|
14
16
|
export class RenderError extends Error {
|
|
15
17
|
diagnostics;
|
|
@@ -20,9 +22,9 @@ export class RenderError extends Error {
|
|
|
20
22
|
this.diagnostics = diagnostics;
|
|
21
23
|
}
|
|
22
24
|
}
|
|
23
|
-
function resolveEngine(layout) {
|
|
25
|
+
function resolveEngine(layout, layoutOptions) {
|
|
24
26
|
if (layout === undefined || layout === 'elk')
|
|
25
|
-
return new ElkLayoutEngine();
|
|
27
|
+
return new ElkLayoutEngine(layoutOptions);
|
|
26
28
|
if (typeof layout === 'string') {
|
|
27
29
|
throw new Error(`unknown layout engine ${JSON.stringify(layout)} (expected 'elk' or a LayoutEngine instance; ` +
|
|
28
30
|
`'viz' is only available from the full build '@yalehwang/plantuml-js')`);
|
|
@@ -32,8 +34,25 @@ function resolveEngine(layout) {
|
|
|
32
34
|
}
|
|
33
35
|
return layout;
|
|
34
36
|
}
|
|
37
|
+
/**
|
|
38
|
+
* Resolve the render engine the same way [[resolveEngine]] resolves layout:
|
|
39
|
+
* the injected instance wins; otherwise the built-in [[SvgRenderer]] takes the
|
|
40
|
+
* renderer-wide static configuration (`theme`, `legend`). Structural validation
|
|
41
|
+
* fails closed instead of rendering an empty SVG.
|
|
42
|
+
*/
|
|
43
|
+
function resolveRenderer(options) {
|
|
44
|
+
const renderer = options.renderer;
|
|
45
|
+
if (renderer === undefined) {
|
|
46
|
+
return new SvgRenderer({ legend: options.legend, theme: options.theme });
|
|
47
|
+
}
|
|
48
|
+
if (typeof renderer.render !== 'function') {
|
|
49
|
+
throw new Error('invalid RenderEngine: a custom engine must expose a render(model, layout) method');
|
|
50
|
+
}
|
|
51
|
+
return renderer;
|
|
52
|
+
}
|
|
35
53
|
export function createRenderer(options = {}) {
|
|
36
|
-
const engine = resolveEngine(options.layout);
|
|
54
|
+
const engine = resolveEngine(options.layout, options.layoutOptions);
|
|
55
|
+
const renderEngine = resolveRenderer(options);
|
|
37
56
|
return {
|
|
38
57
|
async render(puml) {
|
|
39
58
|
// Fail closed on structurally invalid puml: checkSyntax reports instead of
|
|
@@ -41,14 +60,35 @@ export function createRenderer(options = {}) {
|
|
|
41
60
|
const syntax = checkSyntax(puml);
|
|
42
61
|
if (!syntax.ok)
|
|
43
62
|
throw new RenderError(syntax.errors);
|
|
44
|
-
const parsed =
|
|
45
|
-
const model =
|
|
46
|
-
|
|
47
|
-
const svg = renderSvg(model, layout);
|
|
48
|
-
return { svg, diagramType: model.diagramType, warnings: [] };
|
|
63
|
+
const { model: parsed, diagnostics } = parseWithDiagnostics(puml);
|
|
64
|
+
const { svg, model } = await renderParsed(parsed, options, engine, renderEngine);
|
|
65
|
+
return { svg, diagramType: model.diagramType, warnings: diagnostics.map((d) => `line ${d.line}: ${d.message}`) };
|
|
49
66
|
},
|
|
50
67
|
};
|
|
51
68
|
}
|
|
69
|
+
async function renderParsed(parsed, options, engine, renderEngine) {
|
|
70
|
+
const { model: transformed, hiddenTransitive } = transformForRender(parsed, options);
|
|
71
|
+
const model = applyDetail(transformed, options.detail);
|
|
72
|
+
const started = performance.now();
|
|
73
|
+
const layout = await engine.layout(model);
|
|
74
|
+
const layoutMs = Math.round((performance.now() - started) * 100) / 100;
|
|
75
|
+
const svg = renderEngine.render(model, layout, { hiddenTransitive });
|
|
76
|
+
return { model, layout, svg, layoutMs, hiddenTransitive };
|
|
77
|
+
}
|
|
78
|
+
export async function renderDetailed(input, options = {}) {
|
|
79
|
+
const parsed = typeof input === 'string' ? parseForDetailed(input) : input;
|
|
80
|
+
return renderParsed(parsed, options, resolveEngine(options.layout, options.layoutOptions), resolveRenderer(options));
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Parse the string form of {@link renderDetailed} with the same fail-closed
|
|
84
|
+
* semantics as `createRenderer().render` (structured `RenderError`).
|
|
85
|
+
*/
|
|
86
|
+
function parseForDetailed(puml) {
|
|
87
|
+
const syntax = checkSyntax(puml);
|
|
88
|
+
if (!syntax.ok)
|
|
89
|
+
throw new RenderError(syntax.errors);
|
|
90
|
+
return parseWithDiagnostics(puml).model;
|
|
91
|
+
}
|
|
52
92
|
/**
|
|
53
93
|
* Apply the configured view and transforms. A view collapses/expands first,
|
|
54
94
|
* then the transform pipeline runs with the view's defaults (aggregation +
|
|
@@ -59,13 +99,22 @@ export function createRenderer(options = {}) {
|
|
|
59
99
|
* unreadable arrow cluster.
|
|
60
100
|
*/
|
|
61
101
|
function transformForRender(model, options) {
|
|
102
|
+
const overrides = {
|
|
103
|
+
...(options.interPackage ? { interPackage: options.interPackage } : {}),
|
|
104
|
+
...options.transform,
|
|
105
|
+
};
|
|
106
|
+
const mode = options.edgeReduction ?? 'auto';
|
|
107
|
+
const reduction = (m) => overrides.transitiveReduction !== undefined || mode === 'none'
|
|
108
|
+
? {}
|
|
109
|
+
: { transitiveReduction: mode === 'transitive' || shouldAutoReduce(m) };
|
|
62
110
|
if (options.view) {
|
|
63
111
|
const viewed = applyView(model, options.view);
|
|
64
|
-
return
|
|
112
|
+
return transformModelDetailed(viewed, { ...defaultTransformOptions(viewed), ...reduction(viewed), ...overrides });
|
|
113
|
+
}
|
|
114
|
+
if (options.transform) {
|
|
115
|
+
return transformModelDetailed(model, { interPackage: defaultInterPackage(model), ...reduction(model), ...overrides });
|
|
65
116
|
}
|
|
66
|
-
|
|
67
|
-
return transformModel(model, options.transform);
|
|
68
|
-
return transformFullGraph(model);
|
|
117
|
+
return transformModelDetailed(model, { ...defaultTransformOptions(model), ...reduction(model), ...overrides });
|
|
69
118
|
}
|
|
70
119
|
/**
|
|
71
120
|
* Entity count at or above which the default full-graph render enables hub
|
|
@@ -89,14 +138,27 @@ export const FULL_GRAPH_HUB_IN_DEGREE_THRESHOLD = 6;
|
|
|
89
138
|
* drawing of the hub's incoming edges.
|
|
90
139
|
*/
|
|
91
140
|
export function transformFullGraph(model, overrides = {}) {
|
|
92
|
-
|
|
141
|
+
return transformModel(model, { ...defaultTransformOptions(model), ...overrides });
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Apply a view, then the same default transforms as the full graph (computed on
|
|
145
|
+
* the *viewed* model, so a large single-package view summarizes its hubs too).
|
|
146
|
+
* `overrides` wins over the defaults.
|
|
147
|
+
*/
|
|
148
|
+
export function transformView(model, view, overrides = {}) {
|
|
149
|
+
const viewed = applyView(model, view);
|
|
150
|
+
return transformModel(viewed, { ...defaultTransformOptions(viewed), ...overrides });
|
|
151
|
+
}
|
|
152
|
+
function defaultInterPackage(model) {
|
|
153
|
+
return model.entities.length > INTER_PACKAGE_AGGREGATE_ENTITY_THRESHOLD ? 'aggregate' : 'raw';
|
|
154
|
+
}
|
|
155
|
+
function defaultTransformOptions(model) {
|
|
156
|
+
const options = { aggregate: true, cycles: true, interPackage: defaultInterPackage(model) };
|
|
93
157
|
if (model.entities.length > FULL_GRAPH_HUB_NODE_THRESHOLD) {
|
|
94
158
|
options.hubThreshold = FULL_GRAPH_HUB_IN_DEGREE_THRESHOLD;
|
|
95
159
|
}
|
|
96
|
-
return
|
|
160
|
+
return options;
|
|
97
161
|
}
|
|
98
|
-
const ENTITY_DECL = /^(?:abstract\s+)?(?:class|interface|enum)\s+(.+)$/i;
|
|
99
|
-
const ENTITY_KEYWORD = /^(?:abstract\s+)?(?:class|interface|enum)\b/i;
|
|
100
162
|
/**
|
|
101
163
|
* Validate a puml string without throwing. Returns structured diagnostics for
|
|
102
164
|
* structurally invalid input (content outside a diagram, unbalanced
|
|
@@ -153,8 +215,8 @@ export function checkSyntax(puml) {
|
|
|
153
215
|
if (code === '')
|
|
154
216
|
continue;
|
|
155
217
|
if (ENTITY_KEYWORD.test(code)) {
|
|
156
|
-
const match =
|
|
157
|
-
const name = match ? extractDeclaredName(match[
|
|
218
|
+
const match = ENTITY_LINE.exec(code);
|
|
219
|
+
const name = match ? extractDeclaredName(match[3]) : null;
|
|
158
220
|
if (name === null) {
|
|
159
221
|
errors.push({
|
|
160
222
|
line: lineNo,
|
|
@@ -164,6 +226,9 @@ export function checkSyntax(puml) {
|
|
|
164
226
|
}
|
|
165
227
|
}
|
|
166
228
|
}
|
|
229
|
+
for (const d of parseWithDiagnostics(puml).diagnostics) {
|
|
230
|
+
warnings.push({ line: d.line, severity: 'warning', message: d.message });
|
|
231
|
+
}
|
|
167
232
|
if (sawStart && inDiagram) {
|
|
168
233
|
errors.push({
|
|
169
234
|
line: lines.length,
|
|
@@ -204,8 +269,8 @@ function classifyLine(raw) {
|
|
|
204
269
|
if (code === '')
|
|
205
270
|
return { kind: 'comment', explanation: 'PlantUML comment' };
|
|
206
271
|
if (ENTITY_KEYWORD.test(code)) {
|
|
207
|
-
const match =
|
|
208
|
-
const name = match ? extractDeclaredName(match[
|
|
272
|
+
const match = ENTITY_LINE.exec(code);
|
|
273
|
+
const name = match ? extractDeclaredName(match[3]) : null;
|
|
209
274
|
if (name === null) {
|
|
210
275
|
return { kind: 'entity', explanation: 'malformed entity declaration' };
|
|
211
276
|
}
|
|
@@ -228,33 +293,8 @@ function classifyLine(raw) {
|
|
|
228
293
|
};
|
|
229
294
|
return { kind: 'unknown', explanation: 'unrecognised line (ignored)' };
|
|
230
295
|
}
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
* the quoted name is unterminated.
|
|
236
|
-
*/
|
|
237
|
-
function extractDeclaredName(rest) {
|
|
238
|
-
const asIndex = rest.search(/\s+as\s+\S+\s*$/i);
|
|
239
|
-
const core = asIndex === -1 ? rest.trim() : rest.slice(0, asIndex).trim();
|
|
240
|
-
if (core === '')
|
|
241
|
-
return null;
|
|
242
|
-
if (core.startsWith('"')) {
|
|
243
|
-
const close = core.indexOf('"', 1);
|
|
244
|
-
if (close === -1)
|
|
245
|
-
return null;
|
|
246
|
-
return core.slice(1, close);
|
|
247
|
-
}
|
|
248
|
-
return core.split(/\s+/)[0] || null;
|
|
249
|
-
}
|
|
250
|
-
/** Strip PlantUML `'` line comments (a leading `'` or a trailing inline `'`). */
|
|
251
|
-
function stripComment(line) {
|
|
252
|
-
const idx = line.indexOf("'");
|
|
253
|
-
if (idx === -1)
|
|
254
|
-
return line.trim();
|
|
255
|
-
return line.slice(0, idx).trim();
|
|
256
|
-
}
|
|
257
|
-
export { parsePlantUml, ElkLayoutEngine, renderSvg };
|
|
258
|
-
export { computeLayoutMetrics } from './layout-metrics.js';
|
|
259
|
-
export { applyView, transformModel, aggregateRelations, transitiveReduce, summarizeHubs, markCycles, groupByPathPrefix, } from './transform.js';
|
|
296
|
+
export { parsePlantUml, ElkLayoutEngine, renderSvg, SvgRenderer };
|
|
297
|
+
export { toPlantUml } from './parse/serialize.js';
|
|
298
|
+
export { computeLayoutMetrics } from './metrics/layout-metrics.js';
|
|
299
|
+
export { applyView, transformModel, aggregateRelations, transitiveReduce, summarizeHubs, markCycles, groupByPathPrefix, } from './transform/transform.js';
|
|
260
300
|
export { archguardToPlantUml, archguardAliasMap, pathName, displayName, ARCHGUARD_RELATION_ARROWS, } from './archguard.js';
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Edge-label and hub-badge geometry shared by the SVG renderer and the layout
|
|
3
|
+
* metrics, so a drawn label and a measured label are the same rectangle.
|
|
4
|
+
*
|
|
5
|
+
* Weight labels (`×N`) are placed on the edge itself over a white backing box.
|
|
6
|
+
* Candidate anchors run along the edge's segments, longest first; the first
|
|
7
|
+
* candidate that collides with no node and no previously placed label wins, so
|
|
8
|
+
* labels of neighbouring edges do not pile onto each other.
|
|
9
|
+
*/
|
|
10
|
+
import { PlantUmlModel, PlantUmlEntity } from './model.js';
|
|
11
|
+
import { LayoutResult, NodeLayout, Point } from './layout.js';
|
|
12
|
+
import { Rect } from './edge-router.js';
|
|
13
|
+
export declare const EDGE_LABEL_FONT_SIZE = 11;
|
|
14
|
+
export declare const HUB_BADGE_RADIUS = 9;
|
|
15
|
+
export interface EdgeLabelPlacement {
|
|
16
|
+
/** Index into `model.relations` (edge id `rel-<index>`). */
|
|
17
|
+
edge: number;
|
|
18
|
+
kind: 'weight' | 'label';
|
|
19
|
+
text: string;
|
|
20
|
+
rect: Rect;
|
|
21
|
+
}
|
|
22
|
+
/** Hub badge circle: centred on the node's top-right corner, half outside the box. */
|
|
23
|
+
export declare function hubBadgeCircle(node: NodeLayout): {
|
|
24
|
+
cx: number;
|
|
25
|
+
cy: number;
|
|
26
|
+
r: number;
|
|
27
|
+
};
|
|
28
|
+
export declare function hubBadgeRect(node: NodeLayout): Rect;
|
|
29
|
+
/** Bounding boxes of the text lines drawn inside a node (title + members). */
|
|
30
|
+
export declare function entityTextRects(entity: PlantUmlEntity, node: NodeLayout): Rect[];
|
|
31
|
+
export declare function rectsIntersect(a: Rect, b: Rect): boolean;
|
|
32
|
+
/** Same-text labels sharing a source (or target) and the other end's package collapse to one. */
|
|
33
|
+
export declare const LABEL_DEDUPE_MIN = 3;
|
|
34
|
+
/** A collapsed label standing for at least this many edges carries a `×N` count. */
|
|
35
|
+
export declare const LABEL_COUNT_BADGE_MIN = 4;
|
|
36
|
+
/** True when the segment ab touches the rectangle (Liang–Barsky clip). */
|
|
37
|
+
export declare function segmentIntersectsRect(a: Point, b: Point, r: Rect): boolean;
|
|
38
|
+
/** Edge ids (`rel-<i>`) of every segment of every edge other than `ownId` that crosses `rect`. */
|
|
39
|
+
export declare function rectHitsOtherEdge(rect: Rect, edges: {
|
|
40
|
+
id: string;
|
|
41
|
+
points: Point[];
|
|
42
|
+
}[], ownId: string): boolean;
|
|
43
|
+
/** Place every weight label and collect every plain edge label, deterministically. */
|
|
44
|
+
export declare function placeEdgeLabels(model: PlantUmlModel, layout: LayoutResult): EdgeLabelPlacement[];
|