@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.
Files changed (65) hide show
  1. package/README.md +20 -0
  2. package/dist/core.d.ts +106 -13
  3. package/dist/core.js +92 -52
  4. package/dist/foundation/labels.d.ts +44 -0
  5. package/dist/foundation/labels.js +231 -0
  6. package/dist/foundation/layers.d.ts +25 -0
  7. package/dist/foundation/layers.js +101 -0
  8. package/dist/foundation/layout.d.ts +116 -0
  9. package/dist/foundation/layout.js +298 -0
  10. package/dist/{model.d.ts → foundation/model.d.ts} +20 -0
  11. package/dist/foundation/relation-semantics.d.ts +49 -0
  12. package/dist/foundation/relation-semantics.js +112 -0
  13. package/dist/{text-measure.d.ts → foundation/text-measure.d.ts} +2 -0
  14. package/dist/{text-measure.js → foundation/text-measure.js} +20 -1
  15. package/dist/index.d.ts +2 -2
  16. package/dist/index.js +1 -1
  17. package/dist/layout/compound.d.ts +74 -0
  18. package/dist/layout/compound.js +1204 -0
  19. package/dist/layout/elk-graph.d.ts +36 -0
  20. package/dist/layout/elk-graph.js +220 -0
  21. package/dist/layout/elk-result.d.ts +18 -0
  22. package/dist/layout/elk-result.js +127 -0
  23. package/dist/layout/elk.d.ts +80 -33
  24. package/dist/layout/elk.js +229 -320
  25. package/dist/layout/fanout-trunk.d.ts +18 -0
  26. package/dist/layout/fanout-trunk.js +380 -0
  27. package/dist/layout/grid-router.d.ts +69 -0
  28. package/dist/layout/grid-router.js +615 -0
  29. package/dist/layout/lane-spread.d.ts +11 -0
  30. package/dist/layout/lane-spread.js +302 -0
  31. package/dist/layout/package-endpoints.d.ts +10 -0
  32. package/dist/layout/package-endpoints.js +54 -0
  33. package/dist/layout/placement-strategy.d.ts +98 -0
  34. package/dist/layout/placement-strategy.js +26 -0
  35. package/dist/layout/strategy.d.ts +27 -0
  36. package/dist/layout/strategy.js +19 -0
  37. package/dist/layout/trunk-lanes.d.ts +29 -0
  38. package/dist/layout/trunk-lanes.js +308 -0
  39. package/dist/layout/viz.d.ts +3 -3
  40. package/dist/layout/viz.js +13 -17
  41. package/dist/metrics/layout-metrics.d.ts +183 -0
  42. package/dist/metrics/layout-metrics.js +766 -0
  43. package/dist/parse/parser.d.ts +43 -0
  44. package/dist/{parser.js → parse/parser.js} +111 -5
  45. package/dist/parse/serialize.d.ts +32 -0
  46. package/dist/parse/serialize.js +218 -0
  47. package/dist/render/legend.d.ts +15 -0
  48. package/dist/render/legend.js +42 -0
  49. package/dist/render/svg.d.ts +60 -3
  50. package/dist/render/svg.js +326 -98
  51. package/dist/render/theme.d.ts +109 -0
  52. package/dist/render/theme.js +99 -0
  53. package/dist/{transform.d.ts → transform/transform.d.ts} +55 -1
  54. package/dist/{transform.js → transform/transform.js} +277 -29
  55. package/package.json +4 -3
  56. package/dist/layout-metrics.d.ts +0 -42
  57. package/dist/layout-metrics.js +0 -241
  58. package/dist/layout.d.ts +0 -75
  59. package/dist/layout.js +0 -51
  60. package/dist/parser.d.ts +0 -17
  61. /package/dist/{edge-router.d.ts → foundation/edge-router.d.ts} +0 -0
  62. /package/dist/{edge-router.js → foundation/edge-router.js} +0 -0
  63. /package/dist/{model.js → foundation/model.js} +0 -0
  64. /package/dist/{render-dot.d.ts → layout/render-dot.d.ts} +0 -0
  65. /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 { ElkLayoutEngine } from './layout/elk.js';
13
- import { renderSvg } from './render/svg.js';
14
- import { ViewSpec, TransformOptions } from './transform.js';
15
- export type { PlantUmlModel, PlantUmlEntity, PlantUmlMember, PlantUmlRelation, PlantUmlPackage, EntityKind, RelationType, } from './model.js';
16
- export type { LayoutEngine, LayoutResult, NodeLayout, EdgeLayout, Point, } from './layout.js';
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 { computeLayoutMetrics } from './layout-metrics.js';
104
- export type { LayoutMetrics } from './layout-metrics.js';
105
- export { applyView, transformModel, aggregateRelations, transitiveReduce, summarizeHubs, markCycles, groupByPathPrefix, } from './transform.js';
106
- export type { ViewSpec, TransformOptions } from './transform.js';
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 = parsePlantUml(puml);
45
- const model = transformForRender(parsed, options);
46
- const layout = await engine.layout(model);
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 transformModel(viewed, { aggregate: true, cycles: true, ...options.transform });
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
- if (options.transform)
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
- const options = { aggregate: true, cycles: true };
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 transformModel(model, { ...options, ...overrides });
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 = ENTITY_DECL.exec(code);
157
- const name = match ? extractDeclaredName(match[1]) : null;
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 = ENTITY_DECL.exec(code);
208
- const name = match ? extractDeclaredName(match[1]) : null;
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
- * Extract a declaration name from an entity line's tail, mirroring the parser:
233
- * accepts a bare token (`class A`) or a quoted name (`class "User Repo"`), and
234
- * tolerates a trailing `as <alias>`. Returns null when the name is missing or
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[];