@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.
package/src/renderer.ts CHANGED
@@ -20,6 +20,7 @@ import {
20
20
  escapeAttr,
21
21
  safeHref,
22
22
  sanitizeClassName,
23
+ f,
23
24
  } from '@zombie-mermaid/core'
24
25
  import type { FontSizes } from './styles.ts'
25
26
  import {
@@ -215,7 +216,7 @@ function arrowMarkerDefs(): string {
215
216
  * IDs are suffixed with a sanitized color string to avoid collisions.
216
217
  */
217
218
  function arrowMarkerDefsForColor(color: string): string {
218
- return arrowMarkerPair(escapeAttr(color), `-${markerSuffix(color)}`)
219
+ return arrowMarkerPair(escapeAttr(color), f`-${markerSuffix(color)}`)
219
220
  }
220
221
 
221
222
  /**
@@ -233,16 +234,16 @@ function arrowMarkerPair(color: string, idSuffix: string): string {
233
234
  // Pull arrowhead back slightly (refX = w - 1) to prevent clipping at node boundaries.
234
235
  const refX = w - 1
235
236
  // Both fill and a thin stroke for better definition at small sizes.
236
- const style = `fill="${color}" stroke="${color}" stroke-width="0.75" stroke-linejoin="round"`
237
- const polygon = `<polygon points="0 0, ${w} ${h / 2}, 0 ${h}" ${style} />`
237
+ const style = f`fill="${color}" stroke="${color}" stroke-width="0.75" stroke-linejoin="round"`
238
+ const polygon = f`<polygon points="0 0, ${w} ${h / 2}, 0 ${h}" ${style} />`
238
239
  const marker = (id: string, orient: string) =>
239
- ` <marker id="${id}" markerWidth="${w}" markerHeight="${h}" refX="${refX}" refY="${h / 2}" orient="${orient}">` +
240
- `\n ${polygon}` +
240
+ f` <marker id="${id}" markerWidth="${w}" markerHeight="${h}" refX="${refX}" refY="${h / 2}" orient="${orient}">` +
241
+ f`\n ${polygon}` +
241
242
  `\n </marker>`
242
243
  return (
243
- marker(`arrowhead${idSuffix}`, 'auto') +
244
+ marker(f`arrowhead${idSuffix}`, 'auto') +
244
245
  '\n' +
245
- marker(`arrowhead-start${idSuffix}`, 'auto-start-reverse')
246
+ marker(f`arrowhead-start${idSuffix}`, 'auto-start-reverse')
246
247
  )
247
248
  }
248
249
 
@@ -269,19 +270,19 @@ function renderGroup(
269
270
  // data-id: original Mermaid subgraph ID
270
271
  // data-label: display label (may differ from ID)
271
272
  parts.push(
272
- `<g class="subgraph" data-id="${escapeAttr(group.id)}" data-label="${escapeAttr(group.label)}">`,
273
+ f`<g class="subgraph" data-id="${escapeAttr(group.id)}" data-label="${escapeAttr(group.label)}">`,
273
274
  )
274
275
 
275
276
  // Outer rectangle
276
277
  parts.push(
277
- ` <rect x="${group.x}" y="${group.y}" width="${group.width}" height="${group.height}" ` +
278
- `rx="0" ry="0" fill="var(--_group-fill)" stroke="var(--_node-stroke)" stroke-width="${STROKE_WIDTHS.outerBox}" />`,
278
+ f` <rect x="${group.x}" y="${group.y}" width="${group.width}" height="${group.height}" ` +
279
+ f`rx="0" ry="0" fill="var(--_group-fill)" stroke="var(--_node-stroke)" stroke-width="${STROKE_WIDTHS.outerBox}" />`,
279
280
  )
280
281
 
281
282
  // Header band
282
283
  parts.push(
283
- ` <rect x="${group.x}" y="${group.y}" width="${group.width}" height="${headerHeight}" ` +
284
- `rx="0" ry="0" fill="var(--_group-hdr)" stroke="var(--_node-stroke)" stroke-width="${STROKE_WIDTHS.outerBox}" />`,
284
+ f` <rect x="${group.x}" y="${group.y}" width="${group.width}" height="${headerHeight}" ` +
285
+ f`rx="0" ry="0" fill="var(--_group-hdr)" stroke="var(--_node-stroke)" stroke-width="${STROKE_WIDTHS.outerBox}" />`,
285
286
  )
286
287
 
287
288
  // Header label (supports multi-line via <br> tags)
@@ -292,7 +293,7 @@ function renderGroup(
292
293
  group.x + 12,
293
294
  group.y + headerHeight / 2,
294
295
  fontSizes.groupHeader,
295
- `font-size="${fontSizes.groupHeader}" font-weight="${FONT_WEIGHTS.groupHeader}" fill="var(--_text-sec)"`,
296
+ f`font-size="${fontSizes.groupHeader}" font-weight="${FONT_WEIGHTS.groupHeader}" fill="var(--_text-sec)"`,
296
297
  ),
297
298
  )
298
299
 
@@ -356,13 +357,13 @@ function renderEdge(
356
357
  // Build marker attributes based on arrow direction flags
357
358
  // Use color-specific markers when edge has a custom stroke from linkStyle
358
359
  const suffix = edge.inlineStyle?.stroke
359
- ? `-${markerSuffix(edge.inlineStyle.stroke)}`
360
+ ? f`-${markerSuffix(edge.inlineStyle.stroke)}`
360
361
  : ''
361
362
  let markers = ''
362
363
  if (!invisible) {
363
- if (edge.hasArrowEnd) markers += ` marker-end="url(#arrowhead${suffix})"`
364
+ if (edge.hasArrowEnd) markers += f` marker-end="url(#arrowhead${suffix})"`
364
365
  if (edge.hasArrowStart)
365
- markers += ` marker-start="url(#arrowhead-start${suffix})"`
366
+ markers += f` marker-start="url(#arrowhead-start${suffix})"`
366
367
  }
367
368
 
368
369
  // Semantic data attributes for edge identification and inspection:
@@ -374,20 +375,20 @@ function renderEdge(
374
375
  const dataAttrs = [
375
376
  // An animated edge (`e1@{ animate: true }`, with animation enabled)
376
377
  // carries an extra class; its keyframes live in the shared style block.
377
- `class="edge${animate ? ' edge-animated' : ''}"`,
378
- `data-from="${escapeAttr(edge.source)}"`,
379
- `data-to="${escapeAttr(edge.target)}"`,
380
- `data-style="${edge.style}"`,
381
- `data-arrow-start="${edge.hasArrowStart}"`,
382
- `data-arrow-end="${edge.hasArrowEnd}"`,
378
+ f`class="edge${animate ? ' edge-animated' : ''}"`,
379
+ f`data-from="${escapeAttr(edge.source)}"`,
380
+ f`data-to="${escapeAttr(edge.target)}"`,
381
+ f`data-style="${edge.style}"`,
382
+ f`data-arrow-start="${edge.hasArrowStart}"`,
383
+ f`data-arrow-end="${edge.hasArrowEnd}"`,
383
384
  ]
384
385
  if (edge.label) {
385
- dataAttrs.push(`data-label="${escapeAttr(edge.label)}"`)
386
+ dataAttrs.push(f`data-label="${escapeAttr(edge.label)}"`)
386
387
  }
387
388
  if (edge.id) {
388
389
  // Mermaid edge id (`A e1@--> B`), used to target the edge from CSS and
389
390
  // to attach the animation below.
390
- dataAttrs.push(`data-id="${escapeAttr(edge.id)}"`)
391
+ dataAttrs.push(f`data-id="${escapeAttr(edge.id)}"`)
391
392
  }
392
393
 
393
394
  // Marching ants need a dash pattern to march; supply one only when the
@@ -395,19 +396,19 @@ function renderEdge(
395
396
  const animatedDash = animate && !dashArray ? ' stroke-dasharray="8 6"' : ''
396
397
 
397
398
  const geometry = curved
398
- ? `d="${pointsToPath(edge.points, curve)}"`
399
- : `points="${pointsToPolylinePath(edge.points)}"`
399
+ ? f`d="${pointsToPath(edge.points, curve)}"`
400
+ : f`points="${pointsToPolylinePath(edge.points)}"`
400
401
 
401
402
  return (
402
- `<${curved ? 'path' : 'polyline'} ${dataAttrs.join(' ')} ${geometry} ` +
403
- `fill="none" stroke="${strokeColor}" ` +
404
- `stroke-width="${strokeWidth}"${dashArray}${animatedDash}${markers} />`
403
+ f`<${curved ? 'path' : 'polyline'} ${dataAttrs.join(' ')} ${geometry} ` +
404
+ f`fill="none" stroke="${strokeColor}" ` +
405
+ f`stroke-width="${strokeWidth}"${dashArray}${animatedDash}${markers} />`
405
406
  )
406
407
  }
407
408
 
408
409
  /** Convert points to SVG polyline points attribute: "x1,y1 x2,y2 ..." */
409
410
  function pointsToPolylinePath(points: Point[]): string {
410
- return points.map((p) => `${p.x},${p.y}`).join(' ')
411
+ return points.map((p) => f`${p.x},${p.y}`).join(' ')
411
412
  }
412
413
 
413
414
  // `_font` isn't read here but is kept to match the `(entity, font)` signature
@@ -444,15 +445,15 @@ function renderEdgeLabel(
444
445
  fontSizes.edgeLabel,
445
446
  padding,
446
447
  // Use --_text-sec for better contrast (was --_text-muted)
447
- `text-anchor="middle" font-size="${fontSizes.edgeLabel}" font-weight="${FONT_WEIGHTS.edgeLabel}" fill="var(--_text-sec)"`,
448
+ f`text-anchor="middle" font-size="${fontSizes.edgeLabel}" font-weight="${FONT_WEIGHTS.edgeLabel}" fill="var(--_text-sec)"`,
448
449
  // Increased stroke width from 0.5 to 1 for better label separation from edges
449
450
  `rx="2" ry="2" fill="var(--bg)" stroke="var(--_inner-stroke)" stroke-width="1"`,
450
451
  )
451
452
 
452
453
  // Semantic wrapper: links label to its edge via data-from/data-to
453
454
  return (
454
- `<g class="edge-label" data-from="${escapeAttr(edge.source)}" data-to="${escapeAttr(edge.target)}" data-label="${escapeAttr(label)}">\n` +
455
- ` ${content.replace(/\n/g, '\n ')}\n` +
455
+ f`<g class="edge-label" data-from="${escapeAttr(edge.source)}" data-to="${escapeAttr(edge.target)}" data-label="${escapeAttr(label)}">\n` +
456
+ f` ${content.replace(/\n/g, '\n ')}\n` +
456
457
  `</g>`
457
458
  )
458
459
  }
@@ -471,7 +472,7 @@ function requirePoint(points: Point[], index: number): Point {
471
472
  // Unreachable — both call sites in edgeMidpoint only ever pass indices
472
473
  // in [0, points.length - 1].
473
474
  /* v8 ignore next */
474
- throw new Error(`edgeMidpoint: missing point at index ${index}`)
475
+ throw new Error(f`edgeMidpoint: missing point at index ${index}`)
475
476
  }
476
477
  return point
477
478
  }
