@zombie-mermaid/svg-renderer 4.2.2 → 5.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.
Files changed (72) hide show
  1. package/README.md +15 -1
  2. package/dist/index.cjs +52 -52
  3. package/dist/index.cjs.map +1 -1
  4. package/dist/index.d.cts +23 -4
  5. package/dist/index.d.ts +23 -4
  6. package/dist/index.js +897 -849
  7. package/dist/index.js.map +1 -1
  8. package/package.json +14 -5
  9. package/src/__tests__/c4-label-clearance-1290.test.ts +0 -112
  10. package/src/__tests__/c4-line-ends-1208.test.ts +0 -52
  11. package/src/__tests__/c4-mermaid-reference.test.ts +0 -124
  12. package/src/__tests__/c4-svg.test.ts +0 -337
  13. package/src/__tests__/c4-text-width-1210.test.ts +0 -43
  14. package/src/__tests__/class-component-order-1249.test.ts +0 -40
  15. package/src/__tests__/class-diamond-markers-1232.test.ts +0 -59
  16. package/src/__tests__/class-member-right-padding-1238.test.ts +0 -43
  17. package/src/__tests__/class-namespace-1196.test.ts +0 -165
  18. package/src/__tests__/class-namespace-sparse-elk-1196.test.ts +0 -96
  19. package/src/__tests__/class-visibility-markers-1237.test.ts +0 -27
  20. package/src/__tests__/edge-jog-straightening-1388.test.ts +0 -57
  21. package/src/__tests__/elk-adapter-utils.test.ts +0 -166
  22. package/src/__tests__/er-attribute-right-padding-1262.test.ts +0 -47
  23. package/src/__tests__/er-cardinality-markers-1233.test.ts +0 -35
  24. package/src/__tests__/er-default-direction-1250.test.ts +0 -69
  25. package/src/__tests__/er-edge-label-contrast-1244.test.ts +0 -58
  26. package/src/__tests__/fixtures/c4-mermaid-reference.json +0 -626
  27. package/src/__tests__/flowchart-parallel-edge-lanes-1308.test.ts +0 -168
  28. package/src/__tests__/pie-layout.test.ts +0 -215
  29. package/src/__tests__/pie-svg.test.ts +0 -194
  30. package/src/__tests__/render-mermaid-svg.test.ts +0 -64
  31. package/src/__tests__/sequence-label-clearance-1242.test.ts +0 -162
  32. package/src/__tests__/sequence-nested-activation-1241.test.ts +0 -37
  33. package/src/__tests__/svg-edge-to-own-cluster-1310.test.ts +0 -32
  34. package/src/__tests__/xychart-smooth-curve.test.ts +0 -42
  35. package/src/__tests__/xychart-x-label-overlap-1243.test.ts +0 -62
  36. package/src/c4/arial-widths.ts +0 -59
  37. package/src/c4/layout.ts +0 -680
  38. package/src/c4/metrics.ts +0 -261
  39. package/src/c4/renderer.ts +0 -366
  40. package/src/class/layout.ts +0 -525
  41. package/src/class/renderer.ts +0 -669
  42. package/src/edge-curves.ts +0 -205
  43. package/src/elk-instance.ts +0 -292
  44. package/src/er/layout.ts +0 -216
  45. package/src/er/renderer.ts +0 -516
  46. package/src/generated/mono-font-subset.ts +0 -32
  47. package/src/index.ts +0 -277
  48. package/src/layout-engine/back-edges.ts +0 -73
  49. package/src/layout-engine/compound-flat.ts +0 -916
  50. package/src/layout-engine/constants.ts +0 -19
  51. package/src/layout-engine/edge-bundling.ts +0 -458
  52. package/src/layout-engine/elk-adapter-utils.ts +0 -81
  53. package/src/layout-engine/elk-graph-builder.ts +0 -245
  54. package/src/layout-engine/from-elk.ts +0 -767
  55. package/src/layout-engine/inner-edges.ts +0 -329
  56. package/src/layout-engine/jog-straightening.ts +0 -52
  57. package/src/layout-engine/layer-alignment.ts +0 -174
  58. package/src/layout-engine/layout-hints.ts +0 -27
  59. package/src/layout-engine/to-elk.ts +0 -855
  60. package/src/layout-engine.ts +0 -115
  61. package/src/pie/layout.ts +0 -234
  62. package/src/pie/renderer.ts +0 -161
  63. package/src/registry.ts +0 -441
  64. package/src/renderer.ts +0 -1510
  65. package/src/resolve-colors.ts +0 -339
  66. package/src/sequence/layout.ts +0 -766
  67. package/src/sequence/renderer.ts +0 -687
  68. package/src/shape-clipping.ts +0 -197
  69. package/src/styles.ts +0 -145
  70. package/src/title-gaps.ts +0 -161
  71. package/src/xychart/layout.ts +0 -727
  72. package/src/xychart/renderer.ts +0 -676
