@markmap/canvas 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/backend.d.ts +38 -0
- package/dist/draw.d.ts +41 -0
- package/dist/export-D0aKjidm.js +292 -0
- package/dist/export.d.ts +35 -0
- package/dist/images.d.ts +33 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +122 -0
- package/dist/node.d.ts +45 -0
- package/dist/node.js +119 -0
- package/package.json +55 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2020 Gerald
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @markmap/canvas
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Canvas backend for Markmap.
|
|
4
|
+
|
|
5
|
+
See the [Markmap README](https://github.com/markmap/markmap#readme) for how the packages fit together.
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { Primitive } from '@markmap/core';
|
|
2
|
+
import type { ViewBackend, ViewTransform } from '@markmap/view';
|
|
3
|
+
export interface CanvasBackendOptions {
|
|
4
|
+
/** See `ViewBackend.themeSelector`. */
|
|
5
|
+
themeSelector?: string;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Shows a map in a `<canvas>`, for much larger maps than SVG can animate smoothly. Pass
|
|
9
|
+
* it to `Markmap`: `new Markmap(new CanvasBackend(canvas))`. Size the canvas with CSS.
|
|
10
|
+
*
|
|
11
|
+
* Unlike SVG, a canvas has no accessibility tree and its text cannot be selected.
|
|
12
|
+
*/
|
|
13
|
+
export declare class CanvasBackend implements ViewBackend {
|
|
14
|
+
readonly element: HTMLCanvasElement;
|
|
15
|
+
readonly themeSelector?: string;
|
|
16
|
+
private ctx;
|
|
17
|
+
private style;
|
|
18
|
+
private primitives;
|
|
19
|
+
private transform;
|
|
20
|
+
private images;
|
|
21
|
+
private env;
|
|
22
|
+
private frame?;
|
|
23
|
+
private resizeObserver?;
|
|
24
|
+
private darkQuery?;
|
|
25
|
+
constructor(canvas: HTMLCanvasElement, { themeSelector }?: CanvasBackendOptions);
|
|
26
|
+
draw(primitives: Primitive[]): void;
|
|
27
|
+
setTransform(transform: ViewTransform): void;
|
|
28
|
+
setCSS(css: string): void;
|
|
29
|
+
private schedule;
|
|
30
|
+
/** Draw now. Changes are otherwise drawn on the next animation frame, at most once. */
|
|
31
|
+
paint(): void;
|
|
32
|
+
/**
|
|
33
|
+
* Match the bitmap to the element's size and the screen's pixel density. Returns the
|
|
34
|
+
* device pixel ratio, or 0 when the element has no size.
|
|
35
|
+
*/
|
|
36
|
+
private fitToElement;
|
|
37
|
+
destroy(): void;
|
|
38
|
+
}
|
package/dist/draw.d.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { Primitive, Rect } from '@markmap/core';
|
|
2
|
+
import { type TokenLookup } from '@markmap/render';
|
|
3
|
+
import type { LoadedImage } from './images';
|
|
4
|
+
export interface DrawEnv {
|
|
5
|
+
/** Values for the `var(--mm-…)` paints in primitives. */
|
|
6
|
+
tokens: TokenLookup;
|
|
7
|
+
/**
|
|
8
|
+
* Find an image by its source: `undefined` while it is not ready, which leaves it out
|
|
9
|
+
* so the caller can draw again, and `null` if it failed, which draws its fallback.
|
|
10
|
+
* See {@link ImageCache}.
|
|
11
|
+
*/
|
|
12
|
+
image?: (src: string) => LoadedImage | null | undefined;
|
|
13
|
+
/** Parse SVG path data. Defaults to the global `Path2D`; Node has none. */
|
|
14
|
+
createPath?: (d: string) => Path2D;
|
|
15
|
+
/**
|
|
16
|
+
* Families to add to every font, as CSS. Browsers find a font for any character by
|
|
17
|
+
* themselves, but Node's canvas only looks through the families it is told about.
|
|
18
|
+
*/
|
|
19
|
+
fontFallback?: string;
|
|
20
|
+
/** Report something that cannot be drawn. Defaults to warning once per message. */
|
|
21
|
+
warn?: (message: string) => void;
|
|
22
|
+
/**
|
|
23
|
+
* What is on screen. Top-level groups outside it are skipped, and what is too small to
|
|
24
|
+
* read is sketched. Leave it out to draw everything in full, as for exports.
|
|
25
|
+
*/
|
|
26
|
+
view?: DrawView;
|
|
27
|
+
}
|
|
28
|
+
export interface DrawView {
|
|
29
|
+
/** The visible area, in the coordinates of the primitives. */
|
|
30
|
+
rect: Rect;
|
|
31
|
+
/** Screen pixels per unit of those coordinates. */
|
|
32
|
+
scale: number;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Draw primitives on a 2D context, in whatever coordinates the context is set to. Needs
|
|
36
|
+
* no DOM, so it also runs in workers and in Node.
|
|
37
|
+
*
|
|
38
|
+
* Group opacity multiplies down to each primitive instead of fading the group as one
|
|
39
|
+
* layer, so overlapping children of a fading group show through each other.
|
|
40
|
+
*/
|
|
41
|
+
export declare function drawPrimitives(ctx: CanvasRenderingContext2D, primitives: readonly Primitive[], env: DrawEnv): void;
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
import { fontToCss, resolvePaint, themeTokens } from "@markmap/render";
|
|
2
|
+
import { expandRect } from "@markmap/core";
|
|
3
|
+
// src/draw.ts
|
|
4
|
+
var warned = /* @__PURE__ */ new Set();
|
|
5
|
+
function warnOnce(message) {
|
|
6
|
+
if (warned.has(message)) return;
|
|
7
|
+
warned.add(message);
|
|
8
|
+
console.warn(message);
|
|
9
|
+
}
|
|
10
|
+
/** Text without a usable paint is black, as in SVG. */
|
|
11
|
+
var DEFAULT_TEXT_FILL = "#000";
|
|
12
|
+
/** Below this font size on screen, in px, text is drawn as bars. */
|
|
13
|
+
var MIN_TEXT_SIZE = 3;
|
|
14
|
+
/** Below this size on screen, in px, images are drawn as plain boxes. */
|
|
15
|
+
var MIN_IMAGE_SIZE = 6;
|
|
16
|
+
/** How strongly bars and boxes stand in for what they replace. */
|
|
17
|
+
var SKETCH_ALPHA = .4;
|
|
18
|
+
var SKETCH_IMAGE_FILL = "#888";
|
|
19
|
+
/** Parsed paths, kept with their primitives: redrawing the same frame reuses them. */
|
|
20
|
+
var parsedPaths = /* @__PURE__ */ new WeakMap();
|
|
21
|
+
var overlaps = (a, b) => a.x < b.x + b.width && b.x < a.x + a.width && a.y < b.y + b.height && b.y < a.y + a.height;
|
|
22
|
+
/** Width of a run, estimated if the layout did not record it. */
|
|
23
|
+
var runWidth = (run) => run.width ?? run.text.length * run.font.size * .5;
|
|
24
|
+
/**
|
|
25
|
+
* Draw primitives on a 2D context, in whatever coordinates the context is set to. Needs
|
|
26
|
+
* no DOM, so it also runs in workers and in Node.
|
|
27
|
+
*
|
|
28
|
+
* Group opacity multiplies down to each primitive instead of fading the group as one
|
|
29
|
+
* layer, so overlapping children of a fading group show through each other.
|
|
30
|
+
*/
|
|
31
|
+
function drawPrimitives(ctx, primitives, env) {
|
|
32
|
+
const { view } = env;
|
|
33
|
+
const scale = view?.scale;
|
|
34
|
+
const warn = env.warn ?? warnOnce;
|
|
35
|
+
const createPath = env.createPath ?? ((d) => new Path2D(d));
|
|
36
|
+
const paints = /* @__PURE__ */ new Map();
|
|
37
|
+
/** The context's font is costly to set and normalized when read, so track it here. */
|
|
38
|
+
let currentFont;
|
|
39
|
+
const paint = (value) => {
|
|
40
|
+
if (value == null || value === "none") return void 0;
|
|
41
|
+
if (!paints.has(value)) paints.set(value, resolvePaint(value, env.tokens));
|
|
42
|
+
return paints.get(value);
|
|
43
|
+
};
|
|
44
|
+
const finish = (p, fill, stroke) => {
|
|
45
|
+
const fillStyle = paint(p.fill);
|
|
46
|
+
if (fillStyle) {
|
|
47
|
+
ctx.fillStyle = fillStyle;
|
|
48
|
+
fill();
|
|
49
|
+
}
|
|
50
|
+
const strokeStyle = paint(p.stroke);
|
|
51
|
+
if (strokeStyle) {
|
|
52
|
+
ctx.strokeStyle = strokeStyle;
|
|
53
|
+
ctx.lineWidth = p.strokeWidth ?? 1;
|
|
54
|
+
stroke();
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
const pathOf = (p) => {
|
|
58
|
+
let path = parsedPaths.get(p);
|
|
59
|
+
if (!path) {
|
|
60
|
+
path = createPath(p.d);
|
|
61
|
+
parsedPaths.set(p, path);
|
|
62
|
+
}
|
|
63
|
+
return path;
|
|
64
|
+
};
|
|
65
|
+
/** Each run as a bar where its letters would be. */
|
|
66
|
+
const sketchText = (p) => {
|
|
67
|
+
const { globalAlpha } = ctx;
|
|
68
|
+
ctx.globalAlpha = globalAlpha * SKETCH_ALPHA;
|
|
69
|
+
for (const run of p.runs) {
|
|
70
|
+
const { size } = run.font;
|
|
71
|
+
ctx.fillStyle = paint(run.fill) ?? DEFAULT_TEXT_FILL;
|
|
72
|
+
ctx.fillRect(p.x + run.x, p.y + run.y - size * .7, runWidth(run), size * .8);
|
|
73
|
+
}
|
|
74
|
+
ctx.globalAlpha = globalAlpha;
|
|
75
|
+
};
|
|
76
|
+
const sketchImage = (p) => {
|
|
77
|
+
const { globalAlpha } = ctx;
|
|
78
|
+
ctx.globalAlpha = globalAlpha * SKETCH_ALPHA;
|
|
79
|
+
ctx.fillStyle = SKETCH_IMAGE_FILL;
|
|
80
|
+
ctx.fillRect(p.x, p.y, p.w, p.h);
|
|
81
|
+
ctx.globalAlpha = globalAlpha;
|
|
82
|
+
};
|
|
83
|
+
const drawText = (p) => {
|
|
84
|
+
if (scale && p.runs.every((run) => run.font.size * scale < MIN_TEXT_SIZE)) {
|
|
85
|
+
sketchText(p);
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
for (const run of p.runs) {
|
|
89
|
+
const css = fontToCss(run.font);
|
|
90
|
+
const font = env.fontFallback ? `${css}, ${env.fontFallback}` : css;
|
|
91
|
+
if (font !== currentFont) {
|
|
92
|
+
ctx.font = font;
|
|
93
|
+
currentFont = font;
|
|
94
|
+
}
|
|
95
|
+
ctx.fillStyle = paint(run.fill) ?? DEFAULT_TEXT_FILL;
|
|
96
|
+
ctx.fillText(run.text, p.x + run.x, p.y + run.y);
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
const draw = (list, top = false) => {
|
|
100
|
+
for (const p of list) {
|
|
101
|
+
if (p.backends && !p.backends.includes("canvas")) continue;
|
|
102
|
+
switch (p.kind) {
|
|
103
|
+
case "group": {
|
|
104
|
+
if (top && p.bounds && view && !overlaps(p.bounds, view.rect)) break;
|
|
105
|
+
const isolated = p.transform || p.opacity != null || p.clip;
|
|
106
|
+
const outerFont = currentFont;
|
|
107
|
+
if (isolated) ctx.save();
|
|
108
|
+
if (p.transform) ctx.transform(...p.transform);
|
|
109
|
+
if (p.opacity != null) ctx.globalAlpha *= p.opacity;
|
|
110
|
+
if (p.clip) {
|
|
111
|
+
ctx.beginPath();
|
|
112
|
+
ctx.rect(p.clip.x, p.clip.y, p.clip.width, p.clip.height);
|
|
113
|
+
ctx.clip();
|
|
114
|
+
}
|
|
115
|
+
draw(p.children);
|
|
116
|
+
if (isolated) {
|
|
117
|
+
ctx.restore();
|
|
118
|
+
currentFont = outerFont;
|
|
119
|
+
}
|
|
120
|
+
break;
|
|
121
|
+
}
|
|
122
|
+
case "rect":
|
|
123
|
+
ctx.beginPath();
|
|
124
|
+
if (p.r) ctx.roundRect(p.x, p.y, p.w, p.h, p.r);
|
|
125
|
+
else ctx.rect(p.x, p.y, p.w, p.h);
|
|
126
|
+
finish(p, () => ctx.fill(), () => ctx.stroke());
|
|
127
|
+
break;
|
|
128
|
+
case "path": {
|
|
129
|
+
const path = pathOf(p);
|
|
130
|
+
finish(p, () => ctx.fill(path), () => ctx.stroke(path));
|
|
131
|
+
break;
|
|
132
|
+
}
|
|
133
|
+
case "text":
|
|
134
|
+
drawText(p);
|
|
135
|
+
break;
|
|
136
|
+
case "image": {
|
|
137
|
+
if (scale && Math.max(p.w, p.h) * scale < MIN_IMAGE_SIZE) {
|
|
138
|
+
sketchImage(p);
|
|
139
|
+
break;
|
|
140
|
+
}
|
|
141
|
+
const image = env.image?.(p.src);
|
|
142
|
+
if (image) {
|
|
143
|
+
const { x, y, w, h } = fitImage(p, image);
|
|
144
|
+
ctx.drawImage(image.source, x, y, w, h);
|
|
145
|
+
} else if (image === null && p.fallback) draw(p.fallback);
|
|
146
|
+
break;
|
|
147
|
+
}
|
|
148
|
+
case "custom":
|
|
149
|
+
if (p.canvas) {
|
|
150
|
+
ctx.save();
|
|
151
|
+
p.canvas(ctx);
|
|
152
|
+
ctx.restore();
|
|
153
|
+
} else warn("Skipped a custom primitive with no canvas drawing");
|
|
154
|
+
break;
|
|
155
|
+
case "overlay": warn("Skipped an overlay primitive: canvas does not draw them yet");
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
ctx.textAlign = "start";
|
|
160
|
+
ctx.textBaseline = "alphabetic";
|
|
161
|
+
draw(primitives, true);
|
|
162
|
+
}
|
|
163
|
+
/** The largest centered box with the image's aspect ratio, as SVG's `<image>` draws it. */
|
|
164
|
+
function fitImage(box, { width, height }) {
|
|
165
|
+
if (!width || !height) return box;
|
|
166
|
+
const k = Math.min(box.w / width, box.h / height);
|
|
167
|
+
const w = width * k;
|
|
168
|
+
const h = height * k;
|
|
169
|
+
return {
|
|
170
|
+
x: box.x + (box.w - w) / 2,
|
|
171
|
+
y: box.y + (box.h - h) / 2,
|
|
172
|
+
w,
|
|
173
|
+
h
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
// src/images.ts
|
|
177
|
+
function browserLoad(src, crossOrigin) {
|
|
178
|
+
return new Promise((resolve, reject) => {
|
|
179
|
+
const image = new Image();
|
|
180
|
+
if (crossOrigin) image.crossOrigin = crossOrigin;
|
|
181
|
+
image.addEventListener("load", () => resolve({
|
|
182
|
+
source: image,
|
|
183
|
+
width: image.naturalWidth,
|
|
184
|
+
height: image.naturalHeight
|
|
185
|
+
}));
|
|
186
|
+
image.addEventListener("error", () => reject(/* @__PURE__ */ new Error(`Could not load ${src}`)));
|
|
187
|
+
image.src = src;
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Images for {@link drawPrimitives}, which draws synchronously. Asking for an image
|
|
192
|
+
* starts loading it, and gives `undefined` until it is ready. Failures are remembered as
|
|
193
|
+
* `null`, so a broken image is not requested on every frame.
|
|
194
|
+
*/
|
|
195
|
+
var ImageCache = class {
|
|
196
|
+
images = /* @__PURE__ */ new Map();
|
|
197
|
+
loading = /* @__PURE__ */ new Map();
|
|
198
|
+
load;
|
|
199
|
+
onLoad;
|
|
200
|
+
constructor(options = {}) {
|
|
201
|
+
this.load = options.load ?? ((src) => browserLoad(src, options.crossOrigin));
|
|
202
|
+
this.onLoad = options.onLoad;
|
|
203
|
+
}
|
|
204
|
+
get(src) {
|
|
205
|
+
const image = this.images.get(src);
|
|
206
|
+
if (image === void 0) this.start(src);
|
|
207
|
+
return image;
|
|
208
|
+
}
|
|
209
|
+
/** Settles once all these images are loaded or have failed. */
|
|
210
|
+
async preload(sources) {
|
|
211
|
+
await Promise.all(Array.from(sources, (src) => this.start(src)));
|
|
212
|
+
}
|
|
213
|
+
start(src) {
|
|
214
|
+
let task = this.loading.get(src);
|
|
215
|
+
if (!task) {
|
|
216
|
+
task = this.load(src).then((loaded) => {
|
|
217
|
+
this.images.set(src, loaded);
|
|
218
|
+
this.onLoad?.();
|
|
219
|
+
}, () => {
|
|
220
|
+
this.images.set(src, null);
|
|
221
|
+
this.onLoad?.();
|
|
222
|
+
});
|
|
223
|
+
this.loading.set(src, task);
|
|
224
|
+
}
|
|
225
|
+
return task;
|
|
226
|
+
}
|
|
227
|
+
};
|
|
228
|
+
// src/export.ts
|
|
229
|
+
var DEFAULT_MAX_SIZE = 16384;
|
|
230
|
+
/** Pixel size of an export and how to place the drawing in it. */
|
|
231
|
+
function exportLayout(result, options = {}) {
|
|
232
|
+
const { padding = 20, scale = 2, maxSize = DEFAULT_MAX_SIZE } = options;
|
|
233
|
+
const box = expandRect(result.bounds, padding);
|
|
234
|
+
const fitted = Math.min(scale, maxSize / Math.max(box.width, box.height));
|
|
235
|
+
return {
|
|
236
|
+
box,
|
|
237
|
+
scale: fitted,
|
|
238
|
+
width: Math.min(maxSize, Math.ceil(box.width * fitted)),
|
|
239
|
+
height: Math.min(maxSize, Math.ceil(box.height * fitted))
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
/** Sources of all images in `primitives`. */
|
|
243
|
+
function imageSources(primitives) {
|
|
244
|
+
const sources = /* @__PURE__ */ new Set();
|
|
245
|
+
const visit = (list) => {
|
|
246
|
+
for (const p of list) {
|
|
247
|
+
if (p.backends && !p.backends.includes("canvas")) continue;
|
|
248
|
+
if (p.kind === "group") visit(p.children);
|
|
249
|
+
else if (p.kind === "image") sources.add(p.src);
|
|
250
|
+
}
|
|
251
|
+
};
|
|
252
|
+
visit(primitives);
|
|
253
|
+
return sources;
|
|
254
|
+
}
|
|
255
|
+
/** Draw a whole map in full on a context of the size from {@link exportLayout}. */
|
|
256
|
+
function drawExport(ctx, result, options, env) {
|
|
257
|
+
const { box, scale, width, height } = exportLayout(result, options);
|
|
258
|
+
const tokens = themeTokens(result.theme, options.colorScheme);
|
|
259
|
+
const background = resolvePaint("var(--mm-bg)", tokens);
|
|
260
|
+
if (options.background !== false && background) {
|
|
261
|
+
ctx.fillStyle = background;
|
|
262
|
+
ctx.fillRect(0, 0, width, height);
|
|
263
|
+
}
|
|
264
|
+
ctx.setTransform(scale, 0, 0, scale, -box.x * scale, -box.y * scale);
|
|
265
|
+
drawPrimitives(ctx, result.primitives, {
|
|
266
|
+
...env,
|
|
267
|
+
tokens
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Export a map as a PNG, in the browser. Pass a finished result, such as `mm.result` or
|
|
272
|
+
* what `renderTreeAsync` gives. Images load first; ones whose server does not allow
|
|
273
|
+
* cross-origin use show their alt text, because they would make the canvas unreadable.
|
|
274
|
+
*/
|
|
275
|
+
async function toPNG(result, options = {}) {
|
|
276
|
+
const { width, height } = exportLayout(result, options);
|
|
277
|
+
const canvas = document.createElement("canvas");
|
|
278
|
+
canvas.width = width;
|
|
279
|
+
canvas.height = height;
|
|
280
|
+
const ctx = canvas.getContext("2d");
|
|
281
|
+
if (!ctx) throw new Error("Canvas 2D is not available");
|
|
282
|
+
const images = new ImageCache({ crossOrigin: "anonymous" });
|
|
283
|
+
await Promise.all([images.preload(imageSources(result.primitives)), document.fonts?.ready]);
|
|
284
|
+
drawExport(ctx, result, options, { image: (src) => images.get(src) });
|
|
285
|
+
return new Promise((resolve, reject) => {
|
|
286
|
+
canvas.toBlob((blob) => {
|
|
287
|
+
if (blob) resolve(blob);
|
|
288
|
+
else reject(/* @__PURE__ */ new Error(`Could not export a ${width}×${height} image`));
|
|
289
|
+
}, "image/png");
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
export { ImageCache as a, toPNG as i, exportLayout as n, drawPrimitives as o, imageSources as r, drawExport as t };
|
package/dist/export.d.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { type Primitive } from '@markmap/core';
|
|
2
|
+
import { type RenderResult } from '@markmap/render';
|
|
3
|
+
import { type DrawEnv } from './draw';
|
|
4
|
+
export interface ImageExportOptions {
|
|
5
|
+
/** Pixels per layout px. Defaults to 2, which stays sharp on high-density screens. */
|
|
6
|
+
scale?: number;
|
|
7
|
+
/** Space around the drawing, in px. */
|
|
8
|
+
padding?: number;
|
|
9
|
+
/** Fill the background with `--mm-bg`. Otherwise it is transparent. */
|
|
10
|
+
background?: boolean;
|
|
11
|
+
/** Which tokens to use. Defaults to `light`. */
|
|
12
|
+
colorScheme?: 'light' | 'dark';
|
|
13
|
+
/**
|
|
14
|
+
* Largest width or height of the image, in pixels; `scale` is lowered to fit. Browsers
|
|
15
|
+
* refuse canvases past a limit, and the default is the largest most of them allow.
|
|
16
|
+
*/
|
|
17
|
+
maxSize?: number;
|
|
18
|
+
}
|
|
19
|
+
/** Pixel size of an export and how to place the drawing in it. */
|
|
20
|
+
export declare function exportLayout(result: RenderResult, options?: ImageExportOptions): {
|
|
21
|
+
box: import("@markmap/core").Rect;
|
|
22
|
+
scale: number;
|
|
23
|
+
width: number;
|
|
24
|
+
height: number;
|
|
25
|
+
};
|
|
26
|
+
/** Sources of all images in `primitives`. */
|
|
27
|
+
export declare function imageSources(primitives: readonly Primitive[]): Set<string>;
|
|
28
|
+
/** Draw a whole map in full on a context of the size from {@link exportLayout}. */
|
|
29
|
+
export declare function drawExport(ctx: CanvasRenderingContext2D, result: RenderResult, options: ImageExportOptions, env: Omit<DrawEnv, 'tokens' | 'view'>): void;
|
|
30
|
+
/**
|
|
31
|
+
* Export a map as a PNG, in the browser. Pass a finished result, such as `mm.result` or
|
|
32
|
+
* what `renderTreeAsync` gives. Images load first; ones whose server does not allow
|
|
33
|
+
* cross-origin use show their alt text, because they would make the canvas unreadable.
|
|
34
|
+
*/
|
|
35
|
+
export declare function toPNG(result: RenderResult, options?: ImageExportOptions): Promise<Blob>;
|
package/dist/images.d.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/** An image ready to draw, with its natural size. */
|
|
2
|
+
export interface LoadedImage {
|
|
3
|
+
source: CanvasImageSource;
|
|
4
|
+
width: number;
|
|
5
|
+
height: number;
|
|
6
|
+
}
|
|
7
|
+
export interface ImageCacheOptions {
|
|
8
|
+
/** Load an image. Defaults to `Image`, which needs a browser. */
|
|
9
|
+
load?: (src: string) => Promise<LoadedImage>;
|
|
10
|
+
/**
|
|
11
|
+
* CORS mode of the default loader. `anonymous` keeps the canvas readable, so it can
|
|
12
|
+
* be exported, and images from servers that do not allow it fail to load.
|
|
13
|
+
*/
|
|
14
|
+
crossOrigin?: 'anonymous' | 'use-credentials';
|
|
15
|
+
/** Called when an image is ready, so the caller can draw again. */
|
|
16
|
+
onLoad?: () => void;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Images for {@link drawPrimitives}, which draws synchronously. Asking for an image
|
|
20
|
+
* starts loading it, and gives `undefined` until it is ready. Failures are remembered as
|
|
21
|
+
* `null`, so a broken image is not requested on every frame.
|
|
22
|
+
*/
|
|
23
|
+
export declare class ImageCache {
|
|
24
|
+
private images;
|
|
25
|
+
private loading;
|
|
26
|
+
private load;
|
|
27
|
+
private onLoad?;
|
|
28
|
+
constructor(options?: ImageCacheOptions);
|
|
29
|
+
get(src: string): LoadedImage | null | undefined;
|
|
30
|
+
/** Settles once all these images are loaded or have failed. */
|
|
31
|
+
preload(sources: Iterable<string>): Promise<void>;
|
|
32
|
+
private start;
|
|
33
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { a as ImageCache, i as toPNG, n as exportLayout, o as drawPrimitives, r as imageSources, t as drawExport } from "./export-D0aKjidm.js";
|
|
2
|
+
// src/backend.ts
|
|
3
|
+
/**
|
|
4
|
+
* Shows a map in a `<canvas>`, for much larger maps than SVG can animate smoothly. Pass
|
|
5
|
+
* it to `Markmap`: `new Markmap(new CanvasBackend(canvas))`. Size the canvas with CSS.
|
|
6
|
+
*
|
|
7
|
+
* Unlike SVG, a canvas has no accessibility tree and its text cannot be selected.
|
|
8
|
+
*/
|
|
9
|
+
var CanvasBackend = class {
|
|
10
|
+
element;
|
|
11
|
+
themeSelector;
|
|
12
|
+
ctx;
|
|
13
|
+
style;
|
|
14
|
+
primitives = [];
|
|
15
|
+
transform = {
|
|
16
|
+
k: 1,
|
|
17
|
+
x: 0,
|
|
18
|
+
y: 0
|
|
19
|
+
};
|
|
20
|
+
images = new ImageCache({ onLoad: () => this.schedule() });
|
|
21
|
+
env;
|
|
22
|
+
frame;
|
|
23
|
+
resizeObserver;
|
|
24
|
+
darkQuery;
|
|
25
|
+
constructor(canvas, { themeSelector } = {}) {
|
|
26
|
+
const ctx = canvas.getContext("2d");
|
|
27
|
+
if (!ctx) throw new Error("Canvas 2D is not available");
|
|
28
|
+
this.element = canvas;
|
|
29
|
+
this.themeSelector = themeSelector;
|
|
30
|
+
this.ctx = ctx;
|
|
31
|
+
const doc = canvas.ownerDocument;
|
|
32
|
+
this.style = doc.createElement("style");
|
|
33
|
+
const root = canvas.getRootNode();
|
|
34
|
+
(root.host ? root : doc.head).append(this.style);
|
|
35
|
+
const computed = doc.defaultView?.getComputedStyle(canvas);
|
|
36
|
+
this.env = {
|
|
37
|
+
tokens: (name) => computed?.getPropertyValue(`--${name}`),
|
|
38
|
+
image: (src) => this.images.get(src)
|
|
39
|
+
};
|
|
40
|
+
if (typeof ResizeObserver !== "undefined") {
|
|
41
|
+
this.resizeObserver = new ResizeObserver(() => this.paint());
|
|
42
|
+
this.resizeObserver.observe(canvas);
|
|
43
|
+
}
|
|
44
|
+
this.darkQuery = doc.defaultView?.matchMedia?.("(prefers-color-scheme: dark)");
|
|
45
|
+
this.darkQuery?.addEventListener("change", this.schedule);
|
|
46
|
+
}
|
|
47
|
+
draw(primitives) {
|
|
48
|
+
this.primitives = primitives;
|
|
49
|
+
this.schedule();
|
|
50
|
+
}
|
|
51
|
+
setTransform(transform) {
|
|
52
|
+
this.transform = transform;
|
|
53
|
+
this.schedule();
|
|
54
|
+
}
|
|
55
|
+
setCSS(css) {
|
|
56
|
+
this.style.textContent = css;
|
|
57
|
+
this.schedule();
|
|
58
|
+
}
|
|
59
|
+
schedule = () => {
|
|
60
|
+
if (this.frame != null) return;
|
|
61
|
+
const view = this.element.ownerDocument.defaultView;
|
|
62
|
+
this.frame = view?.requestAnimationFrame(() => {
|
|
63
|
+
this.frame = void 0;
|
|
64
|
+
this.paint();
|
|
65
|
+
});
|
|
66
|
+
};
|
|
67
|
+
/** Draw now. Changes are otherwise drawn on the next animation frame, at most once. */
|
|
68
|
+
paint() {
|
|
69
|
+
const { ctx, element } = this;
|
|
70
|
+
const ratio = this.fitToElement();
|
|
71
|
+
if (!ratio) return;
|
|
72
|
+
ctx.setTransform(1, 0, 0, 1, 0, 0);
|
|
73
|
+
ctx.clearRect(0, 0, element.width, element.height);
|
|
74
|
+
const { k, x, y } = this.transform;
|
|
75
|
+
ctx.setTransform(k * ratio, 0, 0, k * ratio, x * ratio, y * ratio);
|
|
76
|
+
drawPrimitives(ctx, this.primitives, {
|
|
77
|
+
...this.env,
|
|
78
|
+
view: {
|
|
79
|
+
rect: {
|
|
80
|
+
x: -x / k,
|
|
81
|
+
y: -y / k,
|
|
82
|
+
width: element.width / ratio / k,
|
|
83
|
+
height: element.height / ratio / k
|
|
84
|
+
},
|
|
85
|
+
scale: k
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Match the bitmap to the element's size and the screen's pixel density. Returns the
|
|
91
|
+
* device pixel ratio, or 0 when the element has no size.
|
|
92
|
+
*/
|
|
93
|
+
fitToElement() {
|
|
94
|
+
const { element } = this;
|
|
95
|
+
const { width, height } = element.getBoundingClientRect();
|
|
96
|
+
if (!width || !height) return 0;
|
|
97
|
+
const ratio = element.ownerDocument.defaultView?.devicePixelRatio || 1;
|
|
98
|
+
const bitmapWidth = Math.round(width * ratio);
|
|
99
|
+
const bitmapHeight = Math.round(height * ratio);
|
|
100
|
+
if (element.width !== bitmapWidth || element.height !== bitmapHeight) {
|
|
101
|
+
element.width = bitmapWidth;
|
|
102
|
+
element.height = bitmapHeight;
|
|
103
|
+
const after = element.getBoundingClientRect();
|
|
104
|
+
if (after.width !== width || after.height !== height) {
|
|
105
|
+
element.style.width = `${width}px`;
|
|
106
|
+
element.style.height = `${height}px`;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return ratio;
|
|
110
|
+
}
|
|
111
|
+
destroy() {
|
|
112
|
+
const view = this.element.ownerDocument.defaultView;
|
|
113
|
+
if (this.frame != null) view?.cancelAnimationFrame(this.frame);
|
|
114
|
+
this.frame = void 0;
|
|
115
|
+
this.resizeObserver?.disconnect();
|
|
116
|
+
this.darkQuery?.removeEventListener("change", this.schedule);
|
|
117
|
+
this.style.remove();
|
|
118
|
+
this.ctx.setTransform(1, 0, 0, 1, 0, 0);
|
|
119
|
+
this.ctx.clearRect(0, 0, this.element.width, this.element.height);
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
export { CanvasBackend, ImageCache, drawExport, drawPrimitives, exportLayout, imageSources, toPNG };
|
package/dist/node.d.ts
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { MarkmapNode, Measurer } from '@markmap/core';
|
|
2
|
+
import { type RenderOptions, type RenderResult } from '@markmap/render';
|
|
3
|
+
import { type ImageExportOptions } from './export';
|
|
4
|
+
import { type LoadedImage } from './images';
|
|
5
|
+
export interface NodeImageOptions {
|
|
6
|
+
/**
|
|
7
|
+
* Folder that images with relative paths are read from, usually the Markdown file's.
|
|
8
|
+
* Nothing outside it can be read. Without it, no local files are read: Markdown from
|
|
9
|
+
* someone else must not be able to pull files off this machine into an image.
|
|
10
|
+
*/
|
|
11
|
+
baseDir?: string;
|
|
12
|
+
/** Return `true` to fetch a remote image. Without it, none are: they show their alt text. */
|
|
13
|
+
allow?: (url: string) => boolean;
|
|
14
|
+
/** Longest wait for a remote image, in ms. */
|
|
15
|
+
timeout?: number;
|
|
16
|
+
}
|
|
17
|
+
export interface NodeFont {
|
|
18
|
+
/** Path to a font file. */
|
|
19
|
+
path: string;
|
|
20
|
+
/** Family name to use it under. Defaults to the name in the file. */
|
|
21
|
+
family?: string;
|
|
22
|
+
}
|
|
23
|
+
export interface RenderPNGOptions extends Omit<RenderOptions, 'measurer' | 'folded'>, ImageExportOptions, NodeImageOptions {
|
|
24
|
+
/** Fonts to load before measuring. System fonts are available without this. */
|
|
25
|
+
fonts?: readonly NodeFont[];
|
|
26
|
+
/** Ids of folded nodes. */
|
|
27
|
+
folded?: ReadonlySet<string>;
|
|
28
|
+
}
|
|
29
|
+
/** The families in `wanted` that are installed, as a CSS list. */
|
|
30
|
+
export declare function fontFallback(available?: Iterable<string>, wanted?: readonly string[]): string;
|
|
31
|
+
/** Load an image the way {@link NodeImageOptions} permits. Rejects if it may not. */
|
|
32
|
+
export declare function loadNodeImage(src: string, options?: NodeImageOptions): Promise<LoadedImage>;
|
|
33
|
+
/**
|
|
34
|
+
* Export a finished result as a PNG, in Node. Measure with {@link createNodeMeasurer} so
|
|
35
|
+
* the text fits what was laid out. Most callers want {@link renderPNG} instead.
|
|
36
|
+
*/
|
|
37
|
+
export declare function toPNG(result: RenderResult, options?: ImageExportOptions & NodeImageOptions): Promise<Buffer>;
|
|
38
|
+
/** Measures with the same text engine and fonts that draw the PNG. */
|
|
39
|
+
export declare function createNodeMeasurer(): Measurer;
|
|
40
|
+
/**
|
|
41
|
+
* Measure, lay out and export a tree as a PNG. Images load through the same rules for
|
|
42
|
+
* sizing and drawing, and each loads once. `plugins` works as for rendering; its image
|
|
43
|
+
* plugin, if any, is replaced by one that can load images here.
|
|
44
|
+
*/
|
|
45
|
+
export declare function renderPNG(root: MarkmapNode, options?: RenderPNGOptions): Promise<Buffer>;
|
package/dist/node.js
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { a as ImageCache, n as exportLayout, r as imageSources, t as drawExport } from "./export-D0aKjidm.js";
|
|
2
|
+
import { createCanvasMeasurer, createImageContent, defaultContentPlugins, renderTreeAsync } from "@markmap/render";
|
|
3
|
+
import { readFile, realpath } from "node:fs/promises";
|
|
4
|
+
import { isAbsolute, relative, resolve, sep } from "node:path";
|
|
5
|
+
import { GlobalFonts, Path2D, createCanvas, loadImage } from "@napi-rs/canvas";
|
|
6
|
+
// src/node.ts
|
|
7
|
+
/**
|
|
8
|
+
* Families with wide coverage on common systems, most useful first. Node's canvas does
|
|
9
|
+
* not fall back to system fonts by itself, so characters these do not reach need `fonts`.
|
|
10
|
+
*/
|
|
11
|
+
var FALLBACK_FAMILIES = [
|
|
12
|
+
"PingFang SC",
|
|
13
|
+
"Hiragino Sans GB",
|
|
14
|
+
"Heiti SC",
|
|
15
|
+
"Hiragino Sans",
|
|
16
|
+
"Apple SD Gothic Neo",
|
|
17
|
+
"Apple Color Emoji",
|
|
18
|
+
"Microsoft YaHei",
|
|
19
|
+
"Yu Gothic",
|
|
20
|
+
"Malgun Gothic",
|
|
21
|
+
"Segoe UI Emoji",
|
|
22
|
+
"Noto Sans CJK SC",
|
|
23
|
+
"Noto Sans CJK JP",
|
|
24
|
+
"Noto Sans CJK KR",
|
|
25
|
+
"Source Han Sans SC",
|
|
26
|
+
"WenQuanYi Micro Hei",
|
|
27
|
+
"Noto Color Emoji"
|
|
28
|
+
];
|
|
29
|
+
/** The families in `wanted` that are installed, as a CSS list. */
|
|
30
|
+
function fontFallback(available = GlobalFonts.families.map(({ family }) => family), wanted = FALLBACK_FAMILIES) {
|
|
31
|
+
const installed = new Set(available);
|
|
32
|
+
return wanted.filter((family) => installed.has(family)).map((family) => `"${family}"`).join(", ");
|
|
33
|
+
}
|
|
34
|
+
var DATA_URL = /^data:[^,]*;base64,/i;
|
|
35
|
+
var REMOTE_URL = /^https?:/i;
|
|
36
|
+
async function readLocal(src, baseDir) {
|
|
37
|
+
if (baseDir == null) throw new Error(`Not reading ${src}: no baseDir`);
|
|
38
|
+
const base = await realpath(baseDir);
|
|
39
|
+
const file = await realpath(resolve(base, decodeURI(src)));
|
|
40
|
+
const path = relative(base, file);
|
|
41
|
+
if (path === ".." || path.startsWith(`..${sep}`) || isAbsolute(path)) throw new Error(`Not reading ${src}: outside baseDir`);
|
|
42
|
+
return readFile(file);
|
|
43
|
+
}
|
|
44
|
+
async function fetchRemote(url, options) {
|
|
45
|
+
if (!options.allow?.(url)) throw new Error(`Not fetching ${url}`);
|
|
46
|
+
const response = await fetch(url, {
|
|
47
|
+
redirect: "error",
|
|
48
|
+
signal: AbortSignal.timeout(options.timeout ?? 1e4)
|
|
49
|
+
});
|
|
50
|
+
if (!response.ok) throw new Error(`Could not fetch ${url}: ${response.status}`);
|
|
51
|
+
return Buffer.from(await response.arrayBuffer());
|
|
52
|
+
}
|
|
53
|
+
/** Load an image the way {@link NodeImageOptions} permits. Rejects if it may not. */
|
|
54
|
+
async function loadNodeImage(src, options = {}) {
|
|
55
|
+
let data;
|
|
56
|
+
if (DATA_URL.test(src)) data = Buffer.from(src.slice(src.indexOf(",") + 1), "base64");
|
|
57
|
+
else if (REMOTE_URL.test(src)) data = await fetchRemote(src, options);
|
|
58
|
+
else data = await readLocal(src, options.baseDir);
|
|
59
|
+
const image = await loadImage(data);
|
|
60
|
+
return {
|
|
61
|
+
source: image,
|
|
62
|
+
width: image.naturalWidth,
|
|
63
|
+
height: image.naturalHeight
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
/** Draw a finished result on a Node canvas. */
|
|
67
|
+
async function encode(result, options, images) {
|
|
68
|
+
const { width, height } = exportLayout(result, options);
|
|
69
|
+
const canvas = createCanvas(width, height);
|
|
70
|
+
await images.preload(imageSources(result.primitives));
|
|
71
|
+
drawExport(canvas.getContext("2d"), result, options, {
|
|
72
|
+
image: (src) => images.get(src),
|
|
73
|
+
createPath: (d) => new Path2D(d),
|
|
74
|
+
fontFallback: fontFallback()
|
|
75
|
+
});
|
|
76
|
+
return canvas.encode("png");
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Export a finished result as a PNG, in Node. Measure with {@link createNodeMeasurer} so
|
|
80
|
+
* the text fits what was laid out. Most callers want {@link renderPNG} instead.
|
|
81
|
+
*/
|
|
82
|
+
function toPNG(result, options = {}) {
|
|
83
|
+
return encode(result, options, new ImageCache({ load: (src) => loadNodeImage(src, options) }));
|
|
84
|
+
}
|
|
85
|
+
/** Measures with the same text engine and fonts that draw the PNG. */
|
|
86
|
+
function createNodeMeasurer() {
|
|
87
|
+
const measurer = createCanvasMeasurer(createCanvas(1, 1).getContext("2d"));
|
|
88
|
+
const fallback = fontFallback();
|
|
89
|
+
return { measure(text, font) {
|
|
90
|
+
const family = fallback ? `${font.family}, ${fallback}` : font.family;
|
|
91
|
+
return measurer.measure(text, {
|
|
92
|
+
...font,
|
|
93
|
+
family
|
|
94
|
+
});
|
|
95
|
+
} };
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Measure, lay out and export a tree as a PNG. Images load through the same rules for
|
|
99
|
+
* sizing and drawing, and each loads once. `plugins` works as for rendering; its image
|
|
100
|
+
* plugin, if any, is replaced by one that can load images here.
|
|
101
|
+
*/
|
|
102
|
+
async function renderPNG(root, options = {}) {
|
|
103
|
+
for (const { path, family } of options.fonts ?? []) if (!GlobalFonts.registerFromPath(path, family)) throw new Error(`Could not load font ${path}`);
|
|
104
|
+
const images = new ImageCache({ load: (src) => loadNodeImage(src, options) });
|
|
105
|
+
const nodeImages = createImageContent({ async loadSize(src) {
|
|
106
|
+
await images.preload([src]);
|
|
107
|
+
const image = images.get(src);
|
|
108
|
+
if (!image) throw new Error(`Could not load ${src}`);
|
|
109
|
+
return image;
|
|
110
|
+
} });
|
|
111
|
+
const plugins = (options.plugins ?? defaultContentPlugins).map((plugin) => plugin.name === nodeImages.name ? nodeImages : plugin);
|
|
112
|
+
return encode(await renderTreeAsync(root, {
|
|
113
|
+
...options,
|
|
114
|
+
plugins,
|
|
115
|
+
measurer: createNodeMeasurer(),
|
|
116
|
+
folded: options.folded ?? /* @__PURE__ */ new Set()
|
|
117
|
+
}), options, images);
|
|
118
|
+
}
|
|
119
|
+
export { createNodeMeasurer, fontFallback, loadNodeImage, renderPNG, toPNG };
|
package/package.json
CHANGED
|
@@ -1,6 +1,58 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@markmap/canvas",
|
|
3
|
-
"version": "0.0.0
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "1.0.0-next.0",
|
|
4
|
+
"description": "Canvas backend for Markmap",
|
|
5
|
+
"homepage": "https://github.com/markmap/markmap#readme",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/markmap/markmap.git",
|
|
10
|
+
"directory": "packages/canvas"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"dist"
|
|
14
|
+
],
|
|
15
|
+
"type": "module",
|
|
16
|
+
"sideEffects": false,
|
|
17
|
+
"exports": {
|
|
18
|
+
".": {
|
|
19
|
+
"types": "./dist/index.d.ts",
|
|
20
|
+
"import": "./dist/index.js"
|
|
21
|
+
},
|
|
22
|
+
"./node": {
|
|
23
|
+
"types": "./dist/node.d.ts",
|
|
24
|
+
"import": "./dist/node.js"
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
"publishConfig": {
|
|
28
|
+
"access": "public"
|
|
29
|
+
},
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"@markmap/core": "1.0.0-next.0",
|
|
32
|
+
"@markmap/render": "1.0.0-next.0"
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"@markmap/parse": "1.0.0-next.0",
|
|
36
|
+
"@markmap/plugin-math": "1.0.0-next.0",
|
|
37
|
+
"@markmap/view": "1.0.0-next.0",
|
|
38
|
+
"@napi-rs/canvas": "^1.1.0",
|
|
39
|
+
"happy-dom": "^21.0.4"
|
|
40
|
+
},
|
|
41
|
+
"peerDependencies": {
|
|
42
|
+
"@markmap/view": "^1.0.0-next.0",
|
|
43
|
+
"@napi-rs/canvas": "^1.1.0"
|
|
44
|
+
},
|
|
45
|
+
"peerDependenciesMeta": {
|
|
46
|
+
"@markmap/view": {
|
|
47
|
+
"optional": true
|
|
48
|
+
},
|
|
49
|
+
"@napi-rs/canvas": {
|
|
50
|
+
"optional": true
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
"scripts": {
|
|
54
|
+
"clean": "rm -rf dist tsconfig.tsbuildinfo",
|
|
55
|
+
"build:types": "tsc -b",
|
|
56
|
+
"build:js": "vite build"
|
|
57
|
+
}
|
|
6
58
|
}
|