@zombie-mermaid/core 3.2.0 → 4.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.2.0",
3
+ "version": "4.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,47 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import { parseMermaid } from '@zombie-mermaid/mermaid-parser'
3
+ import { withoutOwnClusterEdges } from '../cluster-edges.ts'
4
+
5
+ const pairs = (src: string) =>
6
+ withoutOwnClusterEdges(parseMermaid(src)).edges.map(
7
+ (e) => `${e.source}>${e.target}`,
8
+ )
9
+
10
+ describe('withoutOwnClusterEdges (#1310)', () => {
11
+ it('drops an edge from a member node to its subgraph', () => {
12
+ expect(
13
+ pairs('flowchart TD; subgraph Sub; B; end; A --> B; B --> Sub'),
14
+ ).toEqual(['A>B'])
15
+ })
16
+
17
+ it('drops an edge from a subgraph to one of its members', () => {
18
+ expect(
19
+ pairs('flowchart TD; subgraph Sub; B; end; A --> B; Sub --> B'),
20
+ ).toEqual(['A>B'])
21
+ })
22
+
23
+ it('drops edges between a nested member, its enclosing subgraphs, and nested subgraphs', () => {
24
+ expect(
25
+ pairs(
26
+ 'flowchart TD; subgraph Outer; subgraph Inner; B; end; end; A --> B; B --> Outer; Inner --> B; Outer --> Inner',
27
+ ),
28
+ ).toEqual(['A>B'])
29
+ })
30
+
31
+ it('keeps an edge from an outside node to a subgraph', () => {
32
+ expect(
33
+ pairs('flowchart TD; subgraph Sub; B; end; X --> Sub; A --> B'),
34
+ ).toEqual(['X>Sub', 'A>B'])
35
+ })
36
+
37
+ it('keeps an edge between two subgraphs', () => {
38
+ expect(
39
+ pairs('flowchart TD; subgraph P; A; end; subgraph Q; B; end; P --> Q'),
40
+ ).toEqual(['P>Q'])
41
+ })
42
+
43
+ it('returns the same graph object when nothing is dropped', () => {
44
+ const g = parseMermaid('flowchart TD; subgraph Sub; B; end; X --> Sub')
45
+ expect(withoutOwnClusterEdges(g)).toBe(g)
46
+ })
47
+ })
@@ -131,30 +131,33 @@ describe('buildStyleBlock – font handling', () => {
131
131
  })
132
132
  })
133
133
 