@@ -1,516 +0,0 @@
1
- import type {
2
- PositionedErDiagram,
3
- PositionedErEntity,
4
- PositionedErRelationship,
5
- ErAttribute,
6
- Cardinality,
7
- } from '@zombie-mermaid/mermaid-parser'
8
- import type { DiagramColors, SvgEmitOptions } from '@zombie-mermaid/core'
9
- import {
10
- svgOpenTag,
11
- renderMultilineText,
12
- escapeXml as escapeXmlUtil,
13
- escapeAttr,
14
- measureMultilineText,
15
- f,
16
- } from '@zombie-mermaid/core'
17
- import { withDataSrc } from '../renderer.ts'
18
- import {
19
- buildSvgStyleBlock,
20
- FONT_SIZES,
21
- FONT_WEIGHTS,
22
- STROKE_WIDTHS,
23
- estimateTextWidth,
24
- TEXT_BASELINE_SHIFT,
25
- } from '../styles.ts'
26
- import type { FontSizes } from '../styles.ts'
27
-
28
- // ============================================================================
29
- // ER diagram SVG renderer
30
- //
31
- // Renders positioned ER diagrams to SVG.
32
- // All colors use CSS custom properties (var(--_xxx)) from the theme system.
33
- //
34
- // Render order:
35
- // 1. Relationship lines (behind boxes)
36
- // 2. Entity boxes (header + attribute rows)
37
- // 3. Cardinality markers (crow's foot notation)
38
- // 4. Relationship labels
39
- // ============================================================================
40
-
41
- /**
42
- * Relationship-label text: `--fg` mixed into `--bg` at this percentage.
43
- * The shared `--_text-muted` (40%) is ~2.4:1 on the default white theme and
44
- * `--_text-sec` (60%) is ~4.1:1, both under the WCAG AA 4.5:1 text minimum;
45
- * 70% is ~5.6:1 while still reading as secondary to the entity names (#1244).
46
- * Local to ER so other diagram types keep their tuned text hierarchy.
47
- */
48
- const ER_EDGE_LABEL_FG_PERCENT = 70
49
-
50
- /** Font sizes specific to ER diagrams */
51
- const ER_FONT = {
52
- attrSize: 11,
53
- attrWeight: 400,
54
- keySize: 9,
55
- keyWeight: 600,
56
- } as const
57
-
58
- /**
59
- * Render a positioned ER diagram as an SVG string.
60
- *
61
- * @param colors - DiagramColors with bg/fg and optional enrichment variables.
62
- * @param transparent - If true, renders with transparent background.
63
- * @param embedSource - Original diagram source to stamp onto the root `<svg>`
64
- * as `data-src` (from `options.embedSource`). Omitted
65
- * when the option is off.
66
- * @param title - Accessible name (from `options.title`). See svgOpenTag() in
67
- * packages/core/src/theme.ts.
68
- * @param decorative - Marks the SVG decorative (from `options.decorative`).
69
- * @param emit - Strict-CSP controls (from `options.nonce` /
70
- * `options.styleAttribute`, see #216). Default: no nonce,
71
- * root `style` attribute on.
72
- */
73
- export function renderErSvg(
74
- diagram: PositionedErDiagram,
75
- colors: DiagramColors,
76
- font: string = 'Inter',
77
- transparent: boolean = false,
78
- fontSizes: FontSizes = FONT_SIZES,
79
- embedSource?: string,
80
- title?: string,
81
- decorative?: boolean,
82
- emit: SvgEmitOptions = {},
83
- ): string {
84
- const parts: string[] = []
85
-
86
- // SVG root with CSS variables + style block (with mono font) + defs
87
- parts.push(
88
- withDataSrc(
89
- svgOpenTag(
90
- diagram.width,
91
- diagram.height,
92
- colors,
93
- transparent,
94
- title,
95
- decorative,
96
- undefined,
97
- emit.styleAttribute,
98
- ),
99
- embedSource,
100
- ),
101
- )
102
- parts.push(buildSvgStyleBlock(font, true, emit.nonce))
103
- parts.push('<defs>')
104
- parts.push('</defs>') // No marker defs — we draw crow's foot inline
105
-
106
- // 1. Relationship lines
107
- for (const rel of diagram.relationships) {
108
- parts.push(renderRelationshipLine(rel))
109
- }
110
-
111
- // 2. Entity boxes
112
- for (const entity of diagram.entities) {
113
- parts.push(renderEntityBox(entity, fontSizes))
114
- }
115
-
116
- // 3. Cardinality markers at relationship endpoints
117
- for (const rel of diagram.relationships) {
118
- parts.push(renderCardinality(rel))
119
- }
120
-
121
- // 4. Relationship labels
122
- for (const rel of diagram.relationships) {
123
- parts.push(renderRelationshipLabel(rel, fontSizes))
124
- }
125
-
126
- parts.push('</svg>')
127
- return parts.join('\n')
128
- }
129
-
130
- // ============================================================================
131
- // Entity box rendering
132
- // ============================================================================
133
-
134
- /**
135
- * Render an entity box with header and attribute rows.
136
- * Wrapped in <g class="entity"> with semantic data attributes.
137
- */
138
- function renderEntityBox(
139
- entity: PositionedErEntity,
140
- fontSizes: FontSizes,
141
- ): string {
142
- const {
143
- id,
144
- x,
145
- y,
146
- width,
147
- height,
148
- headerHeight,
149
- rowHeight,
150
- label,
151
- attributes,
152
- } = entity
153
- const parts: string[] = []
154
-
155
- // Semantic wrapper with entity metadata
156
- parts.push(
157
- f`<g class="entity" data-id="${escapeAttr(id)}" data-label="${escapeAttr(label)}">`,
158
- )
159
-
160
- // Outer rectangle
161
- parts.push(
162
- f` <rect x="${x}" y="${y}" width="${width}" height="${height}" ` +
163
- f`rx="0" ry="0" fill="var(--_node-fill)" stroke="var(--_node-stroke)" stroke-width="${STROKE_WIDTHS.outerBox}" />`,
164
- )
165
-
166
- // Header background
167
- parts.push(
168
- f` <rect x="${x}" y="${y}" width="${width}" height="${headerHeight}" ` +
169
- f`rx="0" ry="0" fill="var(--_group-hdr)" stroke="var(--_node-stroke)" stroke-width="${STROKE_WIDTHS.outerBox}" />`,
170
- )
171
-
172
- // Entity name (supports multi-line via <br> tags)
173
- parts.push(
174
- ' ' +
175
- renderMultilineText(
176
- label,
177
- x + width / 2,
178
- y + headerHeight / 2,
179
- fontSizes.nodeLabel,
180
- f`text-anchor="middle" font-size="${fontSizes.nodeLabel}" font-weight="700" fill="var(--_text)"`,
181
- ),
182
- )
183
-
184
- // Divider
185
- const attrTop = y + headerHeight
186
- parts.push(
187
- f` <line x1="${x}" y1="${attrTop}" x2="${x + width}" y2="${attrTop}" ` +
188
- f`stroke="var(--_node-stroke)" stroke-width="${STROKE_WIDTHS.innerBox}" />`,
189
- )
190
-
191
- // Attribute rows
192
- for (let i = 0; i < attributes.length; i++) {
193
- const attr = attributes[i]!
194
- const rowY = attrTop + i * rowHeight + rowHeight / 2
195
- parts.push(
196
- ' ' + renderAttribute(attr, x, rowY, width).replace(/\n/g, '\n '),
197
- )
198
- }
199
-
200
- // Empty row placeholder when no attributes
201
- if (attributes.length === 0) {
202
- parts.push(
203
- f` <text x="${x + width / 2}" y="${attrTop + rowHeight / 2}" text-anchor="middle" dy="${TEXT_BASELINE_SHIFT}" ` +
204
- f`font-size="${ER_FONT.attrSize}" fill="var(--_text-faint)" font-style="italic">(no attributes)</text>`,
205
- )
206
- }
207
-
208
- parts.push('</g>')
209
- return parts.join('\n')
210
- }
211
-
212
- /**
213
- * Render a single attribute row with monospace syntax highlighting.
214
- * Layout: [PK badge] type name (left-aligned in mono, name right-aligned)
215
- * Uses <tspan> elements for per-part coloring, matching the class diagram style.
216
- *
217
- * Key badge uses var(--_key-badge) for background tint.
218
- * Comments are shown as tooltips via SVG <title> element.
219
- */
220
- function renderAttribute(
221
- attr: ErAttribute,
222
- boxX: number,
223
- y: number,
224
- boxWidth: number,
225
- ): string {
226
- const parts: string[] = []
227
-
228
- // Wrap in a group if there's a comment (for tooltip support)
229
- const hasComment = attr.comment && attr.comment.length > 0
230
- if (attr.comment && attr.comment.length > 0) {
231
- // Replace <br> with newlines for tooltip display
232
- const tooltipText = attr.comment.replace(/<br\s*\/?>/gi, '\n')
233
- parts.push(f`<g><title>${escapeXml(tooltipText)}</title>`)
234
- }
235
-
236
- // Key badges on the left (keep proportional font — they're visual tags, not code)
237
- let keyWidth = 0
238
- if (attr.keys.length > 0) {
239
- const keyText = attr.keys.join(',')
240
- keyWidth =
241
- estimateTextWidth(keyText, ER_FONT.keySize, ER_FONT.keyWeight) + 8
242
- parts.push(
243
- f`<rect x="${boxX + 6}" y="${y - 7}" width="${keyWidth}" height="14" rx="2" ry="2" ` +
244
- `fill="var(--_key-badge)" />`,
245
- )
246
- parts.push(
247
- f`<text x="${boxX + 6 + keyWidth / 2}" y="${y}" text-anchor="middle" dy="${TEXT_BASELINE_SHIFT}" ` +
248
- f`font-size="${ER_FONT.keySize}" font-weight="${ER_FONT.keyWeight}" fill="var(--_text-sec)">${attr.keys.join(',')}</text>`,
249
- )
250
- }
251
-
252
- // Type (left-aligned after keys, monospace with syntax highlighting)
253
- const typeX = boxX + 8 + (keyWidth > 0 ? keyWidth + 6 : 0)
254
- parts.push(
255
- f`<text x="${typeX}" y="${y}" class="mono" dy="${TEXT_BASELINE_SHIFT}" ` +
256
- f`font-size="${ER_FONT.attrSize}" font-weight="${ER_FONT.attrWeight}">` +
257
- f`<tspan fill="var(--_text-muted)">${escapeXml(attr.type)}</tspan></text>`,
258
- )
259
-
260
- // Name (right-aligned, monospace with syntax highlighting)
261
- const nameX = boxX + boxWidth - 8
262
- parts.push(
263
- f`<text x="${nameX}" y="${y}" class="mono" text-anchor="end" dy="${TEXT_BASELINE_SHIFT}" ` +
264
- f`font-size="${ER_FONT.attrSize}" font-weight="${ER_FONT.attrWeight}">` +
265
- f`<tspan fill="var(--_text-sec)">${escapeXml(attr.name)}</tspan></text>`,
266
- )
267
-
268
- // Close the group if we opened one
269
- if (hasComment) {
270
- parts.push('</g>')
271
- }
272
-
273
- return parts.join('\n')
274
- }
275
-
276
- // ============================================================================
277
- // Relationship rendering
278
- // ============================================================================
279
-
280
- /**
281
- * Render a relationship line with semantic data attributes.
282
- */
283
- function renderRelationshipLine(rel: PositionedErRelationship): string {
284
- if (rel.points.length < 2) return ''
285
-
286
- const pathData = rel.points.map((p) => f`${p.x},${p.y}`).join(' ')
287
- const dashArray = !rel.identifying ? ' stroke-dasharray="6 4"' : ''
288
-
289
- // Semantic data attributes for relationship inspection
290
- const labelAttr = rel.label ? f` data-label="${escapeAttr(rel.label)}"` : ''
291
- const dataAttrs = [
292
- 'class="er-relationship"',
293
- f`data-entity1="${escapeAttr(rel.entity1)}"`,
294
- f`data-entity2="${escapeAttr(rel.entity2)}"`,
295
- f`data-cardinality1="${rel.cardinality1}"`,
296
- f`data-cardinality2="${rel.cardinality2}"`,
297
- f`data-identifying="${rel.identifying}"`,
298
- ]
299
-
300
- return (
301
- f`<polyline ${dataAttrs.join(' ')}${labelAttr} points="${pathData}" fill="none" stroke="var(--_line)" ` +
302
- f`stroke-width="${STROKE_WIDTHS.connector}"${dashArray} />`
303
- )
304
- }
305
-
306
- /** Render a relationship label at the midpoint (supports multi-line) */
307
- function renderRelationshipLabel(
308
- rel: PositionedErRelationship,
309
- fontSizes: FontSizes,
310
- ): string {
311
- if (!rel.label || rel.points.length < 2) return ''
312
-
313
- const mid = midpoint(rel.points)
314
- const metrics = measureMultilineText(
315
- rel.label,
316
- fontSizes.edgeLabel,
317
- FONT_WEIGHTS.edgeLabel,
318
- )
319
-
320
- // Background pill for readability
321
- const bgW = metrics.width + 8
322
- const bgH = metrics.height + 6
323
-
324
- return (
325
- f`<rect x="${mid.x - bgW / 2}" y="${mid.y - bgH / 2}" width="${bgW}" height="${bgH}" rx="2" ry="2" ` +
326
- `fill="var(--bg)" stroke="var(--_inner-stroke)" stroke-width="0.5" />` +
327
- f`\n${renderMultilineText(
328
- rel.label,
329
- mid.x,
330
- mid.y,
331
- fontSizes.edgeLabel,
332
- f`text-anchor="middle" font-size="${fontSizes.edgeLabel}" font-weight="${FONT_WEIGHTS.edgeLabel}" fill="color-mix(in srgb, var(--fg) ${ER_EDGE_LABEL_FG_PERCENT}%, var(--bg))"`,
333
- )}`
334
- )
335
- }
336
-
337
- /**
338
- * Render crow's foot cardinality markers at both endpoints of a relationship.
339
- *
340
- * Crow's foot notation:
341
- * 'one': ─║─ (single vertical line)
342
- * 'zero-one': ─o║─ (circle + single line)
343
- * 'many': ─╢─ (crow's foot + bar)
344
- * 'zero-many': ─o╣─ (circle + crow's foot)
345
- */
346
- function renderCardinality(rel: PositionedErRelationship): string {
347
- if (rel.points.length < 2) return ''
348
- const parts: string[] = []
349
-
350
- // Entity1 side (first point, direction toward second point)
351
- const p1 = rel.points[0]!
352
- const p2 = rel.points[1]!
353
- parts.push(renderCrowsFoot(p1, p2, rel.cardinality1))
354
-
355
- // Entity2 side (last point, direction toward second-to-last point)
356
- const pN = rel.points[rel.points.length - 1]!
357
- const pN1 = rel.points[rel.points.length - 2]!
358
- parts.push(renderCrowsFoot(pN, pN1, rel.cardinality2))
359
-
360
- return parts.join('\n')
361
- }
362
-
363
- /**
364
- * Render a crow's foot marker at a given endpoint.
365
- * `point` is the endpoint, `toward` gives the direction the line comes from.
366
- */
367
- function renderCrowsFoot(
368
- point: { x: number; y: number },
369
- toward: { x: number; y: number },
370
- cardinality: Cardinality,
371
- ): string {
372
- const parts: string[] = []
373
- const sw = STROKE_WIDTHS.connector + 0.25
374
-
375
- // Calculate direction from toward → point (unit vector)
376
- const dx = point.x - toward.x
377
- const dy = point.y - toward.y
378
- const len = Math.sqrt(dx * dx + dy * dy)
379
- if (len === 0) return ''
380
- const ux = dx / len
381
- const uy = dy / len
382
-
383
- // Perpendicular direction
384
- const px = -uy
385
- const py = ux
386
-
387
- // Marker sits 4px from the endpoint, extending 12px back along the edge
388
- const tipX = point.x - ux * 4
389
- const tipY = point.y - uy * 4
390
- const backX = point.x - ux * 16
391
- const backY = point.y - uy * 16
392
-
393
- // Single line: always present for 'one' and part of others
394
- const hasOneLine = cardinality === 'one' || cardinality === 'zero-one'
395
- const hasCrowsFoot = cardinality === 'many' || cardinality === 'zero-many'
396
- const hasCircle = cardinality === 'zero-one' || cardinality === 'zero-many'
397
-
398
- // Draw single vertical line (perpendicular to edge) at the tip
399
- if (hasOneLine) {
400
- const halfW = 6
401
- parts.push(
402
- f`<line x1="${tipX + px * halfW}" y1="${tipY + py * halfW}" ` +
403
- f`x2="${tipX - px * halfW}" y2="${tipY - py * halfW}" ` +
404
- f`stroke="var(--_line)" stroke-width="${sw}" />`,
405
- )
406
- // Second line slightly back for "exactly one" emphasis
407
- const line2X = tipX - ux * 4
408
- const line2Y = tipY - uy * 4
409
- parts.push(
410
- f`<line x1="${line2X + px * halfW}" y1="${line2Y + py * halfW}" ` +
411
- f`x2="${line2X - px * halfW}" y2="${line2Y - py * halfW}" ` +
412
- f`stroke="var(--_line)" stroke-width="${sw}" />`,
413
- )
414
- }
415
-
416
- // Crow's foot (three lines fanning out from tip)
417
- if (hasCrowsFoot) {
418
- const fanW = 7
419
- // Center line
420
- const cfTipX = tipX
421
- const cfTipY = tipY
422
- // Three lines from tip to back, fanning out
423
- parts.push(
424
- // Top fan line
425
- f`<line x1="${cfTipX + px * fanW}" y1="${cfTipY + py * fanW}" ` +
426
- f`x2="${backX}" y2="${backY}" ` +
427
- f`stroke="var(--_line)" stroke-width="${sw}" />`,
428
- )
429
- parts.push(
430
- // Center line
431
- f`<line x1="${cfTipX}" y1="${cfTipY}" ` +
432
- f`x2="${backX}" y2="${backY}" ` +
433
- f`stroke="var(--_line)" stroke-width="${sw}" />`,
434
- )
435
- parts.push(
436
- // Bottom fan line
437
- f`<line x1="${cfTipX - px * fanW}" y1="${cfTipY - py * fanW}" ` +
438
- f`x2="${backX}" y2="${backY}" ` +
439
- f`stroke="var(--_line)" stroke-width="${sw}" />`,
440
- )
441
- }
442
-
443
- // Bar across the foot's convergence point: distinguishes one-or-many
444
- // from a bare crow's foot (#1233)
445
- if (cardinality === 'many') {
446
- const barX = point.x - ux * 20
447
- const barY = point.y - uy * 20
448
- const halfW = 6
449
- parts.push(
450
- f`<line x1="${barX + px * halfW}" y1="${barY + py * halfW}" ` +
451
- f`x2="${barX - px * halfW}" y2="${barY - py * halfW}" ` +
452
- f`stroke="var(--_line)" stroke-width="${sw}" />`,
453
- )
454
- }
455
-
456
- // Circle (for zero variants)
457
- if (hasCircle) {
458
- const circleOffset = hasCrowsFoot ? 20 : 12
459
- const circleX = point.x - ux * circleOffset
460
- const circleY = point.y - uy * circleOffset
461
- parts.push(
462
- f`<circle cx="${circleX}" cy="${circleY}" r="4" ` +
463
- f`fill="var(--bg)" stroke="var(--_line)" stroke-width="${sw}" />`,
464
- )
465
- }
466
-
467
- return parts.join('\n')
468
- }
469
-
470
- /** Compute the arc-length midpoint of a polyline path.
471
- * Walks along each segment, finds the point at exactly 50% of total path length.
472
- * This ensures the label sits ON the path even for orthogonal routes with bends,
473
- * unlike the naive first/last geometric center which floats in space for L/Z shapes. */
474
- function midpoint(points: Array<{ x: number; y: number }>): {
475
- x: number
476
- y: number
477
- } {
478
- if (points.length === 0) return { x: 0, y: 0 }
479
- if (points.length === 1) return points[0]!
480
-
481
- // Compute total path length
482
- let totalLen = 0
483
- for (let i = 1; i < points.length; i++) {
484
- const dx = points[i]!.x - points[i - 1]!.x
485
- const dy = points[i]!.y - points[i - 1]!.y
486
- totalLen += Math.sqrt(dx * dx + dy * dy)
487
- }
488
-
489
- if (totalLen === 0) return points[0]!
490
-
491
- // Walk to 50% of total length, interpolating within the segment that crosses the halfway mark
492
- const halfLen = totalLen / 2
493
- let walked = 0
494
- for (let i = 1; i < points.length; i++) {
495
- const dx = points[i]!.x - points[i - 1]!.x
496
- const dy = points[i]!.y - points[i - 1]!.y
497
- const segLen = Math.sqrt(dx * dx + dy * dy)
498
- if (walked + segLen >= halfLen) {
499
- const t = segLen > 0 ? (halfLen - walked) / segLen : 0
500
- return {
501
- x: points[i - 1]!.x + dx * t,
502
- y: points[i - 1]!.y + dy * t,
503
- }
504
- }
505
- walked += segLen
506
- }
507
-
508
- return points[points.length - 1]!
509
- }
510
-
511
- // ============================================================================
512
- // Utilities
513
- // ============================================================================
514
-
515
- // Use shared escapeXml from multiline-utils
516
- const escapeXml = escapeXmlUtil
@@ -1,32 +0,0 @@
1
- /**
2
- * GENERATED FILE — do not edit by hand.
3
- *
4
- * Regenerate with `pnpm run build:mono-font`
5
- * (scripts/build-mono-font-subset.ts), which subsets
6
- * third_party/fonts/jetbrains-mono-nl/JetBrainsMonoNL-Regular.ttf down to
7
- * Basic Latin + Latin-1 Supplement — the self-hosted, embeddable
8
- * `@font-face` packages/svg-renderer/src/styles.ts's `buildSvgStyleBlock()` inlines
9
- * into an SVG's own `<style>` for its `.mono` rule (class-diagram method
10
- * signatures, ER-diagram attribute types), replacing a Google Fonts CDN
11
- * `@import` this library's SVG output previously depended on by default
12
- * (#1061). See that script's header comment for why this subset is
13
- * deliberately narrower than the site's own (demo/components/generated/
14
- * mono-font-subset.ts).
15
- */
16
-
17
- /** The self-hosted family name every consumer's font stack should list
18
- * first — matches this subset's own `name` table entry. */
19
- export const MONO_FONT_FAMILY = 'JetBrains Mono NL'
20
-
21
- /** The `@font-face` rule embedding the subset as a base64 `woff2` data
22
- * URI — no separate network request, no page-depth-relative path to get
23
- * wrong (see the script header comment for why data-URI over a static
24
- * asset file). */
25
- export const MONO_FONT_FACE_CSS = `@font-face {
26
- font-family: 'JetBrains Mono NL';
27
- font-style: normal;
28
- font-weight: 400;
29
- font-display: swap;
30
- src: url(data:font/woff2;base64,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) format('woff2');
31
- unicode-range: U+0020-007E, U+00A0-00FF;
32
- }`