@zombie-mermaid/core 3.0.0 → 3.1.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zombie-mermaid/core",
3
- "version": "3.0.0",
3
+ "version": "3.1.0",
4
4
  "license": "MIT",
5
5
  "description": "Shared types, theme, and text/statement utilities used by every zombie-mermaid renderer.",
6
6
  "repository": {
@@ -0,0 +1,59 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import { f } from '../render-utils.ts'
3
+
4
+ describe('f tagged template literal', () => {
5
+ it('rounds floats to 2 decimal places', () => {
6
+ expect(f`${3.14159}`).toBe('3.14')
7
+ expect(f`${0.1 + 0.2}`).toBe('0.3')
8
+ expect(f`${1.005}`).toBe('1')
9
+ expect(f`${1.1}`).toBe('1.1')
10
+ expect(f`${2.0}`).toBe('2')
11
+ expect(f`M ${'A'} ${3.14159} L ${10.5} ${-20.999}`).toBe(
12
+ 'M A 3.14 L 10.5 -21',
13
+ )
14
+ })
15
+
16
+ it('keeps integers unchanged', () => {
17
+ expect(f`${42}`).toBe('42')
18
+ expect(f`${0}`).toBe('0')
19
+ expect(f`${-7}`).toBe('-7')
20
+ })
21
+
22
+ it('passes non-number values through as-is', () => {
23
+ expect(f`hello ${'world'}`).toBe('hello world')
24
+ expect(f`${'foo'}bar`).toBe('foobar')
25
+ expect(f`just text`).toBe('just text')
26
+ expect(f`${true}`).toBe('true')
27
+ expect(f`${null}`).toBe('null')
28
+ expect(f`${undefined}`).toBe('undefined')
29
+ })
30
+
31
+ it('normalizes -0 to 0', () => {
32
+ expect(f`${-0}`).toBe('0')
33
+ expect(f`${-0.001}`).toBe('0')
34
+ })
35
+
36
+ it('rounds very small magnitudes down to 0 rather than emitting scientific notation', () => {
37
+ expect(f`${1e-10}`).toBe('0')
38
+ expect(f`${-1e-10}`).toBe('0')
39
+ })
40
+
41
+ it('normalizes non-finite values to 0 instead of emitting invalid SVG tokens', () => {
42
+ // Reachable from degenerate zero-size geometry (e.g. a zero-length
43
+ // vector normalized by dividing by its own length). "NaN"/"Infinity"
44
+ // are not valid SVG numeric attribute values, unlike upstream's
45
+ // behavior of stringifying them literally.
46
+ expect(f`${NaN}`).toBe('0')
47
+ expect(f`${Infinity}`).toBe('0')
48
+ expect(f`${-Infinity}`).toBe('0')
49
+ expect(f`<rect x="${NaN}" width="${10.5}" />`).toBe(
50
+ '<rect x="0" width="10.5" />',
51
+ )
52
+ })
53
+
54
+ it('handles multiple interpolations mixing rounded and passthrough values', () => {
55
+ expect(
56
+ f`<rect x="${1.005}" y="${-20.999}" fill="${'red'}" data-n="${5}" />`,
57
+ ).toBe('<rect x="1" y="-21" fill="red" data-n="5" />')
58
+ })
59
+ })
@@ -10,8 +10,22 @@
10
10
 
11
11
  import { splitStatements } from './statements.ts'
12
12
 
13
+ /**
14
+ * Every diagram type this library can detect and route to a renderer — the
15
+ * runtime source of truth `DiagramType` is derived from, so consumers that
16
+ * need to enumerate the types (e.g. the MCP `list_diagram_types` tool) never
17
+ * keep a duplicate list that could drift.
18
+ */
19
+ export const DIAGRAM_TYPES = [
20
+ 'flowchart',
21
+ 'sequence',
22
+ 'class',
23
+ 'er',
24
+ 'xychart',
25
+ ] as const
26
+
13
27
  /** The diagram types this library can detect and route to a renderer. */
14
- export type DiagramType = 'flowchart' | 'sequence' | 'class' | 'er' | 'xychart'
28
+ export type DiagramType = (typeof DIAGRAM_TYPES)[number]
15
29
 
16
30
  /**
17
31
  * Detect the diagram type from the mermaid source text.
package/src/index.ts CHANGED
@@ -27,6 +27,7 @@ export * from './direction.ts'
27
27
  export * from './direction-override.ts'
28
28
  export * from './init-directive.ts'
29
29
  export * from './multiline-utils.ts'
30
+ export * from './render-utils.ts'
30
31
  export * from './statements.ts'
31
32
  export * from './style-directives.ts'
32
33
  export * from './text-metrics.ts'
@@ -7,6 +7,7 @@
7
7
  // ============================================================================
8
8
 
9
9
  import { LINE_HEIGHT_RATIO } from './text-metrics.ts'
10
+ import { f } from './render-utils.ts'
10
11
 
11
12
  /**
12
13
  * Normalize label text: strip surrounding quotes, convert <br> tags and
@@ -216,7 +217,7 @@ export function renderMultilineText(
216
217
  // Single line — simple text element
217
218
  if (lines.length === 1) {
218
219
  const dy = fontSize * baselineShift
219
- return `<text x="${cx}" y="${cy}" ${attrs} dy="${dy}">${renderLineContent(text)}</text>`
220
+ return f`<text x="${cx}" y="${cy}" ${attrs} dy="${dy}">${renderLineContent(text)}</text>`
220
221
  }
221
222
 
222
223
  // Multi-line — use tspan elements with vertical centering
@@ -228,11 +229,11 @@ export function renderMultilineText(
228
229
  const tspans = lines
229
230
  .map((line, i) => {
230
231
  const dy = i === 0 ? firstDy : lineHeight
231
- return `<tspan x="${cx}" dy="${dy}">${renderLineContent(line)}</tspan>`
232
+ return f`<tspan x="${cx}" dy="${dy}">${renderLineContent(line)}</tspan>`
232
233
  })
233
234
  .join('')
234
235
 
235
- return `<text x="${cx}" y="${cy}" ${attrs}>${tspans}</text>`
236
+ return f`<text x="${cx}" y="${cy}" ${attrs}>${tspans}</text>`
236
237
  }
237
238
 
238
239
  /**
@@ -266,8 +267,8 @@ export function renderMultilineTextWithBackground(
266
267
  const bgHeight = textHeight + padding * 2
267
268
 
268
269
  const rect =
269
- `<rect x="${cx - bgWidth / 2}" y="${cy - bgHeight / 2}" ` +
270
- `width="${bgWidth}" height="${bgHeight}" ${bgAttrs} />`
270
+ f`<rect x="${cx - bgWidth / 2}" y="${cy - bgHeight / 2}" ` +
271
+ f`width="${bgWidth}" height="${bgHeight}" ${bgAttrs} />`
271
272
 
272
273
  const textEl = renderMultilineText(text, cx, cy, fontSize, textAttrs)
273
274
 
@@ -0,0 +1,54 @@
1
+ // ============================================================================
2
+ // SVG numeric formatting — round interpolated coordinates/dimensions to 2dp.
3
+ //
4
+ // Ported from lukilabs/beautiful-mermaid#77 ("Round all floats to 2 decimal
5
+ // places in SVG output", credit: GauBen). ELK layout and the curve/bezier
6
+ // math in packages/svg-renderer/src/edge-curves.ts produce full
7
+ // floating-point precision (e.g. `142.38427299999998`), which upstream's PR
8
+ // pointed out bloats generated SVG for no visual benefit — nothing renders a
9
+ // sub-hundredth-of-a-pixel difference. `f` is a tagged template literal used
10
+ // in place of a plain template literal (`` f`<rect x="${x}" />` `` instead of
11
+ // `` `<rect x="${x}" />` ``) everywhere flowchart/ER/class/sequence markup is
12
+ // built, so every interpolated number gets rounded right where it is
13
+ // stringified, with no change to non-numeric interpolations (ids, colors,
14
+ // already-formatted strings).
15
+ //
16
+ // packages/svg-renderer/src/xychart/{renderer,layout}.ts already has its own
17
+ // rounding (`r()`, 1dp) for unrelated axis-label-formatting reasons — this
18
+ // helper is deliberately not applied there.
19
+ // ============================================================================
20
+
21
+ /**
22
+ * Round a number to at most 2 decimal places for SVG output.
23
+ *
24
+ * Non-finite input (`NaN`/`Infinity`, reachable from degenerate zero-size
25
+ * geometry — e.g. a normalized zero-length vector) is normalized to `0`
26
+ * rather than stringified literally. This is a deliberate improvement over
27
+ * upstream: `"NaN"`/`"Infinity"` are not valid SVG numeric attribute values,
28
+ * so passing them through would hand the browser malformed markup instead of
29
+ * just an odd-looking number. Every other case matches upstream exactly,
30
+ * including collapsing `-0` to `0`.
31
+ */
32
+ function roundCoord(n: number): number {
33
+ if (!Number.isFinite(n)) return 0
34
+ return Number(n.toFixed(2))
35
+ }
36
+
37
+ /**
38
+ * Tagged template literal: stringifies interpolated values, rounding any
39
+ * `number` to at most 2 decimal places first. Non-number values (strings,
40
+ * booleans, etc.) pass through `String()` unchanged, same as an untagged
41
+ * template literal would.
42
+ *
43
+ * @example
44
+ * f`<rect x="${3.14159}" y="${-0.001}" />` // '<rect x="3.14" y="0" />'
45
+ */
46
+ export function f(strings: TemplateStringsArray, ...values: unknown[]): string {
47
+ let out = strings[0] ?? ''
48
+ for (let i = 0; i < values.length; i++) {
49
+ const v = values[i]
50
+ out += typeof v === 'number' ? String(roundCoord(v)) : String(v)
51
+ out += strings[i + 1] ?? ''
52
+ }
53
+ return out
54
+ }
package/src/theme.ts CHANGED
@@ -13,6 +13,7 @@
13
13
 
14
14
  import { escapeXml, escapeAttr } from './multiline-utils.ts'
15
15
  import { parseHexColor } from './color-utils.ts'
16
+ import { f } from './render-utils.ts'
16
17
  import {
17
18
  MONO_FONT_FAMILY as SVG_MONO_FONT_FAMILY,
18
19
  MONO_FONT_FACE_CSS as SVG_MONO_FONT_FACE_CSS,
@@ -705,8 +706,8 @@ export function svgOpenTag(
705
706
  }
706
707
 
707
708
  return (
708
- `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${width} ${height}" ` +
709
- `width="${width}" height="${height}"${a11yAttrs}${styleAttr}>` +
709
+ f`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${width} ${height}" ` +
710
+ f`width="${width}" height="${height}"${a11yAttrs}${styleAttr}>` +
710
711
  titleEl
711
712
  )
712
713
  }