@zombie-mermaid/svg-renderer 3.0.0 → 3.2.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.
@@ -16,6 +16,7 @@ import {
16
16
  escapeXml as escapeXmlUtil,
17
17
  escapeAttr,
18
18
  safeHref,
19
+ f,
19
20
  } from '@zombie-mermaid/core'
20
21
  import { withDataSrc } from '../renderer.ts'
21
22
  import {
@@ -161,13 +162,14 @@ function relationshipMarkerDefs(): string {
161
162
  ` <marker id="cls-inherit" markerWidth="12" markerHeight="10" refX="12" refY="5" orient="auto-start-reverse">` +
162
163
  `\n <polygon points="0 0, 12 5, 0 10" fill="var(--bg)" stroke="var(--_arrow)" stroke-width="1.5" />` +
163
164
  `\n </marker>` +
164
- // Filled diamond (composition) — points at source
165
- `\n <marker id="cls-composition" markerWidth="12" markerHeight="10" refX="0" refY="5" orient="auto-start-reverse">` +
166
- `\n <polygon points="6 0, 12 5, 6 10, 0 5" fill="var(--_arrow)" stroke="var(--_arrow)" stroke-width="1" />` +
165
+ // Filled diamond (composition) — tip sits on the endpoint, body runs along the line;
166
+ // the polygon is inset so the stroke isn't clipped by the marker viewport
167
+ `\n <marker id="cls-composition" markerWidth="18" markerHeight="14" refX="16.5" refY="7" orient="auto-start-reverse">` +
168
+ `\n <polygon points="9 1.5, 16.5 7, 9 12.5, 1.5 7" fill="var(--_arrow)" stroke="var(--_arrow)" stroke-width="1" />` +
167
169
  `\n </marker>` +
168
- // Hollow diamond (aggregation) — points at source
169
- `\n <marker id="cls-aggregation" markerWidth="12" markerHeight="10" refX="0" refY="5" orient="auto-start-reverse">` +
170
- `\n <polygon points="6 0, 12 5, 6 10, 0 5" fill="var(--bg)" stroke="var(--_arrow)" stroke-width="1.5" />` +
170
+ // Hollow diamond (aggregation) — tip sits on the endpoint, body runs along the line
171
+ `\n <marker id="cls-aggregation" markerWidth="18" markerHeight="14" refX="16.5" refY="7" orient="auto-start-reverse">` +
172
+ `\n <polygon points="9 1.5, 16.5 7, 9 12.5, 1.5 7" fill="var(--bg)" stroke="var(--_arrow)" stroke-width="1.5" />` +
171
173
  `\n </marker>` +
172
174
  // Open arrow (association, dependency)
173
175
  `\n <marker id="cls-arrow" markerWidth="8" markerHeight="6" refX="8" refY="3" orient="auto-start-reverse">` +
@@ -230,20 +232,20 @@ function renderClassBox(
230
232
  // data-label: class name
231
233
  // data-annotation: stereotype (interface, abstract, etc.)
232
234
  const annotationAttr = cls.annotation
233
- ? ` data-annotation="${escapeAttr(cls.annotation)}"`
235
+ ? f` data-annotation="${escapeAttr(cls.annotation)}"`
234
236
  : ''
235
237
  // A style class from `cssClass`/`class A name`/`:::name` is emitted onto
236
238
  // the group so external CSS can target it, after the same allowlist the
237
239
  // flowchart renderer applies.
238
240
  const safeClassName = sanitizeClassName(cls.className)
239
241
  const groupAttrs = [
240
- `class="${safeClassName ? `class-node ${safeClassName}` : 'class-node'}"`,
241
- `data-id="${escapeAttr(cls.id)}"`,
242
- `data-label="${escapeAttr(cls.label)}"`,
242
+ f`class="${safeClassName ? f`class-node ${safeClassName}` : 'class-node'}"`,
243
+ f`data-id="${escapeAttr(cls.id)}"`,
244
+ f`data-label="${escapeAttr(cls.label)}"`,
243
245
  ]
244
246
  // `click ClassName call fn()` is parsed, never invoked, and never written
245
247
  // into the markup — see renderNode() in packages/svg-renderer/src/renderer.ts for the rationale.
246
- parts.push(`<g ${groupAttrs.join(' ')}${annotationAttr}>`)
248
+ parts.push(f`<g ${groupAttrs.join(' ')}${annotationAttr}>`)
247
249
 
248
250
  // An href becomes a real SVG link, which needs no script to work.
249
251
  // `interactivity: 'none'` strips it — a link is meaningless in
@@ -251,28 +253,28 @@ function renderClassBox(
251
253
  const href = linksEnabled ? safeHref(interaction?.href) : undefined
252
254
  if (href) {
253
255
  const targetAttr = interaction?.target
254
- ? ` target="${escapeAttr(interaction.target)}"`
256
+ ? f` target="${escapeAttr(interaction.target)}"`
255
257
  : ''
256
- parts.push(` <a href="${escapeAttr(href)}"${targetAttr}>`)
258
+ parts.push(f` <a href="${escapeAttr(href)}"${targetAttr}>`)
257
259
  }
258
260
 
259
261
  if (linksEnabled && interaction?.tooltip) {
260
262
  // <title> is SVG's native tooltip — no script, no CSS. Gated by
261
263
  // linksEnabled alongside href above: both come from the same `click`
262
264
  // statement, and 'none' strips both.
263
- parts.push(` <title>${escapeXml(interaction.tooltip)}</title>`)
265
+ parts.push(f` <title>${escapeXml(interaction.tooltip)}</title>`)
264
266
  }
265
267
 
266
268
  // Outer rectangle (full box)
267
269
  parts.push(
268
- ` <rect x="${x}" y="${y}" width="${width}" height="${height}" ` +
269
- `rx="0" ry="0" fill="${fill}" stroke="${stroke}" stroke-width="${strokeWidth}" />`,
270
+ f` <rect x="${x}" y="${y}" width="${width}" height="${height}" ` +
271
+ f`rx="0" ry="0" fill="${fill}" stroke="${stroke}" stroke-width="${strokeWidth}" />`,
270
272
  )
271
273
 
272
274
  // Header background
273
275
  parts.push(
274
- ` <rect x="${x}" y="${y}" width="${width}" height="${headerHeight}" ` +
275
- `rx="0" ry="0" fill="${headerFill}" stroke="${stroke}" stroke-width="${strokeWidth}" />`,
276
+ f` <rect x="${x}" y="${y}" width="${width}" height="${headerHeight}" ` +
277
+ f`rx="0" ry="0" fill="${headerFill}" stroke="${stroke}" stroke-width="${strokeWidth}" />`,
276
278
  )
277
279
 
278
280
  // Annotation (<<interface>>, <<abstract>>, etc.)
@@ -280,9 +282,9 @@ function renderClassBox(
280
282
  if (cls.annotation) {
281
283
  const annotY = y + 12
282
284
  parts.push(
283
- ` <text x="${x + width / 2}" y="${annotY}" text-anchor="middle" dy="${TEXT_BASELINE_SHIFT}" ` +
284
- `font-size="${CLS_FONT.annotationSize}" font-weight="${CLS_FONT.annotationWeight}" ` +
285
- `font-style="italic" fill="${textColor ?? 'var(--_text-muted)'}">&lt;&lt;${escapeXml(cls.annotation)}&gt;&gt;</text>`,
285
+ f` <text x="${x + width / 2}" y="${annotY}" text-anchor="middle" dy="${TEXT_BASELINE_SHIFT}" ` +
286
+ f`font-size="${CLS_FONT.annotationSize}" font-weight="${CLS_FONT.annotationWeight}" ` +
287
+ f`font-style="italic" fill="${textColor ?? 'var(--_text-muted)'}">&lt;&lt;${escapeXml(cls.annotation)}&gt;&gt;</text>`,
286
288
  )
287
289
  nameY = y + headerHeight / 2 + 6
288
290
  }
@@ -295,15 +297,15 @@ function renderClassBox(
295
297
  x + width / 2,
296
298
  nameY,
297
299
  fontSizes.nodeLabel,
298
- `text-anchor="middle" font-size="${fontSizes.nodeLabel}" font-weight="700" fill="${textColor ?? 'var(--_text)'}"`,
300
+ f`text-anchor="middle" font-size="${fontSizes.nodeLabel}" font-weight="700" fill="${textColor ?? 'var(--_text)'}"`,
299
301
  ),
300
302
  )
301
303
 
302
304
  // Divider line between header and attributes
303
305
  const attrTop = y + headerHeight
304
306
  parts.push(
305
- ` <line x1="${x}" y1="${attrTop}" x2="${x + width}" y2="${attrTop}" ` +
306
- `stroke="${stroke}" stroke-width="${STROKE_WIDTHS.innerBox}" />`,
307
+ f` <line x1="${x}" y1="${attrTop}" x2="${x + width}" y2="${attrTop}" ` +
308
+ f`stroke="${stroke}" stroke-width="${STROKE_WIDTHS.innerBox}" />`,
307
309
  )
308
310
 
309
311
  // Attributes
@@ -317,8 +319,8 @@ function renderClassBox(
317
319
  // Divider line between attributes and methods
318
320
  const methodTop = attrTop + attrHeight
319
321
  parts.push(
320
- ` <line x1="${x}" y1="${methodTop}" x2="${x + width}" y2="${methodTop}" ` +
321
- `stroke="${stroke}" stroke-width="${STROKE_WIDTHS.innerBox}" />`,
322
+ f` <line x1="${x}" y1="${methodTop}" x2="${x + width}" y2="${methodTop}" ` +
323
+ f`stroke="${stroke}" stroke-width="${STROKE_WIDTHS.innerBox}" />`,
322
324
  )
323
325
 
324
326
  // Methods
@@ -366,27 +368,27 @@ function renderMember(
366
368
 
367
369
  if (member.visibility) {
368
370
  spans.push(
369
- `<tspan fill="${faint}">${escapeXml(member.visibility)} </tspan>`,
371
+ f`<tspan fill="${faint}">${escapeXml(member.visibility)} </tspan>`,
370
372
  )
371
373
  }
372
374
 
373
375
  // Add parentheses for methods to distinguish from attributes, including parameters if present
374
376
  const displayName = member.isMethod
375
- ? `${member.name}(${member.params || ''})`
377
+ ? f`${member.name}(${member.params || ''})`
376
378
  : member.name
377
379
  // False positive: displayName is passed through escapeXml() (see packages/core/src/multiline-utils.ts),
378
380
  // which escapes &, <, >, ", ' before interpolation, so this is not raw/unescaped HTML.
379
- spans.push(`<tspan fill="${secondary}">${escapeXml(displayName)}</tspan>`) // nosemgrep: javascript.express.security.injection.raw-html-format.raw-html-format
381
+ spans.push(f`<tspan fill="${secondary}">${escapeXml(displayName)}</tspan>`) // nosemgrep: javascript.express.security.injection.raw-html-format.raw-html-format
380
382
 
381
383
  if (member.type) {
382
- spans.push(`<tspan fill="${faint}">: </tspan>`)
383
- spans.push(`<tspan fill="${muted}">${escapeXml(member.type)}</tspan>`)
384
+ spans.push(f`<tspan fill="${faint}">: </tspan>`)
385
+ spans.push(f`<tspan fill="${muted}">${escapeXml(member.type)}</tspan>`)
384
386
  }
385
387
 
386
388
  return (
387
- `<text x="${x}" y="${y}" class="mono" dy="${TEXT_BASELINE_SHIFT}" ` +
388
- `font-size="${CLS_FONT.memberSize}" font-weight="${CLS_FONT.memberWeight}"${fontStyle}${decoration}>` +
389
- `${spans.join('')}</text>`
389
+ f`<text x="${x}" y="${y}" class="mono" dy="${TEXT_BASELINE_SHIFT}" ` +
390
+ f`font-size="${CLS_FONT.memberSize}" font-weight="${CLS_FONT.memberWeight}"${fontStyle}${decoration}>` +
391
+ f`${spans.join('')}</text>`
390
392
  )
391
393
  }
392
394
 
@@ -408,33 +410,33 @@ function renderNote(note: PositionedClassNote, fontSizes: FontSizes): string {
408
410
  const { x, y, width: w, height: h } = note
409
411
  const forAttr =
410
412
  note.forClass !== undefined
411
- ? ` data-for="${escapeAttr(note.forClass)}"`
413
+ ? f` data-for="${escapeAttr(note.forClass)}"`
412
414
  : ''
413
415
 
414
416
  // Note body: (x,y) → (x+w-fold,y) → (x+w,y+fold) → (x+w,y+h) → (x,y+h)
415
417
  const bodyPoints = [
416
- `${x},${y}`,
417
- `${x + w - NOTE_FOLD},${y}`,
418
- `${x + w},${y + NOTE_FOLD}`,
419
- `${x + w},${y + h}`,
420
- `${x},${y + h}`,
418
+ f`${x},${y}`,
419
+ f`${x + w - NOTE_FOLD},${y}`,
420
+ f`${x + w},${y + NOTE_FOLD}`,
421
+ f`${x + w},${y + h}`,
422
+ f`${x},${y + h}`,
421
423
  ].join(' ')
422
424
  const foldPoints =
423
- `${x + w - NOTE_FOLD},${y} ${x + w},${y + NOTE_FOLD} ` +
424
- `${x + w - NOTE_FOLD},${y + NOTE_FOLD}`
425
+ f`${x + w - NOTE_FOLD},${y} ${x + w},${y + NOTE_FOLD} ` +
426
+ f`${x + w - NOTE_FOLD},${y + NOTE_FOLD}`
425
427
 
426
428
  return (
427
- `<g class="class-note"${forAttr}>` +
428
- `\n <polygon points="${bodyPoints}" ` +
429
- `fill="var(--bg)" stroke="var(--_node-stroke)" stroke-width="${STROKE_WIDTHS.innerBox}" />` +
430
- `\n <polygon points="${foldPoints}" ` +
431
- `fill="var(--_inner-stroke)" stroke="var(--_node-stroke)" stroke-width="${STROKE_WIDTHS.innerBox}" />` +
432
- `\n ${renderMultilineText(
429
+ f`<g class="class-note"${forAttr}>` +
430
+ f`\n <polygon points="${bodyPoints}" ` +
431
+ f`fill="var(--bg)" stroke="var(--_node-stroke)" stroke-width="${STROKE_WIDTHS.innerBox}" />` +
432
+ f`\n <polygon points="${foldPoints}" ` +
433
+ f`fill="var(--_inner-stroke)" stroke="var(--_node-stroke)" stroke-width="${STROKE_WIDTHS.innerBox}" />` +
434
+ f`\n ${renderMultilineText(
433
435
  note.text,
434
436
  x + w / 2,
435
437
  y + h / 2,
436
438
  fontSizes.edgeLabel,
437
- `font-size="${fontSizes.edgeLabel}" text-anchor="middle" font-weight="${FONT_WEIGHTS.edgeLabel}" fill="var(--_text-muted)"`,
439
+ f`font-size="${fontSizes.edgeLabel}" text-anchor="middle" font-weight="${FONT_WEIGHTS.edgeLabel}" fill="var(--_text-muted)"`,
438
440
  )}` +
439
441
  `\n</g>`
440
442
  )
@@ -446,14 +448,14 @@ function renderNote(note: PositionedClassNote, fontSizes: FontSizes): string {
446
448
  */
447
449
  function renderNoteLink(note: PositionedClassNote): string {
448
450
  if (!note.linkPoints || note.linkPoints.length < 2) return ''
449
- const pathData = note.linkPoints.map((p) => `${p.x},${p.y}`).join(' ')
451
+ const pathData = note.linkPoints.map((p) => f`${p.x},${p.y}`).join(' ')
450
452
  const forAttr =
451
453
  note.forClass !== undefined
452
- ? ` data-for="${escapeAttr(note.forClass)}"`
454
+ ? f` data-for="${escapeAttr(note.forClass)}"`
453
455
  : ''
454
456
  return (
455
- `<polyline class="class-note-link"${forAttr} points="${pathData}" ` +
456
- `fill="none" stroke="var(--_line)" stroke-width="${STROKE_WIDTHS.connector}" stroke-dasharray="2 3" />`
457
+ f`<polyline class="class-note-link"${forAttr} points="${pathData}" ` +
458
+ f`fill="none" stroke="var(--_line)" stroke-width="${STROKE_WIDTHS.connector}" stroke-dasharray="2 3" />`
457
459
  )
458
460
  }
459
461
 
@@ -468,7 +470,7 @@ function renderNoteLink(note: PositionedClassNote): string {
468
470
  function renderRelationship(rel: PositionedClassRelationship): string {
469
471
  if (rel.points.length < 2) return ''
470
472
 
471
- const pathData = rel.points.map((p) => `${p.x},${p.y}`).join(' ')
473
+ const pathData = rel.points.map((p) => f`${p.x},${p.y}`).join(' ')
472
474
  const isDashed = rel.type === 'dependency' || rel.type === 'realization'
473
475
  const dashArray = isDashed ? ' stroke-dasharray="6 4"' : ''
474
476
 
@@ -484,24 +486,26 @@ function renderRelationship(rel: PositionedClassRelationship): string {
484
486
  // - data-label: relationship label if present
485
487
  const dataAttrs = [
486
488
  'class="class-relationship"',
487
- `data-from="${escapeAttr(rel.from)}"`,
488
- `data-to="${escapeAttr(rel.to)}"`,
489
- `data-type="${rel.type}"`,
490
- `data-marker-at="${rel.markerAt}"`,
489
+ f`data-from="${escapeAttr(rel.from)}"`,
490
+ f`data-to="${escapeAttr(rel.to)}"`,
491
+ f`data-type="${rel.type}"`,
492
+ f`data-marker-at="${rel.markerAt}"`,
491
493
  ]
492
494
  if (rel.label) {
493
- dataAttrs.push(`data-label="${escapeAttr(rel.label)}"`)
495
+ dataAttrs.push(f`data-label="${escapeAttr(rel.label)}"`)
494
496
  }
495
497
  if (rel.fromCardinality) {
496
- dataAttrs.push(`data-from-cardinality="${escapeAttr(rel.fromCardinality)}"`)
498
+ dataAttrs.push(
499
+ f`data-from-cardinality="${escapeAttr(rel.fromCardinality)}"`,
500
+ )
497
501
  }
498
502
  if (rel.toCardinality) {
499
- dataAttrs.push(`data-to-cardinality="${escapeAttr(rel.toCardinality)}"`)
503
+ dataAttrs.push(f`data-to-cardinality="${escapeAttr(rel.toCardinality)}"`)
500
504
  }
501
505
 
502
506
  return (
503
- `<polyline ${dataAttrs.join(' ')} points="${pathData}" fill="none" stroke="var(--_line)" ` +
504
- `stroke-width="${STROKE_WIDTHS.connector}"${dashArray}${markers} />`
507
+ f`<polyline ${dataAttrs.join(' ')} points="${pathData}" fill="none" stroke="var(--_line)" ` +
508
+ f`stroke-width="${STROKE_WIDTHS.connector}"${dashArray}${markers} />`
505
509
  )
506
510
  }
507
511
 
@@ -519,9 +523,9 @@ function getRelationshipMarkers(
519
523
  if (!markerId) return ''
520
524
 
521
525
  if (markerAt === 'from') {
522
- return ` marker-start="url(#${markerId})"`
526
+ return f` marker-start="url(#${markerId})"`
523
527
  } else {
524
- return ` marker-end="url(#${markerId})"`
528
+ return f` marker-end="url(#${markerId})"`
525
529
  }
526
530
  }
527
531
 
@@ -562,7 +566,7 @@ function renderRelationshipLabels(
562
566
  pos.x,
563
567
  pos.y - 8,
564
568
  fontSizes.edgeLabel,
565
- `font-size="${fontSizes.edgeLabel}" text-anchor="middle" font-weight="${FONT_WEIGHTS.edgeLabel}" fill="var(--_text-muted)"`,
569
+ f`font-size="${fontSizes.edgeLabel}" text-anchor="middle" font-weight="${FONT_WEIGHTS.edgeLabel}" fill="var(--_text-muted)"`,
566
570
  ),
567
571
  )
568
572
  }
@@ -578,7 +582,7 @@ function renderRelationshipLabels(
578
582
  p.x + offset.x,
579
583
  p.y + offset.y,
580
584
  fontSizes.edgeLabel,
581
- `font-size="${fontSizes.edgeLabel}" text-anchor="middle" font-weight="${FONT_WEIGHTS.edgeLabel}" fill="var(--_text-muted)"`,
585
+ f`font-size="${fontSizes.edgeLabel}" text-anchor="middle" font-weight="${FONT_WEIGHTS.edgeLabel}" fill="var(--_text-muted)"`,
582
586
  ),
583
587
  )
584
588
  }
@@ -594,7 +598,7 @@ function renderRelationshipLabels(
594
598
  p.x + offset.x,
595
599
  p.y + offset.y,
596
600
  fontSizes.edgeLabel,
597
- `font-size="${fontSizes.edgeLabel}" text-anchor="middle" font-weight="${FONT_WEIGHTS.edgeLabel}" fill="var(--_text-muted)"`,
601
+ f`font-size="${fontSizes.edgeLabel}" text-anchor="middle" font-weight="${FONT_WEIGHTS.edgeLabel}" fill="var(--_text-muted)"`,
598
602
  ),
599
603
  )
600
604
  }
@@ -10,10 +10,11 @@
10
10
  // ============================================================================
11
11
 
12
12
  import type { Point, CurveStyle } from '@zombie-mermaid/core'
13
+ import { f } from '@zombie-mermaid/core'
13
14
 
14
15
  /** `M x y` for the first point. */
15
16
  function moveTo(p: Point): string {
16
- return `M ${p.x} ${p.y}`
17
+ return f`M ${p.x} ${p.y}`
17
18
  }
18
19
 
19
20
  /** Straight segments through every point — the historical default. */
@@ -22,7 +23,7 @@ function linearPath(points: Point[]): string {
22
23
  moveTo(points[0]!) +
23
24
  points
24
25
  .slice(1)
25
- .map((p) => ` L ${p.x} ${p.y}`)
26
+ .map((p) => f` L ${p.x} ${p.y}`)
26
27
  .join('')
27
28
  )
28
29
  }
@@ -51,14 +52,14 @@ function basisPath(points: Point[]): string {
51
52
 
52
53
  /** One B-spline segment across three successive control points. */
53
54
  const segment = (p0: Point, p1: Point, p2: Point): string =>
54
- ` C ${(2 * p0.x + p1.x) / 3} ${(2 * p0.y + p1.y) / 3},` +
55
- ` ${(p0.x + 2 * p1.x) / 3} ${(p0.y + 2 * p1.y) / 3},` +
56
- ` ${(p0.x + 4 * p1.x + p2.x) / 6} ${(p0.y + 4 * p1.y + p2.y) / 6}`
55
+ f` C ${(2 * p0.x + p1.x) / 3} ${(2 * p0.y + p1.y) / 3},` +
56
+ f` ${(p0.x + 2 * p1.x) / 3} ${(p0.y + 2 * p1.y) / 3},` +
57
+ f` ${(p0.x + 4 * p1.x + p2.x) / 6} ${(p0.y + 4 * p1.y + p2.y) / 6}`
57
58
 
58
59
  // Lead-in to the first knot, one sixth of the way along the opening leg.
59
60
  parts.push(
60
- ` L ${(5 * points[0]!.x + points[1]!.x) / 6}` +
61
- ` ${(5 * points[0]!.y + points[1]!.y) / 6}`,
61
+ f` L ${(5 * points[0]!.x + points[1]!.x) / 6}` +
62
+ f` ${(5 * points[0]!.y + points[1]!.y) / 6}`,
62
63
  )
63
64
 
64
65
  for (let i = 2; i < points.length; i++) {
@@ -67,7 +68,7 @@ function basisPath(points: Point[]): string {
67
68
 
68
69
  // d3 closes by feeding the last point twice, then drawing to it.
69
70
  parts.push(segment(points[points.length - 2]!, last, last))
70
- parts.push(` L ${last.x} ${last.y}`)
71
+ parts.push(f` L ${last.x} ${last.y}`)
71
72
 
72
73
  return parts.join('')
73
74
  }
@@ -109,7 +110,7 @@ function naturalPath(points: Point[]): string {
109
110
  // A zero-length leg means duplicated routed points; there is no corner
110
111
  // to round, so pass straight through.
111
112
  if (inLen === 0 || outLen === 0) {
112
- parts.push(` L ${corner.x} ${corner.y}`)
113
+ parts.push(f` L ${corner.x} ${corner.y}`)
113
114
  continue
114
115
  }
115
116
 
@@ -123,12 +124,12 @@ function naturalPath(points: Point[]): string {
123
124
  const endY = corner.y + ((next.y - corner.y) / outLen) * radius
124
125
 
125
126
  // Straight up to the fillet, then a quadratic through the corner.
126
- parts.push(` L ${startX} ${startY}`)
127
- parts.push(` Q ${corner.x} ${corner.y}, ${endX} ${endY}`)
127
+ parts.push(f` L ${startX} ${startY}`)
128
+ parts.push(f` Q ${corner.x} ${corner.y}, ${endX} ${endY}`)
128
129
  }
129
130
 
130
131
  const last = points[points.length - 1]!
131
- parts.push(` L ${last.x} ${last.y}`)
132
+ parts.push(f` L ${last.x} ${last.y}`)
132
133
 
133
134
  return parts.join('')
134
135
  }
@@ -155,17 +156,17 @@ function stepPath(points: Point[], where: 'mid' | 'before' | 'after'): string {
155
156
  const b = points[i + 1]!
156
157
 
157
158
  if (i === lastIndex - 1) {
158
- parts.push(` L ${b.x} ${b.y}`)
159
+ parts.push(f` L ${b.x} ${b.y}`)
159
160
  break
160
161
  }
161
162
 
162
163
  if (where === 'before') {
163
- parts.push(` L ${a.x} ${b.y} L ${b.x} ${b.y}`)
164
+ parts.push(f` L ${a.x} ${b.y} L ${b.x} ${b.y}`)
164
165
  } else if (where === 'after') {
165
- parts.push(` L ${b.x} ${a.y} L ${b.x} ${b.y}`)
166
+ parts.push(f` L ${b.x} ${a.y} L ${b.x} ${b.y}`)
166
167
  } else {
167
168
  const midY = (a.y + b.y) / 2
168
- parts.push(` L ${a.x} ${midY} L ${b.x} ${midY} L ${b.x} ${b.y}`)
169
+ parts.push(f` L ${a.x} ${midY} L ${b.x} ${midY} L ${b.x} ${b.y}`)
169
170
  }
170
171
  }
171
172