@yalehwang/plantuml-js 0.5.0 → 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 +17 -3
- package/dist/core.js +12 -5
- package/dist/foundation/layout.d.ts +28 -0
- package/dist/foundation/layout.js +72 -4
- package/dist/foundation/model.d.ts +8 -0
- package/dist/parse/serialize.d.ts +10 -4
- package/dist/parse/serialize.js +18 -10
- 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
|
@@ -79,8 +79,8 @@ export interface RenderResult {
|
|
|
79
79
|
}
|
|
80
80
|
/**
|
|
81
81
|
* Everything {@link renderDetailed} computed for one diagram: the model that was
|
|
82
|
-
* actually laid out and drawn, the
|
|
83
|
-
* 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.
|
|
84
84
|
*
|
|
85
85
|
* This is a **public, stable API** (available from both the `.` and `./elk`
|
|
86
86
|
* entries). Consumers such as an architecture tab should read geometry from
|
|
@@ -101,8 +101,20 @@ export interface RenderResult {
|
|
|
101
101
|
* they can be translated into a thumbnail directly.
|
|
102
102
|
*/
|
|
103
103
|
export interface DetailedRender {
|
|
104
|
-
/**
|
|
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
|
+
*/
|
|
105
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;
|
|
106
118
|
/** Geometry actually used by the renderer; its node/package/edge rectangles are a stable contract (see above). */
|
|
107
119
|
layout: LayoutResult;
|
|
108
120
|
svg: string;
|
|
@@ -138,6 +150,8 @@ export declare function createRenderer(options?: RendererOptions): Renderer;
|
|
|
138
150
|
* Render one diagram through the exact pipeline `createRenderer` uses
|
|
139
151
|
* (view/transforms → detail → layout → svg) and return every intermediate, so
|
|
140
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.
|
|
141
155
|
*
|
|
142
156
|
* Accepts either a puml string (the counterpart of
|
|
143
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,16 +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, {
|
|
72
73
|
memberMaxRows: options.memberMaxRows,
|
|
73
74
|
memberMaxChars: options.memberMaxChars,
|
|
74
75
|
});
|
|
75
76
|
const started = performance.now();
|
|
76
|
-
const
|
|
77
|
+
const raw = await engine.layout(model);
|
|
77
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));
|
|
78
85
|
const svg = renderEngine.render(model, layout, { hiddenTransitive });
|
|
79
|
-
return { model, layout, svg, layoutMs, hiddenTransitive };
|
|
86
|
+
return { sourceModel, model, layout, svg, layoutMs, hiddenTransitive };
|
|
80
87
|
}
|
|
81
88
|
export async function renderDetailed(input, options = {}) {
|
|
82
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
|
|
@@ -125,6 +147,12 @@ export interface MemberDisplayLimits {
|
|
|
125
147
|
* of each drawn line (`memberMaxChars`). Truncation only sets
|
|
126
148
|
* `PlantUmlMember.display`; `raw` keeps the full declaration. Without limits
|
|
127
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.
|
|
128
156
|
*/
|
|
129
157
|
export declare function applyDetail(model: PlantUmlModel, detail?: DetailLevel, limits?: MemberDisplayLimits): PlantUmlModel;
|
|
130
158
|
/**
|
|
@@ -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
|
|
@@ -104,6 +162,12 @@ export function autoDetail(entityCount) {
|
|
|
104
162
|
* of each drawn line (`memberMaxChars`). Truncation only sets
|
|
105
163
|
* `PlantUmlMember.display`; `raw` keeps the full declaration. Without limits
|
|
106
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.
|
|
107
171
|
*/
|
|
108
172
|
export function applyDetail(model, detail, limits = {}) {
|
|
109
173
|
const level = detail ?? autoDetail(model.entities.length);
|
|
@@ -122,24 +186,28 @@ export function applyDetail(model, detail, limits = {}) {
|
|
|
122
186
|
return {
|
|
123
187
|
...model,
|
|
124
188
|
entities: model.entities.map((entity) => {
|
|
125
|
-
const
|
|
189
|
+
const authored = entity.members.filter((member) => member.synthetic !== true);
|
|
190
|
+
const total = authored.length;
|
|
126
191
|
if (level === 'names')
|
|
127
|
-
return
|
|
192
|
+
return entity.members.length === 0 ? entity : { ...entity, members: [] };
|
|
128
193
|
const rowCap = memberMaxRows !== undefined
|
|
129
194
|
? memberMaxRows
|
|
130
195
|
: level === 'compact'
|
|
131
196
|
? COMPACT_MEMBER_ROWS
|
|
132
197
|
: total;
|
|
133
198
|
if (total <= rowCap) {
|
|
134
|
-
|
|
199
|
+
if (!hasLimits && authored.length === entity.members.length)
|
|
200
|
+
return entity;
|
|
201
|
+
return { ...entity, members: authored.map(limitMember) };
|
|
135
202
|
}
|
|
136
|
-
const kept =
|
|
203
|
+
const kept = authored.slice(0, rowCap).map(limitMember);
|
|
137
204
|
const more = {
|
|
138
205
|
visibility: 'public',
|
|
139
206
|
name: '…',
|
|
140
207
|
kind: 'field',
|
|
141
208
|
modifiers: [],
|
|
142
209
|
raw: `… +${total - rowCap} more (${total} members)`,
|
|
210
|
+
synthetic: true,
|
|
143
211
|
};
|
|
144
212
|
return { ...entity, members: [...kept, more] };
|
|
145
213
|
}),
|
|
@@ -27,6 +27,14 @@ export interface PlantUmlMember {
|
|
|
27
27
|
* inspector views keep the full declaration.
|
|
28
28
|
*/
|
|
29
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;
|
|
30
38
|
}
|
|
31
39
|
export interface PlantUmlEntity {
|
|
32
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();
|