@zombie-mermaid/svg-renderer 2.2.1 → 3.0.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.
@@ -0,0 +1,351 @@
1
+ // ============================================================================
2
+ // zombie-mermaid — SVG diagram-type registry
3
+ //
4
+ // A per-type registration table `renderMermaidSVG` (./index.ts) looks up
5
+ // instead of hand-maintaining its own `switch (diagramType)` over the same
6
+ // `DiagramType`. Moved here from the umbrella's `src/diagram-registry.ts`
7
+ // under issue #1111, for parity with `@zombie-mermaid/ascii-renderer`'s own
8
+ // `registry.ts` — see docs/decisions/diagram-type-registry-partial.md for
9
+ // why the SVG and ASCII halves stayed two separate tables rather than one
10
+ // shared `DiagramModule` with both a `renderSvg` and a `renderAscii` method
11
+ // (short version: one table made this module import out of
12
+ // `packages/ascii-renderer/`, while that package's `index.ts` imported this
13
+ // one back, a cycle that blocked the monorepo split and dragged `elkjs`
14
+ // into `dist/ascii.js`).
15
+ //
16
+ // Every diagram type is registered here, including 'flowchart' — see the
17
+ // decision doc for why it was the last holdout (its ASCII path had no
18
+ // per-type wrapper function to slot in until #745 extracted
19
+ // `renderFlowchartAscii`, and its SVG path carries `%%{init: ...}%%`
20
+ // directive handling no other type has — see `flowchartModule.layoutForSvg`
21
+ // below for how that's folded in without growing `SvgRenderContext`). The
22
+ // front door (`renderMermaidSVG` in ./index.ts) has no fallback switch left;
23
+ // every `DiagramType` is a hit here.
24
+ //
25
+ // `packages/core/src/diagram-type.ts` (the `DiagramType` union + `detectDiagramType`)
26
+ // stays exactly as-is and is what the front door uses to key into this
27
+ // table — this module doesn't touch detection.
28
+ //
29
+ // `parseMermaid` (flowchart/state parsing) reaches out to the umbrella's
30
+ // `../../../src/parser.ts` rather than a `@zombie-mermaid/*` package
31
+ // specifier — flowchart/state parsing was deliberately never extracted into
32
+ // `@zombie-mermaid/mermaid-parser` (see that package's scoping doc), the same
33
+ // pre-existing boundary call `packages/ascii-renderer/src/flowchart.ts`
34
+ // already reaches across for the ASCII side. `vite.config.ts` bundles that
35
+ // file straight into this package's own dist, exactly as it already does
36
+ // for `ascii-renderer` — see that file's header comment.
37
+ // ============================================================================
38
+
39
+ import type {
40
+ DiagramType,
41
+ RenderOptions,
42
+ DiagramColors,
43
+ SvgEmitOptions,
44
+ MermaidGraph,
45
+ PositionedGraph,
46
+ CurveStyle,
47
+ Statement,
48
+ } from '@zombie-mermaid/core'
49
+ import type { FontSizes } from './styles.ts'
50
+ import { withDirectionOverride } from '@zombie-mermaid/core'
51
+
52
+ import {
53
+ parseXYChart,
54
+ parseErDiagram,
55
+ parseSequenceDiagram,
56
+ parseClassDiagram,
57
+ } from '@zombie-mermaid/mermaid-parser'
58
+ import type {
59
+ XYChart,
60
+ PositionedXYChart,
61
+ ErDiagram,
62
+ PositionedErDiagram,
63
+ SequenceDiagram,
64
+ PositionedSequenceDiagram,
65
+ ClassDiagram,
66
+ PositionedClassDiagram,
67
+ } from '@zombie-mermaid/mermaid-parser'
68
+ import { layoutXYChart } from './xychart/layout.ts'
69
+ import { renderXYChartSvg } from './xychart/renderer.ts'
70
+ import { layoutErDiagramSync } from './er/layout.ts'
71
+ import { renderErSvg } from './er/renderer.ts'
72
+ import { layoutSequenceDiagram } from './sequence/layout.ts'
73
+ import { renderSequenceSvg } from './sequence/renderer.ts'
74
+ import { layoutClassDiagramSync } from './class/layout.ts'
75
+ import { renderClassSvg } from './class/renderer.ts'
76
+ import { layoutGraphSync } from './layout-engine.ts'
77
+ import { renderSvg as renderFlowchartSvg } from './renderer.ts'
78
+ import { parseMermaid } from '../../../src/parser.ts'
79
+
80
+ /**
81
+ * Parameters shared by every per-type SVG renderer today, factored out of
82
+ * the positional-argument lists that otherwise differ type to type (the
83
+ * inconsistency issue #533 flags — confirmed by reading all five renderer
84
+ * signatures: `embedSource`/`title`/`decorative`/`emit` shift position
85
+ * across `renderSvg`, `renderSequenceSvg`, `renderClassSvg`, `renderErSvg`,
86
+ * `renderXYChartSvg`, and `renderErSvg` drops `linksEnabled` entirely).
87
+ *
88
+ * Fields only one or two types need (`curve`, `animationEnabled`,
89
+ * `linksEnabled`, `interactive`) are deliberately left OUT of this shared
90
+ * shape rather than grown in for every exception — each type's own
91
+ * `renderSvg` adapter below derives those straight from `options`, exactly
92
+ * as each existing `renderMermaidSVGRaw` switch case already did.
93
+ */
94
+ export interface SvgRenderContext {
95
+ colors: DiagramColors
96
+ font: string
97
+ transparent: boolean
98
+ fontSizes: FontSizes
99
+ embedSource?: string
100
+ title?: string
101
+ decorative?: boolean
102
+ emit: SvgEmitOptions
103
+ }
104
+
105
+ /**
106
+ * One diagram type's full registration.
107
+ *
108
+ * `layoutForSvg` is deliberately SVG-only — it is NOT shared with the ASCII
109
+ * side, unlike the issue's original `{ detect, parse, layout, renderSvg,
110
+ * renderAscii }` sketch. Reading every ASCII per-type module confirmed why a
111
+ * single shared `layout` step would be fiction, not simplification, for this
112
+ * codebase: SVG layout produces pixel coordinates (`PositionedXYChart`,
113
+ * `PositionedErDiagram`, …), while every ASCII renderer does its own,
114
+ * unrelated grid/canvas layout internally — see e.g.
115
+ * `packages/ascii-renderer/src/xychart.ts`'s file header: "Uses the parsed
116
+ * XYChart type directly (not PositionedXYChart) since pixel coordinates
117
+ * don't map to character grids." `parse` genuinely is shared in the sense
118
+ * that both front doors call the same `parseXYChart`/`parseErDiagram`, but
119
+ * each ASCII renderer reruns that parse itself from raw text — which is why
120
+ * the ASCII entries live in `packages/ascii-renderer/src/registry.ts` as
121
+ * plain `(text, …) => string` functions instead of a `renderAscii` method on
122
+ * this interface.
123
+ *
124
+ * `parse` takes both `lines` (the pre-split statement list from
125
+ * `splitStatements(decoded)`, computed once by the front door — what every
126
+ * already-registered type's parser wants) and `text` (the raw, un-split
127
+ * source `flowchartModule` below needs instead): `parseMermaid`'s
128
+ * `%%{init: ...}%%` directive extraction reads raw, un-commented lines that
129
+ * `splitStatements` has already discarded by the time `lines` exists, and
130
+ * its multi-line-statement continuation merging needs each statement's
131
+ * *originating physical line* grouping, which `splitStatements`'s flattened
132
+ * array has already lost. `xychartModule`/`erModule`/`sequenceModule`/
133
+ * `classModule` all ignore the second parameter — JS/TS functions may take
134
+ * fewer parameters than their declared type allows, so `parse: parseXYChart`
135
+ * (etc.) is unchanged from before this parameter was added.
136
+ */
137
+ export interface DiagramModule<TDiagram = unknown, TPositioned = unknown> {
138
+ readonly type: DiagramType
139
+ parse(lines: Statement[], text: string): TDiagram
140
+ layoutForSvg(diagram: TDiagram, options: RenderOptions): TPositioned
141
+ renderSvg(
142
+ positioned: TPositioned,
143
+ ctx: SvgRenderContext,
144
+ options: RenderOptions,
145
+ ): string
146
+ }
147
+
148
+ const xychartModule: DiagramModule<XYChart, PositionedXYChart> = {
149
+ type: 'xychart',
150
+ parse: parseXYChart,
151
+ layoutForSvg: layoutXYChart,
152
+ renderSvg(positioned, ctx, options) {
153
+ // Mirrors resolveXYChartInteractive() in the umbrella's src/index.ts
154
+ // exactly — `interactivity` wins when set, otherwise the deprecated
155
+ // `interactive` boolean keeps controlling this as before.
156
+ const interactive =
157
+ options.interactivity !== undefined
158
+ ? options.interactivity === 'full'
159
+ : (options.interactive ?? false)
160
+ return renderXYChartSvg(
161
+ positioned,
162
+ ctx.colors,
163
+ ctx.font,
164
+ ctx.transparent,
165
+ interactive,
166
+ ctx.embedSource,
167
+ ctx.title,
168
+ ctx.decorative,
169
+ ctx.emit,
170
+ )
171
+ },
172
+ }
173
+
174
+ const sequenceModule: DiagramModule<
175
+ SequenceDiagram,
176
+ PositionedSequenceDiagram
177
+ > = {
178
+ type: 'sequence',
179
+ parse: parseSequenceDiagram,
180
+ layoutForSvg: layoutSequenceDiagram,
181
+ renderSvg(positioned, ctx) {
182
+ return renderSequenceSvg(
183
+ positioned,
184
+ ctx.colors,
185
+ ctx.font,
186
+ ctx.transparent,
187
+ ctx.fontSizes,
188
+ ctx.embedSource,
189
+ ctx.title,
190
+ ctx.decorative,
191
+ ctx.emit,
192
+ )
193
+ },
194
+ }
195
+
196
+ /**
197
+ * Mirrors `resolveLinksEnabled()` in the umbrella's src/index.ts exactly
198
+ * (`interactivity` defaults unset to `'static'`; only `'none'` turns links
199
+ * off) — duplicated here rather than imported since that helper is private
200
+ * to src/index.ts, which now imports this module (importing it back would
201
+ * cycle).
202
+ */
203
+ function resolveLinksEnabled(options: RenderOptions): boolean {
204
+ const interactivity = options.interactivity ?? 'static'
205
+ return interactivity !== 'none'
206
+ }
207
+
208
+ /**
209
+ * Mirrors `resolveAnimationEnabled()` in the umbrella's src/index.ts exactly
210
+ * (`interactivity` defaults unset to `'static'`; only `'full'` turns
211
+ * animation on) — duplicated here for the same import-direction reason
212
+ * `resolveLinksEnabled` above is.
213
+ */
214
+ function resolveAnimationEnabled(options: RenderOptions): boolean {
215
+ const interactivity = options.interactivity ?? 'static'
216
+ return interactivity === 'full'
217
+ }
218
+
219
+ const classModule: DiagramModule<ClassDiagram, PositionedClassDiagram> = {
220
+ type: 'class',
221
+ parse: parseClassDiagram,
222
+ layoutForSvg: layoutClassDiagramSync,
223
+ renderSvg(positioned, ctx, options) {
224
+ return renderClassSvg(
225
+ positioned,
226
+ ctx.colors,
227
+ ctx.font,
228
+ ctx.transparent,
229
+ ctx.fontSizes,
230
+ ctx.embedSource,
231
+ ctx.title,
232
+ ctx.decorative,
233
+ resolveLinksEnabled(options),
234
+ ctx.emit,
235
+ )
236
+ },
237
+ }
238
+
239
+ const erModule: DiagramModule<ErDiagram, PositionedErDiagram> = {
240
+ type: 'er',
241
+ parse: parseErDiagram,
242
+ // `options.direction` replaces the diagram's own top-level `direction`
243
+ // line, if any, before layout — same order as the umbrella's original
244
+ // 'er' case: parse, then withDirectionOverride, then layout.
245
+ layoutForSvg(diagram, options) {
246
+ return layoutErDiagramSync(
247
+ withDirectionOverride(diagram, options.direction),
248
+ options,
249
+ )
250
+ },
251
+ renderSvg(positioned, ctx) {
252
+ return renderErSvg(
253
+ positioned,
254
+ ctx.colors,
255
+ ctx.font,
256
+ ctx.transparent,
257
+ ctx.fontSizes,
258
+ ctx.embedSource,
259
+ ctx.title,
260
+ ctx.decorative,
261
+ ctx.emit,
262
+ )
263
+ },
264
+ }
265
+
266
+ /**
267
+ * `renderSvg`'s (the low-level flowchart/state SVG emitter, imported above
268
+ * as `renderFlowchartSvg`) `curve` parameter is the one piece of
269
+ * `SvgRenderContext`-adjacent state no other registered type needs: a
270
+ * `%%{init: {"flowchart": {"curve": ...}}}%%` directive on the diagram
271
+ * itself can supply a default, and `options.curve` always wins when set —
272
+ * see `applyInitConfig()` in `packages/core/src/init-directive.ts`, which
273
+ * the umbrella's pre-registry flowchart path used to call directly.
274
+ *
275
+ * That resolution needs the parsed diagram (for `initConfig`) AND the
276
+ * caller's `options` together, and the *result* is only needed later, by
277
+ * `renderSvg` — so rather than teach `SvgRenderContext` a diagram-specific
278
+ * `curve` field (every other field there is genuinely shared across types),
279
+ * `layoutForSvg` below resolves it once and carries it forward on
280
+ * `PositionedFlowchart`, right next to the `PositionedGraph` it was
281
+ * resolved alongside. `animationEnabled`/`linksEnabled` need no such
282
+ * carry-through: both derive from `options` alone via the
283
+ * `resolveAnimationEnabled`/`resolveLinksEnabled` helpers above, exactly
284
+ * like `class`'s `linksEnabled` already does, so `flowchartModule.renderSvg`
285
+ * just calls them directly.
286
+ */
287
+ export interface PositionedFlowchart {
288
+ graph: PositionedGraph
289
+ curve: CurveStyle
290
+ }
291
+
292
+ const flowchartModule: DiagramModule<MermaidGraph, PositionedFlowchart> = {
293
+ type: 'flowchart',
294
+ // Ignores `lines` — see the `parse` doc comment on `DiagramModule` above
295
+ // for why flowchart/state needs the raw `text` instead.
296
+ parse: (_lines, text) => parseMermaid(text),
297
+ layoutForSvg(diagram, options) {
298
+ // Same order as the umbrella's original fallback: direction override,
299
+ // then layout. `options.curve` wins over the diagram's own
300
+ // `%%{init: ...}%%` directive, which wins over the 'linear' default —
301
+ // see the doc comment on `PositionedFlowchart` above.
302
+ const graph = withDirectionOverride(diagram, options.direction)
303
+ return {
304
+ graph: layoutGraphSync(graph, options),
305
+ curve: options.curve ?? diagram.initConfig?.curve ?? 'linear',
306
+ }
307
+ },
308
+ renderSvg(positioned, ctx, options) {
309
+ return renderFlowchartSvg(
310
+ positioned.graph,
311
+ ctx.colors,
312
+ ctx.font,
313
+ ctx.transparent,
314
+ ctx.fontSizes,
315
+ positioned.curve,
316
+ ctx.embedSource,
317
+ resolveAnimationEnabled(options),
318
+ resolveLinksEnabled(options),
319
+ ctx.title,
320
+ ctx.decorative,
321
+ ctx.emit,
322
+ )
323
+ },
324
+ }
325
+
326
+ /**
327
+ * The registry proper — every `DiagramType` is looked up here by the SVG
328
+ * front door (`renderMermaidSVG` in ./index.ts), which has no fallback
329
+ * switch left. The ASCII front door's equivalent table is `asciiRegistry`
330
+ * in `packages/ascii-renderer/src/registry.ts`.
331
+ *
332
+ * Typed with `any` type parameters at the map level: each entry's own
333
+ * `TDiagram`/`TPositioned` are only known inside that entry's own closure
334
+ * (see `xychartModule`/`erModule`/`flowchartModule` above, which are fully
335
+ * typed); the map just needs one consistent shape to hold heterogeneous
336
+ * entries in.
337
+ */
338
+ type AnyDiagramModule = DiagramModule<
339
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- see comment above; each entry is fully typed at its own definition site.
340
+ any,
341
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- see comment above; each entry is fully typed at its own definition site.
342
+ any
343
+ >
344
+
345
+ export const diagramRegistry: Record<DiagramType, AnyDiagramModule> = {
346
+ xychart: xychartModule,
347
+ er: erModule,
348
+ sequence: sequenceModule,
349
+ class: classModule,
350
+ flowchart: flowchartModule,
351
+ }
package/src/layout.ts DELETED
@@ -1,8 +0,0 @@
1
- /**
2
- * Layout module for flowchart and state diagrams.
3
- *
4
- * Uses ELK.js for graph layout — battle-tested, full subgraph support,
5
- * orthogonal edge routing, and direction overrides.
6
- */
7
-
8
- export { layoutGraphSync } from './layout-engine.ts'