@yalehwang/plantuml-js 0.4.1 → 0.5.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 +23 -1
- package/dist/core.d.ts +29 -3
- package/dist/core.js +15 -5
- package/dist/foundation/layout.d.ts +46 -1
- package/dist/foundation/layout.js +99 -9
- package/dist/foundation/model.d.ts +15 -0
- package/dist/parse/serialize.d.ts +10 -4
- package/dist/parse/serialize.js +18 -10
- package/dist/render/svg.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -72,7 +72,7 @@ console.log(svg);
|
|
|
72
72
|
|---|---|
|
|
73
73
|
| `createRenderer({ layout })` | Build a renderer. `layout` is `'elk'` (default), `'viz'` (experimental), or a custom `LayoutEngine`. |
|
|
74
74
|
| `renderer.render(puml)` | Parse + layout + emit SVG. Returns `{ svg, diagramType, warnings }`. Throws `RenderError` on structurally invalid puml. |
|
|
75
|
-
| `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`. |
|
|
75
|
+
| `renderDetailed(pumlOrModel, options?)` | Run the same pipeline as `renderer.render` and return every intermediate: `{ model, sourceModel, layout, svg, layoutMs, hiddenTransitive }`. Accepts a puml string (throws `RenderError` on invalid input) or an already-parsed `PlantUmlModel`. `model` is the post-detail model that was drawn — **not suitable for export** (detail truncation may have replaced member rows); `sourceModel` is the complete pre-detail model, so export with `toPlantUml(sourceModel)`. `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`. |
|
|
76
76
|
| `parsePlantUml(puml)` | Parse a puml string into a `PlantUmlModel`. |
|
|
77
77
|
| `checkSyntax(puml)` | Validate without throwing. Returns `{ ok, errors, warnings }` of line-accurate `SyntaxDiagnostic`s. |
|
|
78
78
|
| `explain(puml)` | Explain each line. Returns `LineExplanation[]` (line, kind, explanation). |
|
|
@@ -98,6 +98,28 @@ without re-routing. `model` is the diagram actually drawn (after view,
|
|
|
98
98
|
transforms and detail), and `hiddenTransitive` counts the edges dropped by
|
|
99
99
|
transitive reduction.
|
|
100
100
|
|
|
101
|
+
`model` is a **drawing** model: `detail`/`memberMaxRows` may have replaced rows
|
|
102
|
+
with a synthetic `… +N more (M members)` marker, so serializing it is lossy.
|
|
103
|
+
For a lossless export (copy/download the current diagram as PlantUML), serialize
|
|
104
|
+
`sourceModel` — the same model after view/transforms but before detail:
|
|
105
|
+
|
|
106
|
+
```ts
|
|
107
|
+
const { sourceModel } = await renderDetailed(puml, { detail: 'compact' });
|
|
108
|
+
const text = toPlantUml(sourceModel); // every authored member, no marker rows
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
`toPlantUml` skips synthetic members, so `toPlantUml(model)` never emits a
|
|
112
|
+
`+ … +N more` line either — it is simply only as complete as what was drawn.
|
|
113
|
+
|
|
114
|
+
The canvas is fitted to **the routes and labels that are drawn**, not only to
|
|
115
|
+
the boxes the layout engine measures: `layout.width`/`height` (and therefore
|
|
116
|
+
the SVG `viewBox`) contain every edge route point and every edge label box, with
|
|
117
|
+
a margin of at least **16px** on all four sides. An edge label beside an outer
|
|
118
|
+
detour route is therefore never clipped at the SVG border, and a host can rely
|
|
119
|
+
on `0 0 width height` containing the whole drawing. Engine geometry that
|
|
120
|
+
already fits is left untouched, so the canvas never shrinks and unchanged
|
|
121
|
+
diagrams render identically.
|
|
122
|
+
|
|
101
123
|
### Error semantics
|
|
102
124
|
|
|
103
125
|
`createRenderer` fails closed on an unknown layout name (`'elk'`, `'viz'`, or a
|
package/dist/core.d.ts
CHANGED
|
@@ -46,6 +46,18 @@ export interface RendererOptions {
|
|
|
46
46
|
* Default: chosen by entity count (≤40 full, ≤150 compact, else names).
|
|
47
47
|
*/
|
|
48
48
|
detail?: DetailLevel;
|
|
49
|
+
/**
|
|
50
|
+
* Maximum member rows drawn per entity; surplus members collapse into the
|
|
51
|
+
* existing `… +N more (M members)` row. Applied before layout, so the box is
|
|
52
|
+
* sized for what is drawn. Default: unlimited for `full`, 4 for `compact`.
|
|
53
|
+
*/
|
|
54
|
+
memberMaxRows?: number;
|
|
55
|
+
/**
|
|
56
|
+
* Cut each drawn member line to this many characters (ending with `…`).
|
|
57
|
+
* Only the drawn text changes: `PlantUmlMember.raw` keeps the full
|
|
58
|
+
* declaration. Default: unlimited.
|
|
59
|
+
*/
|
|
60
|
+
memberMaxChars?: number;
|
|
49
61
|
/**
|
|
50
62
|
* `'aggregate'` folds the relations between two packages into one weighted
|
|
51
63
|
* box-to-box corridor edge (originals kept in its `<title>`); `'raw'` draws
|
|
@@ -67,8 +79,8 @@ export interface RenderResult {
|
|
|
67
79
|
}
|
|
68
80
|
/**
|
|
69
81
|
* Everything {@link renderDetailed} computed for one diagram: the model that was
|
|
70
|
-
* actually laid out and drawn, the
|
|
71
|
-
* emitted, and the reduction bookkeeping.
|
|
82
|
+
* actually laid out and drawn, the pre-detail model to export from, the geometry
|
|
83
|
+
* the renderer used, the SVG it emitted, and the reduction bookkeeping.
|
|
72
84
|
*
|
|
73
85
|
* This is a **public, stable API** (available from both the `.` and `./elk`
|
|
74
86
|
* entries). Consumers such as an architecture tab should read geometry from
|
|
@@ -89,8 +101,20 @@ export interface RenderResult {
|
|
|
89
101
|
* they can be translated into a thumbnail directly.
|
|
90
102
|
*/
|
|
91
103
|
export interface DetailedRender {
|
|
92
|
-
/**
|
|
104
|
+
/**
|
|
105
|
+
* The model actually laid out and drawn (after view, transforms **and**
|
|
106
|
+
* detail). Detail truncation has already dropped members here — row caps
|
|
107
|
+
* replace them with a `synthetic` `… +N more` marker — so this model is for
|
|
108
|
+
* drawing/inspection, **not for export**. Export from {@link sourceModel}.
|
|
109
|
+
*/
|
|
93
110
|
model: PlantUmlModel;
|
|
111
|
+
/**
|
|
112
|
+
* The complete model after view/transforms but **before** detail truncation:
|
|
113
|
+
* every authored member, with `raw` intact. Serialize this to export what was
|
|
114
|
+
* parsed — `toPlantUml(renderDetailed(input).sourceModel)` — instead of
|
|
115
|
+
* `model`, whose rows detail may have capped.
|
|
116
|
+
*/
|
|
117
|
+
sourceModel: PlantUmlModel;
|
|
94
118
|
/** Geometry actually used by the renderer; its node/package/edge rectangles are a stable contract (see above). */
|
|
95
119
|
layout: LayoutResult;
|
|
96
120
|
svg: string;
|
|
@@ -126,6 +150,8 @@ export declare function createRenderer(options?: RendererOptions): Renderer;
|
|
|
126
150
|
* Render one diagram through the exact pipeline `createRenderer` uses
|
|
127
151
|
* (view/transforms → detail → layout → svg) and return every intermediate, so
|
|
128
152
|
* tooling can read real geometry instead of reverse-engineering the SVG.
|
|
153
|
+
* `model` is the post-detail model that was drawn; `sourceModel` is the
|
|
154
|
+
* pre-detail model to serialize for a lossless export.
|
|
129
155
|
*
|
|
130
156
|
* Accepts either a puml string (the counterpart of
|
|
131
157
|
* `createRenderer(options).render(puml)`, with the same fail-closed
|
package/dist/core.js
CHANGED
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
* engine + `renderDot`.
|
|
8
8
|
*/
|
|
9
9
|
import { parsePlantUml, parseWithDiagnostics, ENTITY_LINE, ENTITY_KEYWORD, extractDeclaredName, stripComment } from './parse/parser.js';
|
|
10
|
-
import { applyDetail } from './foundation/layout.js';
|
|
10
|
+
import { applyDetail, fitLayoutToContent } from './foundation/layout.js';
|
|
11
|
+
import { placeEdgeLabels } from './foundation/labels.js';
|
|
11
12
|
import { ElkLayoutEngine } from './layout/elk.js';
|
|
12
13
|
import { renderSvg, SvgRenderer, } from './render/svg.js';
|
|
13
14
|
import { applyView, transformModel, transformModelDetailed, shouldAutoReduce, INTER_PACKAGE_AGGREGATE_ENTITY_THRESHOLD } from './transform/transform.js';
|
|
@@ -67,13 +68,22 @@ export function createRenderer(options = {}) {
|
|
|
67
68
|
};
|
|
68
69
|
}
|
|
69
70
|
async function renderParsed(parsed, options, engine, renderEngine) {
|
|
70
|
-
const { model:
|
|
71
|
-
const model = applyDetail(
|
|
71
|
+
const { model: sourceModel, hiddenTransitive } = transformForRender(parsed, options);
|
|
72
|
+
const model = applyDetail(sourceModel, options.detail, {
|
|
73
|
+
memberMaxRows: options.memberMaxRows,
|
|
74
|
+
memberMaxChars: options.memberMaxChars,
|
|
75
|
+
});
|
|
72
76
|
const started = performance.now();
|
|
73
|
-
const
|
|
77
|
+
const raw = await engine.layout(model);
|
|
74
78
|
const layoutMs = Math.round((performance.now() - started) * 100) / 100;
|
|
79
|
+
// The engines size the canvas from boxes and routes; edge labels are placed
|
|
80
|
+
// by the renderer and can hang outside that box (a plain label beside an
|
|
81
|
+
// outer detour route lands at a negative x). Fit the canvas to the routes and
|
|
82
|
+
// labels, so the returned layout and the SVG viewBox both contain them with
|
|
83
|
+
// a margin.
|
|
84
|
+
const layout = fitLayoutToContent(raw, placeEdgeLabels(model, raw).map((label) => label.rect));
|
|
75
85
|
const svg = renderEngine.render(model, layout, { hiddenTransitive });
|
|
76
|
-
return { model, layout, svg, layoutMs, hiddenTransitive };
|
|
86
|
+
return { sourceModel, model, layout, svg, layoutMs, hiddenTransitive };
|
|
77
87
|
}
|
|
78
88
|
export async function renderDetailed(input, options = {}) {
|
|
79
89
|
const parsed = typeof input === 'string' ? parseForDetailed(input) : input;
|
|
@@ -40,6 +40,28 @@ export interface LayoutResult {
|
|
|
40
40
|
export interface LayoutEngine {
|
|
41
41
|
layout(model: PlantUmlModel): Promise<LayoutResult>;
|
|
42
42
|
}
|
|
43
|
+
/** Empty border the canvas keeps around every drawn mark (px). */
|
|
44
|
+
export declare const CANVAS_CONTENT_MARGIN = 16;
|
|
45
|
+
/** Axis-aligned rectangle, structurally compatible with `labels`/`edge-router` rects. */
|
|
46
|
+
export interface ContentRect {
|
|
47
|
+
x: number;
|
|
48
|
+
y: number;
|
|
49
|
+
width: number;
|
|
50
|
+
height: number;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Grow and translate a layout so every edge route point and the supplied extra
|
|
54
|
+
* rectangles (edge labels) stays at least `margin` from the canvas side. Routes
|
|
55
|
+
* and labels are drawn from the layout coordinates but are not part of
|
|
56
|
+
* `layout.width`/`height` as produced by the engines, so a label hung beside an
|
|
57
|
+
* outer detour route can sit at a negative x and be clipped by a `0 0 w h`
|
|
58
|
+
* viewBox.
|
|
59
|
+
*
|
|
60
|
+
* Boxes are left alone — the engines already place them inside their own
|
|
61
|
+
* margin — so this never moves a diagram whose routes and labels already fit:
|
|
62
|
+
* the input is returned untouched and the canvas never shrinks.
|
|
63
|
+
*/
|
|
64
|
+
export declare function fitLayoutToContent(layout: LayoutResult, extraRects?: readonly ContentRect[], margin?: number): LayoutResult;
|
|
43
65
|
/**
|
|
44
66
|
* Shared drawing metrics. The renderer reads these so entity sizing and the
|
|
45
67
|
* emitted SVG stay in lockstep: the font family and size are bound to the same
|
|
@@ -105,11 +127,34 @@ export declare const DETAIL_COMPACT_MAX_ENTITIES = 150;
|
|
|
105
127
|
/** Member rows kept per entity in `compact` mode. */
|
|
106
128
|
export declare const COMPACT_MEMBER_ROWS = 4;
|
|
107
129
|
export declare function autoDetail(entityCount: number): DetailLevel;
|
|
130
|
+
/**
|
|
131
|
+
* Optional caps on the member text a diagram draws. Both are applied by
|
|
132
|
+
* [[applyDetail]] before layout, so an entity is sized for exactly what the
|
|
133
|
+
* renderer emits; neither mutates `PlantUmlMember.raw`.
|
|
134
|
+
*/
|
|
135
|
+
export interface MemberDisplayLimits {
|
|
136
|
+
/** Maximum member rows drawn per entity; the rest collapse into the count row. */
|
|
137
|
+
memberMaxRows?: number;
|
|
138
|
+
/** Cut each drawn member line to this many characters, ending with `…`. */
|
|
139
|
+
memberMaxChars?: number;
|
|
140
|
+
}
|
|
108
141
|
/**
|
|
109
142
|
* Reduce member rows to the requested detail level *before* layout, so boxes
|
|
110
143
|
* are sized for what is actually drawn. `undefined` picks by entity count.
|
|
144
|
+
*
|
|
145
|
+
* `limits` caps the drawn rows per entity (`memberMaxRows`, replacing
|
|
146
|
+
* [[COMPACT_MEMBER_ROWS]] for `compact` and also capping `full`) and the width
|
|
147
|
+
* of each drawn line (`memberMaxChars`). Truncation only sets
|
|
148
|
+
* `PlantUmlMember.display`; `raw` keeps the full declaration. Without limits
|
|
149
|
+
* the result is byte-for-byte what it always was.
|
|
150
|
+
*
|
|
151
|
+
* Row caps replace dropped members with one `… +N more (M members)` row marked
|
|
152
|
+
* `synthetic: true`: it is drawing scaffolding, not authored content, so the
|
|
153
|
+
* serializer skips it and a lossless export reads the pre-detail model
|
|
154
|
+
* (`renderDetailed().sourceModel`). Re-applying `applyDetail` to an already
|
|
155
|
+
* detailed model counts only the real members, so markers never stack.
|
|
111
156
|
*/
|
|
112
|
-
export declare function applyDetail(model: PlantUmlModel, detail?: DetailLevel): PlantUmlModel;
|
|
157
|
+
export declare function applyDetail(model: PlantUmlModel, detail?: DetailLevel, limits?: MemberDisplayLimits): PlantUmlModel;
|
|
113
158
|
/**
|
|
114
159
|
* Group nodes into rows: within one package (or the top level), nodes whose
|
|
115
160
|
* vertical extents overlap belong to the same layered-layout rank. Returns
|
|
@@ -5,6 +5,64 @@
|
|
|
5
5
|
* engines (ELK, Graphviz, a custom tuner) is an injection, not a rewrite.
|
|
6
6
|
*/
|
|
7
7
|
import { measureTextWidth, MONOSPACE_FONT_SIZE } from './text-measure.js';
|
|
8
|
+
/** Empty border the canvas keeps around every drawn mark (px). */
|
|
9
|
+
export const CANVAS_CONTENT_MARGIN = 16;
|
|
10
|
+
/**
|
|
11
|
+
* Grow and translate a layout so every edge route point and the supplied extra
|
|
12
|
+
* rectangles (edge labels) stays at least `margin` from the canvas side. Routes
|
|
13
|
+
* and labels are drawn from the layout coordinates but are not part of
|
|
14
|
+
* `layout.width`/`height` as produced by the engines, so a label hung beside an
|
|
15
|
+
* outer detour route can sit at a negative x and be clipped by a `0 0 w h`
|
|
16
|
+
* viewBox.
|
|
17
|
+
*
|
|
18
|
+
* Boxes are left alone — the engines already place them inside their own
|
|
19
|
+
* margin — so this never moves a diagram whose routes and labels already fit:
|
|
20
|
+
* the input is returned untouched and the canvas never shrinks.
|
|
21
|
+
*/
|
|
22
|
+
export function fitLayoutToContent(layout, extraRects = [], margin = CANVAS_CONTENT_MARGIN) {
|
|
23
|
+
let minX = Infinity;
|
|
24
|
+
let minY = Infinity;
|
|
25
|
+
let maxX = -Infinity;
|
|
26
|
+
let maxY = -Infinity;
|
|
27
|
+
const take = (x, y) => {
|
|
28
|
+
if (x < minX)
|
|
29
|
+
minX = x;
|
|
30
|
+
if (x > maxX)
|
|
31
|
+
maxX = x;
|
|
32
|
+
if (y < minY)
|
|
33
|
+
minY = y;
|
|
34
|
+
if (y > maxY)
|
|
35
|
+
maxY = y;
|
|
36
|
+
};
|
|
37
|
+
for (const edge of layout.edges ?? []) {
|
|
38
|
+
for (const point of edge.points)
|
|
39
|
+
take(point.x, point.y);
|
|
40
|
+
}
|
|
41
|
+
for (const rect of extraRects) {
|
|
42
|
+
take(rect.x, rect.y);
|
|
43
|
+
take(rect.x + rect.width, rect.y + rect.height);
|
|
44
|
+
}
|
|
45
|
+
if (minX === Infinity)
|
|
46
|
+
return layout;
|
|
47
|
+
const dx = Math.max(0, margin - minX);
|
|
48
|
+
const dy = Math.max(0, margin - minY);
|
|
49
|
+
const width = Math.max(layout.width + dx, maxX + dx + margin);
|
|
50
|
+
const height = Math.max(layout.height + dy, maxY + dy + margin);
|
|
51
|
+
if (dx === 0 && dy === 0 && width === layout.width && height === layout.height) {
|
|
52
|
+
return layout;
|
|
53
|
+
}
|
|
54
|
+
return {
|
|
55
|
+
...layout,
|
|
56
|
+
nodes: layout.nodes.map((n) => ({ ...n, x: n.x + dx, y: n.y + dy })),
|
|
57
|
+
edges: layout.edges.map((e) => ({
|
|
58
|
+
...e,
|
|
59
|
+
points: e.points.map((p) => ({ x: p.x + dx, y: p.y + dy })),
|
|
60
|
+
})),
|
|
61
|
+
packages: layout.packages?.map((p) => ({ ...p, x: p.x + dx, y: p.y + dy })),
|
|
62
|
+
width,
|
|
63
|
+
height,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
8
66
|
/**
|
|
9
67
|
* Shared drawing metrics. The renderer reads these so entity sizing and the
|
|
10
68
|
* emitted SVG stay in lockstep: the font family and size are bound to the same
|
|
@@ -74,7 +132,7 @@ export function compartmentHeights(entity) {
|
|
|
74
132
|
export function estimateEntitySize(entity) {
|
|
75
133
|
const lines = [
|
|
76
134
|
displayName(entity),
|
|
77
|
-
...entity.members.map((m) => m.raw),
|
|
135
|
+
...entity.members.map((m) => m.display ?? m.raw),
|
|
78
136
|
];
|
|
79
137
|
const longest = lines.reduce((max, line) => Math.max(max, measureTextWidth(line, METRICS.fontSize)), 0);
|
|
80
138
|
const width = snapUp(Math.max(METRICS.minWidth, longest + METRICS.paddingX * 2));
|
|
@@ -98,26 +156,58 @@ export function autoDetail(entityCount) {
|
|
|
98
156
|
/**
|
|
99
157
|
* Reduce member rows to the requested detail level *before* layout, so boxes
|
|
100
158
|
* are sized for what is actually drawn. `undefined` picks by entity count.
|
|
159
|
+
*
|
|
160
|
+
* `limits` caps the drawn rows per entity (`memberMaxRows`, replacing
|
|
161
|
+
* [[COMPACT_MEMBER_ROWS]] for `compact` and also capping `full`) and the width
|
|
162
|
+
* of each drawn line (`memberMaxChars`). Truncation only sets
|
|
163
|
+
* `PlantUmlMember.display`; `raw` keeps the full declaration. Without limits
|
|
164
|
+
* the result is byte-for-byte what it always was.
|
|
165
|
+
*
|
|
166
|
+
* Row caps replace dropped members with one `… +N more (M members)` row marked
|
|
167
|
+
* `synthetic: true`: it is drawing scaffolding, not authored content, so the
|
|
168
|
+
* serializer skips it and a lossless export reads the pre-detail model
|
|
169
|
+
* (`renderDetailed().sourceModel`). Re-applying `applyDetail` to an already
|
|
170
|
+
* detailed model counts only the real members, so markers never stack.
|
|
101
171
|
*/
|
|
102
|
-
export function applyDetail(model, detail) {
|
|
172
|
+
export function applyDetail(model, detail, limits = {}) {
|
|
103
173
|
const level = detail ?? autoDetail(model.entities.length);
|
|
104
|
-
|
|
174
|
+
const { memberMaxRows, memberMaxChars } = limits;
|
|
175
|
+
const hasLimits = memberMaxRows !== undefined || memberMaxChars !== undefined;
|
|
176
|
+
if (level === 'full' && !hasLimits)
|
|
105
177
|
return model;
|
|
178
|
+
const limitMember = (member) => {
|
|
179
|
+
if (memberMaxChars === undefined || member.raw.length <= memberMaxChars)
|
|
180
|
+
return member;
|
|
181
|
+
return {
|
|
182
|
+
...member,
|
|
183
|
+
display: `${member.raw.slice(0, Math.max(0, memberMaxChars - 1))}…`,
|
|
184
|
+
};
|
|
185
|
+
};
|
|
106
186
|
return {
|
|
107
187
|
...model,
|
|
108
188
|
entities: model.entities.map((entity) => {
|
|
109
|
-
const
|
|
189
|
+
const authored = entity.members.filter((member) => member.synthetic !== true);
|
|
190
|
+
const total = authored.length;
|
|
110
191
|
if (level === 'names')
|
|
111
|
-
return
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
192
|
+
return entity.members.length === 0 ? entity : { ...entity, members: [] };
|
|
193
|
+
const rowCap = memberMaxRows !== undefined
|
|
194
|
+
? memberMaxRows
|
|
195
|
+
: level === 'compact'
|
|
196
|
+
? COMPACT_MEMBER_ROWS
|
|
197
|
+
: total;
|
|
198
|
+
if (total <= rowCap) {
|
|
199
|
+
if (!hasLimits && authored.length === entity.members.length)
|
|
200
|
+
return entity;
|
|
201
|
+
return { ...entity, members: authored.map(limitMember) };
|
|
202
|
+
}
|
|
203
|
+
const kept = authored.slice(0, rowCap).map(limitMember);
|
|
115
204
|
const more = {
|
|
116
205
|
visibility: 'public',
|
|
117
206
|
name: '…',
|
|
118
207
|
kind: 'field',
|
|
119
208
|
modifiers: [],
|
|
120
|
-
raw: `… +${total -
|
|
209
|
+
raw: `… +${total - rowCap} more (${total} members)`,
|
|
210
|
+
synthetic: true,
|
|
121
211
|
};
|
|
122
212
|
return { ...entity, members: [...kept, more] };
|
|
123
213
|
}),
|
|
@@ -20,6 +20,21 @@ export interface PlantUmlMember {
|
|
|
20
20
|
optional?: boolean;
|
|
21
21
|
/** Full authored declaration incl. visibility symbol, for faithful display. */
|
|
22
22
|
raw: string;
|
|
23
|
+
/**
|
|
24
|
+
* Text actually drawn for this member, when it differs from `raw` (e.g. cut
|
|
25
|
+
* to a display width by `applyDetail`). Layout and rendering read
|
|
26
|
+
* `display ?? raw`; `raw` itself is never modified, so serialization and
|
|
27
|
+
* inspector views keep the full declaration.
|
|
28
|
+
*/
|
|
29
|
+
display?: string;
|
|
30
|
+
/**
|
|
31
|
+
* True when the member never existed in the source: it is a drawn-only
|
|
32
|
+
* stand-in synthesized by `applyDetail` (the `… +N more (M members)` count
|
|
33
|
+
* row) to stand for the rows the detail level dropped. It is not part of the
|
|
34
|
+
* parsed model, so it must never be exported: [[toPlantUml]] skips it, and
|
|
35
|
+
* the lossless export path serializes `renderDetailed().sourceModel` instead.
|
|
36
|
+
*/
|
|
37
|
+
synthetic?: true;
|
|
23
38
|
}
|
|
24
39
|
export interface PlantUmlEntity {
|
|
25
40
|
/** Stable node identity inside the diagram (the alias when `as` is used). */
|
|
@@ -1,13 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* PlantUmlModel → PlantUML class-diagram text.
|
|
3
3
|
*
|
|
4
|
-
* The inverse of [[parsePlantUml]]
|
|
5
|
-
*
|
|
6
|
-
* as source a user can copy and paste. Pure and synchronous — the model is the
|
|
7
|
-
* only input, so the caller composes the scope by picking the model
|
|
4
|
+
* The inverse of [[parsePlantUml]]. The model is the only input, so the caller
|
|
5
|
+
* composes the scope by picking the model
|
|
8
6
|
* (`toPlantUml(applyView(model, {kind: 'neighborhood', entity, hops}))`); this
|
|
9
7
|
* function never chooses a range on its own.
|
|
10
8
|
*
|
|
9
|
+
* For a **lossless export** of what `renderDetailed` parsed, pass its
|
|
10
|
+
* `sourceModel` (the post-view/transform, pre-detail model):
|
|
11
|
+
* `toPlantUml(renderDetailed(input).sourceModel)`. Passing `model` instead
|
|
12
|
+
* serializes the model that was drawn — detail truncation has already dropped
|
|
13
|
+
* rows there, so the text is only as complete as the drawing. Members marked
|
|
14
|
+
* `synthetic` (the `… +N more (M members)` count row `applyDetail` adds) are
|
|
15
|
+
* drawing scaffolding, not authored content, and are always skipped.
|
|
16
|
+
*
|
|
11
17
|
* Shapes that only exist after parsing (stub/external/usageCount, aggregated
|
|
12
18
|
* edge weights, cycle markers) are rendering semantics with no PlantUML
|
|
13
19
|
* spelling; they serialize as their plain-text equivalent (stub → class,
|
package/dist/parse/serialize.js
CHANGED
|
@@ -1,13 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* PlantUmlModel → PlantUML class-diagram text.
|
|
3
3
|
*
|
|
4
|
-
* The inverse of [[parsePlantUml]]
|
|
5
|
-
*
|
|
6
|
-
* as source a user can copy and paste. Pure and synchronous — the model is the
|
|
7
|
-
* only input, so the caller composes the scope by picking the model
|
|
4
|
+
* The inverse of [[parsePlantUml]]. The model is the only input, so the caller
|
|
5
|
+
* composes the scope by picking the model
|
|
8
6
|
* (`toPlantUml(applyView(model, {kind: 'neighborhood', entity, hops}))`); this
|
|
9
7
|
* function never chooses a range on its own.
|
|
10
8
|
*
|
|
9
|
+
* For a **lossless export** of what `renderDetailed` parsed, pass its
|
|
10
|
+
* `sourceModel` (the post-view/transform, pre-detail model):
|
|
11
|
+
* `toPlantUml(renderDetailed(input).sourceModel)`. Passing `model` instead
|
|
12
|
+
* serializes the model that was drawn — detail truncation has already dropped
|
|
13
|
+
* rows there, so the text is only as complete as the drawing. Members marked
|
|
14
|
+
* `synthetic` (the `… +N more (M members)` count row `applyDetail` adds) are
|
|
15
|
+
* drawing scaffolding, not authored content, and are always skipped.
|
|
16
|
+
*
|
|
11
17
|
* Shapes that only exist after parsing (stub/external/usageCount, aggregated
|
|
12
18
|
* edge weights, cycle markers) are rendering semantics with no PlantUML
|
|
13
19
|
* spelling; they serialize as their plain-text equivalent (stub → class,
|
|
@@ -131,10 +137,13 @@ function entityLines(entity, depth, aliases) {
|
|
|
131
137
|
const indent = INDENT.repeat(depth);
|
|
132
138
|
const alias = aliases.alias(entity.id, entity.name);
|
|
133
139
|
const head = `${entityKeyword(entity)} ${quoteName(entity.name)}${genericSuffix(entity)} as ${alias}`;
|
|
134
|
-
|
|
140
|
+
// Synthetic rows (applyDetail's `… +N more` marker) stand in for members the
|
|
141
|
+
// drawing dropped; they are not authored content, so they never serialize.
|
|
142
|
+
const members = entity.members.filter((member) => member.synthetic !== true);
|
|
143
|
+
if (members.length === 0)
|
|
135
144
|
return [`${indent}${head}`];
|
|
136
145
|
const lines = [`${indent}${head} {`];
|
|
137
|
-
for (const member of
|
|
146
|
+
for (const member of members) {
|
|
138
147
|
lines.push(`${indent}${INDENT}${memberLine(member)}`);
|
|
139
148
|
}
|
|
140
149
|
lines.push(`${indent}}`);
|
|
@@ -146,10 +155,9 @@ function genericSuffix(entity) {
|
|
|
146
155
|
}
|
|
147
156
|
/**
|
|
148
157
|
* One member row. The authored `raw` text is authoritative (it carries the
|
|
149
|
-
* visibility symbol and the exact signature)
|
|
150
|
-
*
|
|
151
|
-
*
|
|
152
|
-
* row while keeping the marker text verbatim.
|
|
158
|
+
* visibility symbol and the exact signature); a declaration without a leading
|
|
159
|
+
* visibility symbol gets one prefixed so the row stays parseable. Synthetic
|
|
160
|
+
* detail markers never reach this function (see [[entityLines]]).
|
|
153
161
|
*/
|
|
154
162
|
function memberLine(member) {
|
|
155
163
|
const raw = (member.raw ?? '').trim();
|
package/dist/render/svg.js
CHANGED
|
@@ -166,7 +166,7 @@ function renderEntity(entity, node, theme) {
|
|
|
166
166
|
parts.push(rule(opsTop, 'ops'));
|
|
167
167
|
const titleBaseline = node.y + METRICS.paddingY + METRICS.fontSize + 1;
|
|
168
168
|
parts.push(textEl(displayName(entity), node.x + node.width / 2, titleBaseline, 'middle', font.size.title, font.weight.title, color.textTitle, theme, fontStyle));
|
|
169
|
-
const memberText = (member, idx, top, cls) => textEl(member.raw, node.x + METRICS.paddingX, top + METRICS.lineHeight * idx + METRICS.fontSize + 1, 'start', font.size.member, font.weight.member, color.textMember, theme, ` class="${cls}"`);
|
|
169
|
+
const memberText = (member, idx, top, cls) => textEl(member.display ?? member.raw, node.x + METRICS.paddingX, top + METRICS.lineHeight * idx + METRICS.fontSize + 1, 'start', font.size.member, font.weight.member, color.textMember, theme, ` class="${cls}"`);
|
|
170
170
|
attrs.forEach((member, i) => {
|
|
171
171
|
parts.push(memberText(member, i, titleBottom, 'uml-attr'));
|
|
172
172
|
});
|