@markmap/render 0.0.0-stage → 1.0.0-next.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.js ADDED
@@ -0,0 +1,1984 @@
1
+ import { expandRect, hashString, isSafeCSSValue, multiply, oppositeSide, readJSONOptions, scaleAround, translate, unionRects, unknownOptionWarnings, validateOptions, validators } from "@markmap/core";
2
+ import { flextree } from "d3-flextree";
3
+ // src/content.ts
4
+ var SAFE_URL = /^(?:https?:|mailto:|tel:|#|\/|\.{1,2}\/|[^:/?#]*(?:[/?#]|$))/i;
5
+ /** Keep http(s), mailto, tel and relative URLs. Anything else, such as `javascript:`, is dropped. */
6
+ function sanitizeUrl(url) {
7
+ const trimmed = url.trim();
8
+ return SAFE_URL.test(trimmed) ? trimmed : void 0;
9
+ }
10
+ /** Bold, italic, inline code, links, strikethrough, highlight and task checkboxes. */
11
+ var basicContent = {
12
+ name: "basic",
13
+ transform(content, node) {
14
+ const { checked } = node.data;
15
+ if (typeof checked !== "boolean") return content;
16
+ return [{
17
+ type: "checkbox",
18
+ checked
19
+ }, ...content];
20
+ },
21
+ styles: {
22
+ strong: () => ({ font: { weight: 700 } }),
23
+ emphasis: () => ({ font: { style: "italic" } }),
24
+ delete: (_inline, style) => ({ decoration: { strike: style.fill } }),
25
+ mark: () => ({ decoration: {
26
+ background: "var(--mm-mark-bg)",
27
+ radius: 2
28
+ } }),
29
+ inlineCode: (_inline, style, ctx) => ({
30
+ font: {
31
+ family: ctx.theme.codeFontFamily,
32
+ size: style.font.size * .875,
33
+ weight: 400,
34
+ style: "normal"
35
+ },
36
+ fill: "var(--mm-code-text)",
37
+ decoration: {
38
+ background: "var(--mm-code-bg)",
39
+ radius: 3,
40
+ padding: style.font.size * .2
41
+ }
42
+ }),
43
+ link: (inline) => {
44
+ const href = sanitizeUrl(inline.url);
45
+ return {
46
+ fill: "var(--mm-link)",
47
+ decoration: href ? { href } : void 0
48
+ };
49
+ }
50
+ },
51
+ atoms: { checkbox: {
52
+ measure(_inline, style) {
53
+ const size = Math.round(style.font.size * .8);
54
+ return {
55
+ width: size + style.font.size * .35,
56
+ height: size,
57
+ baseline: size
58
+ };
59
+ },
60
+ render(inline, box, style) {
61
+ const size = box.height;
62
+ const stroke = Math.max(1, style.font.size / 12);
63
+ const frame = {
64
+ kind: "rect",
65
+ x: box.x + stroke / 2,
66
+ y: box.y + stroke / 2,
67
+ w: size - stroke,
68
+ h: size - stroke,
69
+ r: size * .2,
70
+ stroke: "var(--mm-checkbox)",
71
+ strokeWidth: stroke,
72
+ fill: inline.checked ? "var(--mm-checkbox)" : "none"
73
+ };
74
+ if (!inline.checked) return [frame];
75
+ const [x, y] = [box.x, box.y];
76
+ return [frame, {
77
+ kind: "path",
78
+ d: `M${x + size * .25},${y + size * .52}L${x + size * .43},${y + size * .7}L${x + size * .76},${y + size * .32}`,
79
+ stroke: "var(--mm-bg)",
80
+ strokeWidth: stroke * 1.5
81
+ }];
82
+ }
83
+ } }
84
+ };
85
+ /**
86
+ * Adds a dot after the label of nodes with notes, with a hit area that asks the
87
+ * view to open them. The dot is exported; the hit area is interactive-only.
88
+ */
89
+ var noteContent = {
90
+ name: "notes",
91
+ transform(content, node) {
92
+ const count = node.notes.length;
93
+ if (!count) return content;
94
+ return [...content, {
95
+ type: "noteMarker",
96
+ count
97
+ }];
98
+ },
99
+ atoms: { noteMarker: {
100
+ measure(_inline, style) {
101
+ const dot = Math.round(style.font.size * .4);
102
+ return {
103
+ width: style.font.size * .35 + dot,
104
+ height: dot,
105
+ baseline: dot / 2 + style.font.size * .27
106
+ };
107
+ },
108
+ render(_inline, box) {
109
+ const dot = box.height;
110
+ const x = box.x + box.width - dot;
111
+ const hit = Math.max(16, dot * 2.5);
112
+ return [{
113
+ kind: "rect",
114
+ x,
115
+ y: box.y,
116
+ w: dot,
117
+ h: dot,
118
+ r: dot / 2,
119
+ fill: "var(--mm-note-dot)"
120
+ }, {
121
+ kind: "hit",
122
+ action: "notes",
123
+ rect: {
124
+ x: x + dot / 2 - hit / 2,
125
+ y: box.y + dot / 2 - hit / 2,
126
+ width: hit,
127
+ height: hit
128
+ }
129
+ }];
130
+ }
131
+ } }
132
+ };
133
+ // src/image.ts
134
+ /** Layout size while the real one is unknown. */
135
+ var PLACEHOLDER = {
136
+ width: 40,
137
+ height: 30
138
+ };
139
+ var DEFAULT_MAX_WIDTH = 320;
140
+ var DATA_IMAGE = /^data:image\/(?:png|jpe?g|gif|webp|avif);base64,[a-z0-9+/=\s]+$/i;
141
+ /** Like {@link sanitizeUrl}, plus base64 raster `data:` images. */
142
+ function sanitizeImageUrl(url) {
143
+ const trimmed = url.trim();
144
+ return DATA_IMAGE.test(trimmed) ? trimmed : sanitizeUrl(trimmed);
145
+ }
146
+ function browserLoader() {
147
+ if (typeof Image === "undefined") return void 0;
148
+ return (src) => new Promise((resolve, reject) => {
149
+ const img = new Image();
150
+ img.addEventListener("load", () => resolve({
151
+ width: img.naturalWidth,
152
+ height: img.naturalHeight
153
+ }));
154
+ img.addEventListener("error", () => reject(/* @__PURE__ */ new Error(`Could not load ${src}`)));
155
+ img.src = src;
156
+ });
157
+ }
158
+ /** Size from the given dimensions and, when known, the natural size; `undefined` if unknowable yet. */
159
+ function fit(given, natural) {
160
+ const { width, height } = given;
161
+ if (width && height) return {
162
+ width,
163
+ height
164
+ };
165
+ if (!natural?.width || !natural.height) return void 0;
166
+ const ratio = natural.width / natural.height;
167
+ if (width) return {
168
+ width,
169
+ height: width / ratio
170
+ };
171
+ if (height) return {
172
+ width: height * ratio,
173
+ height
174
+ };
175
+ return natural;
176
+ }
177
+ /** A guess that keeps any given dimension, used until the natural size is known. */
178
+ var guess = ({ width, height }) => width || height ? {
179
+ width: width ?? height,
180
+ height: height ?? width
181
+ } : PLACEHOLDER;
182
+ /** Size of an image's alt text, drawn in place of the image. */
183
+ function altSize(alt, style, ctx) {
184
+ const { font } = style;
185
+ return {
186
+ width: ctx.measurer.measure(alt, font),
187
+ height: font.lineHeight,
188
+ baseline: (font.lineHeight + font.size * .7) / 2
189
+ };
190
+ }
191
+ function altText(alt, box, style, ctx) {
192
+ if (!alt) return [];
193
+ const { baseline } = altSize(alt, style, ctx);
194
+ const run = {
195
+ text: alt,
196
+ x: 0,
197
+ y: baseline,
198
+ font: style.font,
199
+ fill: style.fill
200
+ };
201
+ return [{
202
+ kind: "text",
203
+ x: box.x,
204
+ y: box.y,
205
+ runs: [run]
206
+ }];
207
+ }
208
+ /**
209
+ * Draws Markdown images and sized `<img>` tags inside labels. Sizes come from the tag,
210
+ * from a cache of loaded sizes, or from a placeholder while the image loads; the view
211
+ * then renders again (see `InlineContext.defer`). Images are capped at the label wrap
212
+ * width, keeping their aspect ratio. Images that fail to load show their alt text.
213
+ */
214
+ function createImageContent(options = {}) {
215
+ const loadSize = options.loadSize ?? browserLoader();
216
+ const allow = options.allow ?? (() => true);
217
+ /** Natural sizes; `null` when loading failed. */
218
+ const sizes = /* @__PURE__ */ new Map();
219
+ const loading = /* @__PURE__ */ new Map();
220
+ const load = (src, ctx) => {
221
+ if (!loadSize || sizes.has(src)) return;
222
+ let task = loading.get(src);
223
+ if (!task) {
224
+ task = loadSize(src).then((size) => sizes.set(src, size), () => sizes.set(src, null)).finally(() => loading.delete(src));
225
+ loading.set(src, task);
226
+ }
227
+ ctx.defer?.(task);
228
+ };
229
+ /** Size of the image as drawn, or `undefined` to show its alt text. */
230
+ const imageSize = (inline, ctx) => {
231
+ const src = sanitizeImageUrl(inline.url);
232
+ if (!src || !allow(src)) return void 0;
233
+ const given = inline.data ?? {};
234
+ const natural = sizes.get(src);
235
+ let size = fit(given, natural ?? void 0);
236
+ if (!size) {
237
+ if (natural === null) return void 0;
238
+ load(src, ctx);
239
+ size = guess(given);
240
+ }
241
+ const max = ctx.wrapWidth && ctx.wrapWidth > 0 ? ctx.wrapWidth : options.maxWidth ?? DEFAULT_MAX_WIDTH;
242
+ return size.width > max ? {
243
+ width: max,
244
+ height: size.height * max / size.width
245
+ } : size;
246
+ };
247
+ return {
248
+ name: "images",
249
+ atoms: { image: {
250
+ measure(inline, style, ctx) {
251
+ const size = imageSize(inline, ctx);
252
+ return size ? {
253
+ ...size,
254
+ baseline: size.height
255
+ } : altSize(inline.alt ?? "", style, ctx);
256
+ },
257
+ render(inline, box, style, ctx) {
258
+ const src = imageSize(inline, ctx) && sanitizeImageUrl(inline.url);
259
+ const alt = altText(inline.alt, box, style, ctx);
260
+ if (!src) return alt;
261
+ const { x, y, width: w, height: h } = box;
262
+ const image = {
263
+ kind: "image",
264
+ x,
265
+ y,
266
+ w,
267
+ h,
268
+ src
269
+ };
270
+ if (alt.length) image.fallback = [{
271
+ kind: "group",
272
+ clip: box,
273
+ children: alt
274
+ }];
275
+ return [image];
276
+ }
277
+ } }
278
+ };
279
+ }
280
+ /** Images with sizes loaded in the browser. */
281
+ var imageContent = createImageContent();
282
+ // src/default-plugins.ts
283
+ /** Plugins used when none are given: basic styles, images, and note dots. */
284
+ var defaultContentPlugins = [
285
+ basicContent,
286
+ imageContent,
287
+ noteContent
288
+ ];
289
+ // src/layouts/tree.ts
290
+ /** Return the subtree that is visible when the `folded` nodes are collapsed. */
291
+ function visibleNodes(root, folded) {
292
+ const result = [];
293
+ const visit = (node) => {
294
+ result.push(node);
295
+ if (!folded?.has(node.id)) node.children.forEach(visit);
296
+ };
297
+ visit(root);
298
+ return result;
299
+ }
300
+ /** Tidy tree layout, root on the left, growing to the right. */
301
+ function layoutTree(root, options) {
302
+ const { sizes, folded, levelGap = 80, siblingGap = 5 } = options;
303
+ const sizeOf = (node) => {
304
+ const size = sizes.get(node.id);
305
+ if (!size) throw new Error(`Missing size for node ${node.id}`);
306
+ return size;
307
+ };
308
+ const layout = flextree({
309
+ children: (node) => folded?.has(node.id) ? void 0 : node.children,
310
+ nodeSize: (h) => {
311
+ const { width, height } = sizeOf(h.data);
312
+ return [height, width + levelGap];
313
+ },
314
+ spacing: (a, b) => a.parent === b.parent ? siblingGap : siblingGap * 2
315
+ });
316
+ const tree = layout.hierarchy(root);
317
+ layout(tree);
318
+ const nodes = [];
319
+ const byId = /* @__PURE__ */ new Map();
320
+ const convert = (h, parent) => {
321
+ const { width, height } = sizeOf(h.data);
322
+ const item = {
323
+ node: h.data,
324
+ parent,
325
+ children: [],
326
+ depth: h.depth,
327
+ folded: !!folded?.has(h.data.id) && h.data.children.length > 0,
328
+ rect: {
329
+ x: h.y,
330
+ y: h.x - height / 2,
331
+ width,
332
+ height
333
+ },
334
+ direction: "right"
335
+ };
336
+ nodes.push(item);
337
+ byId.set(item.node.id, item);
338
+ for (const child of h.children ?? []) item.children.push(convert(child, item));
339
+ return item;
340
+ };
341
+ convert(tree);
342
+ return {
343
+ nodes,
344
+ byId,
345
+ bounds: unionRects(nodes.map((n) => n.rect))
346
+ };
347
+ }
348
+ /** The classic markmap layout: a tidy tree growing to the right. */
349
+ var treeLayout = {
350
+ name: "tree",
351
+ layout: layoutTree
352
+ };
353
+ // src/layouts/two-sided.ts
354
+ /** Leaves under a node, ignoring folds, so folding never moves a branch to the other side. */
355
+ function leafCount(node) {
356
+ return node.children.length ? node.children.reduce((sum, c) => sum + leafCount(c), 0) : 1;
357
+ }
358
+ /**
359
+ * How many of the root's first children go on the right; the rest go on the left. Splits
360
+ * the list where the two sides' leaf counts are closest, keeping document order on both
361
+ * sides and favoring the right on a tie.
362
+ */
363
+ function splitBranches(children) {
364
+ const weights = children.map(leafCount);
365
+ const total = weights.reduce((a, b) => a + b, 0);
366
+ let best = children.length;
367
+ let bestDiff = Infinity;
368
+ let right = 0;
369
+ for (let k = 1; k <= children.length; k++) {
370
+ right += weights[k - 1];
371
+ const diff = Math.abs(right - (total - right));
372
+ if (diff <= bestDiff) {
373
+ best = k;
374
+ bestDiff = diff;
375
+ }
376
+ }
377
+ return best;
378
+ }
379
+ /**
380
+ * A mind map: the root in the middle, its first branches growing to the right and the
381
+ * rest to the left, balanced by size. Both sides keep document order top to bottom.
382
+ */
383
+ function layoutTwoSided(root, options) {
384
+ if (options.folded?.has(root.id) || root.children.length < 2) return layoutTree(root, options);
385
+ const split = splitBranches(root.children);
386
+ const side = (children) => layoutTree({
387
+ ...root,
388
+ children
389
+ }, options);
390
+ const right = side(root.children.slice(0, split));
391
+ const left = side(root.children.slice(split));
392
+ const rootRect = right.nodes[0].rect;
393
+ const place = /* @__PURE__ */ new Map();
394
+ for (const item of right.nodes) place.set(item.node.id, {
395
+ rect: item.rect,
396
+ direction: "right"
397
+ });
398
+ for (const item of left.nodes.slice(1)) {
399
+ const { rect } = item;
400
+ const x = rootRect.x * 2 + rootRect.width - rect.x - rect.width;
401
+ place.set(item.node.id, {
402
+ rect: {
403
+ ...rect,
404
+ x
405
+ },
406
+ direction: "left"
407
+ });
408
+ }
409
+ const nodes = [];
410
+ const byId = /* @__PURE__ */ new Map();
411
+ const visit = (node, depth, parent) => {
412
+ const { rect, direction } = place.get(node.id);
413
+ const folded = !!options.folded?.has(node.id) && node.children.length > 0;
414
+ const item = {
415
+ node,
416
+ parent,
417
+ children: [],
418
+ depth,
419
+ folded,
420
+ rect,
421
+ direction
422
+ };
423
+ nodes.push(item);
424
+ byId.set(node.id, item);
425
+ if (!folded) for (const child of node.children) item.children.push(visit(child, depth + 1, item));
426
+ return item;
427
+ };
428
+ visit(root, 0);
429
+ return {
430
+ nodes,
431
+ byId,
432
+ bounds: unionRects(nodes.map((n) => n.rect))
433
+ };
434
+ }
435
+ /** Root in the middle, branches on both sides. */
436
+ var twoSidedLayout = {
437
+ name: "two-sided",
438
+ layout: layoutTwoSided
439
+ };
440
+ // src/layouts/org-chart.ts
441
+ /**
442
+ * Siblings sit side by side here, and labels need more room across than above each
443
+ * other, so the sibling gap is this many times `siblingGap`.
444
+ */
445
+ var SIBLING_GAP_FACTOR = 3;
446
+ /**
447
+ * Top-down tidy tree: the root at the top, children in a row below their parent.
448
+ * `levelGap` is the gap between levels; siblings are `siblingGap × 3`
449
+ * apart, doubled between cousins.
450
+ */
451
+ function layoutOrgChart(root, options) {
452
+ const { sizes, folded, levelGap = 80, siblingGap = 5 } = options;
453
+ const scaledGap = siblingGap * SIBLING_GAP_FACTOR;
454
+ const sizeOf = (node) => {
455
+ const size = sizes.get(node.id);
456
+ if (!size) throw new Error(`Missing size for node ${node.id}`);
457
+ return size;
458
+ };
459
+ const layout = flextree({
460
+ children: (node) => folded?.has(node.id) ? void 0 : node.children,
461
+ nodeSize: (h) => {
462
+ const { width, height } = sizeOf(h.data);
463
+ return [width, height + levelGap];
464
+ },
465
+ spacing: (a, b) => a.parent === b.parent ? scaledGap : scaledGap * 2
466
+ });
467
+ const tree = layout.hierarchy(root);
468
+ layout(tree);
469
+ const nodes = [];
470
+ const byId = /* @__PURE__ */ new Map();
471
+ const convert = (h, parent) => {
472
+ const { width, height } = sizeOf(h.data);
473
+ const item = {
474
+ node: h.data,
475
+ parent,
476
+ children: [],
477
+ depth: h.depth,
478
+ folded: !!folded?.has(h.data.id) && h.data.children.length > 0,
479
+ rect: {
480
+ x: h.x - width / 2,
481
+ y: h.y,
482
+ width,
483
+ height
484
+ },
485
+ direction: "bottom"
486
+ };
487
+ nodes.push(item);
488
+ byId.set(item.node.id, item);
489
+ for (const child of h.children ?? []) item.children.push(convert(child, item));
490
+ return item;
491
+ };
492
+ convert(tree);
493
+ return {
494
+ nodes,
495
+ byId,
496
+ bounds: unionRects(nodes.map((n) => n.rect))
497
+ };
498
+ }
499
+ /** Org chart: root at the top, growing downwards. */
500
+ var orgChartLayout = {
501
+ name: "org-chart",
502
+ layout: layoutOrgChart
503
+ };
504
+ // src/measure.ts
505
+ function fontToCss(font) {
506
+ return `${font.style} ${font.weight} ${font.size}px ${font.family}`;
507
+ }
508
+ var ASCII_WIDTHS = [
509
+ 278,
510
+ 278,
511
+ 355,
512
+ 556,
513
+ 556,
514
+ 889,
515
+ 667,
516
+ 191,
517
+ 333,
518
+ 333,
519
+ 389,
520
+ 584,
521
+ 278,
522
+ 333,
523
+ 278,
524
+ 278,
525
+ 556,
526
+ 556,
527
+ 556,
528
+ 556,
529
+ 556,
530
+ 556,
531
+ 556,
532
+ 556,
533
+ 556,
534
+ 556,
535
+ 278,
536
+ 278,
537
+ 584,
538
+ 584,
539
+ 584,
540
+ 556,
541
+ 1015,
542
+ 667,
543
+ 667,
544
+ 722,
545
+ 722,
546
+ 667,
547
+ 611,
548
+ 778,
549
+ 722,
550
+ 278,
551
+ 500,
552
+ 667,
553
+ 556,
554
+ 833,
555
+ 722,
556
+ 778,
557
+ 667,
558
+ 778,
559
+ 722,
560
+ 667,
561
+ 611,
562
+ 722,
563
+ 667,
564
+ 944,
565
+ 667,
566
+ 667,
567
+ 611,
568
+ 278,
569
+ 278,
570
+ 278,
571
+ 469,
572
+ 556,
573
+ 333,
574
+ 556,
575
+ 556,
576
+ 500,
577
+ 556,
578
+ 556,
579
+ 278,
580
+ 556,
581
+ 556,
582
+ 222,
583
+ 222,
584
+ 500,
585
+ 222,
586
+ 833,
587
+ 556,
588
+ 556,
589
+ 556,
590
+ 556,
591
+ 333,
592
+ 500,
593
+ 278,
594
+ 556,
595
+ 500,
596
+ 722,
597
+ 500,
598
+ 500,
599
+ 500,
600
+ 334,
601
+ 260,
602
+ 334,
603
+ 584
604
+ ];
605
+ /** East Asian wide characters, fullwidth forms and emoji take a full em. */
606
+ var WIDE = /[\u1100-\u115f\u2e80-\u303e\u3041-\u33ff\u3400-\u4dbf\u4e00-\u9fff\ua960-\ua97f\uac00-\ud7a3\uf900-\ufaff\ufe30-\ufe4f\uff00-\uff60\uffe0-\uffe6\p{Extended_Pictographic}]/u;
607
+ /** Combining marks, variation selectors and format characters such as ZWJ. */
608
+ var ZERO_WIDTH = /^[\p{Mn}\p{Me}\p{Cf}]$/u;
609
+ /**
610
+ * Approximates widths from Helvetica metrics, without a font or canvas. Good enough
611
+ * for Node rendering and tests; use {@link createCanvasMeasurer} in the browser.
612
+ */
613
+ var approximateMeasurer = { measure(text, font) {
614
+ const mono = /mono|consolas|courier|menlo/i.test(font.family);
615
+ let em = 0;
616
+ for (const char of text) {
617
+ const code = char.codePointAt(0);
618
+ if (code >= 32 && code <= 126) em += mono ? .6 : ASCII_WIDTHS[code - 32] / 1e3;
619
+ else if (ZERO_WIDTH.test(char)) continue;
620
+ else if (WIDE.test(char)) em += 1;
621
+ else em += .6;
622
+ }
623
+ return em * font.size * (font.weight >= 600 ? 1.06 : 1);
624
+ } };
625
+ /** Measures with a canvas 2D context, caching results per font and text. */
626
+ function createCanvasMeasurer(ctx) {
627
+ const context = ctx ?? createContext();
628
+ const cache = /* @__PURE__ */ new Map();
629
+ return { measure(text, font) {
630
+ const css = fontToCss(font);
631
+ const key = `${css}\n${text}`;
632
+ let width = cache.get(key);
633
+ if (width == null) {
634
+ if (context.font !== css) context.font = css;
635
+ width = context.measureText(text).width;
636
+ cache.set(key, width);
637
+ }
638
+ return width;
639
+ } };
640
+ }
641
+ function createContext() {
642
+ const context = (typeof OffscreenCanvas === "undefined" ? document.createElement("canvas") : new OffscreenCanvas(1, 1)).getContext("2d");
643
+ if (!context) throw new Error("Canvas 2D is not available");
644
+ return context;
645
+ }
646
+ // src/themes/branch.ts
647
+ /**
648
+ * Pick a color index for a node from its path of ids, for themes that color by branch.
649
+ * The node's id is hashed, so colors stay put as the tree changes. With `colorDepth`
650
+ * (the root is level 1), nodes deeper than that level use their ancestor's id instead.
651
+ */
652
+ function branchIndex(path, count, colorDepth = 0) {
653
+ const id = colorDepth > 0 && path.length > colorDepth ? path[colorDepth - 1] : path.at(-1);
654
+ return Number.parseInt(hashString(id), 36) % Math.max(1, count);
655
+ }
656
+ /** The d3 category10 palette, used by the built-in themes. */
657
+ var DEFAULT_COLORS = [
658
+ "#1f77b4",
659
+ "#ff7f0e",
660
+ "#2ca02c",
661
+ "#d62728",
662
+ "#9467bd",
663
+ "#8c564b",
664
+ "#e377c2",
665
+ "#7f7f7f",
666
+ "#bcbd22",
667
+ "#17becf"
668
+ ];
669
+ /** Branch color tokens and a lookup from a node's path to its `var(--mm-branch-N)`. */
670
+ function branchColors(options) {
671
+ const colors = options.color?.length ? options.color : DEFAULT_COLORS;
672
+ const colorDepth = options.colorDepth ?? 0;
673
+ return {
674
+ tokens: Object.fromEntries(colors.map((color, i) => [`mm-branch-${i}`, color])),
675
+ colorOf: (path) => `var(--mm-branch-${branchIndex(path, colors.length, colorDepth)})`
676
+ };
677
+ }
678
+ // src/themes/shared.ts
679
+ /** Tokens for inline content, shared by the built-in themes. */
680
+ var CONTENT_TOKENS = {
681
+ "mm-link": "#0097e6",
682
+ "mm-code-bg": "#f0f0f0",
683
+ "mm-code-text": "#555",
684
+ "mm-mark-bg": "#ffeaa7",
685
+ "mm-checkbox": "#555",
686
+ "mm-note-dot": "#999",
687
+ "mm-syntax-keyword": "#cf222e",
688
+ "mm-syntax-string": "#0a3069",
689
+ "mm-syntax-comment": "#6e7781",
690
+ "mm-syntax-number": "#0550ae",
691
+ "mm-syntax-title": "#8250df",
692
+ "mm-syntax-attr": "#116329"
693
+ };
694
+ var DARK_CONTENT_TOKENS = {
695
+ "mm-link": "#4fb3f6",
696
+ "mm-code-bg": "#2f2f33",
697
+ "mm-code-text": "#d4d4d4",
698
+ "mm-mark-bg": "#6b5a1e",
699
+ "mm-checkbox": "#bbb",
700
+ "mm-note-dot": "#888",
701
+ "mm-syntax-keyword": "#ff7b72",
702
+ "mm-syntax-string": "#a5d6ff",
703
+ "mm-syntax-comment": "#8b949e",
704
+ "mm-syntax-number": "#79c0ff",
705
+ "mm-syntax-title": "#d2a8ff",
706
+ "mm-syntax-attr": "#7ee787"
707
+ };
708
+ var SYSTEM_FONT = {
709
+ family: "-apple-system, BlinkMacSystemFont, \"Segoe UI\", Helvetica, Arial, sans-serif",
710
+ size: 16,
711
+ weight: 400,
712
+ style: "normal",
713
+ lineHeight: 20
714
+ };
715
+ var CODE_FONT = "ui-monospace, SFMono-Regular, Menlo, Consolas, monospace";
716
+ /** Wrapped lines hug the edge that faces the parent. */
717
+ function alignFor(direction) {
718
+ if (direction === "left") return "end";
719
+ if (direction === "right") return "start";
720
+ return "center";
721
+ }
722
+ /** An S-curve that leaves and arrives along the anchors' axis. */
723
+ function sCurve(from, to) {
724
+ const vertical = from.side === "top" || from.side === "bottom";
725
+ const mx = (from.x + to.x) / 2;
726
+ const my = (from.y + to.y) / 2;
727
+ const controls = vertical ? `${from.x},${my},${to.x},${my}` : `${mx},${from.y},${mx},${to.y}`;
728
+ return `M${from.x},${from.y}C${controls},${to.x},${to.y}`;
729
+ }
730
+ /** The middle of one side of a box at (0, 0), pushed `gap` px outwards. */
731
+ function sidePoint(width, height, side, gap = 0) {
732
+ if (side === "left") return {
733
+ x: -gap,
734
+ y: height / 2
735
+ };
736
+ if (side === "right") return {
737
+ x: width + gap,
738
+ y: height / 2
739
+ };
740
+ if (side === "top") return {
741
+ x: width / 2,
742
+ y: -gap
743
+ };
744
+ return {
745
+ x: width / 2,
746
+ y: height + gap
747
+ };
748
+ }
749
+ // src/themes/cards.ts
750
+ var RADIUS = 8;
751
+ var TOGGLE_RADIUS = 5;
752
+ /** Each node in a rounded card outlined in its branch color. */
753
+ function createCardsTheme(options = {}) {
754
+ const { tokens: branchTokens, colorOf } = branchColors(options);
755
+ return {
756
+ name: "cards",
757
+ options: {
758
+ color: validators.colors(),
759
+ colorDepth: validators.number(0, true)
760
+ },
761
+ configure: (next) => createCardsTheme({
762
+ ...options,
763
+ ...next
764
+ }),
765
+ tokens: {
766
+ "mm-text": "#222",
767
+ "mm-bg": "#f6f7f9",
768
+ "mm-card-bg": "#fff",
769
+ ...CONTENT_TOKENS,
770
+ ...branchTokens
771
+ },
772
+ darkTokens: {
773
+ "mm-text": "#e6e6e6",
774
+ "mm-bg": "#17181b",
775
+ "mm-card-bg": "#25262b",
776
+ ...DARK_CONTENT_TOKENS
777
+ },
778
+ font: SYSTEM_FONT,
779
+ codeFontFamily: CODE_FONT,
780
+ textFill: "var(--mm-text)",
781
+ node: {
782
+ padding: [
783
+ 6,
784
+ 12,
785
+ 6,
786
+ 12
787
+ ],
788
+ minHeight: 32,
789
+ textAlign: (ctx) => alignFor(ctx.direction),
790
+ presence: (presence, box) => ({
791
+ opacity: presence,
792
+ transform: scaleAround(.6 + .4 * presence, box.width / 2, box.height / 2)
793
+ }),
794
+ render(_node, box, ctx) {
795
+ const color = colorOf(ctx.path);
796
+ const card = {
797
+ kind: "rect",
798
+ x: 0,
799
+ y: 0,
800
+ w: box.width,
801
+ h: box.height,
802
+ r: RADIUS,
803
+ fill: "var(--mm-card-bg)",
804
+ stroke: color,
805
+ strokeWidth: ctx.depth === 0 ? 2.5 : 1.5
806
+ };
807
+ if (!ctx.hasChildren) return [card];
808
+ const at = sidePoint(box.width, box.height, ctx.direction);
809
+ return [card, {
810
+ kind: "rect",
811
+ x: at.x - TOGGLE_RADIUS,
812
+ y: at.y - TOGGLE_RADIUS,
813
+ w: 10,
814
+ h: 10,
815
+ r: TOGGLE_RADIUS,
816
+ fill: ctx.folded ? color : "var(--mm-card-bg)",
817
+ stroke: color,
818
+ strokeWidth: 1.5
819
+ }];
820
+ },
821
+ anchor(rect, side) {
822
+ const at = sidePoint(rect.width, rect.height, side);
823
+ return {
824
+ x: rect.x + at.x,
825
+ y: rect.y + at.y,
826
+ side
827
+ };
828
+ }
829
+ },
830
+ edge: { render: (from, to, ctx) => [{
831
+ kind: "path",
832
+ d: sCurve(from, to),
833
+ stroke: colorOf(ctx.node.path),
834
+ strokeWidth: 1.5
835
+ }] }
836
+ };
837
+ }
838
+ var cardsTheme = createCardsTheme();
839
+ // src/themes/classic.ts
840
+ var CIRCLE_RADIUS = 6;
841
+ /** Space between a top anchor and the label below it. */
842
+ var TOP_GAP = 4;
843
+ /** Line width shrinks with depth: 4, 2.5, 1.75, … */
844
+ var lineWidth = (depth) => 1 + 3 / 2 ** depth;
845
+ /** The original markmap look: underlined labels, curved edges and fold circles. */
846
+ function createClassicTheme(options = {}) {
847
+ const { tokens: branchTokens, colorOf } = branchColors(options);
848
+ return {
849
+ name: "classic",
850
+ options: {
851
+ color: validators.colors(),
852
+ colorDepth: validators.number(0, true)
853
+ },
854
+ configure: (next) => createClassicTheme({
855
+ ...options,
856
+ ...next
857
+ }),
858
+ tokens: {
859
+ "mm-text": "#333",
860
+ "mm-bg": "#fff",
861
+ "mm-toggle-bg": "#fff",
862
+ ...CONTENT_TOKENS,
863
+ ...branchTokens
864
+ },
865
+ darkTokens: {
866
+ "mm-text": "#ddd",
867
+ "mm-bg": "#1e1e1e",
868
+ "mm-toggle-bg": "#1e1e1e",
869
+ ...DARK_CONTENT_TOKENS
870
+ },
871
+ font: SYSTEM_FONT,
872
+ codeFontFamily: CODE_FONT,
873
+ textFill: "var(--mm-text)",
874
+ node: {
875
+ padding: [
876
+ 0,
877
+ 8,
878
+ 0,
879
+ 8
880
+ ],
881
+ minHeight: 16,
882
+ textAlign: (ctx) => alignFor(ctx.direction),
883
+ render(_node, box, ctx) {
884
+ const color = colorOf(ctx.path);
885
+ const width = lineWidth(ctx.depth);
886
+ const y = box.height + width / 2;
887
+ const cx = {
888
+ left: 0,
889
+ right: box.width,
890
+ top: box.width / 2,
891
+ bottom: box.width / 2
892
+ }[ctx.direction];
893
+ const chrome = [{
894
+ kind: "path",
895
+ d: `M0,${y}H${box.width}`,
896
+ stroke: color,
897
+ strokeWidth: width
898
+ }];
899
+ if (!ctx.hasChildren) return chrome;
900
+ return [...chrome, {
901
+ kind: "rect",
902
+ x: cx - CIRCLE_RADIUS,
903
+ y: y - CIRCLE_RADIUS,
904
+ w: 12,
905
+ h: 12,
906
+ r: CIRCLE_RADIUS,
907
+ fill: ctx.folded ? color : "var(--mm-toggle-bg)",
908
+ stroke: color,
909
+ strokeWidth: 1.5
910
+ }];
911
+ },
912
+ anchor(rect, side, ctx) {
913
+ const y = rect.y + rect.height + lineWidth(ctx.depth) / 2;
914
+ if (side === "left") return {
915
+ x: rect.x,
916
+ y,
917
+ side
918
+ };
919
+ if (side === "right") return {
920
+ x: rect.x + rect.width,
921
+ y,
922
+ side
923
+ };
924
+ return {
925
+ x: rect.x + rect.width / 2,
926
+ y: side === "top" ? rect.y - TOP_GAP : y,
927
+ side
928
+ };
929
+ }
930
+ },
931
+ edge: { render: (from, to, ctx) => [{
932
+ kind: "path",
933
+ d: sCurve(from, to),
934
+ stroke: colorOf(ctx.node.path),
935
+ strokeWidth: lineWidth(ctx.node.depth)
936
+ }] }
937
+ };
938
+ }
939
+ var classicTheme = createClassicTheme();
940
+ // src/themes/minimal.ts
941
+ /** Space between a label and where its edges start. */
942
+ var GAP = 6;
943
+ var DOT_RADIUS = 3.5;
944
+ /** Plain text joined by thin gray lines; a small dot marks nodes that fold. */
945
+ var minimalTheme = {
946
+ name: "minimal",
947
+ tokens: {
948
+ "mm-text": "#333",
949
+ "mm-bg": "#fff",
950
+ "mm-edge": "#c4c4c4",
951
+ ...CONTENT_TOKENS
952
+ },
953
+ darkTokens: {
954
+ "mm-text": "#ddd",
955
+ "mm-bg": "#1e1e1e",
956
+ "mm-edge": "#555",
957
+ ...DARK_CONTENT_TOKENS
958
+ },
959
+ font: SYSTEM_FONT,
960
+ codeFontFamily: CODE_FONT,
961
+ textFill: "var(--mm-text)",
962
+ node: {
963
+ padding: [
964
+ 2,
965
+ 4,
966
+ 2,
967
+ 4
968
+ ],
969
+ textAlign: (ctx) => alignFor(ctx.direction),
970
+ render(_node, box, ctx) {
971
+ if (!ctx.hasChildren) return [];
972
+ const at = sidePoint(box.width, box.height, ctx.direction, GAP);
973
+ return [{
974
+ kind: "rect",
975
+ x: at.x - DOT_RADIUS,
976
+ y: at.y - DOT_RADIUS,
977
+ w: DOT_RADIUS * 2,
978
+ h: DOT_RADIUS * 2,
979
+ r: DOT_RADIUS,
980
+ fill: ctx.folded ? "var(--mm-edge)" : "var(--mm-bg)",
981
+ stroke: "var(--mm-edge)",
982
+ strokeWidth: 1.5
983
+ }];
984
+ },
985
+ anchor(rect, side) {
986
+ const at = sidePoint(rect.width, rect.height, side, GAP);
987
+ return {
988
+ x: rect.x + at.x,
989
+ y: rect.y + at.y,
990
+ side
991
+ };
992
+ }
993
+ },
994
+ edge: { render: (from, to) => [{
995
+ kind: "path",
996
+ d: sCurve(from, to),
997
+ stroke: "var(--mm-edge)",
998
+ strokeWidth: 1
999
+ }] }
1000
+ };
1001
+ // src/options.ts
1002
+ /** Layouts Markdown can pick by name with the `layout` option. */
1003
+ var builtinLayouts = Object.freeze({
1004
+ [treeLayout.name]: treeLayout,
1005
+ [twoSidedLayout.name]: twoSidedLayout,
1006
+ [orgChartLayout.name]: orgChartLayout
1007
+ });
1008
+ /** Themes Markdown can pick by name with the `theme` option. */
1009
+ var builtinThemes = Object.freeze({
1010
+ [classicTheme.name]: classicTheme,
1011
+ [cardsTheme.name]: cardsTheme,
1012
+ [minimalTheme.name]: minimalTheme
1013
+ });
1014
+ /** Look up a name from Markdown, warning when it is not registered. */
1015
+ function pick(kind, name, registry, warnings) {
1016
+ if (name == null) return void 0;
1017
+ if (Object.hasOwn(registry, name)) return registry[name];
1018
+ const names = Object.keys(registry).map((key) => `\`${key}\``);
1019
+ warnings.push(`Unknown ${kind} \`${name}\`; available: ${names.join(", ")}`);
1020
+ }
1021
+ /**
1022
+ * Apply the theme's own options (for example `color`) and return the keys it did not
1023
+ * claim. Themes without options leave everything in `rest`.
1024
+ */
1025
+ function configureTheme(theme, input) {
1026
+ if (!theme.options) return {
1027
+ theme,
1028
+ rest: input,
1029
+ warnings: []
1030
+ };
1031
+ const { options, rest, warnings } = validateOptions(input, theme.options);
1032
+ return {
1033
+ theme: Object.keys(options).length && theme.configure ? theme.configure(options) : theme,
1034
+ rest,
1035
+ warnings
1036
+ };
1037
+ }
1038
+ /** Markdown option keys that map straight onto render options. */
1039
+ var RENDER_KEYS = [
1040
+ "wrapWidth",
1041
+ "levelGap",
1042
+ "siblingGap"
1043
+ ];
1044
+ /**
1045
+ * Validate untrusted Markdown options (usually `frontmatter.markmap`) and apply them on
1046
+ * top of `base`: shared keys first, then the theme's own keys. Anything left is reported.
1047
+ */
1048
+ function resolveRenderOptions(base, input, { layouts = builtinLayouts, themes = builtinThemes } = {}) {
1049
+ const core = readJSONOptions(input);
1050
+ const result = { ...base };
1051
+ for (const key of RENDER_KEYS) if (core.options[key] != null) result[key] = core.options[key];
1052
+ const pickWarnings = [];
1053
+ const layout = pick("layout", core.options.layout, layouts, pickWarnings);
1054
+ if (layout) result.layout = layout;
1055
+ const themed = configureTheme(pick("theme", core.options.theme, themes, pickWarnings) ?? base.theme ?? classicTheme, core.rest);
1056
+ if (themed.theme !== (base.theme ?? classicTheme)) result.theme = themed.theme;
1057
+ return {
1058
+ options: result,
1059
+ json: core.options,
1060
+ warnings: [
1061
+ ...core.warnings,
1062
+ ...pickWarnings,
1063
+ ...themed.warnings,
1064
+ ...unknownOptionWarnings(themed.rest)
1065
+ ]
1066
+ };
1067
+ }
1068
+ // src/paint.ts
1069
+ /** Nesting depth at which token references are assumed to loop. */
1070
+ var MAX_DEPTH = 8;
1071
+ /**
1072
+ * Resolve CSS custom properties in a paint, for drawing outside CSS (canvas, PNG export).
1073
+ * Every `var(--name)` and `var(--name, fallback)` is replaced by the token's value, which
1074
+ * may refer to other tokens, or else by its fallback. Anything else is kept as is.
1075
+ * Returns `undefined` when a variable has neither, or tokens refer to each other in a loop.
1076
+ */
1077
+ function resolvePaint(paint, tokens) {
1078
+ if (paint == null) return void 0;
1079
+ return substitute(paint, typeof tokens === "function" ? tokens : (name) => tokens[name], 0);
1080
+ }
1081
+ function substitute(value, lookup, depth) {
1082
+ if (depth > MAX_DEPTH) return void 0;
1083
+ let out = "";
1084
+ let from = 0;
1085
+ for (;;) {
1086
+ const start = value.indexOf("var(", from);
1087
+ if (start < 0) return out + value.slice(from);
1088
+ const end = closingParen(value, start + 4);
1089
+ if (end < 0) return void 0;
1090
+ const inner = value.slice(start + 4, end);
1091
+ const comma = topLevelComma(inner);
1092
+ const name = (comma < 0 ? inner : inner.slice(0, comma)).trim();
1093
+ if (!name.startsWith("--")) return void 0;
1094
+ const token = lookup(name.slice(2))?.trim();
1095
+ const replacement = token ? substitute(token, lookup, depth + 1) : comma < 0 ? void 0 : substitute(inner.slice(comma + 1).trim(), lookup, depth + 1);
1096
+ if (replacement == null) return void 0;
1097
+ out += value.slice(from, start) + replacement;
1098
+ from = end + 1;
1099
+ }
1100
+ }
1101
+ /** Index of the `)` closing a parenthesis opened just before `from`, or -1. */
1102
+ function closingParen(value, from) {
1103
+ let depth = 1;
1104
+ for (let i = from; i < value.length; i += 1) if (value[i] === "(") depth += 1;
1105
+ else if (value[i] === ")" && --depth === 0) return i;
1106
+ return -1;
1107
+ }
1108
+ function topLevelComma(value) {
1109
+ let depth = 0;
1110
+ for (let i = 0; i < value.length; i += 1) if (value[i] === "(") depth += 1;
1111
+ else if (value[i] === ")") depth -= 1;
1112
+ else if (value[i] === "," && depth === 0) return i;
1113
+ return -1;
1114
+ }
1115
+ /** A theme's tokens for one color scheme: dark tokens override light ones. */
1116
+ function themeTokens(theme, scheme = "light") {
1117
+ return scheme === "dark" ? {
1118
+ ...theme.tokens,
1119
+ ...theme.darkTokens
1120
+ } : { ...theme.tokens };
1121
+ }
1122
+ // src/text.ts
1123
+ function registry(plugins) {
1124
+ const styles = /* @__PURE__ */ new Map();
1125
+ const atoms = /* @__PURE__ */ new Map();
1126
+ for (const plugin of plugins) {
1127
+ for (const [type, fn] of Object.entries(plugin.styles ?? {})) styles.set(type, fn);
1128
+ for (const [type, fn] of Object.entries(plugin.atoms ?? {})) atoms.set(type, fn);
1129
+ }
1130
+ return {
1131
+ styles,
1132
+ atoms
1133
+ };
1134
+ }
1135
+ /** Apply plugin transforms to a node's content, in plugin order. */
1136
+ function transformContent(plugins, ctx) {
1137
+ let content = ctx.node.content;
1138
+ for (const plugin of plugins) if (plugin.transform) content = plugin.transform(content, ctx.node);
1139
+ return content;
1140
+ }
1141
+ /** Turn inline content into styled items. Unknown containers pass their children through. */
1142
+ function flatten(content, base, reg, ctx) {
1143
+ const items = [];
1144
+ const text = (value, style, spans) => {
1145
+ if (value) items.push({
1146
+ kind: "text",
1147
+ text: value.replace(/\s*\n\s*/g, " "),
1148
+ style,
1149
+ spans,
1150
+ width: 0
1151
+ });
1152
+ };
1153
+ const visit = (nodes, style, spans) => {
1154
+ for (const node of nodes) {
1155
+ const atom = reg.atoms.get(node.type);
1156
+ if (atom) {
1157
+ const size = atom.measure(node, style, ctx);
1158
+ items.push({
1159
+ kind: "atom",
1160
+ inline: node,
1161
+ renderer: atom,
1162
+ size,
1163
+ style,
1164
+ spans,
1165
+ width: size.width
1166
+ });
1167
+ continue;
1168
+ }
1169
+ if (node.type === "break") {
1170
+ items.push({
1171
+ kind: "newline",
1172
+ style,
1173
+ spans,
1174
+ width: 0
1175
+ });
1176
+ continue;
1177
+ }
1178
+ const styler = reg.styles.get(node.type);
1179
+ if (styler) {
1180
+ const result = styler(node, style, ctx);
1181
+ const inner = {
1182
+ font: {
1183
+ ...style.font,
1184
+ ...result.font
1185
+ },
1186
+ fill: result.fill ?? style.fill
1187
+ };
1188
+ const span = result.decoration && {
1189
+ decoration: result.decoration,
1190
+ style: inner
1191
+ };
1192
+ const innerSpans = span ? [...spans, span] : spans;
1193
+ const padding = result.decoration?.padding ?? 0;
1194
+ if (padding) items.push({
1195
+ kind: "pad",
1196
+ style: inner,
1197
+ spans: innerSpans,
1198
+ width: padding
1199
+ });
1200
+ if ("value" in node) text(node.value, inner, innerSpans);
1201
+ else if ("children" in node) visit(node.children, inner, innerSpans);
1202
+ if (padding) items.push({
1203
+ kind: "pad",
1204
+ style: inner,
1205
+ spans: innerSpans,
1206
+ width: padding
1207
+ });
1208
+ } else if (node.type === "image") text(node.alt ?? "", style, spans);
1209
+ else if ("value" in node) text(node.value, style, spans);
1210
+ else if ("children" in node) visit(node.children, style, spans);
1211
+ }
1212
+ };
1213
+ visit(content, base, []);
1214
+ return items;
1215
+ }
1216
+ var segmenter = new Intl.Segmenter(void 0, { granularity: "word" });
1217
+ var graphemes = new Intl.Segmenter(void 0, { granularity: "grapheme" });
1218
+ var CJK = /[⺀-鿿가-힣豈-﫿＀-￯]/u;
1219
+ /** Characters that must not start a line. */
1220
+ var NO_BREAK_BEFORE = /^[\p{Pe}\p{Pf}\p{Po}、。,!?]/u;
1221
+ /** Group items into unbreakable units, using word boundaries and CJK rules. */
1222
+ function toUnits(items, ctx) {
1223
+ const units = [];
1224
+ let current;
1225
+ let prevSpace = true;
1226
+ let prevText = "";
1227
+ const start = (space) => {
1228
+ current = {
1229
+ items: [],
1230
+ width: 0,
1231
+ space,
1232
+ newline: false
1233
+ };
1234
+ units.push(current);
1235
+ return current;
1236
+ };
1237
+ for (const item of items) {
1238
+ if (item.kind === "newline") {
1239
+ units.push({
1240
+ items: [],
1241
+ width: 0,
1242
+ space: false,
1243
+ newline: true
1244
+ });
1245
+ current = void 0;
1246
+ prevSpace = true;
1247
+ continue;
1248
+ }
1249
+ if (item.kind === "atom") {
1250
+ start(false).items.push(item);
1251
+ current.width = item.width;
1252
+ current = void 0;
1253
+ prevSpace = true;
1254
+ continue;
1255
+ }
1256
+ if (item.kind === "pad") {
1257
+ const unit = !current || current.space || prevSpace ? start(false) : current;
1258
+ unit.items.push(item);
1259
+ unit.width += item.width;
1260
+ prevSpace = false;
1261
+ continue;
1262
+ }
1263
+ for (const { segment } of segmenter.segment(item.text)) {
1264
+ const space = /^\s+$/.test(segment);
1265
+ const breakable = space || prevSpace || (CJK.test(segment[0]) || CJK.test(prevText.at(-1) ?? "")) && !NO_BREAK_BEFORE.test(segment);
1266
+ const value = space ? " " : segment;
1267
+ const unit = !current || breakable || current.space ? start(space) : current;
1268
+ const width = ctx.measurer.measure(value, item.style.font);
1269
+ const last = unit.items.at(-1);
1270
+ if (last?.kind === "text" && last.style === item.style && last.spans === item.spans) {
1271
+ last.text += value;
1272
+ last.width += width;
1273
+ } else unit.items.push({
1274
+ ...item,
1275
+ text: value,
1276
+ width
1277
+ });
1278
+ unit.width += width;
1279
+ prevSpace = space;
1280
+ prevText = segment;
1281
+ }
1282
+ }
1283
+ return units;
1284
+ }
1285
+ /** Break a unit wider than the line into grapheme-sized units. */
1286
+ function splitUnit(unit, ctx) {
1287
+ const result = [];
1288
+ for (const item of unit.items) {
1289
+ if (item.kind !== "text") {
1290
+ result.push({
1291
+ items: [item],
1292
+ width: item.width,
1293
+ space: false,
1294
+ newline: false
1295
+ });
1296
+ continue;
1297
+ }
1298
+ for (const { segment } of graphemes.segment(item.text)) {
1299
+ const width = ctx.measurer.measure(segment, item.style.font);
1300
+ result.push({
1301
+ items: [{
1302
+ ...item,
1303
+ text: segment,
1304
+ width
1305
+ }],
1306
+ width,
1307
+ space: false,
1308
+ newline: false
1309
+ });
1310
+ }
1311
+ }
1312
+ return result;
1313
+ }
1314
+ function breakLines(units, wrapWidth, ctx) {
1315
+ const lines = [[]];
1316
+ let lineWidth = 0;
1317
+ const push = (unit) => {
1318
+ const line = lines.at(-1);
1319
+ if (unit.newline) {
1320
+ lines.push([]);
1321
+ lineWidth = 0;
1322
+ return;
1323
+ }
1324
+ if (unit.space && !line.length) return;
1325
+ if (wrapWidth > 0 && !unit.space && line.length && lineWidth + unit.width > wrapWidth) {
1326
+ while (line.at(-1)?.space) lineWidth -= line.pop().width;
1327
+ lines.push([]);
1328
+ lineWidth = 0;
1329
+ if (unit.width > wrapWidth) return void splitUnit(unit, ctx).forEach(push);
1330
+ } else if (wrapWidth > 0 && !line.length && unit.width > wrapWidth) {
1331
+ const parts = splitUnit(unit, ctx);
1332
+ if (parts.length > 1) return void parts.forEach(push);
1333
+ }
1334
+ lines.at(-1).push(unit);
1335
+ lineWidth += unit.width;
1336
+ };
1337
+ units.forEach(push);
1338
+ for (const line of lines) while (line.at(-1)?.space) line.pop();
1339
+ return lines;
1340
+ }
1341
+ /** Lay out inline content into runs, decorations and atoms, wrapping at `wrapWidth`. */
1342
+ function layoutText(content, options) {
1343
+ const { style, plugins, ctx } = options;
1344
+ const lines = breakLines(toUnits(flatten(content, style, registry(plugins), ctx), ctx), options.wrapWidth ?? 0, ctx);
1345
+ const { font } = style;
1346
+ const textAscent = (font.lineHeight + font.size * .7) / 2;
1347
+ const backgrounds = [];
1348
+ const overlays = [];
1349
+ const atoms = [];
1350
+ const links = [];
1351
+ const runs = [];
1352
+ const lineWidths = lines.map((line) => line.reduce((sum, unit) => sum + unit.items.reduce((w, item) => w + item.width, 0), 0));
1353
+ const width = Math.max(0, ...lineWidths);
1354
+ const alignFactor = {
1355
+ start: 0,
1356
+ center: .5,
1357
+ end: 1
1358
+ }[options.align ?? "start"];
1359
+ let top = 0;
1360
+ for (const [index, line] of lines.entries()) {
1361
+ const placed = [];
1362
+ let x = (width - lineWidths[index]) * alignFactor;
1363
+ for (const item of line.flatMap((unit) => unit.items)) {
1364
+ placed.push({
1365
+ item,
1366
+ x
1367
+ });
1368
+ x += item.width;
1369
+ }
1370
+ let ascent = textAscent;
1371
+ let descent = font.lineHeight - textAscent;
1372
+ for (const { item } of placed) {
1373
+ if (item.kind !== "atom") continue;
1374
+ ascent = Math.max(ascent, item.size.baseline);
1375
+ descent = Math.max(descent, item.size.height - item.size.baseline);
1376
+ }
1377
+ const baseline = top + ascent;
1378
+ const lineHeight = ascent + descent;
1379
+ for (const { item, x: itemX } of placed) if (item.kind === "text") {
1380
+ const last = runs.at(-1);
1381
+ if (last && last.y === baseline && last.font === item.style.font && last.fill === item.style.fill && last.x + (last.width ?? 0) === itemX) {
1382
+ last.text += item.text;
1383
+ last.width = (last.width ?? 0) + item.width;
1384
+ } else {
1385
+ const run = {
1386
+ text: item.text,
1387
+ x: itemX,
1388
+ y: baseline,
1389
+ font: item.style.font,
1390
+ fill: item.style.fill,
1391
+ width: item.width
1392
+ };
1393
+ runs.push(run);
1394
+ }
1395
+ } else if (item.kind === "atom") {
1396
+ const box = {
1397
+ x: itemX,
1398
+ y: baseline - item.size.baseline,
1399
+ width: item.size.width,
1400
+ height: item.size.height
1401
+ };
1402
+ atoms.push(...item.renderer.render(item.inline, box, item.style, ctx));
1403
+ }
1404
+ const extents = /* @__PURE__ */ new Map();
1405
+ for (const { item, x: itemX } of placed) for (const span of item.spans) {
1406
+ const extent = extents.get(span);
1407
+ if (extent) extent[1] = itemX + item.width;
1408
+ else extents.set(span, [itemX, itemX + item.width]);
1409
+ }
1410
+ for (const [span, [x0, x1]] of extents) {
1411
+ const { decoration: d, style: s } = span;
1412
+ if (d.background) backgrounds.push({
1413
+ kind: "rect",
1414
+ x: x0,
1415
+ y: baseline - s.font.size * .95,
1416
+ w: x1 - x0,
1417
+ h: s.font.size * 1.3,
1418
+ r: d.radius,
1419
+ fill: d.background
1420
+ });
1421
+ if (d.strike) {
1422
+ const y = baseline - s.font.size * .3;
1423
+ overlays.push({
1424
+ kind: "path",
1425
+ d: `M${x0},${y}H${x1}`,
1426
+ stroke: d.strike,
1427
+ strokeWidth: Math.max(1, s.font.size / 14)
1428
+ });
1429
+ }
1430
+ if (d.href) links.push({
1431
+ kind: "link",
1432
+ href: d.href,
1433
+ rect: {
1434
+ x: x0,
1435
+ y: top,
1436
+ width: x1 - x0,
1437
+ height: lineHeight
1438
+ }
1439
+ });
1440
+ }
1441
+ top += lineHeight;
1442
+ }
1443
+ return {
1444
+ primitives: [
1445
+ ...backgrounds,
1446
+ ...runs.length ? [{
1447
+ kind: "text",
1448
+ x: 0,
1449
+ y: 0,
1450
+ runs
1451
+ }] : [],
1452
+ ...overlays,
1453
+ ...atoms,
1454
+ ...links
1455
+ ],
1456
+ runs,
1457
+ width,
1458
+ height: top,
1459
+ lines: lines.length
1460
+ };
1461
+ }
1462
+ // src/render.ts
1463
+ /** Ids from the root to every node, both included. */
1464
+ function nodePaths(root) {
1465
+ const paths = /* @__PURE__ */ new Map();
1466
+ const visit = (node, parent) => {
1467
+ const path = [...parent, node.id];
1468
+ paths.set(node.id, path);
1469
+ for (const child of node.children) visit(child, path);
1470
+ };
1471
+ visit(root, []);
1472
+ return paths;
1473
+ }
1474
+ /** Lay out a node's label. Alignment does not change the size. */
1475
+ function labelLayout(node, theme, options, align, defer) {
1476
+ const plugins = options.plugins ?? defaultContentPlugins;
1477
+ const ctx = {
1478
+ theme,
1479
+ measurer: options.measurer,
1480
+ node,
1481
+ wrapWidth: options.wrapWidth,
1482
+ defer
1483
+ };
1484
+ return layoutText(transformContent(plugins, ctx), {
1485
+ style: {
1486
+ font: theme.font,
1487
+ fill: theme.textFill
1488
+ },
1489
+ wrapWidth: options.wrapWidth,
1490
+ plugins,
1491
+ ctx,
1492
+ align
1493
+ });
1494
+ }
1495
+ /**
1496
+ * Measure the label and box of every visible node. Content that is not ready yet (see
1497
+ * {@link InlineContext.defer}) is reported to `defer`.
1498
+ */
1499
+ function measureTree(root, options, defer) {
1500
+ const theme = options.theme ?? classicTheme;
1501
+ const [top, right, bottom, left] = theme.node.padding;
1502
+ const result = /* @__PURE__ */ new Map();
1503
+ const paths = nodePaths(root);
1504
+ for (const node of visibleNodes(root, options.folded)) {
1505
+ const path = paths.get(node.id);
1506
+ const depth = path.length - 1;
1507
+ const text = labelLayout(node, theme, options, void 0, defer);
1508
+ const ctx = {
1509
+ depth,
1510
+ path,
1511
+ hasChildren: node.children.length > 0,
1512
+ folded: !!options.folded?.has(node.id)
1513
+ };
1514
+ const box = theme.node.measure ? theme.node.measure(text, node, ctx) : {
1515
+ width: text.width + left + right,
1516
+ height: Math.max(text.height + top + bottom, theme.node.minHeight ?? 0)
1517
+ };
1518
+ result.set(node.id, {
1519
+ text,
1520
+ box
1521
+ });
1522
+ }
1523
+ return result;
1524
+ }
1525
+ /** Measure, lay out and draw a tree as backend-neutral primitives. */
1526
+ function renderTree(root, options) {
1527
+ const theme = options.theme ?? classicTheme;
1528
+ const tasks = [];
1529
+ const measured = measureTree(root, options, (task) => tasks.push(task));
1530
+ const paths = nodePaths(root);
1531
+ const contexts = /* @__PURE__ */ new Map();
1532
+ const contextOf = (item) => {
1533
+ let ctx = contexts.get(item.node.id);
1534
+ if (!ctx) {
1535
+ ctx = {
1536
+ depth: item.depth,
1537
+ path: paths.get(item.node.id),
1538
+ hasChildren: item.node.children.length > 0,
1539
+ folded: item.folded,
1540
+ direction: item.direction
1541
+ };
1542
+ contexts.set(item.node.id, ctx);
1543
+ }
1544
+ return ctx;
1545
+ };
1546
+ const layout = (options.layout ?? treeLayout).layout(root, {
1547
+ sizes: new Map([...measured].map(([id, { box }]) => [id, box])),
1548
+ folded: options.folded,
1549
+ levelGap: options.levelGap,
1550
+ siblingGap: options.siblingGap
1551
+ });
1552
+ const state = { nodes: layout.nodes.map((item) => {
1553
+ const ctx = contextOf(item);
1554
+ let { text } = measured.get(item.node.id);
1555
+ const align = theme.node.textAlign?.(ctx) ?? "start";
1556
+ if (align !== "start" && text.lines > 1) text = labelLayout(item.node, theme, options, align);
1557
+ return {
1558
+ node: item.node,
1559
+ parentId: item.parent?.node.id,
1560
+ rect: item.rect,
1561
+ ctx,
1562
+ text,
1563
+ presence: 1
1564
+ };
1565
+ }) };
1566
+ const primitives = drawState(state, theme);
1567
+ const bounds = unionRects([layout.bounds, primitiveBounds(primitives)]);
1568
+ const pending = tasks.length ? Promise.allSettled(tasks).then(() => void 0) : void 0;
1569
+ return {
1570
+ theme,
1571
+ layout,
1572
+ primitives,
1573
+ bounds,
1574
+ contexts,
1575
+ state,
1576
+ ...pending && { pending }
1577
+ };
1578
+ }
1579
+ /**
1580
+ * Render, wait for content that was measured with a placeholder, and render again, until
1581
+ * everything is final (at most `rounds` times). Useful outside a live view, e.g. for
1582
+ * exporting SVG with images.
1583
+ */
1584
+ async function renderTreeAsync(root, options, rounds = 3) {
1585
+ let result = renderTree(root, options);
1586
+ for (let i = 1; i < rounds && result.pending; i++) {
1587
+ await result.pending;
1588
+ result = renderTree(root, options);
1589
+ }
1590
+ return result;
1591
+ }
1592
+ /**
1593
+ * Draw a state as primitives: edges first, then nodes, each in a keyed group so
1594
+ * backends can patch them. Edge anchors come from the theme, so animating node rects
1595
+ * moves edges along with them.
1596
+ */
1597
+ function drawState(state, theme) {
1598
+ const byId = new Map(state.nodes.map((item) => [item.node.id, item]));
1599
+ const looks = /* @__PURE__ */ new Map();
1600
+ const lookOf = (item) => {
1601
+ if (item.presence >= 1) return {};
1602
+ let look = looks.get(item.node.id);
1603
+ if (!look) {
1604
+ const box = {
1605
+ width: item.rect.width,
1606
+ height: item.rect.height
1607
+ };
1608
+ look = theme.node.presence?.(item.presence, box, item.ctx) ?? { opacity: item.presence };
1609
+ looks.set(item.node.id, look);
1610
+ }
1611
+ return look;
1612
+ };
1613
+ const edges = [];
1614
+ for (const target of state.nodes) {
1615
+ const source = target.parentId ? byId.get(target.parentId) : void 0;
1616
+ if (!source) continue;
1617
+ const from = theme.node.anchor(source.rect, target.ctx.direction, source.ctx);
1618
+ const to = theme.node.anchor(target.rect, oppositeSide(target.ctx.direction), target.ctx);
1619
+ edges.push({
1620
+ kind: "group",
1621
+ key: `edge:${target.node.id}`,
1622
+ nodeId: target.node.id,
1623
+ bounds: edgeBounds(from, to),
1624
+ ...opacityOf(lookOf(target)),
1625
+ children: theme.edge.render(from, to, {
1626
+ source: source.node,
1627
+ target: target.node,
1628
+ node: target.ctx
1629
+ })
1630
+ });
1631
+ }
1632
+ const [top, , , left] = theme.node.padding;
1633
+ const nodes = state.nodes.map((item) => ({
1634
+ kind: "group",
1635
+ key: `node:${item.node.id}`,
1636
+ nodeId: item.node.id,
1637
+ bounds: expandRect(item.rect, BOUNDS_MARGIN),
1638
+ transform: lookOf(item).transform ? multiply(translate(item.rect.x, item.rect.y), lookOf(item).transform) : translate(item.rect.x, item.rect.y),
1639
+ ...opacityOf(lookOf(item)),
1640
+ children: [...theme.node.render(item.node, {
1641
+ width: item.rect.width,
1642
+ height: item.rect.height
1643
+ }, item.ctx), {
1644
+ kind: "group",
1645
+ transform: translate(left, top),
1646
+ children: item.text.primitives
1647
+ }]
1648
+ }));
1649
+ return [...edges, ...nodes];
1650
+ }
1651
+ /** Room around a node or edge for what themes draw past it, such as fold circles. */
1652
+ var BOUNDS_MARGIN = 24;
1653
+ var edgeBounds = (a, b) => expandRect({
1654
+ x: Math.min(a.x, b.x),
1655
+ y: Math.min(a.y, b.y),
1656
+ width: Math.abs(a.x - b.x),
1657
+ height: Math.abs(a.y - b.y)
1658
+ }, BOUNDS_MARGIN);
1659
+ var opacityOf = ({ opacity }) => opacity != null && opacity < 1 ? { opacity: Math.max(0, opacity) } : {};
1660
+ /** Absolute points of a path made of `M`, `L`, `H`, `V` and `C` commands. */
1661
+ function pathPoints(d) {
1662
+ const points = [];
1663
+ let [x, y] = [0, 0];
1664
+ for (const [, cmd, args] of d.matchAll(/([MLHVC])([^MLHVC]*)/g)) {
1665
+ const values = args.match(/-?(?:\d+\.?\d*|\.\d+)(?:e[-+]?\d+)?/gi)?.map(Number) ?? [];
1666
+ if (cmd === "H") for (const v of values) points.push([x = v, y]);
1667
+ else if (cmd === "V") for (const v of values) points.push([x, y = v]);
1668
+ else for (let i = 0; i + 1 < values.length; i += 2) points.push([x = values[i], y = values[i + 1]]);
1669
+ }
1670
+ return points;
1671
+ }
1672
+ /**
1673
+ * Bounding box of rects, paths and images, including stroke widths. Curve control
1674
+ * points are included, so curves give a safe over-estimate. Text is not included:
1675
+ * it sits inside node boxes, which callers add from the layout.
1676
+ */
1677
+ function primitiveBounds(primitives) {
1678
+ const rects = [];
1679
+ /** Add a box around a point, whose stroke scales with the matrix. */
1680
+ const addPoint = ([a, b, c, d, e, f], x, y, size) => {
1681
+ const s = size * Math.max(Math.hypot(a, b), Math.hypot(c, d));
1682
+ rects.push({
1683
+ x: a * x + c * y + e - s,
1684
+ y: b * x + d * y + f - s,
1685
+ width: 2 * s,
1686
+ height: 2 * s
1687
+ });
1688
+ };
1689
+ const addBox = (matrix, { x, y, width, height }, size) => {
1690
+ addPoint(matrix, x, y, size);
1691
+ addPoint(matrix, x + width, y, size);
1692
+ addPoint(matrix, x, y + height, size);
1693
+ addPoint(matrix, x + width, y + height, size);
1694
+ };
1695
+ const visit = (items, outer) => {
1696
+ for (const p of items) if (p.kind === "group") {
1697
+ const matrix = p.transform ? multiply(outer, p.transform) : outer;
1698
+ if (p.clip) addBox(matrix, p.clip, 0);
1699
+ else visit(p.children, matrix);
1700
+ } else if (p.kind === "rect" || p.kind === "image") {
1701
+ const box = {
1702
+ x: p.x,
1703
+ y: p.y,
1704
+ width: p.w,
1705
+ height: p.h
1706
+ };
1707
+ addBox(outer, box, p.kind === "rect" ? strokeOf(p) : 0);
1708
+ } else if (p.kind === "path") {
1709
+ const s = strokeOf(p);
1710
+ for (const [x, y] of pathPoints(p.d)) addPoint(outer, x, y, s);
1711
+ }
1712
+ };
1713
+ visit(primitives, translate(0, 0));
1714
+ return unionRects(rects);
1715
+ }
1716
+ function strokeOf(p) {
1717
+ return p.stroke ? (p.strokeWidth ?? 1) / 2 : 0;
1718
+ }
1719
+ // src/svg.ts
1720
+ var ESCAPES = {
1721
+ "&": "&amp;",
1722
+ "<": "&lt;",
1723
+ ">": "&gt;",
1724
+ "\"": "&quot;",
1725
+ "'": "&#39;"
1726
+ };
1727
+ function escapeXml(value) {
1728
+ return value.replace(/[&<>"']/g, (char) => ESCAPES[char]);
1729
+ }
1730
+ var num = (value) => String(Math.round(value * 100) / 100);
1731
+ function attrs(values) {
1732
+ let result = "";
1733
+ for (const [name, value] of Object.entries(values)) {
1734
+ if (value == null || value === "") continue;
1735
+ result += ` ${name}="${escapeXml(typeof value === "number" ? num(value) : value)}"`;
1736
+ }
1737
+ return result;
1738
+ }
1739
+ function style(values) {
1740
+ const parts = Object.entries(values).filter(([, value]) => value != null).map(([name, value]) => `${name}:${typeof value === "number" ? num(value) : value}`);
1741
+ return parts.length ? parts.join(";") : void 0;
1742
+ }
1743
+ /** Paints live in `style`, because `var()` is not reliable in presentation attributes. */
1744
+ function paint(p) {
1745
+ return style({
1746
+ fill: p.fill ?? "none",
1747
+ stroke: p.stroke,
1748
+ "stroke-width": p.stroke ? p.strokeWidth : void 0
1749
+ });
1750
+ }
1751
+ function runToSVG(run) {
1752
+ const { font } = run;
1753
+ return `<tspan${attrs({
1754
+ x: run.x,
1755
+ y: run.y,
1756
+ style: style({
1757
+ "font-family": font.family,
1758
+ "font-size": `${num(font.size)}px`,
1759
+ "font-weight": font.weight,
1760
+ "font-style": font.style === "italic" ? "italic" : void 0,
1761
+ fill: run.fill
1762
+ })
1763
+ })}>${escapeXml(run.text)}</tspan>`;
1764
+ }
1765
+ /** Markup per primitive list, so unchanged content is not serialized on every frame. */
1766
+ var markupCache = {
1767
+ plain: /* @__PURE__ */ new WeakMap(),
1768
+ interactive: /* @__PURE__ */ new WeakMap()
1769
+ };
1770
+ /**
1771
+ * Serialize primitives as SVG markup. Primitive lists are treated as immutable: the
1772
+ * markup of each list is cached, which makes redrawing animation frames cheap.
1773
+ */
1774
+ function primitivesToSVG(primitives, options = {}) {
1775
+ const cache = options.interactive ? markupCache.interactive : markupCache.plain;
1776
+ let out = cache.get(primitives);
1777
+ if (out == null) {
1778
+ out = serialize(primitives, options);
1779
+ cache.set(primitives, out);
1780
+ }
1781
+ return out;
1782
+ }
1783
+ function serialize(primitives, options) {
1784
+ let out = "";
1785
+ for (const p of primitives) {
1786
+ if (p.backends && !p.backends.includes("svg")) continue;
1787
+ switch (p.kind) {
1788
+ case "group": {
1789
+ const content = primitivesToSVG(p.children, options);
1790
+ out += `<g${attrs({
1791
+ transform: p.transform && `matrix(${p.transform.map(num).join(" ")})`,
1792
+ opacity: p.opacity,
1793
+ "data-node-id": p.nodeId
1794
+ })}>${p.clip ? clipped(content, p.clip) : content}</g>`;
1795
+ break;
1796
+ }
1797
+ case "rect":
1798
+ out += `<rect${attrs({
1799
+ x: p.x,
1800
+ y: p.y,
1801
+ width: p.w,
1802
+ height: p.h,
1803
+ rx: p.r,
1804
+ style: paint(p)
1805
+ })}/>`;
1806
+ break;
1807
+ case "path":
1808
+ out += `<path${attrs({
1809
+ d: p.d,
1810
+ style: paint(p)
1811
+ })}/>`;
1812
+ break;
1813
+ case "text":
1814
+ out += `<text${attrs({
1815
+ transform: `translate(${num(p.x)} ${num(p.y)})`,
1816
+ "xml:space": "preserve"
1817
+ })}>${p.runs.map(runToSVG).join("")}</text>`;
1818
+ break;
1819
+ case "image":
1820
+ out += `<image${attrs({
1821
+ x: p.x,
1822
+ y: p.y,
1823
+ width: p.w,
1824
+ height: p.h,
1825
+ href: p.src
1826
+ })}/>`;
1827
+ break;
1828
+ case "link":
1829
+ out += `<a${attrs({
1830
+ href: p.href,
1831
+ target: "_blank"
1832
+ })}><rect${attrs({
1833
+ x: p.rect.x,
1834
+ y: p.rect.y,
1835
+ width: p.rect.width,
1836
+ height: p.rect.height,
1837
+ style: "fill:transparent"
1838
+ })}/></a>`;
1839
+ break;
1840
+ case "custom":
1841
+ out += p.svg?.() ?? "";
1842
+ break;
1843
+ case "hit": if (options.interactive) out += `<rect${attrs({
1844
+ x: p.rect.x,
1845
+ y: p.rect.y,
1846
+ width: p.rect.width,
1847
+ height: p.rect.height,
1848
+ "data-action": p.action,
1849
+ style: "fill:transparent;cursor:pointer"
1850
+ })}/>`;
1851
+ }
1852
+ }
1853
+ return out;
1854
+ }
1855
+ /** A nested `<svg>` hides what overflows it, without needing an id like `<clipPath>`. */
1856
+ function clipped(content, { x, y, width, height }) {
1857
+ return `<svg${attrs({
1858
+ x,
1859
+ y,
1860
+ width,
1861
+ height,
1862
+ viewBox: [
1863
+ x,
1864
+ y,
1865
+ width,
1866
+ height
1867
+ ].map(num).join(" ")
1868
+ })}>${content}</svg>`;
1869
+ }
1870
+ /** Theme tokens as CSS custom property declarations. */
1871
+ function tokensToCSS(theme) {
1872
+ const tokens = isTheme(theme) ? theme.tokens : theme;
1873
+ return Object.entries(tokens).filter(([name, value]) => /^[\w-]+$/.test(name) && isSafeCSSValue(value)).map(([name, value]) => `--${name}:${value};`).join("");
1874
+ }
1875
+ var isTheme = (value) => typeof value.tokens === "object";
1876
+ /**
1877
+ * Rules that set a theme's tokens on `selector`. Dark tokens override light ones for
1878
+ * `dark`, and inside a `prefers-color-scheme: dark` query for `auto`.
1879
+ */
1880
+ function themeCSS(theme, colorScheme = "light", selector = ".markmap") {
1881
+ const light = `${selector}{${tokensToCSS(theme.tokens)}}`;
1882
+ if (!theme.darkTokens || colorScheme === "light") return light;
1883
+ const dark = `${selector}{${tokensToCSS(theme.darkTokens)}}`;
1884
+ return colorScheme === "dark" ? light + dark : `${light}@media (prefers-color-scheme: dark){${dark}}`;
1885
+ }
1886
+ /** A standalone SVG document. Theme tokens are embedded so user CSS can still override them. */
1887
+ function toSVGString(result, options = {}) {
1888
+ const { padding = 20, background = true, colorScheme } = options;
1889
+ const box = expandRect(result.bounds, padding);
1890
+ const viewBox = [
1891
+ box.x,
1892
+ box.y,
1893
+ box.width,
1894
+ box.height
1895
+ ].map(num).join(" ");
1896
+ const css = themeCSS(result.theme, colorScheme);
1897
+ const bg = background ? `<rect${attrs({
1898
+ x: box.x,
1899
+ y: box.y,
1900
+ width: box.width,
1901
+ height: box.height,
1902
+ style: "fill:var(--mm-bg)"
1903
+ })}/>` : "";
1904
+ return `<svg xmlns="http://www.w3.org/2000/svg" class="markmap"${attrs({
1905
+ width: box.width,
1906
+ height: box.height,
1907
+ viewBox
1908
+ })}><style>${css}</style>${bg}${primitivesToSVG(result.primitives)}</svg>`;
1909
+ }
1910
+ // src/animate.ts
1911
+ var easeCubicInOut = (t) => t < .5 ? 4 * t * t * t : 1 - (-2 * t + 2) ** 3 / 2;
1912
+ var lerp = (a, b, t) => a + (b - a) * t;
1913
+ var lerpRect = (a, b, t) => ({
1914
+ x: lerp(a.x, b.x, t),
1915
+ y: lerp(a.y, b.y, t),
1916
+ width: lerp(a.width, b.width, t),
1917
+ height: lerp(a.height, b.height, t)
1918
+ });
1919
+ /** The closest ancestor of `item` (by its id path) that exists in `byId`. */
1920
+ function nearestAncestor(item, byId) {
1921
+ const { path } = item.ctx;
1922
+ for (let i = path.length - 2; i >= 0; i--) {
1923
+ const found = byId.get(path[i]);
1924
+ if (found) return found;
1925
+ }
1926
+ }
1927
+ /**
1928
+ * The default {@link Layout.interpolate}: nodes in both frames move in a straight line;
1929
+ * entering nodes grow out of their nearest ancestor's anchor (on the side they grow
1930
+ * towards) and leaving nodes shrink back into their nearest surviving ancestor's.
1931
+ */
1932
+ function interpolateFrames(from, to, t, ctx) {
1933
+ const fromById = new Map(from.nodes.map((item) => [item.node.id, item]));
1934
+ const toById = new Map(to.nodes.map((item) => [item.node.id, item]));
1935
+ /** A rect of `item`'s size whose incoming anchor sits on `ancestor`'s outgoing one. */
1936
+ const collapsedInto = (item, ancestor) => {
1937
+ const side = item.ctx.direction;
1938
+ const point = ctx.anchor(ancestor, side);
1939
+ const offset = ctx.anchor({
1940
+ ...item,
1941
+ rect: {
1942
+ ...item.rect,
1943
+ x: 0,
1944
+ y: 0
1945
+ }
1946
+ }, oppositeSide(side));
1947
+ return {
1948
+ ...item.rect,
1949
+ x: point.x - offset.x,
1950
+ y: point.y - offset.y
1951
+ };
1952
+ };
1953
+ const nodes = to.nodes.map((item) => {
1954
+ const old = fromById.get(item.node.id);
1955
+ if (old) return {
1956
+ ...item,
1957
+ rect: lerpRect(old.rect, item.rect, t),
1958
+ presence: lerp(old.presence, item.presence, t)
1959
+ };
1960
+ const ancestor = nearestAncestor(item, fromById);
1961
+ const start = ancestor ? collapsedInto(item, ancestor) : item.rect;
1962
+ return {
1963
+ ...item,
1964
+ rect: lerpRect(start, item.rect, t),
1965
+ presence: item.presence * t
1966
+ };
1967
+ });
1968
+ for (const item of from.nodes) {
1969
+ if (toById.has(item.node.id)) continue;
1970
+ const ancestor = nearestAncestor(item, toById);
1971
+ const end = ancestor ? collapsedInto(item, ancestor) : item.rect;
1972
+ nodes.push({
1973
+ ...item,
1974
+ rect: lerpRect(item.rect, end, t),
1975
+ presence: item.presence * (1 - t)
1976
+ });
1977
+ }
1978
+ return { nodes };
1979
+ }
1980
+ /** Interpolate draw states with the layout's own interpolation, or the default one. */
1981
+ function interpolateStates(from, to, t, theme, layout) {
1982
+ return (layout?.interpolate ?? interpolateFrames)(from, to, t, { anchor: (item, side) => theme.node.anchor(item.rect, side, item.ctx) });
1983
+ }
1984
+ export { DEFAULT_COLORS, approximateMeasurer, basicContent, branchColors, branchIndex, builtinLayouts, builtinThemes, cardsTheme, classicTheme, configureTheme, createCanvasMeasurer, createCardsTheme, createClassicTheme, createImageContent, defaultContentPlugins, drawState, easeCubicInOut, escapeXml, fontToCss, imageContent, interpolateFrames, interpolateStates, layoutOrgChart, layoutText, layoutTree, layoutTwoSided, measureTree, minimalTheme, nodePaths, noteContent, orgChartLayout, primitiveBounds, primitivesToSVG, renderTree, renderTreeAsync, resolvePaint, resolveRenderOptions, sanitizeImageUrl, sanitizeUrl, splitBranches, themeCSS, themeTokens, toSVGString, tokensToCSS, transformContent, treeLayout, twoSidedLayout, visibleNodes };