134
- describe('buildStyleBlock – mono font (#1061)', () => {
134
+ describe('buildStyleBlock – mono font embed (#1061, #1319)', () => {
135
+ // core ships no font data; the caller supplies the embed (svg-renderer's
136
+ // buildSvgStyleBlock supplies the real subset — see src/__tests__/styles.test.ts).
137
+ const embed = {
138
+ family: 'Test Mono',
139
+ faceCss:
140
+ "@font-face { font-family: 'Test Mono'; src: url(data:font/woff2;base64,AAAA) format('woff2'); }",
141
+ }
142
+
135
143
  it('never reaches out to Google Fonts for the .mono rule', () => {
136
- const block = buildStyleBlock('Inter', true)
137
- // The main `font` still gets its own Google Fonts @import (unrelated to
138
- // this regression test), but nothing in the block should ever request
139
- // JetBrains Mono from a third-party CDN.
140
- expect(block).not.toContain('fonts.googleapis.com/css2?family=JetBrains')
144
+ const block = buildStyleBlock('Inter', embed)
145
+ expect(block).not.toContain('fonts.googleapis.com/css2?family=Test')
141
146
  })
142
147
 
143
- it('embeds a self-hosted @font-face as a base64 woff2 data URI for the mono font', () => {
144
- const block = buildStyleBlock('Inter', true)
145
- expect(block).toContain('@font-face')
146
- expect(block).toContain('data:font/woff2;base64,')
147
- expect(block).toContain("font-family: 'JetBrains Mono NL';")
148
+ it('inlines the supplied @font-face verbatim', () => {
149
+ const block = buildStyleBlock('Inter', embed)
150
+ expect(block).toContain(` ${embed.faceCss}`)
148
151
  })
149
152
 
150
- it("leads the .mono rule's font stack with the self-hosted family name", () => {
151
- const block = buildStyleBlock('Inter', true)
153
+ it("leads the .mono rule's font stack with the supplied family name", () => {
154
+ const block = buildStyleBlock('Inter', embed)
152
155
  expect(block).toContain(
153
- " .mono { font-family: 'JetBrains Mono NL', 'SF Mono', 'Fira Code', ui-monospace, monospace; }",
156
+ " .mono { font-family: 'Test Mono', 'SF Mono', 'Fira Code', ui-monospace, monospace; }",
154
157
  )
155
158
  })
156
159
 
157
- it('emits neither the @font-face embed nor the .mono rule when hasMonoFont is false', () => {
160
+ it('emits neither the @font-face embed nor the .mono rule when mono is false', () => {
158
161
  const block = buildStyleBlock('Inter', false)
159
162
  expect(block).not.toContain('@font-face')
160
163
  expect(block).not.toContain('.mono')
@@ -0,0 +1,78 @@
1
+ // ============================================================================
2
+ // Edges between a node and a subgraph that contains it (#1310)
3
+ //
4
+ // Real mermaid.js (11.17.2, checked headlessly) accepts `B --> Sub` with B
5
+ // inside Sub, and `Sub --> B`, without erroring, but its layout produces a
6
+ // zero-length path (`M x,yZ`) for the edge: no line joins the two, only a
7
+ // stray arrowhead marker is left floating beside the node. Both renderers
8
+ // here treat such an edge as undrawable and omit it; this module is the one
9
+ // place that decides which edges those are, so the SVG and ASCII renderers
10
+ // cannot disagree.
11
+ // ============================================================================
12
+
13
+ import type { MermaidGraph, MermaidSubgraph } from './types.ts'
14
+
15
+ /** Collect `id` and every descendant subgraph into `byId`. */
16
+ function indexSubgraph(
17
+ sg: MermaidSubgraph,
18
+ byId: Map<string, MermaidSubgraph>,
19
+ ): void {
20
+ byId.set(sg.id, sg)
21
+ for (const child of sg.children) indexSubgraph(child, byId)
22
+ }
23
+
24
+ /**
25
+ * True when one endpoint of `edge` is a subgraph id and the other is a node or
26
+ * subgraph nested inside it, at any depth.
27
+ */
28
+ export function isEdgeWithinOwnCluster(
29
+ edge: { source: string; target: string },
30
+ subgraphById: Map<string, MermaidSubgraph>,
31
+ ): boolean {
32
+ const contains = (clusterId: string, innerId: string): boolean => {
33
+ const sg = subgraphById.get(clusterId)
34
+ if (!sg) return false
35
+ const inside = new Set<string>()
36
+ collectInsideIds(sg, inside)
37
+ return inside.has(innerId)
38
+ }
39
+ return (
40
+ contains(edge.target, edge.source) || contains(edge.source, edge.target)
41
+ )
42
+ }
43
+
44
+ /** Every node id and descendant subgraph id nested inside `sg`. */
45
+ function collectInsideIds(sg: MermaidSubgraph, out: Set<string>): void {
46
+ for (const id of sg.nodeIds) out.add(id)
47
+ for (const child of sg.children) {
48
+ out.add(child.id)
49
+ collectInsideIds(child, out)
50
+ }
51
+ }
52
+
53
+ /**
54
+ * `graph` without its edges between a node and a subgraph that contains it.
55
+ * The parser also registers a subgraph id used as an edge endpoint as a
56
+ * placeholder entry in `graph.nodes`; one no remaining edge refers to is
57
+ * removed too, so the result equals the graph parsed without those edges.
58
+ * Returns `graph` itself (same reference) when it has none, and never mutates
59
+ * its input.
60
+ */
61
+ export function withoutOwnClusterEdges(graph: MermaidGraph): MermaidGraph {
62
+ if (graph.subgraphs.length === 0) return graph
63
+ const byId = new Map<string, MermaidSubgraph>()
64
+ for (const sg of graph.subgraphs) indexSubgraph(sg, byId)
65
+ const edges = graph.edges.filter((e) => !isEdgeWithinOwnCluster(e, byId))
66
+ if (edges.length === graph.edges.length) return graph
67
+
68
+ const referenced = new Set<string>()
69
+ for (const e of edges) {
70
+ referenced.add(e.source)
71
+ referenced.add(e.target)
72
+ }
73
+ const nodes = new Map(graph.nodes)
74
+ for (const id of byId.keys()) {
75
+ if (!referenced.has(id)) nodes.delete(id)
76
+ }
77
+ return { ...graph, nodes, edges }
78
+ }
package/src/index.ts CHANGED
@@ -21,6 +21,7 @@
21
21
  // ============================================================================
22
22
 
23
23
  export * from './click-directive.ts'
24
+ export * from './cluster-edges.ts'
24
25
  export * from './color-utils.ts'
25
26
  export * from './diagram-type.ts'
26
27
  export * from './direction.ts'
package/src/theme.ts CHANGED
@@ -14,10 +14,6 @@
14
14
  import { escapeXml, escapeAttr } from './multiline-utils.ts'
15
15
  import { parseHexColor } from './color-utils.ts'
16
16
  import { f } from './render-utils.ts'
17
- import {
18
- MONO_FONT_FAMILY as SVG_MONO_FONT_FAMILY,
19
- MONO_FONT_FACE_CSS as SVG_MONO_FONT_FACE_CSS,
20
- } from './generated/mono-font-subset.ts'
21
17
 
22
18
  // ============================================================================
23
19
  // Types
@@ -39,6 +35,17 @@ export interface SvgEmitOptions {
39
35
  styleAttribute?: boolean
40
36
  }
41
37
 
38
+ /**
39
+ * A self-hosted monospace font a caller embeds in the SVG `<style>` block
40
+ * for the `.mono` rule. See `buildStyleBlock`.
41
+ */
42
+ export interface MonoFontEmbed {
43
+ /** Font family name, listed first in the `.mono` rule's font stack. */
44
+ family: string
45
+ /** The complete `@font-face { ... }` rule (typically a base64 data URI). */
46
+ faceCss: string
47
+ }
48
+
42
49
  /**
43
50
  * Diagram color configuration.
44
51
  *
@@ -418,24 +425,17 @@ export function styleOpenTag(nonce?: string): string {
418
425
  * Any other value is treated as a literal font name: sanitized and quoted
419
426
  * exactly as before.
420
427
  *
421
- * `hasMonoFont`'s `.mono` rule (class-diagram method signatures, ER-diagram
422
- * attribute types) never reaches out to Google Fonts at all — it used to
423
- * `@import` 'JetBrains Mono' from Google's CDN, a third-party network
424
- * dependency a published library's default SVG output had no business
425
- * making (#1061, filed as a follow-up from #1059's ASCII-side self-hosting
426
- * fix). It now embeds {@link SVG_MONO_FONT_FACE_CSS} — a small, subsetted
427
- * (Basic Latin + Latin-1 Supplement), self-hosted `@font-face` as a base64
428
- * `woff2` data URI, generated by scripts/build-mono-font-subset.ts into
429
- * ./generated/mono-font-subset.ts — directly in the SVG's own `<style>`
430
- * block. Unlike `font` above, this isn't caller-configurable: it's the same
431
- * embed for every consumer, on by default, with no network request either
432
- * way — a strict improvement over the dead-or-alive CDN fetch it replaces,
433
- * and the only way a *standalone* SVG (no surrounding host page providing
434
- * its own copy of the font) renders `.mono` text correctly out of the box.
428
+ * `mono`'s `.mono` rule (class-diagram method signatures, ER-diagram
429
+ * attribute types) never reaches out to Google Fonts at all. The caller
430
+ * supplies a self-hosted {@link MonoFontEmbed} (a base64 `woff2` `@font-face`
431
+ * plus its family name), which is inlined directly in the SVG's own `<style>`
432
+ * block (#1061). The font data itself lives in `@zombie-mermaid/svg-renderer`
433
+ * (see its `buildSvgStyleBlock`), not here, so ASCII-only consumers of
434
+ * `core` never carry it (#1319). Pass `false` to omit the `.mono` rule.
435
435
  */
436
436
  export function buildStyleBlock(
437
437
  font: string,
438
- hasMonoFont: boolean,
438
+ mono: MonoFontEmbed | false,
439
439
  nonce?: string,
440
440
  ): string {
441
441
  const isVarReference = isSafeCssVarReference(font)
@@ -475,11 +475,11 @@ export function buildStyleBlock(
475
475
  // Self-hosted, embedded @font-face for `.mono` text — see the doc
476
476
  // comment above for why this never goes through the CDN-import/skip
477
477
  // logic `font` above does.
478
- ...(hasMonoFont ? [` ${SVG_MONO_FONT_FACE_CSS}`] : []),
478
+ ...(mono ? [` ${mono.faceCss}`] : []),
479
479
  ` text { font-family: ${isVarReference ? safeFont : `'${safeFont}'`}, system-ui, sans-serif; }`,
480
- ...(hasMonoFont
480
+ ...(mono
481
481
  ? [
482
- ` .mono { font-family: '${SVG_MONO_FONT_FAMILY}', 'SF Mono', 'Fira Code', ui-monospace, monospace; }`,
482
+ ` .mono { font-family: '${mono.family}', 'SF Mono', 'Fira Code', ui-monospace, monospace; }`,
483
483
  ]
484
484
  : []),
485
485
  ` svg {${derivedVars}`,
@@ -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/core/src/theme.ts's `buildStyleBlock()` 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
- }`