@yalehwang/plantuml-js 0.2.2 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -0
- package/dist/core.d.ts +102 -9
- package/dist/core.js +91 -51
- 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 +59 -0
- package/dist/layout/compound.js +1178 -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 +63 -33
- package/dist/layout/elk.js +215 -321
- 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/layout-metrics.d.ts +183 -0
- package/dist/layout/layout-metrics.js +766 -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/parser.d.ts +27 -1
- package/dist/parser.js +111 -5
- package/dist/render/labels.d.ts +44 -0
- package/dist/render/labels.js +231 -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/serialize.d.ts +32 -0
- package/dist/serialize.js +218 -0
- package/dist/transform.d.ts +55 -1
- package/dist/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/{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 → foundation/render-dot.d.ts} +0 -0
- /package/dist/{render-dot.js → foundation/render-dot.js} +0 -0
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Theme — every visual decision of the SVG renderer as a token. `svg.ts` and
|
|
3
|
+
* `legend.ts` read only from a resolved Theme, so no colour, size or dash
|
|
4
|
+
* pattern lives as a literal in drawing code. Rationale and the font decision
|
|
5
|
+
* are recorded in docs/visual-design.md.
|
|
6
|
+
*/
|
|
7
|
+
export const defaultTheme = {
|
|
8
|
+
font: {
|
|
9
|
+
family: 'monospace',
|
|
10
|
+
size: { title: 13, member: 12, packageLabel: 12, edgeLabel: 11, badge: 11 },
|
|
11
|
+
weight: { title: 'bold', member: 'normal', packageLabel: 'bold' },
|
|
12
|
+
},
|
|
13
|
+
color: {
|
|
14
|
+
canvas: '#ffffff',
|
|
15
|
+
stroke: '#64748b',
|
|
16
|
+
strokeStub: '#7c8ba1',
|
|
17
|
+
nodeFill: '#ffffff',
|
|
18
|
+
stubFill: '#f1f5f9',
|
|
19
|
+
textTitle: '#1e293b',
|
|
20
|
+
textMember: '#475569',
|
|
21
|
+
textPackage: '#334155',
|
|
22
|
+
edge: '#5b6a80',
|
|
23
|
+
edgeWeak: '#566479',
|
|
24
|
+
labelHalo: '#ffffff',
|
|
25
|
+
warn: '#3b0a0a',
|
|
26
|
+
accent: '#475569',
|
|
27
|
+
accentText: '#ffffff',
|
|
28
|
+
packageFills: ['#f8fafc', '#f1f5f9', '#eef2f7', '#e8eef5'],
|
|
29
|
+
packageStroke: '#94a3b8',
|
|
30
|
+
legendStroke: '#94a3b8',
|
|
31
|
+
},
|
|
32
|
+
shape: { nodeRadius: 6, packageRadius: 8, edgeCornerRadius: 6 },
|
|
33
|
+
stroke: {
|
|
34
|
+
node: 1,
|
|
35
|
+
package: 1,
|
|
36
|
+
edge: 1,
|
|
37
|
+
arrow: 1,
|
|
38
|
+
weightStep: 0.5,
|
|
39
|
+
edgeWidthMax: 4,
|
|
40
|
+
feedback: 1.5,
|
|
41
|
+
heavyWeight: 3,
|
|
42
|
+
opacity: { light: 0.8, medium: 0.9, strong: 1, feedback: 0.95 },
|
|
43
|
+
edgeNonScaling: true,
|
|
44
|
+
labelHalo: 3,
|
|
45
|
+
},
|
|
46
|
+
dash: { weak: '6,3', stub: '5,3', feedback: '10,3,2,3' },
|
|
47
|
+
arrow: { triangle: 12, chevron: 10, diamond: 14 },
|
|
48
|
+
grid: 8,
|
|
49
|
+
};
|
|
50
|
+
/** Merge a partial theme over the defaults (arrays replace, objects merge). */
|
|
51
|
+
export function resolveTheme(overrides) {
|
|
52
|
+
return mergeDeep(defaultTheme, overrides ?? {});
|
|
53
|
+
}
|
|
54
|
+
function mergeDeep(base, over) {
|
|
55
|
+
if (over === undefined)
|
|
56
|
+
return base;
|
|
57
|
+
if (typeof base === 'object' && base !== null && !Array.isArray(base) &&
|
|
58
|
+
typeof over === 'object' && over !== null && !Array.isArray(over)) {
|
|
59
|
+
const out = {};
|
|
60
|
+
const b = base;
|
|
61
|
+
const o = over;
|
|
62
|
+
for (const key of Object.keys(b))
|
|
63
|
+
out[key] = mergeDeep(b[key], o[key]);
|
|
64
|
+
return out;
|
|
65
|
+
}
|
|
66
|
+
return over;
|
|
67
|
+
}
|
|
68
|
+
/** Every colour value of a theme (for token-membership assertions). */
|
|
69
|
+
export function themeColors(theme) {
|
|
70
|
+
const out = new Set();
|
|
71
|
+
for (const v of Object.values(theme.color)) {
|
|
72
|
+
if (Array.isArray(v))
|
|
73
|
+
v.forEach((c) => out.add(c));
|
|
74
|
+
else
|
|
75
|
+
out.add(v);
|
|
76
|
+
}
|
|
77
|
+
return out;
|
|
78
|
+
}
|
|
79
|
+
/** `fg` at `alpha` composited over `bg` (both `#rrggbb`), as a `#rrggbb` colour. */
|
|
80
|
+
export function blendOver(fg, bg, alpha) {
|
|
81
|
+
const f = parseInt(fg.slice(1), 16);
|
|
82
|
+
const b = parseInt(bg.slice(1), 16);
|
|
83
|
+
const mix = (shift) => Math.round(alpha * ((f >> shift) & 255) + (1 - alpha) * ((b >> shift) & 255));
|
|
84
|
+
return `#${((mix(16) << 16) | (mix(8) << 8) | mix(0)).toString(16).padStart(6, '0')}`;
|
|
85
|
+
}
|
|
86
|
+
/** WCAG relative luminance contrast ratio between two `#rrggbb` colours. */
|
|
87
|
+
export function contrastRatio(a, b) {
|
|
88
|
+
const la = luminance(a);
|
|
89
|
+
const lb = luminance(b);
|
|
90
|
+
return (Math.max(la, lb) + 0.05) / (Math.min(la, lb) + 0.05);
|
|
91
|
+
}
|
|
92
|
+
function luminance(hex) {
|
|
93
|
+
const n = parseInt(hex.slice(1), 16);
|
|
94
|
+
const ch = [(n >> 16) & 255, (n >> 8) & 255, n & 255].map((v) => {
|
|
95
|
+
const s = v / 255;
|
|
96
|
+
return s <= 0.03928 ? s / 12.92 : ((s + 0.055) / 1.055) ** 2.4;
|
|
97
|
+
});
|
|
98
|
+
return 0.2126 * ch[0] + 0.7152 * ch[1] + 0.0722 * ch[2];
|
|
99
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PlantUmlModel → PlantUML class-diagram text.
|
|
3
|
+
*
|
|
4
|
+
* The inverse of [[parsePlantUml]]: `toPlantUml(renderDetailed(...).model)`
|
|
5
|
+
* reproduces the diagram that was actually drawn (after view/transform/detail)
|
|
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
|
|
8
|
+
* (`toPlantUml(applyView(model, {kind: 'neighborhood', entity, hops}))`); this
|
|
9
|
+
* function never chooses a range on its own.
|
|
10
|
+
*
|
|
11
|
+
* Shapes that only exist after parsing (stub/external/usageCount, aggregated
|
|
12
|
+
* edge weights, cycle markers) are rendering semantics with no PlantUML
|
|
13
|
+
* spelling; they serialize as their plain-text equivalent (stub → class,
|
|
14
|
+
* weighted edge → one edge). The parts that do have a spelling — packages
|
|
15
|
+
* (nested), entity kind/generics/alias, member rows, relation arrow/label/
|
|
16
|
+
* cardinality — round-trip back through [[parsePlantUml]] unchanged.
|
|
17
|
+
*
|
|
18
|
+
* The displayed name is always emitted quoted (`class "<<function>> halt"
|
|
19
|
+
* as n0`), so names carrying a stereotype prefix or `/` path separators survive
|
|
20
|
+
* the round trip as authored.
|
|
21
|
+
*/
|
|
22
|
+
import type { PlantUmlModel } from './foundation/model.js';
|
|
23
|
+
/**
|
|
24
|
+
* Serialize a model to `@startuml … @enduml` class-diagram source.
|
|
25
|
+
*
|
|
26
|
+
* Emitted layout: nested `package` blocks first (each holding the entities that
|
|
27
|
+
* carry its `packageId`), then package-less entities, then every relation.
|
|
28
|
+
* Entities always use the quoted form with an explicit alias, which is the only
|
|
29
|
+
* form that carries a display name containing whitespace, `/`, or a
|
|
30
|
+
* `<<stereotype>>` prefix.
|
|
31
|
+
*/
|
|
32
|
+
export declare function toPlantUml(model: PlantUmlModel): string;
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PlantUmlModel → PlantUML class-diagram text.
|
|
3
|
+
*
|
|
4
|
+
* The inverse of [[parsePlantUml]]: `toPlantUml(renderDetailed(...).model)`
|
|
5
|
+
* reproduces the diagram that was actually drawn (after view/transform/detail)
|
|
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
|
|
8
|
+
* (`toPlantUml(applyView(model, {kind: 'neighborhood', entity, hops}))`); this
|
|
9
|
+
* function never chooses a range on its own.
|
|
10
|
+
*
|
|
11
|
+
* Shapes that only exist after parsing (stub/external/usageCount, aggregated
|
|
12
|
+
* edge weights, cycle markers) are rendering semantics with no PlantUML
|
|
13
|
+
* spelling; they serialize as their plain-text equivalent (stub → class,
|
|
14
|
+
* weighted edge → one edge). The parts that do have a spelling — packages
|
|
15
|
+
* (nested), entity kind/generics/alias, member rows, relation arrow/label/
|
|
16
|
+
* cardinality — round-trip back through [[parsePlantUml]] unchanged.
|
|
17
|
+
*
|
|
18
|
+
* The displayed name is always emitted quoted (`class "<<function>> halt"
|
|
19
|
+
* as n0`), so names carrying a stereotype prefix or `/` path separators survive
|
|
20
|
+
* the round trip as authored.
|
|
21
|
+
*/
|
|
22
|
+
/** A bare PlantUML identifier (what `as <alias>` accepts). */
|
|
23
|
+
const PLANTUML_ID = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
24
|
+
const INDENT = ' ';
|
|
25
|
+
const VISIBILITY_SYMBOL = {
|
|
26
|
+
public: '+',
|
|
27
|
+
private: '-',
|
|
28
|
+
protected: '#',
|
|
29
|
+
'package-private': '~',
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* One arrow per relation type, written in canonical source→target direction.
|
|
33
|
+
* The parser's own arrow table also accepts the swapped mirror forms
|
|
34
|
+
* (`<|--`, `--o`, …); emitting the canonical form is what makes the round trip
|
|
35
|
+
* stable.
|
|
36
|
+
*/
|
|
37
|
+
const RELATION_ARROW = {
|
|
38
|
+
inheritance: '--|>',
|
|
39
|
+
implementation: '..|>',
|
|
40
|
+
dependency: '..>',
|
|
41
|
+
association: '-->',
|
|
42
|
+
aggregation: 'o--',
|
|
43
|
+
composition: '*--',
|
|
44
|
+
};
|
|
45
|
+
/**
|
|
46
|
+
* Serialize a model to `@startuml … @enduml` class-diagram source.
|
|
47
|
+
*
|
|
48
|
+
* Emitted layout: nested `package` blocks first (each holding the entities that
|
|
49
|
+
* carry its `packageId`), then package-less entities, then every relation.
|
|
50
|
+
* Entities always use the quoted form with an explicit alias, which is the only
|
|
51
|
+
* form that carries a display name containing whitespace, `/`, or a
|
|
52
|
+
* `<<stereotype>>` prefix.
|
|
53
|
+
*/
|
|
54
|
+
export function toPlantUml(model) {
|
|
55
|
+
const entities = new AliasBook('entity');
|
|
56
|
+
const packages = new AliasBook('package');
|
|
57
|
+
const lines = ['@startuml'];
|
|
58
|
+
const nodes = packageNodes(model);
|
|
59
|
+
const emitted = new Set();
|
|
60
|
+
for (const node of nodes.roots)
|
|
61
|
+
emitPackage(node, 0, lines, entities, packages, emitted);
|
|
62
|
+
// Defensive: a parentId cycle leaves nodes unreachable from any root; emit
|
|
63
|
+
// each remaining package as a root rather than silently dropping it.
|
|
64
|
+
for (const node of nodes.all) {
|
|
65
|
+
if (!emitted.has(node.pkg.id))
|
|
66
|
+
emitPackage(node, 0, lines, entities, packages, emitted);
|
|
67
|
+
}
|
|
68
|
+
for (const entity of nodes.topLevel) {
|
|
69
|
+
lines.push(...entityLines(entity, 0, entities));
|
|
70
|
+
}
|
|
71
|
+
for (const relation of model.relations) {
|
|
72
|
+
lines.push(relationLine(relation, entities));
|
|
73
|
+
}
|
|
74
|
+
lines.push('@enduml');
|
|
75
|
+
return lines.join('\n');
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Index packages into a tree, attaching each entity to the innermost package it
|
|
79
|
+
* names. An entity whose `packageId` names no declared package is emitted at
|
|
80
|
+
* the top level instead of being dropped.
|
|
81
|
+
*/
|
|
82
|
+
function packageNodes(model) {
|
|
83
|
+
const byId = new Map();
|
|
84
|
+
const all = [];
|
|
85
|
+
for (const pkg of model.packages) {
|
|
86
|
+
// A duplicated id keeps its first package; the parser never produces one.
|
|
87
|
+
if (byId.has(pkg.id))
|
|
88
|
+
continue;
|
|
89
|
+
const node = { pkg, children: [], entities: [] };
|
|
90
|
+
byId.set(pkg.id, node);
|
|
91
|
+
all.push(node);
|
|
92
|
+
}
|
|
93
|
+
const roots = [];
|
|
94
|
+
for (const node of all) {
|
|
95
|
+
const parentId = node.pkg.parentId;
|
|
96
|
+
const parent = parentId === undefined ? undefined : byId.get(parentId);
|
|
97
|
+
if (parent !== undefined && parent !== node)
|
|
98
|
+
parent.children.push(node);
|
|
99
|
+
else
|
|
100
|
+
roots.push(node);
|
|
101
|
+
}
|
|
102
|
+
const topLevel = [];
|
|
103
|
+
for (const entity of model.entities) {
|
|
104
|
+
const owner = entity.packageId === undefined ? undefined : byId.get(entity.packageId);
|
|
105
|
+
if (owner !== undefined)
|
|
106
|
+
owner.entities.push(entity);
|
|
107
|
+
else
|
|
108
|
+
topLevel.push(entity);
|
|
109
|
+
}
|
|
110
|
+
return { roots, all, topLevel };
|
|
111
|
+
}
|
|
112
|
+
function emitPackage(node, depth, lines, entities, packages, emitted) {
|
|
113
|
+
if (emitted.has(node.pkg.id))
|
|
114
|
+
return;
|
|
115
|
+
emitted.add(node.pkg.id);
|
|
116
|
+
const indent = INDENT.repeat(depth);
|
|
117
|
+
const alias = packages.alias(node.pkg.id, node.pkg.name);
|
|
118
|
+
lines.push(`${indent}package ${quoteName(node.pkg.name)} as ${alias} {`);
|
|
119
|
+
for (const entity of node.entities) {
|
|
120
|
+
lines.push(...entityLines(entity, depth + 1, entities));
|
|
121
|
+
}
|
|
122
|
+
for (const child of node.children) {
|
|
123
|
+
emitPackage(child, depth + 1, lines, entities, packages, emitted);
|
|
124
|
+
}
|
|
125
|
+
lines.push(`${indent}}`);
|
|
126
|
+
}
|
|
127
|
+
function entityKeyword(entity) {
|
|
128
|
+
return entity.kind === 'abstract' ? 'abstract class' : entity.kind;
|
|
129
|
+
}
|
|
130
|
+
function entityLines(entity, depth, aliases) {
|
|
131
|
+
const indent = INDENT.repeat(depth);
|
|
132
|
+
const alias = aliases.alias(entity.id, entity.name);
|
|
133
|
+
const head = `${entityKeyword(entity)} ${quoteName(entity.name)}${genericSuffix(entity)} as ${alias}`;
|
|
134
|
+
if (entity.members.length === 0)
|
|
135
|
+
return [`${indent}${head}`];
|
|
136
|
+
const lines = [`${indent}${head} {`];
|
|
137
|
+
for (const member of entity.members) {
|
|
138
|
+
lines.push(`${indent}${INDENT}${memberLine(member)}`);
|
|
139
|
+
}
|
|
140
|
+
lines.push(`${indent}}`);
|
|
141
|
+
return lines;
|
|
142
|
+
}
|
|
143
|
+
function genericSuffix(entity) {
|
|
144
|
+
const generics = entity.generics ?? [];
|
|
145
|
+
return generics.length === 0 ? '' : `~${generics.join(',')}~`;
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* One member row. The authored `raw` text is authoritative (it carries the
|
|
149
|
+
* visibility symbol and the exact signature), except for synthesized rows —
|
|
150
|
+
* the detail pipeline's `… +N more (M members)` truncation marker has no
|
|
151
|
+
* visibility symbol, so it gets one prefixed to stay parseable as a member
|
|
152
|
+
* row while keeping the marker text verbatim.
|
|
153
|
+
*/
|
|
154
|
+
function memberLine(member) {
|
|
155
|
+
const raw = (member.raw ?? '').trim();
|
|
156
|
+
const symbol = VISIBILITY_SYMBOL[member.visibility] ?? '+';
|
|
157
|
+
if (raw === '')
|
|
158
|
+
return symbol;
|
|
159
|
+
if (/^[+\-#~]/.test(raw))
|
|
160
|
+
return raw;
|
|
161
|
+
return `${symbol} ${raw}`;
|
|
162
|
+
}
|
|
163
|
+
function relationLine(relation, aliases) {
|
|
164
|
+
const source = aliases.alias(relation.source, relation.source);
|
|
165
|
+
const target = aliases.alias(relation.target, relation.target);
|
|
166
|
+
const parts = [source];
|
|
167
|
+
if (relation.sourceCardinality !== undefined)
|
|
168
|
+
parts.push(quoteName(relation.sourceCardinality));
|
|
169
|
+
parts.push(RELATION_ARROW[relation.type] ?? '-->');
|
|
170
|
+
if (relation.targetCardinality !== undefined)
|
|
171
|
+
parts.push(quoteName(relation.targetCardinality));
|
|
172
|
+
parts.push(target);
|
|
173
|
+
let line = parts.join(' ');
|
|
174
|
+
if (relation.label !== undefined && relation.label !== '') {
|
|
175
|
+
line += ` : ${relation.label.replace(/[\r\n]+/g, ' ')}`;
|
|
176
|
+
}
|
|
177
|
+
return line;
|
|
178
|
+
}
|
|
179
|
+
/** Quote a display name; a `"` inside a PlantUML name is written as `'`. */
|
|
180
|
+
function quoteName(name) {
|
|
181
|
+
const safe = name.replace(/[\r\n\t]+/g, ' ').replace(/"/g, "'");
|
|
182
|
+
return `"${safe}"`;
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Deterministic, collision-free alias assignment, in emission order. A
|
|
186
|
+
* plant-legal id is kept verbatim (so a parsed model round-trips its ids); an
|
|
187
|
+
* id PlantUML cannot spell (view-internal ids such as `ov::src`, `stub::x`)
|
|
188
|
+
* falls back to an identifier derived from the display name.
|
|
189
|
+
*/
|
|
190
|
+
class AliasBook {
|
|
191
|
+
used = new Set();
|
|
192
|
+
byId = new Map();
|
|
193
|
+
fallback;
|
|
194
|
+
constructor(fallback) {
|
|
195
|
+
this.fallback = fallback;
|
|
196
|
+
}
|
|
197
|
+
alias(id, name) {
|
|
198
|
+
const existing = this.byId.get(id);
|
|
199
|
+
if (existing !== undefined)
|
|
200
|
+
return existing;
|
|
201
|
+
const base = PLANTUML_ID.test(id) ? id : identifierFrom(name, this.fallback);
|
|
202
|
+
let alias = base;
|
|
203
|
+
let suffix = 2;
|
|
204
|
+
while (this.used.has(alias))
|
|
205
|
+
alias = `${base}_${suffix++}`;
|
|
206
|
+
this.used.add(alias);
|
|
207
|
+
this.byId.set(id, alias);
|
|
208
|
+
return alias;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
/** Derive a bare identifier from a display name (`Src Layer` → `Src_Layer`). */
|
|
212
|
+
function identifierFrom(name, fallback) {
|
|
213
|
+
const cleaned = name
|
|
214
|
+
.replace(/[^A-Za-z0-9_]+/g, '_')
|
|
215
|
+
.replace(/^_+|_+$/g, '');
|
|
216
|
+
const base = cleaned === '' ? fallback : cleaned;
|
|
217
|
+
return /^[A-Za-z_]/.test(base) ? base : `_${base}`;
|
|
218
|
+
}
|
package/dist/transform.d.ts
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* input and produces the same output for the same input, so a transformed
|
|
10
10
|
* render stays byte-for-byte reproducible.
|
|
11
11
|
*/
|
|
12
|
-
import { PlantUmlModel } from './model.js';
|
|
12
|
+
import { PlantUmlModel } from './foundation/model.js';
|
|
13
13
|
/** Options driving the model-transform pipeline. */
|
|
14
14
|
export interface TransformOptions {
|
|
15
15
|
/**
|
|
@@ -25,6 +25,12 @@ export interface TransformOptions {
|
|
|
25
25
|
* hub summarization. Default undefined.
|
|
26
26
|
*/
|
|
27
27
|
hubThreshold?: number;
|
|
28
|
+
/**
|
|
29
|
+
* `'aggregate'` folds all relations between two packages into one
|
|
30
|
+
* box-to-box weighted corridor edge; `'raw'` keeps every edge. Default:
|
|
31
|
+
* `'aggregate'` above [[INTER_PACKAGE_AGGREGATE_ENTITY_THRESHOLD]] entities.
|
|
32
|
+
*/
|
|
33
|
+
interPackage?: 'aggregate' | 'raw';
|
|
28
34
|
/** Detect cycles and mark one edge per cycle as a `feedback` arc. Default false. */
|
|
29
35
|
cycles?: boolean;
|
|
30
36
|
}
|
|
@@ -44,19 +50,59 @@ export type ViewSpec = {
|
|
|
44
50
|
} | {
|
|
45
51
|
kind: 'package';
|
|
46
52
|
id: string;
|
|
53
|
+
stubGroupThreshold?: number;
|
|
47
54
|
} | {
|
|
48
55
|
kind: 'neighborhood';
|
|
49
56
|
entity: string;
|
|
50
57
|
hops: number;
|
|
51
58
|
};
|
|
59
|
+
/** Entity count above which inter-package edges aggregate into corridors by default. */
|
|
60
|
+
export declare const INTER_PACKAGE_AGGREGATE_ENTITY_THRESHOLD = 150;
|
|
61
|
+
/** Stub count above which a single-package view groups stubs by name prefix. */
|
|
62
|
+
export declare const DEFAULT_STUB_GROUP_THRESHOLD = 8;
|
|
52
63
|
/**
|
|
53
64
|
* Run the transform pipeline over a model. Order matters: aggregation first
|
|
54
65
|
* (so transitive reduction sees one edge per pair), then reduction, then hub
|
|
55
66
|
* summarization, then cycle marking on the final edge set.
|
|
56
67
|
*/
|
|
57
68
|
export declare function transformModel(model: PlantUmlModel, options?: TransformOptions): PlantUmlModel;
|
|
69
|
+
/** [[transformModel]] plus the number of edges the transitive reduction hid. */
|
|
70
|
+
export declare function transformModelDetailed(model: PlantUmlModel, options?: TransformOptions): {
|
|
71
|
+
model: PlantUmlModel;
|
|
72
|
+
hiddenTransitive: number;
|
|
73
|
+
};
|
|
74
|
+
/** Edge count ÷ node count above which `edgeReduction: 'auto'` reduces a pure dependency graph. */
|
|
75
|
+
export declare const AUTO_REDUCTION_EDGE_NODE_RATIO = 3;
|
|
76
|
+
/**
|
|
77
|
+
* Whether `edgeReduction: 'auto'` applies: every relation is bundlable
|
|
78
|
+
* (dependency/association) and there are more than
|
|
79
|
+
* [[AUTO_REDUCTION_EDGE_NODE_RATIO]] times as many edges as nodes.
|
|
80
|
+
*/
|
|
81
|
+
export declare function shouldAutoReduce(model: PlantUmlModel): boolean;
|
|
82
|
+
/** Incoming edges at which an external type's edges collapse into a count badge. */
|
|
83
|
+
export declare const EXTERNAL_SUMMARY_MIN_IN_EDGES = 3;
|
|
84
|
+
/** Id of the synthetic package that gathers implicit/external types. */
|
|
85
|
+
export declare const EXTERNAL_PACKAGE_ID = "__external__";
|
|
86
|
+
/**
|
|
87
|
+
* Implicit/external types (`Error`, `EventEmitter`, …) receive many parallel
|
|
88
|
+
* edges that would fly across the whole canvas. Once one has `minEdges` or more
|
|
89
|
+
* incoming relations they are dropped and replaced by a `usageCount` badge that
|
|
90
|
+
* carries the original incoming-edge count.
|
|
91
|
+
*/
|
|
92
|
+
export declare function summarizeExternals(model: PlantUmlModel, minEdges: number): PlantUmlModel;
|
|
93
|
+
/** Gather top-level external entities into one synthetic External package. */
|
|
94
|
+
export declare function groupExternals(model: PlantUmlModel): PlantUmlModel;
|
|
58
95
|
/** Merge relations sharing (source, target) into one edge with `weight`. */
|
|
59
96
|
export declare function aggregateRelations(model: PlantUmlModel): PlantUmlModel;
|
|
97
|
+
/**
|
|
98
|
+
* Fold every relation between two different packages into one corridor edge
|
|
99
|
+
* per ordered package pair: `weight` = number of originals, `descriptions`
|
|
100
|
+
* keeps each original, and the endpoints are the entities carrying most of the
|
|
101
|
+
* traffic with `sourcePackageId`/`targetPackageId` set so the layout clips the
|
|
102
|
+
* edge to the frame boundaries. Intra-package edges and edges touching
|
|
103
|
+
* package-less entities, or between nested packages, are left as they are.
|
|
104
|
+
*/
|
|
105
|
+
export declare function aggregateInterPackage(model: PlantUmlModel): PlantUmlModel;
|
|
60
106
|
/**
|
|
61
107
|
* Remove a relation A→B when A still reaches B through another path (an edge
|
|
62
108
|
* derivable by transitivity). Only the redundant direct edge is dropped; the
|
|
@@ -90,3 +136,11 @@ export declare function applyView(model: PlantUmlModel, view?: ViewSpec): PlantU
|
|
|
90
136
|
* edges are dropped (they do not cross the collapsed boundary).
|
|
91
137
|
*/
|
|
92
138
|
export declare function groupByPathPrefix(model: PlantUmlModel, depth: number): PlantUmlModel;
|
|
139
|
+
/**
|
|
140
|
+
* Past `threshold` stubs, merge stubs sharing a name prefix into one group stub
|
|
141
|
+
* (`Src Cli … (×6)`), dropping trailing words until the group count fits (or nothing shortens any
|
|
142
|
+
* more). The edges to the members of a group collapse onto
|
|
143
|
+
* the group stub per (entity, direction): `weight` counts the originals and
|
|
144
|
+
* `descriptions` keeps each one for the edge's `<title>`.
|
|
145
|
+
*/
|
|
146
|
+
export declare function groupStubs(model: Pick<PlantUmlModel, 'entities' | 'relations'>, threshold: number): Pick<PlantUmlModel, 'entities' | 'relations'>;
|