@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/LICENSE +21 -0
- package/README.md +4 -2
- package/dist/animate.d.ts +12 -0
- package/dist/content.d.ts +10 -0
- package/dist/default-plugins.d.ts +3 -0
- package/dist/image.d.ts +23 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +1984 -0
- package/dist/layouts/index.d.ts +3 -0
- package/dist/layouts/org-chart.d.ts +9 -0
- package/dist/layouts/tree.d.ts +7 -0
- package/dist/layouts/two-sided.d.ts +14 -0
- package/dist/measure.d.ts +11 -0
- package/dist/options.d.ts +33 -0
- package/dist/paint.d.ts +12 -0
- package/dist/render.d.ts +74 -0
- package/dist/svg.d.ts +31 -0
- package/dist/text.d.ts +20 -0
- package/dist/themes/branch.d.ts +22 -0
- package/dist/themes/cards.d.ts +6 -0
- package/dist/themes/classic.d.ts +6 -0
- package/dist/themes/minimal.d.ts +3 -0
- package/dist/themes/shared.d.ts +38 -0
- package/package.json +36 -3
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
|
+
"&": "&",
|
|
1722
|
+
"<": "<",
|
|
1723
|
+
">": ">",
|
|
1724
|
+
"\"": """,
|
|
1725
|
+
"'": "'"
|
|
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 };
|