@@ -539,14 +540,14 @@ function renderNode(
539
540
  // This enables reliable node identification without heuristics
540
541
  const parts: string[] = []
541
542
  const safeClassName = sanitizeClassName(node.className)
542
- const classAttr = safeClassName ? `node ${safeClassName}` : 'node'
543
+ const classAttr = safeClassName ? f`node ${safeClassName}` : 'node'
543
544
 
544
545
  const interaction = node.interaction
545
546
  const groupAttrs = [
546
- `class="${classAttr}"`,
547
- `data-id="${escapeAttr(node.id)}"`,
548
- `data-label="${escapeAttr(node.label)}"`,
549
- `data-shape="${node.shape}"`,
547
+ f`class="${classAttr}"`,
548
+ f`data-id="${escapeAttr(node.id)}"`,
549
+ f`data-label="${escapeAttr(node.label)}"`,
550
+ f`data-shape="${node.shape}"`,
550
551
  ]
551
552
  /*
552
553
  * `click A call fn()` is deliberately absent from the markup. This renderer
@@ -558,7 +559,7 @@ function renderNode(
558
559
  * attribute once emitted here was removed in #216 — see
559
560
  * docs/decisions/no-script-interactivity.md.
560
561
  */
561
- parts.push(`<g ${groupAttrs.join(' ')}>`)
562
+ parts.push(f`<g ${groupAttrs.join(' ')}>`)
562
563
 
563
564
  // An href becomes a real SVG link, which needs no script to work.
564
565
  // `interactivity: 'none'` strips it — a link is meaningless in
@@ -567,21 +568,21 @@ function renderNode(
567
568
  const indent = href ? ' ' : ' '
568
569
  if (href) {
569
570
  const targetAttr = interaction?.target
570
- ? ` target="${escapeAttr(interaction.target)}"`
571
+ ? f` target="${escapeAttr(interaction.target)}"`
571
572
  : ''
572
- parts.push(` <a href="${escapeAttr(href)}"${targetAttr}>`)
573
+ parts.push(f` <a href="${escapeAttr(href)}"${targetAttr}>`)
573
574
  }
574
575
 
575
576
  if (linksEnabled && interaction?.tooltip) {
576
577
  // <title> is SVG's native tooltip — no script, no CSS. Gated by
577
578
  // linksEnabled alongside href above: both come from the same `click`
578
579
  // statement, and 'none' strips both.
579
- parts.push(`${indent}<title>${escapeXml(interaction.tooltip)}</title>`)
580
+ parts.push(f`${indent}<title>${escapeXml(interaction.tooltip)}</title>`)
580
581
  }
581
582
 
582
- parts.push(`${indent}${shape.replace(/\n/g, `\n${indent}`)}`)
583
+ parts.push(f`${indent}${shape.replace(/\n/g, f`\n${indent}`)}`)
583
584
  if (label) {
584
- parts.push(`${indent}${label.replace(/\n/g, `\n${indent}`)}`)
585
+ parts.push(f`${indent}${label.replace(/\n/g, f`\n${indent}`)}`)
585
586
  }
586
587
 
587
588
  if (href) parts.push(' </a>')
@@ -700,8 +701,8 @@ function renderRect(
700
701
  sw: string,
701
702
  ): string {
702
703
  return (
703
- `<rect x="${x}" y="${y}" width="${w}" height="${h}" ` +
704
- `rx="0" ry="0" fill="${fill}" stroke="${stroke}" stroke-width="${sw}" />`
704
+ f`<rect x="${x}" y="${y}" width="${w}" height="${h}" ` +
705
+ f`rx="0" ry="0" fill="${fill}" stroke="${stroke}" stroke-width="${sw}" />`
705
706
  )
706
707
  }
707
708
 
@@ -715,8 +716,8 @@ function renderRoundedRect(
715
716
  sw: string,
716
717
  ): string {
717
718
  return (
718
- `<rect x="${x}" y="${y}" width="${w}" height="${h}" ` +
719
- `rx="6" ry="6" fill="${fill}" stroke="${stroke}" stroke-width="${sw}" />`
719
+ f`<rect x="${x}" y="${y}" width="${w}" height="${h}" ` +
720
+ f`rx="6" ry="6" fill="${fill}" stroke="${stroke}" stroke-width="${sw}" />`
720
721
  )
721
722
  }
722
723
 
@@ -731,8 +732,8 @@ function renderStadium(
731
732
  ): string {
732
733
  const r = h / 2
733
734
  return (
734
- `<rect x="${x}" y="${y}" width="${w}" height="${h}" ` +
735
- `rx="${r}" ry="${r}" fill="${fill}" stroke="${stroke}" stroke-width="${sw}" />`
735
+ f`<rect x="${x}" y="${y}" width="${w}" height="${h}" ` +
736
+ f`rx="${r}" ry="${r}" fill="${fill}" stroke="${stroke}" stroke-width="${sw}" />`
736
737
  )
737
738
  }
738
739
 
@@ -749,8 +750,8 @@ function renderCircle(
749
750
  const cy = y + h / 2
750
751
  const r = Math.min(w, h) / 2
751
752
  return (
752
- `<circle cx="${cx}" cy="${cy}" r="${r}" ` +
753
- `fill="${fill}" stroke="${stroke}" stroke-width="${sw}" />`
753
+ f`<circle cx="${cx}" cy="${cy}" r="${r}" ` +
754
+ f`fill="${fill}" stroke="${stroke}" stroke-width="${sw}" />`
754
755
  )
755
756
  }
756
757
 
@@ -768,13 +769,13 @@ function renderDiamond(
768
769
  const hw = w / 2
769
770
  const hh = h / 2
770
771
  const points = [
771
- `${cx},${cy - hh}`, // top
772
- `${cx + hw},${cy}`, // right
773
- `${cx},${cy + hh}`, // bottom
774
- `${cx - hw},${cy}`, // left
772
+ f`${cx},${cy - hh}`, // top
773
+ f`${cx + hw},${cy}`, // right
774
+ f`${cx},${cy + hh}`, // bottom
775
+ f`${cx - hw},${cy}`, // left
775
776
  ].join(' ')
776
777
 
777
- return `<polygon points="${points}" fill="${fill}" stroke="${stroke}" stroke-width="${sw}" />`
778
+ return f`<polygon points="${points}" fill="${fill}" stroke="${stroke}" stroke-width="${sw}" />`
778
779
  }
779
780
 
780
781
  // --- Batch 1 shapes ---
@@ -791,12 +792,12 @@ function renderSubroutine(
791
792
  ): string {
792
793
  const inset = 8 // distance from edge to inner vertical line
793
794
  return (
794
- `<rect x="${x}" y="${y}" width="${w}" height="${h}" ` +
795
- `rx="0" ry="0" fill="${fill}" stroke="${stroke}" stroke-width="${sw}" />` +
796
- `\n<line x1="${x + inset}" y1="${y}" x2="${x + inset}" y2="${y + h}" ` +
797
- `stroke="${stroke}" stroke-width="${sw}" />` +
798
- `\n<line x1="${x + w - inset}" y1="${y}" x2="${x + w - inset}" y2="${y + h}" ` +
799
- `stroke="${stroke}" stroke-width="${sw}" />`
795
+ f`<rect x="${x}" y="${y}" width="${w}" height="${h}" ` +
796
+ f`rx="0" ry="0" fill="${fill}" stroke="${stroke}" stroke-width="${sw}" />` +
797
+ f`\n<line x1="${x + inset}" y1="${y}" x2="${x + inset}" y2="${y + h}" ` +
798
+ f`stroke="${stroke}" stroke-width="${sw}" />` +
799
+ f`\n<line x1="${x + w - inset}" y1="${y}" x2="${x + w - inset}" y2="${y + h}" ` +
800
+ f`stroke="${stroke}" stroke-width="${sw}" />`
800
801
  )
801
802
  }
802
803
 
@@ -815,10 +816,10 @@ function renderDoubleCircle(
815
816
  const outerR = Math.min(w, h) / 2
816
817
  const innerR = outerR - 5 // 5px gap between rings
817
818
  return (
818
- `<circle cx="${cx}" cy="${cy}" r="${outerR}" ` +
819
- `fill="${fill}" stroke="${stroke}" stroke-width="${sw}" />` +
820
- `\n<circle cx="${cx}" cy="${cy}" r="${innerR}" ` +
821
- `fill="${fill}" stroke="${stroke}" stroke-width="${sw}" />`
819
+ f`<circle cx="${cx}" cy="${cy}" r="${outerR}" ` +
820
+ f`fill="${fill}" stroke="${stroke}" stroke-width="${sw}" />` +
821
+ f`\n<circle cx="${cx}" cy="${cy}" r="${innerR}" ` +
822
+ f`fill="${fill}" stroke="${stroke}" stroke-width="${sw}" />`
822
823
  )
823
824
  }
824
825
 
@@ -834,15 +835,15 @@ function renderHexagon(
834
835
  ): string {
835
836
  const inset = h / 4 // horizontal inset for the angled sides
836
837
  const points = [
837
- `${x + inset},${y}`, // top-left
838
- `${x + w - inset},${y}`, // top-right
839
- `${x + w},${y + h / 2}`, // mid-right
840
- `${x + w - inset},${y + h}`, // bottom-right
841
- `${x + inset},${y + h}`, // bottom-left
842
- `${x},${y + h / 2}`, // mid-left
838
+ f`${x + inset},${y}`, // top-left
839
+ f`${x + w - inset},${y}`, // top-right
840
+ f`${x + w},${y + h / 2}`, // mid-right
841
+ f`${x + w - inset},${y + h}`, // bottom-right
842
+ f`${x + inset},${y + h}`, // bottom-left
843
+ f`${x},${y + h / 2}`, // mid-left
843
844
  ].join(' ')
844
845
 
845
- return `<polygon points="${points}" fill="${fill}" stroke="${stroke}" stroke-width="${sw}" />`
846
+ return f`<polygon points="${points}" fill="${fill}" stroke="${stroke}" stroke-width="${sw}" />`
846
847
  }
847
848
 
848
849
  // --- Batch 2 shapes ---
@@ -864,17 +865,17 @@ function renderCylinder(
864
865
 
865
866
  return (
866
867
  // Body rectangle (no top border — covered by top ellipse)
867
- `<rect x="${x}" y="${bodyTop}" width="${w}" height="${bodyH}" ` +
868
- `fill="${fill}" stroke="none" />` +
868
+ f`<rect x="${x}" y="${bodyTop}" width="${w}" height="${bodyH}" ` +
869
+ f`fill="${fill}" stroke="none" />` +
869
870
  // Left and right body borders
870
- `\n<line x1="${x}" y1="${bodyTop}" x2="${x}" y2="${bodyTop + bodyH}" stroke="${stroke}" stroke-width="${sw}" />` +
871
- `\n<line x1="${x + w}" y1="${bodyTop}" x2="${x + w}" y2="${bodyTop + bodyH}" stroke="${stroke}" stroke-width="${sw}" />` +
871
+ f`\n<line x1="${x}" y1="${bodyTop}" x2="${x}" y2="${bodyTop + bodyH}" stroke="${stroke}" stroke-width="${sw}" />` +
872
+ f`\n<line x1="${x + w}" y1="${bodyTop}" x2="${x + w}" y2="${bodyTop + bodyH}" stroke="${stroke}" stroke-width="${sw}" />` +
872
873
  // Bottom ellipse (half visible)
873
- `\n<ellipse cx="${cx}" cy="${y + h - ry}" rx="${w / 2}" ry="${ry}" ` +
874
- `fill="${fill}" stroke="${stroke}" stroke-width="${sw}" />` +
874
+ f`\n<ellipse cx="${cx}" cy="${y + h - ry}" rx="${w / 2}" ry="${ry}" ` +
875
+ f`fill="${fill}" stroke="${stroke}" stroke-width="${sw}" />` +
875
876
  // Top ellipse (full, on top)
876
- `\n<ellipse cx="${cx}" cy="${bodyTop}" rx="${w / 2}" ry="${ry}" ` +
877
- `fill="${fill}" stroke="${stroke}" stroke-width="${sw}" />`
877
+ f`\n<ellipse cx="${cx}" cy="${bodyTop}" rx="${w / 2}" ry="${ry}" ` +
878
+ f`fill="${fill}" stroke="${stroke}" stroke-width="${sw}" />`
878
879
  )
879
880
  }
880
881
 
@@ -890,14 +891,14 @@ function renderAsymmetric(
890
891
  ): string {
891
892
  const indent = 12 // how far the point indents
892
893
  const points = [
893
- `${x + indent},${y}`, // top-left (indented)
894
- `${x + w},${y}`, // top-right
895
- `${x + w},${y + h}`, // bottom-right
896
- `${x + indent},${y + h}`, // bottom-left (indented)
897
- `${x},${y + h / 2}`, // left point
894
+ f`${x + indent},${y}`, // top-left (indented)
895
+ f`${x + w},${y}`, // top-right
896
+ f`${x + w},${y + h}`, // bottom-right
897
+ f`${x + indent},${y + h}`, // bottom-left (indented)
898
+ f`${x},${y + h / 2}`, // left point
898
899
  ].join(' ')
899
900
 
900
- return `<polygon points="${points}" fill="${fill}" stroke="${stroke}" stroke-width="${sw}" />`
901
+ return f`<polygon points="${points}" fill="${fill}" stroke="${stroke}" stroke-width="${sw}" />`
901
902
  }
902
903
 
903
904
  /** Trapezoid [/text\]: wider bottom, narrower top */
@@ -912,13 +913,13 @@ function renderTrapezoid(
912
913
  ): string {
913
914
  const inset = w * 0.15 // top edge is narrower by this amount on each side
914
915
  const points = [
915
- `${x + inset},${y}`, // top-left (indented)
916
- `${x + w - inset},${y}`, // top-right (indented)
917
- `${x + w},${y + h}`, // bottom-right (full width)
918
- `${x},${y + h}`, // bottom-left (full width)
916
+ f`${x + inset},${y}`, // top-left (indented)
917
+ f`${x + w - inset},${y}`, // top-right (indented)
918
+ f`${x + w},${y + h}`, // bottom-right (full width)
919
+ f`${x},${y + h}`, // bottom-left (full width)
919
920
  ].join(' ')
920
921
 
921
- return `<polygon points="${points}" fill="${fill}" stroke="${stroke}" stroke-width="${sw}" />`
922
+ return f`<polygon points="${points}" fill="${fill}" stroke="${stroke}" stroke-width="${sw}" />`
922
923
  }
923
924
 
924
925
  /** Trapezoid-alt [\text/]: wider top, narrower bottom */
@@ -933,13 +934,13 @@ function renderTrapezoidAlt(
933
934
  ): string {
934
935
  const inset = w * 0.15 // bottom edge is narrower
935
936
  const points = [
936
- `${x},${y}`, // top-left (full width)
937
- `${x + w},${y}`, // top-right (full width)
938
- `${x + w - inset},${y + h}`, // bottom-right (indented)
939
- `${x + inset},${y + h}`, // bottom-left (indented)
937
+ f`${x},${y}`, // top-left (full width)
938
+ f`${x + w},${y}`, // top-right (full width)
939
+ f`${x + w - inset},${y + h}`, // bottom-right (indented)
940
+ f`${x + inset},${y + h}`, // bottom-left (indented)
940
941
  ].join(' ')
941
942
 
942
- return `<polygon points="${points}" fill="${fill}" stroke="${stroke}" stroke-width="${sw}" />`
943
+ return f`<polygon points="${points}" fill="${fill}" stroke="${stroke}" stroke-width="${sw}" />`
943
944
  }
944
945
 
945
946
  /**
@@ -960,13 +961,13 @@ function renderParallelogram(
960
961
  ): string {
961
962
  const inset = w * 0.15
962
963
  const points = [
963
- `${x + inset},${y}`, // top-left (shifted right)
964
- `${x + w},${y}`, // top-right
965
- `${x + w - inset},${y + h}`, // bottom-right (shifted left)
966
- `${x},${y + h}`, // bottom-left
964
+ f`${x + inset},${y}`, // top-left (shifted right)
965
+ f`${x + w},${y}`, // top-right
966
+ f`${x + w - inset},${y + h}`, // bottom-right (shifted left)
967
+ f`${x},${y + h}`, // bottom-left
967
968
  ].join(' ')
968
969
 
969
- return `<polygon points="${points}" fill="${fill}" stroke="${stroke}" stroke-width="${sw}" />`
970
+ return f`<polygon points="${points}" fill="${fill}" stroke="${stroke}" stroke-width="${sw}" />`
970
971
  }
971
972
 
972
973
  /** Parallelogram-alt [\text\]: leans left — the mirror of renderParallelogram. */
@@ -981,13 +982,13 @@ function renderParallelogramAlt(
981
982
  ): string {
982
983
  const inset = w * 0.15
983
984
  const points = [
984
- `${x},${y}`, // top-left
985
- `${x + w - inset},${y}`, // top-right (shifted left)
986
- `${x + w},${y + h}`, // bottom-right
987
- `${x + inset},${y + h}`, // bottom-left (shifted right)
985
+ f`${x},${y}`, // top-left
986
+ f`${x + w - inset},${y}`, // top-right (shifted left)
987
+ f`${x + w},${y + h}`, // bottom-right
988
+ f`${x + inset},${y + h}`, // bottom-left (shifted right)
988
989
  ].join(' ')
989
990
 
990
- return `<polygon points="${points}" fill="${fill}" stroke="${stroke}" stroke-width="${sw}" />`
991
+ return f`<polygon points="${points}" fill="${fill}" stroke="${stroke}" stroke-width="${sw}" />`
991
992
  }
992
993
 
993
994
  // --- Expanded-syntax shapes (`A@{ shape: ... }`) ---
@@ -999,7 +1000,7 @@ function renderParallelogramAlt(
999
1000
 
1000
1001
  /** Shared attribute string for a filled, stroked path. */
1001
1002
  function shapeAttrs(fill: string, stroke: string, sw: string): string {
1002
- return `fill="${fill}" stroke="${stroke}" stroke-width="${sw}"`
1003
+ return f`fill="${fill}" stroke="${stroke}" stroke-width="${sw}"`
1003
1004
  }
1004
1005
 
1005
1006
  /**
@@ -1024,9 +1025,9 @@ function renderDocument(
1024
1025
  // height. Ending the curve anywhere else makes every document node
1025
1026
  // visibly lopsided.
1026
1027
  const d =
1027
- `M ${x} ${y} L ${x + w} ${y} L ${x + w} ${base} ` +
1028
- `C ${x + w * 0.75} ${base + waveH * 1.8} ${x + w * 0.25} ${base - waveH * 0.8} ${x} ${base} Z`
1029
- return `<path d="${d}" ${shapeAttrs(fill, stroke, sw)} />`
1028
+ f`M ${x} ${y} L ${x + w} ${y} L ${x + w} ${base} ` +
1029
+ f`C ${x + w * 0.75} ${base + waveH * 1.8} ${x + w * 0.25} ${base - waveH * 0.8} ${x} ${base} Z`
1030
+ return f`<path d="${d}" ${shapeAttrs(fill, stroke, sw)} />`
1030
1031
  }
1031
1032
 
1032
1033
  /** Stacked document / process: offset copies behind the front shape. */
@@ -1048,15 +1049,15 @@ function renderStacked(
1048
1049
  // Two offset copies behind, back to front.
1049
1050
  for (let i = 2; i >= 1; i--) {
1050
1051
  parts.push(
1051
- `<rect x="${x + offset * i}" y="${y + offset * (2 - i)}" width="${frontW}" height="${frontH}" ` +
1052
- `${shapeAttrs(fill, stroke, sw)} />`,
1052
+ f`<rect x="${x + offset * i}" y="${y + offset * (2 - i)}" width="${frontW}" height="${frontH}" ` +
1053
+ f`${shapeAttrs(fill, stroke, sw)} />`,
1053
1054
  )
1054
1055
  }
1055
1056
 
1056
1057
  parts.push(
1057
1058
  isDocument
1058
1059
  ? renderDocument(x, y + offset * 2, frontW, frontH, fill, stroke, sw)
1059
- : `<rect x="${x}" y="${y + offset * 2}" width="${frontW}" height="${frontH}" ${shapeAttrs(fill, stroke, sw)} />`,
1060
+ : f`<rect x="${x}" y="${y + offset * 2}" width="${frontW}" height="${frontH}" ${shapeAttrs(fill, stroke, sw)} />`,
1060
1061
  )
1061
1062
 
1062
1063
  return parts.join('\n')
@@ -1074,13 +1075,13 @@ function renderCard(
1074
1075
  ): string {
1075
1076
  const notch = Math.min(14, w * 0.12, h * 0.3)
1076
1077
  const points = [
1077
- `${x + notch},${y}`,
1078
- `${x + w},${y}`,
1079
- `${x + w},${y + h}`,
1080
- `${x},${y + h}`,
1081
- `${x},${y + notch}`,
1078
+ f`${x + notch},${y}`,
1079
+ f`${x + w},${y}`,
1080
+ f`${x + w},${y + h}`,
1081
+ f`${x},${y + h}`,
1082
+ f`${x},${y + notch}`,
1082
1083
  ].join(' ')
1083
- return `<polygon points="${points}" ${shapeAttrs(fill, stroke, sw)} />`
1084
+ return f`<polygon points="${points}" ${shapeAttrs(fill, stroke, sw)} />`
1084
1085
  }
1085
1086
 
1086
1087
  /** Lined process: rectangle with a vertical rule inset from the left edge. */
@@ -1095,8 +1096,8 @@ function renderLinedProcess(
1095
1096
  ): string {
1096
1097
  const inset = Math.min(12, w * 0.12)
1097
1098
  return (
1098
- `<rect x="${x}" y="${y}" width="${w}" height="${h}" ${shapeAttrs(fill, stroke, sw)} />\n` +
1099
- `<line x1="${x + inset}" y1="${y}" x2="${x + inset}" y2="${y + h}" stroke="${stroke}" stroke-width="${sw}" />`
1099
+ f`<rect x="${x}" y="${y}" width="${w}" height="${h}" ${shapeAttrs(fill, stroke, sw)} />\n` +
1100
+ f`<line x1="${x + inset}" y1="${y}" x2="${x + inset}" y2="${y + h}" stroke="${stroke}" stroke-width="${sw}" />`
1100
1101
  )
1101
1102
  }
1102
1103
 
@@ -1112,8 +1113,8 @@ function renderDividedProcess(
1112
1113
  ): string {
1113
1114
  const split = y + Math.min(16, h * 0.3)
1114
1115
  return (
1115
- `<rect x="${x}" y="${y}" width="${w}" height="${h}" ${shapeAttrs(fill, stroke, sw)} />\n` +
1116
- `<line x1="${x}" y1="${split}" x2="${x + w}" y2="${split}" stroke="${stroke}" stroke-width="${sw}" />`
1116
+ f`<rect x="${x}" y="${y}" width="${w}" height="${h}" ${shapeAttrs(fill, stroke, sw)} />\n` +
1117
+ f`<line x1="${x}" y1="${split}" x2="${x + w}" y2="${split}" stroke="${stroke}" stroke-width="${sw}" />`
1117
1118
  )
1118
1119
  }
1119
1120
 
@@ -1130,9 +1131,9 @@ function renderWindowPane(
1130
1131
  const vx = x + Math.min(16, w * 0.16)
1131
1132
  const hy = y + Math.min(14, h * 0.28)
1132
1133
  return (
1133
- `<rect x="${x}" y="${y}" width="${w}" height="${h}" ${shapeAttrs(fill, stroke, sw)} />\n` +
1134
- `<line x1="${vx}" y1="${y}" x2="${vx}" y2="${y + h}" stroke="${stroke}" stroke-width="${sw}" />\n` +
1135
- `<line x1="${x}" y1="${hy}" x2="${x + w}" y2="${hy}" stroke="${stroke}" stroke-width="${sw}" />`
1134
+ f`<rect x="${x}" y="${y}" width="${w}" height="${h}" ${shapeAttrs(fill, stroke, sw)} />\n` +
1135
+ f`<line x1="${vx}" y1="${y}" x2="${vx}" y2="${y + h}" stroke="${stroke}" stroke-width="${sw}" />\n` +
1136
+ f`<line x1="${x}" y1="${hy}" x2="${x + w}" y2="${hy}" stroke="${stroke}" stroke-width="${sw}" />`
1136
1137
  )
1137
1138
  }
1138
1139
 
@@ -1148,9 +1149,9 @@ function renderTriangle(
1148
1149
  flipped: boolean,
1149
1150
  ): string {
1150
1151
  const points = flipped
1151
- ? [`${x},${y}`, `${x + w},${y}`, `${x + w / 2},${y + h}`]
1152
- : [`${x + w / 2},${y}`, `${x + w},${y + h}`, `${x},${y + h}`]
1153
- return `<polygon points="${points.join(' ')}" ${shapeAttrs(fill, stroke, sw)} />`
1152
+ ? [f`${x},${y}`, f`${x + w},${y}`, f`${x + w / 2},${y + h}`]
1153
+ : [f`${x + w / 2},${y}`, f`${x + w},${y + h}`, f`${x},${y + h}`]
1154
+ return f`<polygon points="${points.join(' ')}" ${shapeAttrs(fill, stroke, sw)} />`
1154
1155
  }
1155
1156
 
1156
1157
  /** Filled circle (junction): a solid dot, so it takes the stroke color as fill. */
@@ -1162,7 +1163,7 @@ function renderFilledCircle(
1162
1163
  stroke: string,
1163
1164
  ): string {
1164
1165
  const r = Math.min(w, h) / 2 - 2
1165
- return `<circle cx="${x + w / 2}" cy="${y + h / 2}" r="${r}" fill="${stroke}" stroke="${stroke}" />`
1166
+ return f`<circle cx="${x + w / 2}" cy="${y + h / 2}" r="${r}" fill="${stroke}" stroke="${stroke}" />`
1166
1167
  }
1167
1168
 
1168
1169
  /** Crossed circle (summary): circle with an X through it. */
@@ -1181,9 +1182,9 @@ function renderCrossedCircle(
1181
1182
  // Cross arms meet the circumference at 45°, so offset by r/√2.
1182
1183
  const d = r / Math.SQRT2
1183
1184
  return (
1184
- `<circle cx="${cx}" cy="${cy}" r="${r}" ${shapeAttrs(fill, stroke, sw)} />\n` +
1185
- `<line x1="${cx - d}" y1="${cy - d}" x2="${cx + d}" y2="${cy + d}" stroke="${stroke}" stroke-width="${sw}" />\n` +
1186
- `<line x1="${cx + d}" y1="${cy - d}" x2="${cx - d}" y2="${cy + d}" stroke="${stroke}" stroke-width="${sw}" />`
1185
+ f`<circle cx="${cx}" cy="${cy}" r="${r}" ${shapeAttrs(fill, stroke, sw)} />\n` +
1186
+ f`<line x1="${cx - d}" y1="${cy - d}" x2="${cx + d}" y2="${cy + d}" stroke="${stroke}" stroke-width="${sw}" />\n` +
1187
+ f`<line x1="${cx + d}" y1="${cy - d}" x2="${cx - d}" y2="${cy + d}" stroke="${stroke}" stroke-width="${sw}" />`
1187
1188
  )
1188
1189
  }
1189
1190
 
@@ -1196,7 +1197,7 @@ function renderForkJoin(
1196
1197
  stroke: string,
1197
1198
  ): string {
1198
1199
  const barH = Math.min(8, h)
1199
- return `<rect x="${x}" y="${y + (h - barH) / 2}" width="${w}" height="${barH}" fill="${stroke}" stroke="${stroke}" />`
1200
+ return f`<rect x="${x}" y="${y + (h - barH) / 2}" width="${w}" height="${barH}" fill="${stroke}" stroke="${stroke}" />`
1200
1201
  }
1201
1202
 
1202
1203
  /** Notched pentagon (loop limit): both top corners clipped. */
@@ -1211,14 +1212,14 @@ function renderNotchedPentagon(
1211
1212
  ): string {
1212
1213
  const notch = Math.min(14, w * 0.12, h * 0.3)
1213
1214
  const points = [
1214
- `${x + notch},${y}`,
1215
- `${x + w - notch},${y}`,
1216
- `${x + w},${y + notch}`,
1217
- `${x + w},${y + h}`,
1218
- `${x},${y + h}`,
1219
- `${x},${y + notch}`,
1215
+ f`${x + notch},${y}`,
1216
+ f`${x + w - notch},${y}`,
1217
+ f`${x + w},${y + notch}`,
1218
+ f`${x + w},${y + h}`,
1219
+ f`${x},${y + h}`,
1220
+ f`${x},${y + notch}`,
1220
1221
  ].join(' ')
1221
- return `<polygon points="${points}" ${shapeAttrs(fill, stroke, sw)} />`
1222
+ return f`<polygon points="${points}" ${shapeAttrs(fill, stroke, sw)} />`
1222
1223
  }
1223
1224
 
1224
1225
  /** Sloped rectangle (manual input): top edge slopes up to the right. */
@@ -1233,12 +1234,12 @@ function renderSlopedRectangle(
1233
1234
  ): string {
1234
1235
  const slope = Math.min(12, h * 0.3)
1235
1236
  const points = [
1236
- `${x},${y + slope}`,
1237
- `${x + w},${y}`,
1238
- `${x + w},${y + h}`,
1239
- `${x},${y + h}`,
1237
+ f`${x},${y + slope}`,
1238
+ f`${x + w},${y}`,
1239
+ f`${x + w},${y + h}`,
1240
+ f`${x},${y + h}`,
1240
1241
  ].join(' ')
1241
- return `<polygon points="${points}" ${shapeAttrs(fill, stroke, sw)} />`
1242
+ return f`<polygon points="${points}" ${shapeAttrs(fill, stroke, sw)} />`
1242
1243
  }
1243
1244
 
1244
1245
  /** Flag / paper tape: wavy top and bottom edges. */
@@ -1253,11 +1254,11 @@ function renderFlag(
1253
1254
  ): string {
1254
1255
  const waveH = Math.min(8, h * 0.14)
1255
1256
  const d =
1256
- `M ${x} ${y + waveH} ` +
1257
- `C ${x + w * 0.25} ${y - waveH} ${x + w * 0.75} ${y + waveH * 2} ${x + w} ${y + waveH} ` +
1258
- `L ${x + w} ${y + h - waveH} ` +
1259
- `C ${x + w * 0.75} ${y + h + waveH} ${x + w * 0.25} ${y + h - waveH * 2} ${x} ${y + h - waveH} Z`
1260
- return `<path d="${d}" ${shapeAttrs(fill, stroke, sw)} />`
1257
+ f`M ${x} ${y + waveH} ` +
1258
+ f`C ${x + w * 0.25} ${y - waveH} ${x + w * 0.75} ${y + waveH * 2} ${x + w} ${y + waveH} ` +
1259
+ f`L ${x + w} ${y + h - waveH} ` +
1260
+ f`C ${x + w * 0.75} ${y + h + waveH} ${x + w * 0.25} ${y + h - waveH * 2} ${x} ${y + h - waveH} Z`
1261
+ return f`<path d="${d}" ${shapeAttrs(fill, stroke, sw)} />`
1261
1262
  }
1262
1263
 
1263
1264
  /** Bow-tie rectangle (stored data): left and right edges curve inward. */
@@ -1272,11 +1273,11 @@ function renderBowTie(
1272
1273
  ): string {
1273
1274
  const bow = Math.min(14, w * 0.12)
1274
1275
  const d =
1275
- `M ${x + bow} ${y} L ${x + w} ${y} ` +
1276
- `Q ${x + w - bow * 1.4} ${y + h / 2} ${x + w} ${y + h} ` +
1277
- `L ${x + bow} ${y + h} ` +
1278
- `Q ${x + bow * 1.4} ${y + h / 2} ${x + bow} ${y} Z`
1279
- return `<path d="${d}" ${shapeAttrs(fill, stroke, sw)} />`
1276
+ f`M ${x + bow} ${y} L ${x + w} ${y} ` +
1277
+ f`Q ${x + w - bow * 1.4} ${y + h / 2} ${x + w} ${y + h} ` +
1278
+ f`L ${x + bow} ${y + h} ` +
1279
+ f`Q ${x + bow * 1.4} ${y + h / 2} ${x + bow} ${y} Z`
1280
+ return f`<path d="${d}" ${shapeAttrs(fill, stroke, sw)} />`
1280
1281
  }
1281
1282
 
1282
1283
  /** Delay / half-rounded rectangle: the right end is a semicircle. */
@@ -1292,10 +1293,10 @@ function renderHalfRounded(
1292
1293
  const r = h / 2
1293
1294
  const straight = Math.max(0, w - r)
1294
1295
  const d =
1295
- `M ${x} ${y} L ${x + straight} ${y} ` +
1296
- `A ${r} ${r} 0 0 1 ${x + straight} ${y + h} ` +
1297
- `L ${x} ${y + h} Z`
1298
- return `<path d="${d}" ${shapeAttrs(fill, stroke, sw)} />`
1296
+ f`M ${x} ${y} L ${x + straight} ${y} ` +
1297
+ f`A ${r} ${r} 0 0 1 ${x + straight} ${y + h} ` +
1298
+ f`L ${x} ${y + h} Z`
1299
+ return f`<path d="${d}" ${shapeAttrs(fill, stroke, sw)} />`
1299
1300
  }
1300
1301
 
1301
1302
  /**
@@ -1320,28 +1321,28 @@ function renderBraces(
1320
1321
  // what a brace annotation means. The rect stays only to reserve the
1321
1322
  // label area; `_fill` is deliberately unused.
1322
1323
  const parts = [
1323
- `<rect x="${x}" y="${y}" width="${w}" height="${h}" fill="none" stroke="none" />`,
1324
+ f`<rect x="${x}" y="${y}" width="${w}" height="${h}" fill="none" stroke="none" />`,
1324
1325
  ]
1325
1326
 
1326
1327
  const leftBrace =
1327
- `M ${x + armW} ${y} Q ${x} ${y} ${x} ${y + h * 0.25} ` +
1328
- `Q ${x} ${y + h / 2} ${x - armW * 0.4} ${y + h / 2} ` +
1329
- `Q ${x} ${y + h / 2} ${x} ${y + h * 0.75} ` +
1330
- `Q ${x} ${y + h} ${x + armW} ${y + h}`
1328
+ f`M ${x + armW} ${y} Q ${x} ${y} ${x} ${y + h * 0.25} ` +
1329
+ f`Q ${x} ${y + h / 2} ${x - armW * 0.4} ${y + h / 2} ` +
1330
+ f`Q ${x} ${y + h / 2} ${x} ${y + h * 0.75} ` +
1331
+ f`Q ${x} ${y + h} ${x + armW} ${y + h}`
1331
1332
  const rightBrace =
1332
- `M ${x + w - armW} ${y} Q ${x + w} ${y} ${x + w} ${y + h * 0.25} ` +
1333
- `Q ${x + w} ${y + h / 2} ${x + w + armW * 0.4} ${y + h / 2} ` +
1334
- `Q ${x + w} ${y + h / 2} ${x + w} ${y + h * 0.75} ` +
1335
- `Q ${x + w} ${y + h} ${x + w - armW} ${y + h}`
1333
+ f`M ${x + w - armW} ${y} Q ${x + w} ${y} ${x + w} ${y + h * 0.25} ` +
1334
+ f`Q ${x + w} ${y + h / 2} ${x + w + armW * 0.4} ${y + h / 2} ` +
1335
+ f`Q ${x + w} ${y + h / 2} ${x + w} ${y + h * 0.75} ` +
1336
+ f`Q ${x + w} ${y + h} ${x + w - armW} ${y + h}`
1336
1337
 
1337
1338
  if (side === 'left' || side === 'both') {
1338
1339
  parts.push(
1339
- `<path d="${leftBrace}" fill="none" stroke="${stroke}" stroke-width="${sw}" />`,
1340
+ f`<path d="${leftBrace}" fill="none" stroke="${stroke}" stroke-width="${sw}" />`,
1340
1341
  )
1341
1342
  }
1342
1343
  if (side === 'right' || side === 'both') {
1343
1344
  parts.push(
1344
- `<path d="${rightBrace}" fill="none" stroke="${stroke}" stroke-width="${sw}" />`,
1345
+ f`<path d="${rightBrace}" fill="none" stroke="${stroke}" stroke-width="${sw}" />`,
1345
1346
  )
1346
1347
  }
1347
1348
 
@@ -1359,15 +1360,15 @@ function renderBolt(
1359
1360
  sw: string,
1360
1361
  ): string {
1361
1362
  const points = [
1362
- `${x + w * 0.42},${y}`,
1363
- `${x + w},${y}`,
1364
- `${x + w * 0.62},${y + h * 0.42}`,
1365
- `${x + w},${y + h * 0.42}`,
1366
- `${x + w * 0.3},${y + h}`,
1367
- `${x + w * 0.5},${y + h * 0.55}`,
1368
- `${x},${y + h * 0.55}`,
1363
+ f`${x + w * 0.42},${y}`,
1364
+ f`${x + w},${y}`,
1365
+ f`${x + w * 0.62},${y + h * 0.42}`,
1366
+ f`${x + w},${y + h * 0.42}`,
1367
+ f`${x + w * 0.3},${y + h}`,
1368
+ f`${x + w * 0.5},${y + h * 0.55}`,
1369
+ f`${x},${y + h * 0.55}`,
1369
1370
  ].join(' ')
1370
- return `<polygon points="${points}" ${shapeAttrs(fill, stroke, sw)} />`
1371
+ return f`<polygon points="${points}" ${shapeAttrs(fill, stroke, sw)} />`
1371
1372
  }
1372
1373
 
1373
1374
  // --- Batch 3: State diagram pseudostates ---
@@ -1377,7 +1378,7 @@ function renderStateStart(x: number, y: number, w: number, h: number): string {
1377
1378
  const cx = x + w / 2
1378
1379
  const cy = y + h / 2
1379
1380
  const r = Math.min(w, h) / 2 - 2
1380
- return `<circle cx="${cx}" cy="${cy}" r="${r}" fill="var(--_text)" stroke="none" />`
1381
+ return f`<circle cx="${cx}" cy="${cy}" r="${r}" fill="var(--_text)" stroke="none" />`
1381
1382
  }
1382
1383
 
1383
1384
  /** State end: bullseye — outer ring + inner filled circle using primary text color */
@@ -1387,9 +1388,9 @@ function renderStateEnd(x: number, y: number, w: number, h: number): string {
1387
1388
  const outerR = Math.min(w, h) / 2 - 2
1388
1389
  const innerR = outerR - 4
1389
1390
  return (
1390
- `<circle cx="${cx}" cy="${cy}" r="${outerR}" ` +
1391
- `fill="none" stroke="var(--_text)" stroke-width="${STROKE_WIDTHS.innerBox * 2}" />` +
1392
- `\n<circle cx="${cx}" cy="${cy}" r="${innerR}" fill="var(--_text)" stroke="none" />`
1391
+ f`<circle cx="${cx}" cy="${cy}" r="${outerR}" ` +
1392
+ f`fill="none" stroke="var(--_text)" stroke-width="${STROKE_WIDTHS.innerBox * 2}" />` +
1393
+ f`\n<circle cx="${cx}" cy="${cy}" r="${innerR}" fill="var(--_text)" stroke="none" />`
1393
1394
  )
1394
1395
  }
1395
1396
 
@@ -1423,7 +1424,7 @@ function renderNodeLabel(
1423
1424
  getReadableTextColor(node.inlineStyle?.fill, 'var(--_text)'),
1424
1425
  )
1425
1426
 
1426
- let attrs = `text-anchor="middle" font-size="${fontSizes.nodeLabel}" font-weight="${FONT_WEIGHTS.nodeLabel}" fill="${textColor}"`
1427
+ let attrs = f`text-anchor="middle" font-size="${fontSizes.nodeLabel}" font-weight="${FONT_WEIGHTS.nodeLabel}" fill="${textColor}"`
1427
1428
 
1428
1429
  // Per-node font-family override (from `style A font-family:...` or
1429
1430
  // classDef/class). Emitted as an inline `style` attribute rather than a
@@ -1436,7 +1437,7 @@ function renderNodeLabel(
1436
1437
  // keeps falling back to the global font stack. See issue #57.
1437
1438
  const fontFamily = node.inlineStyle?.['font-family']
1438
1439
  if (fontFamily) {
1439
- attrs += ` style="font-family: ${escapeAttr(fontFamily)};"`
1440
+ attrs += f` style="font-family: ${escapeAttr(fontFamily)};"`
1440
1441
  }
1441
1442
 
1442
1443
  return renderMultilineText(node.label, cx, cy, fontSizes.nodeLabel, attrs)
@@ -1480,6 +1481,6 @@ export function withDataSrc(
1480
1481
  if (source === undefined) return svgTag
1481
1482
  return svgTag.replace(
1482
1483
  '<svg ',
1483
- `<svg data-src="${escapeMultilineAttr(source)}" `,
1484
+ f`<svg data-src="${escapeMultilineAttr(source)}" `,
1484
1485
  )
1485
1486
  }