@zombie-mermaid/core 4.0.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/dist/index.cjs +8 -15
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +38 -15
- package/dist/index.d.ts +38 -15
- package/dist/index.js +179 -144
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/cluster-edges.test.ts +47 -0
- package/src/__tests__/theme.test.ts +18 -15
- package/src/cluster-edges.ts +78 -0
- package/src/index.ts +1 -0
- package/src/theme.ts +22 -22
- package/src/generated/mono-font-subset.ts +0 -32
package/package.json
CHANGED
|
@@ -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',
|
|
137
|
-
|
|
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('
|
|
144
|
-
const block = buildStyleBlock('Inter',
|
|
145
|
-
expect(block).toContain(
|
|
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
|
|
151
|
-
const block = buildStyleBlock('Inter',
|
|
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: '
|
|
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
|
|
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
|
-
* `
|
|
422
|
-
* attribute types) never reaches out to Google Fonts at all
|
|
423
|
-
*
|
|
424
|
-
*
|
|
425
|
-
*
|
|
426
|
-
*
|
|
427
|
-
*
|
|
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
|
-
|
|
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
|
-
...(
|
|
478
|
+
...(mono ? [` ${mono.faceCss}`] : []),
|
|
479
479
|
` text { font-family: ${isVarReference ? safeFont : `'${safeFont}'`}, system-ui, sans-serif; }`,
|
|
480
|
-
...(
|
|
480
|
+
...(mono
|
|
481
481
|
? [
|
|
482
|
-
` .mono { font-family: '${
|
|
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
|
-
}`
|