@zombie-mermaid/core 4.0.0 → 4.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zombie-mermaid/core",
3
- "version": "4.0.0",
3
+ "version": "4.2.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
+ })
@@ -59,6 +59,21 @@ describe('detectDiagramType', () => {
59
59
  expect(detectDiagramType('xychart-foo\nx-axis [a, b]')).toBe('flowchart')
60
60
  })
61
61
 
62
+ it('detects pie charts with or without trailing header content', () => {
63
+ expect(detectDiagramType('pie\n"A" : 1')).toBe('pie')
64
+ expect(detectDiagramType(' pie ')).toBe('pie')
65
+ expect(detectDiagramType('pie showData\n"A" : 1')).toBe('pie')
66
+ expect(detectDiagramType('pie title Pets\n"A" : 1')).toBe('pie')
67
+ expect(detectDiagramType('pie\ttitle Pets')).toBe('pie')
68
+ expect(detectDiagramType('%% comment\npie\n"A" : 1')).toBe('pie')
69
+ })
70
+
71
+ it('matches the pie header case-sensitively and as a whole word, like Mermaid', () => {
72
+ expect(detectDiagramType('Pie\n"A" : 1')).toBe('flowchart')
73
+ expect(detectDiagramType('PIE\n"A" : 1')).toBe('flowchart')
74
+ expect(detectDiagramType('pieChart\n"A" : 1')).toBe('flowchart')
75
+ })
76
+
62
77
  it('falls back to flowchart for unrecognized input', () => {
63
78
  expect(detectDiagramType('')).toBe('flowchart')
64
79
  expect(detectDiagramType('not a real header')).toBe('flowchart')
@@ -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
+ }
@@ -25,6 +25,7 @@ export const DIAGRAM_TYPES = [
25
25
  'architecture',
26
26
 
27
27
  'c4',
28
+ 'pie',
28
29
  ] as const
29
30
 
30
31
  /** The diagram types this library can detect and route to a renderer. */
@@ -40,7 +41,14 @@ export type DiagramType = (typeof DIAGRAM_TYPES)[number]
40
41
  * routing and parsing can never disagree about where the header ends.
41
42
  */
42
43
  export function detectDiagramType(text: string): DiagramType {
43
- const firstLine = splitStatements(text)[0]?.text.toLowerCase() ?? ''
44
+ const header = splitStatements(text)[0]?.text ?? ''
45
+ const firstLine = header.toLowerCase()
46
+
47
+ // Case-sensitive, unlike the other headers: Mermaid's pie detector is
48
+ // `/^\s*pie/` and its grammar keyword `pie` must be followed by
49
+ // whitespace, a comment, or the end of input, so `Pie` and `pie:` are not
50
+ // pie charts there either.
51
+ if (/^pie(?:\s|$)/.test(header)) return 'pie'
44
52
 
45
53
  if (/^xychart(?:-beta)?(?:\s|$)/.test(firstLine)) return 'xychart'
46
54
  if (/^architecture(?:-beta)?\s*$/.test(firstLine)) return 'architecture'
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'
@@ -30,7 +30,7 @@ export function normalizeBrTags(label: string): string {
30
30
  ? unquoted.slice(1, -1)
31
31
  : unquoted
32
32
 
33
- return (
33
+ return decodeMermaidEntities(
34
34
  unfenced
35
35
  .replace(/<br\s*\/?>/gi, '\n')
36
36
  .replace(/\\n/g, '\n')
@@ -38,7 +38,71 @@ export function normalizeBrTags(label: string): string {
38
38
  // Markdown formatting → HTML tags (order matters: ** before *)
39
39
  .replace(/\*\*(.+?)\*\*/g, '<b>$1</b>')
40
40
  .replace(/(?<!\*)\*([^\s*](?:[^*]*[^\s*])?)\*(?!\*)/g, '<i>$1</i>')
41
- .replace(/~~(.+?)~~/g, '<s>$1</s>')
41
+ .replace(/~~(.+?)~~/g, '<s>$1</s>'),
42
+ )
43
+ }
44
+
45
+ const MAX_CODEPOINT = 0x10ffff
46
+
47
+ const NAMED_ENTITIES: Record<string, string> = {
48
+ quot: '"',
49
+ amp: '&',
50
+ lt: '<',
51
+ gt: '>',
52
+ apos: "'",
53
+ nbsp: '\u00a0',
54
+ }
55
+
56
+ /** Code point -> character, or undefined when it is not a usable scalar value. */
57
+ function codePointToChar(cp: number): string | undefined {
58
+ const usable =
59
+ Number.isInteger(cp) &&
60
+ cp > 0 &&
61
+ cp <= MAX_CODEPOINT &&
62
+ !(cp >= 0xd800 && cp <= 0xdfff)
63
+ return usable ? String.fromCodePoint(cp) : undefined
64
+ }
65
+
66
+ /**
67
+ * Decode Mermaid's entity codes (`#quot;`, `#lt;`, `#35;`, `#x5B;`), the way
68
+ * Mermaid writes characters that would otherwise end a label or break its
69
+ * syntax. Call it on a single already-parsed label, never on diagram source:
70
+ * decoding up front would let the characters it produces (`<`, `"`, `[`)
71
+ * change how the line parses, and would hit `#fff;` colors in style lines.
72
+ * Unknown names and out-of-range numbers are left untouched.
73
+ */
74
+ export function decodeMermaidEntities(text: string): string {
75
+ return text.replace(
76
+ /#([xX][0-9a-fA-F]+|[0-9]+|[a-zA-Z]+);/g,
77
+ (match, code: string) => {
78
+ if (/^x/i.test(code)) {
79
+ return codePointToChar(parseInt(code.slice(1), 16)) ?? match
80
+ }
81
+ if (/^[0-9]+$/.test(code)) {
82
+ return codePointToChar(parseInt(code, 10)) ?? match
83
+ }
84
+ return NAMED_ENTITIES[code.toLowerCase()] ?? match
85
+ },
86
+ )
87
+ }
88
+
89
+ /**
90
+ * Decode HTML/XML entities (`&quot;`, `&lt;`, `&#35;`, `&#x5B;`) in one label,
91
+ * in a single pass so `&amp;lt;` becomes `&lt;` rather than `<`. Used by the
92
+ * ASCII renderer, which (unlike SVG) has no source-level decode step.
93
+ */
94
+ export function decodeXmlEntitiesInLabel(text: string): string {
95
+ return text.replace(
96
+ /&(#[xX][0-9a-fA-F]+|#[0-9]+|quot|amp|lt|gt|apos);/g,
97
+ (match, code: string) => {
98
+ if (/^#x/i.test(code)) {
99
+ return codePointToChar(parseInt(code.slice(2), 16)) ?? match
100
+ }
101
+ if (code.startsWith('#')) {
102
+ return codePointToChar(parseInt(code.slice(1), 10)) ?? match
103
+ }
104
+ return NAMED_ENTITIES[code] ?? match
105
+ },
42
106
  )
43
107
  }
44
108
 
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}`,
@@ -560,6 +560,14 @@ function nextTitleId(): string {
560
560
  return `zm-title-${titleIdCounter}`
561
561
  }
562
562
 
563
+ /** Same scheme as `nextTitleId`, for the root `<desc>` (`description`). */
564
+ let descIdCounter = 0
565
+
566
+ function nextDescId(): string {
567
+ descIdCounter += 1
568
+ return `zm-desc-${descIdCounter}`
569
+ }
570
+
563
571
  /**
564
572
  * Test-only: reset the title-id counter so id assertions in tests don't
565
573
  * depend on how many titled diagrams earlier tests in the same file rendered.
@@ -567,6 +575,7 @@ function nextTitleId(): string {
567
575
  */
568
576
  export function __resetSvgTitleIdCounterForTests(): void {
569
577
  titleIdCounter = 0
578
+ descIdCounter = 0
570
579
  }
571
580
 
572
581
  /**
@@ -669,6 +678,11 @@ export function themeStyleDeclarations(
669
678
  * See #239 — forces no root `role` so the link
670
679
  * stays reachable, regardless of `decorative`.
671
680
  * @param styleAttribute - Emit the root `style="…"` attribute. Default true.
681
+ * @param description - Accessible description: a `<desc id="zm-desc-N">`
682
+ * after the title plus `aria-describedby` on the root.
683
+ * Follows the same rules as `title` (dropped when
684
+ * decorative without interactive links). Used for
685
+ * Mermaid's `accDescr`.
672
686
  */
673
687
  export function svgOpenTag(
674
688
  width: number,
@@ -679,6 +693,7 @@ export function svgOpenTag(
679
693
  decorative?: boolean,
680
694
  hasInteractiveLinks?: boolean,
681
695
  styleAttribute: boolean = true,
696
+ description?: string,
682
697
  ): string {
683
698
  const styleAttr = styleAttribute
684
699
  ? ` style="${themeStyleDeclarations(colors, transparent)}"`
@@ -686,23 +701,24 @@ export function svgOpenTag(
686
701
 
687
702
  let a11yAttrs = ''
688
703
  let titleEl = ''
689
- if (hasInteractiveLinks) {
690
- // No root role: role="img" or aria-hidden would hide a real, focusable
691
- // <a href> descendant from assistive tech while leaving it Tab-reachable.
692
- if (title) {
693
- const id = nextTitleId()
694
- a11yAttrs = ` aria-labelledby="${id}"`
695
- titleEl = `\n <title id="${id}">${escapeXml(title)}</title>`
696
- }
697
- } else if (decorative) {
704
+ const named = hasInteractiveLinks || !decorative
705
+ if (!hasInteractiveLinks && decorative) {
698
706
  a11yAttrs = ' aria-hidden="true"'
699
- } else {
707
+ } else if (!hasInteractiveLinks) {
708
+ // No root role with interactive links: role="img" or aria-hidden would
709
+ // hide a real, focusable <a href> descendant from assistive tech while
710
+ // leaving it Tab-reachable.
700
711
  a11yAttrs = ' role="img"'
701
- if (title) {
702
- const id = nextTitleId()
703
- a11yAttrs += ` aria-labelledby="${id}"`
704
- titleEl = `\n <title id="${id}">${escapeXml(title)}</title>`
705
- }
712
+ }
713
+ if (named && title) {
714
+ const id = nextTitleId()
715
+ a11yAttrs += ` aria-labelledby="${id}"`
716
+ titleEl = `\n <title id="${id}">${escapeXml(title)}</title>`
717
+ }
718
+ if (named && description) {
719
+ const id = nextDescId()
720
+ a11yAttrs += ` aria-describedby="${id}"`
721
+ titleEl += `\n <desc id="${id}">${escapeXml(description)}</desc>`
706
722
  }
707
723
 
708
724
  return (
@@ -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
- }`