@antv/infographic 0.2.4 → 0.2.6
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/README.md +2 -2
- package/README.zh-CN.md +2 -2
- package/dist/infographic.min.js +88 -78
- package/dist/infographic.min.js.map +1 -1
- package/esm/exporter/index.d.ts +1 -1
- package/esm/exporter/index.js +1 -1
- package/esm/exporter/svg.js +223 -2
- package/esm/exporter/types.d.ts +5 -0
- package/esm/index.d.ts +4 -2
- package/esm/index.js +3 -2
- package/esm/renderer/fonts/loader.js +63 -8
- package/esm/renderer/stylize/gradient.js +1 -1
- package/esm/resource/index.d.ts +1 -1
- package/esm/resource/index.js +1 -1
- package/esm/resource/load-tracker.d.ts +6 -0
- package/esm/resource/load-tracker.js +36 -0
- package/esm/resource/loader.d.ts +1 -0
- package/esm/resource/loader.js +27 -14
- package/esm/runtime/Infographic.js +13 -0
- package/esm/utils/is-browser.d.ts +1 -0
- package/esm/utils/is-browser.js +68 -0
- package/esm/utils/measure-text.d.ts +1 -0
- package/esm/utils/measure-text.js +9 -7
- package/lib/exporter/index.d.ts +1 -1
- package/lib/exporter/index.js +2 -1
- package/lib/exporter/svg.js +223 -2
- package/lib/exporter/types.d.ts +5 -0
- package/lib/index.d.ts +4 -2
- package/lib/index.js +6 -3
- package/lib/renderer/fonts/loader.js +63 -8
- package/lib/renderer/stylize/gradient.js +1 -1
- package/lib/resource/index.d.ts +1 -1
- package/lib/resource/index.js +3 -1
- package/lib/resource/load-tracker.d.ts +6 -0
- package/lib/resource/load-tracker.js +42 -0
- package/lib/resource/loader.d.ts +1 -0
- package/lib/resource/loader.js +30 -14
- package/lib/runtime/Infographic.js +13 -0
- package/lib/utils/is-browser.d.ts +1 -0
- package/lib/utils/is-browser.js +71 -0
- package/lib/utils/measure-text.d.ts +1 -0
- package/lib/utils/measure-text.js +11 -7
- package/package.json +1 -1
- package/src/exporter/index.ts +1 -1
- package/src/exporter/svg.ts +254 -2
- package/src/exporter/types.ts +5 -0
- package/src/index.ts +8 -3
- package/src/renderer/fonts/loader.ts +82 -9
- package/src/renderer/stylize/gradient.ts +1 -1
- package/src/resource/index.ts +1 -1
- package/src/resource/load-tracker.ts +51 -0
- package/src/resource/loader.ts +27 -12
- package/src/runtime/Infographic.tsx +12 -0
- package/src/utils/is-browser.ts +79 -0
- package/src/utils/measure-text.ts +11 -7
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.isBrowser = isBrowser;
|
|
4
|
+
let _isBrowser;
|
|
5
|
+
function isBrowser() {
|
|
6
|
+
if (_isBrowser !== undefined) {
|
|
7
|
+
return _isBrowser;
|
|
8
|
+
}
|
|
9
|
+
if (typeof window === 'undefined' || typeof document === 'undefined') {
|
|
10
|
+
_isBrowser = false;
|
|
11
|
+
return false;
|
|
12
|
+
}
|
|
13
|
+
const body = document.body;
|
|
14
|
+
if (!body) {
|
|
15
|
+
_isBrowser = false;
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
let hasRealLayout = false;
|
|
19
|
+
try {
|
|
20
|
+
const el = document.createElement('div');
|
|
21
|
+
el.style.cssText = `
|
|
22
|
+
position: absolute;
|
|
23
|
+
left: 11px;
|
|
24
|
+
top: 13px;
|
|
25
|
+
width: 37px;
|
|
26
|
+
height: 19px;
|
|
27
|
+
padding: 0;
|
|
28
|
+
margin: 0;
|
|
29
|
+
border: 0;
|
|
30
|
+
visibility: hidden;
|
|
31
|
+
`;
|
|
32
|
+
body.appendChild(el);
|
|
33
|
+
void el.offsetHeight;
|
|
34
|
+
const rect = el.getBoundingClientRect();
|
|
35
|
+
body.removeChild(el);
|
|
36
|
+
hasRealLayout =
|
|
37
|
+
rect.width === 37 &&
|
|
38
|
+
rect.height === 19 &&
|
|
39
|
+
rect.left !== 0 &&
|
|
40
|
+
rect.top !== 0;
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
hasRealLayout = false;
|
|
44
|
+
}
|
|
45
|
+
if (!hasRealLayout) {
|
|
46
|
+
_isBrowser = false;
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
let hasRealCanvas = false;
|
|
50
|
+
try {
|
|
51
|
+
const canvas = document.createElement('canvas');
|
|
52
|
+
canvas.width = 100;
|
|
53
|
+
canvas.height = 50;
|
|
54
|
+
const ctx = canvas.getContext('2d');
|
|
55
|
+
if (!ctx) {
|
|
56
|
+
_isBrowser = false;
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
ctx.font = '20px sans-serif';
|
|
60
|
+
const metrics = ctx.measureText('Hello');
|
|
61
|
+
hasRealCanvas =
|
|
62
|
+
typeof metrics.width === 'number' &&
|
|
63
|
+
metrics.width > 0 &&
|
|
64
|
+
metrics.width < 1000;
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
hasRealCanvas = false;
|
|
68
|
+
}
|
|
69
|
+
_isBrowser = hasRealCanvas;
|
|
70
|
+
return hasRealCanvas;
|
|
71
|
+
}
|
|
@@ -3,21 +3,25 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.setFontExtendFactor = void 0;
|
|
6
7
|
exports.measureText = measureText;
|
|
7
8
|
const measury_1 = require("measury");
|
|
8
9
|
const AlibabaPuHuiTi_Regular_1 = __importDefault(require("measury/fonts/AlibabaPuHuiTi-Regular"));
|
|
9
10
|
const renderer_1 = require("../renderer");
|
|
10
11
|
const font_1 = require("./font");
|
|
12
|
+
const is_browser_1 = require("./is-browser");
|
|
11
13
|
const is_node_1 = require("./is-node");
|
|
14
|
+
let FONT_EXTEND_FACTOR = 1.01;
|
|
15
|
+
const setFontExtendFactor = (factor) => {
|
|
16
|
+
FONT_EXTEND_FACTOR = factor;
|
|
17
|
+
};
|
|
18
|
+
exports.setFontExtendFactor = setFontExtendFactor;
|
|
12
19
|
if (is_node_1.isNode) {
|
|
13
20
|
(0, measury_1.registerFont)(AlibabaPuHuiTi_Regular_1.default);
|
|
14
21
|
}
|
|
15
|
-
const canUseDOM = !is_node_1.isNode && typeof window !== 'undefined' && typeof document !== 'undefined';
|
|
16
22
|
let canvasContext = null;
|
|
17
23
|
let measureSpan = null;
|
|
18
24
|
function getCanvasContext() {
|
|
19
|
-
if (!canUseDOM)
|
|
20
|
-
return null;
|
|
21
25
|
if (canvasContext)
|
|
22
26
|
return canvasContext;
|
|
23
27
|
const canvas = document.createElement('canvas');
|
|
@@ -25,7 +29,7 @@ function getCanvasContext() {
|
|
|
25
29
|
return canvasContext;
|
|
26
30
|
}
|
|
27
31
|
function getMeasureSpan() {
|
|
28
|
-
if (!
|
|
32
|
+
if (!document.body)
|
|
29
33
|
return null;
|
|
30
34
|
if (measureSpan)
|
|
31
35
|
return measureSpan;
|
|
@@ -97,12 +101,12 @@ function measureText(text = '', attrs) {
|
|
|
97
101
|
lineHeight,
|
|
98
102
|
};
|
|
99
103
|
const fallback = () => (0, measury_1.measureText)(content, options);
|
|
100
|
-
const metrics =
|
|
104
|
+
const metrics = (0, is_browser_1.isBrowser)()
|
|
101
105
|
? (measureTextInBrowser(content, options) ?? fallback())
|
|
102
106
|
: fallback();
|
|
103
107
|
// 额外添加 1% 宽高
|
|
104
108
|
return {
|
|
105
|
-
width: Math.ceil(metrics.width *
|
|
106
|
-
height: Math.ceil(metrics.height *
|
|
109
|
+
width: Math.ceil(metrics.width * FONT_EXTEND_FACTOR),
|
|
110
|
+
height: Math.ceil(metrics.height * FONT_EXTEND_FACTOR),
|
|
107
111
|
};
|
|
108
112
|
}
|
package/package.json
CHANGED
package/src/exporter/index.ts
CHANGED
package/src/exporter/svg.ts
CHANGED
|
@@ -23,12 +23,17 @@ export async function exportToSVG(
|
|
|
23
23
|
svg: SVGSVGElement,
|
|
24
24
|
options: Omit<SVGExportOptions, 'type'> = {},
|
|
25
25
|
) {
|
|
26
|
-
const { embedResources = true } = options;
|
|
26
|
+
const { embedResources = true, removeIds = false } = options;
|
|
27
27
|
const clonedSVG = svg.cloneNode(true) as SVGSVGElement;
|
|
28
28
|
const { width, height } = getViewBox(svg);
|
|
29
29
|
setAttributes(clonedSVG, { width, height });
|
|
30
30
|
|
|
31
|
-
|
|
31
|
+
if (removeIds) {
|
|
32
|
+
inlineUseElements(clonedSVG);
|
|
33
|
+
inlineDefsReferences(clonedSVG);
|
|
34
|
+
} else {
|
|
35
|
+
await embedIcons(clonedSVG);
|
|
36
|
+
}
|
|
32
37
|
await embedFonts(clonedSVG, embedResources);
|
|
33
38
|
|
|
34
39
|
cleanSVG(clonedSVG);
|
|
@@ -62,6 +67,253 @@ function getDefs(svg: SVGSVGElement) {
|
|
|
62
67
|
return _defs;
|
|
63
68
|
}
|
|
64
69
|
|
|
70
|
+
function inlineUseElements(svg: SVGSVGElement) {
|
|
71
|
+
const uses = Array.from(svg.querySelectorAll<SVGUseElement>('use'));
|
|
72
|
+
if (!uses.length) return;
|
|
73
|
+
|
|
74
|
+
uses.forEach((use) => {
|
|
75
|
+
const href = getUseHref(use);
|
|
76
|
+
if (!href || !href.startsWith('#')) return;
|
|
77
|
+
const target = resolveUseTarget(svg, href);
|
|
78
|
+
if (!target || target === use) return;
|
|
79
|
+
|
|
80
|
+
const replacement = createInlineElement(use, target);
|
|
81
|
+
if (!replacement) return;
|
|
82
|
+
use.replaceWith(replacement);
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function getUseHref(use: SVGUseElement) {
|
|
87
|
+
return use.getAttribute('href') ?? use.getAttribute('xlink:href');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function resolveUseTarget(svg: SVGSVGElement, href: string) {
|
|
91
|
+
const localTarget = svg.querySelector(href);
|
|
92
|
+
if (localTarget) return localTarget as SVGElement;
|
|
93
|
+
const docTarget = document.querySelector(href);
|
|
94
|
+
return docTarget as SVGElement | null;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function createInlineElement(use: SVGUseElement, target: SVGElement) {
|
|
98
|
+
const tag = target.tagName.toLowerCase();
|
|
99
|
+
if (tag === 'symbol') {
|
|
100
|
+
return materializeSymbol(use, target as SVGSymbolElement);
|
|
101
|
+
}
|
|
102
|
+
if (tag === 'svg') {
|
|
103
|
+
return materializeSVG(use, target as SVGSVGElement);
|
|
104
|
+
}
|
|
105
|
+
return materializeElement(use, target);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function materializeSymbol(use: SVGUseElement, symbol: SVGSymbolElement) {
|
|
109
|
+
const symbolClone = symbol.cloneNode(true) as SVGSymbolElement;
|
|
110
|
+
const svg = createElement<SVGSVGElement>('svg');
|
|
111
|
+
|
|
112
|
+
applyAttributes(svg, symbolClone, new Set(['id']));
|
|
113
|
+
applyAttributes(svg, use, new Set(['href', 'xlink:href']));
|
|
114
|
+
|
|
115
|
+
while (symbolClone.firstChild) {
|
|
116
|
+
svg.appendChild(symbolClone.firstChild);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return svg;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function materializeSVG(use: SVGUseElement, source: SVGSVGElement) {
|
|
123
|
+
const clone = source.cloneNode(true) as SVGSVGElement;
|
|
124
|
+
clone.removeAttribute('id');
|
|
125
|
+
applyAttributes(clone, use, new Set(['href', 'xlink:href']));
|
|
126
|
+
return clone;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function materializeElement(use: SVGUseElement, source: SVGElement) {
|
|
130
|
+
const clone = source.cloneNode(true) as SVGElement;
|
|
131
|
+
clone.removeAttribute('id');
|
|
132
|
+
|
|
133
|
+
const wrapper = createElement<SVGGElement>('g');
|
|
134
|
+
applyAttributes(
|
|
135
|
+
wrapper,
|
|
136
|
+
use,
|
|
137
|
+
new Set(['href', 'xlink:href', 'x', 'y', 'width', 'height', 'transform']),
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
const transform = buildUseTransform(use);
|
|
141
|
+
if (transform) {
|
|
142
|
+
wrapper.setAttribute('transform', transform);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
wrapper.appendChild(clone);
|
|
146
|
+
return wrapper;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function buildUseTransform(use: SVGUseElement) {
|
|
150
|
+
const x = use.getAttribute('x');
|
|
151
|
+
const y = use.getAttribute('y');
|
|
152
|
+
const translate = x || y ? `translate(${x ?? 0} ${y ?? 0})` : '';
|
|
153
|
+
const transform = use.getAttribute('transform') ?? '';
|
|
154
|
+
if (translate && transform) return `${translate} ${transform}`;
|
|
155
|
+
return translate || transform || null;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function applyAttributes(
|
|
159
|
+
target: SVGElement,
|
|
160
|
+
source: SVGElement,
|
|
161
|
+
exclude: Set<string> = new Set(),
|
|
162
|
+
) {
|
|
163
|
+
Array.from(source.attributes).forEach((attr) => {
|
|
164
|
+
if (exclude.has(attr.name)) return;
|
|
165
|
+
if (attr.name === 'style') {
|
|
166
|
+
mergeStyleAttribute(target, attr.value);
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
if (attr.name === 'class') {
|
|
170
|
+
mergeClassAttribute(target, attr.value);
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
target.setAttribute(attr.name, attr.value);
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function mergeStyleAttribute(target: SVGElement, value: string) {
|
|
178
|
+
const current = target.getAttribute('style');
|
|
179
|
+
if (!current) {
|
|
180
|
+
target.setAttribute('style', value);
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
const separator = current.trim().endsWith(';') ? '' : ';';
|
|
184
|
+
target.setAttribute('style', `${current}${separator}${value}`);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function mergeClassAttribute(target: SVGElement, value: string) {
|
|
188
|
+
const current = target.getAttribute('class');
|
|
189
|
+
if (!current) {
|
|
190
|
+
target.setAttribute('class', value);
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
target.setAttribute('class', `${current} ${value}`.trim());
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const urlRefRegex = /url\(\s*['"]?#([^'")\s]+)['"]?\s*\)/g;
|
|
197
|
+
function inlineDefsReferences(svg: SVGSVGElement) {
|
|
198
|
+
const referencedIds = collectReferencedIds(svg);
|
|
199
|
+
if (referencedIds.size === 0) {
|
|
200
|
+
removeDefs(svg);
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const defsDataUrl = createDefsDataUrl(svg, referencedIds);
|
|
205
|
+
if (!defsDataUrl) return;
|
|
206
|
+
|
|
207
|
+
traverse(svg, (node) => {
|
|
208
|
+
if (node.tagName.toLowerCase() === 'defs') return false;
|
|
209
|
+
const attrs = Array.from(node.attributes);
|
|
210
|
+
attrs.forEach((attr) => {
|
|
211
|
+
const value = attr.value;
|
|
212
|
+
if (!value.includes('url(')) return;
|
|
213
|
+
const updated = value.replace(urlRefRegex, (_match, id) => {
|
|
214
|
+
const encodedId = encodeURIComponent(id);
|
|
215
|
+
return `url("${defsDataUrl}#${encodedId}")`;
|
|
216
|
+
});
|
|
217
|
+
if (updated !== value) node.setAttribute(attr.name, updated);
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
removeDefs(svg);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function collectReferencedIds(svg: SVGSVGElement) {
|
|
225
|
+
const ids = new Set<string>();
|
|
226
|
+
traverse(svg, (node) => {
|
|
227
|
+
if (node.tagName.toLowerCase() === 'defs') return false;
|
|
228
|
+
collectIdsFromAttributes(node, (id) => ids.add(id));
|
|
229
|
+
});
|
|
230
|
+
return ids;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function collectIdsFromAttributes(
|
|
234
|
+
node: SVGElement,
|
|
235
|
+
addId: (id: string) => void,
|
|
236
|
+
) {
|
|
237
|
+
for (const attr of Array.from(node.attributes)) {
|
|
238
|
+
const value = attr.value;
|
|
239
|
+
if (value.includes('url(')) {
|
|
240
|
+
for (const match of value.matchAll(urlRefRegex)) {
|
|
241
|
+
if (match[1]) addId(match[1]);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
if (
|
|
245
|
+
(attr.name === 'href' || attr.name === 'xlink:href') &&
|
|
246
|
+
value[0] === '#'
|
|
247
|
+
) {
|
|
248
|
+
addId(value.slice(1));
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function createDefsDataUrl(svg: SVGSVGElement, ids: Set<string>) {
|
|
254
|
+
if (ids.size === 0) return null;
|
|
255
|
+
|
|
256
|
+
const collected = collectDefElements(svg, ids);
|
|
257
|
+
if (collected.size === 0) return null;
|
|
258
|
+
|
|
259
|
+
const defsSvg = createElement<SVGSVGElement>('svg', {
|
|
260
|
+
xmlns: 'http://www.w3.org/2000/svg',
|
|
261
|
+
'xmlns:xlink': 'http://www.w3.org/1999/xlink',
|
|
262
|
+
});
|
|
263
|
+
const defs = createElement<SVGDefsElement>('defs');
|
|
264
|
+
|
|
265
|
+
collected.forEach((node) => {
|
|
266
|
+
defs.appendChild(node.cloneNode(true));
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
if (!defs.children.length) return null;
|
|
270
|
+
defsSvg.appendChild(defs);
|
|
271
|
+
|
|
272
|
+
const serialized = new XMLSerializer().serializeToString(defsSvg);
|
|
273
|
+
return 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(serialized);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function collectDefElements(svg: SVGSVGElement, ids: Set<string>) {
|
|
277
|
+
const collected = new Map<string, SVGElement>();
|
|
278
|
+
const queue = Array.from(ids);
|
|
279
|
+
const queued = new Set(queue);
|
|
280
|
+
const visited = new Set<string>();
|
|
281
|
+
const enqueue = (id: string) => {
|
|
282
|
+
if (visited.has(id) || queued.has(id)) return;
|
|
283
|
+
queue.push(id);
|
|
284
|
+
queued.add(id);
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
while (queue.length) {
|
|
288
|
+
const id = queue.shift()!;
|
|
289
|
+
if (visited.has(id)) continue;
|
|
290
|
+
visited.add(id);
|
|
291
|
+
|
|
292
|
+
const selector = `#${escapeCssId(id)}`;
|
|
293
|
+
const target = svg.querySelector(selector);
|
|
294
|
+
if (!target) continue;
|
|
295
|
+
collected.set(id, target as SVGElement);
|
|
296
|
+
|
|
297
|
+
traverse(target as SVGElement, (node) => {
|
|
298
|
+
collectIdsFromAttributes(node, enqueue);
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
return collected;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function escapeCssId(id: string) {
|
|
306
|
+
if (globalThis.CSS && typeof globalThis.CSS.escape === 'function') {
|
|
307
|
+
return globalThis.CSS.escape(id);
|
|
308
|
+
}
|
|
309
|
+
return id.replace(/([!"#$%&'()*+,./:;<=>?@[\]^`{|}~])/g, '\\$1');
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function removeDefs(svg: SVGSVGElement) {
|
|
313
|
+
const defsList = Array.from(svg.querySelectorAll('defs'));
|
|
314
|
+
defsList.forEach((defs) => defs.remove());
|
|
315
|
+
}
|
|
316
|
+
|
|
65
317
|
function cleanSVG(svg: SVGSVGElement) {
|
|
66
318
|
removeBtnGroup(svg);
|
|
67
319
|
removeTransientContainer(svg);
|
package/src/exporter/types.ts
CHANGED
package/src/index.ts
CHANGED
|
@@ -14,6 +14,7 @@ export {
|
|
|
14
14
|
ZoomWheel,
|
|
15
15
|
} from './editor/interactions';
|
|
16
16
|
export { EditBar, Plugin, ResizeElement } from './editor/plugins';
|
|
17
|
+
export { exportToSVG } from './exporter';
|
|
17
18
|
export {
|
|
18
19
|
Defs,
|
|
19
20
|
Ellipse,
|
|
@@ -38,9 +39,8 @@ export {
|
|
|
38
39
|
getFont,
|
|
39
40
|
getFonts,
|
|
40
41
|
getPalette,
|
|
41
|
-
getPalettes,
|
|
42
42
|
getPaletteColor,
|
|
43
|
-
|
|
43
|
+
getPalettes,
|
|
44
44
|
registerFont,
|
|
45
45
|
registerPalette,
|
|
46
46
|
registerPattern,
|
|
@@ -51,9 +51,14 @@ export { Infographic } from './runtime';
|
|
|
51
51
|
export { parseSyntax } from './syntax';
|
|
52
52
|
export { getTemplate, getTemplates, registerTemplate } from './templates';
|
|
53
53
|
export { getTheme, getThemes, registerTheme } from './themes';
|
|
54
|
-
export { parseSVG } from './utils';
|
|
54
|
+
export { parseSVG, setFontExtendFactor } from './utils';
|
|
55
55
|
|
|
56
56
|
export type { EditBarOptions } from './editor';
|
|
57
|
+
export type {
|
|
58
|
+
ExportOptions,
|
|
59
|
+
PNGExportOptions,
|
|
60
|
+
SVGExportOptions,
|
|
61
|
+
} from './exporter';
|
|
57
62
|
export type {
|
|
58
63
|
Bounds,
|
|
59
64
|
ComponentType,
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getSvgLoadPromise,
|
|
3
|
+
trackSvgLoadPromise,
|
|
4
|
+
} from '../../resource/load-tracker';
|
|
1
5
|
import { join, normalizeFontWeightName, splitFontFamily } from '../../utils';
|
|
2
6
|
import { getFont, getFonts } from './registry';
|
|
3
7
|
|
|
@@ -36,6 +40,66 @@ const FONT_LOAD_MAP = new WeakMap<
|
|
|
36
40
|
HTMLHeadElement,
|
|
37
41
|
Map<string, HTMLLinkElement>
|
|
38
42
|
>();
|
|
43
|
+
const FONT_PROMISE_MAP = new WeakMap<
|
|
44
|
+
HTMLHeadElement,
|
|
45
|
+
Map<string, Promise<void>>
|
|
46
|
+
>();
|
|
47
|
+
|
|
48
|
+
function trackFontPromise(
|
|
49
|
+
target: HTMLHeadElement,
|
|
50
|
+
id: string,
|
|
51
|
+
promise: Promise<void>,
|
|
52
|
+
): Promise<void> {
|
|
53
|
+
let map = FONT_PROMISE_MAP.get(target);
|
|
54
|
+
if (!map) {
|
|
55
|
+
map = new Map();
|
|
56
|
+
FONT_PROMISE_MAP.set(target, map);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
map.set(id, promise);
|
|
60
|
+
|
|
61
|
+
promise.finally(() => {
|
|
62
|
+
const map = FONT_PROMISE_MAP.get(target);
|
|
63
|
+
if (!map) return;
|
|
64
|
+
if (map.get(id) === promise) map.delete(id);
|
|
65
|
+
if (map.size === 0) FONT_PROMISE_MAP.delete(target);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
return promise;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function isLinkLoaded(link: HTMLLinkElement): boolean {
|
|
72
|
+
if (link.dataset.infographicFontLoaded === 'true') return true;
|
|
73
|
+
try {
|
|
74
|
+
return !!link.sheet;
|
|
75
|
+
} catch {
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function getFontLoadPromise(
|
|
81
|
+
target: HTMLHeadElement,
|
|
82
|
+
id: string,
|
|
83
|
+
link?: HTMLLinkElement,
|
|
84
|
+
): Promise<void> {
|
|
85
|
+
const existing = FONT_PROMISE_MAP.get(target)?.get(id);
|
|
86
|
+
if (existing) return existing;
|
|
87
|
+
|
|
88
|
+
if (!link || isLinkLoaded(link)) {
|
|
89
|
+
return trackFontPromise(target, id, Promise.resolve());
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const promise = new Promise<void>((resolve) => {
|
|
93
|
+
const done = () => {
|
|
94
|
+
link.dataset.infographicFontLoaded = 'true';
|
|
95
|
+
resolve();
|
|
96
|
+
};
|
|
97
|
+
link.addEventListener('load', done, { once: true });
|
|
98
|
+
link.addEventListener('error', done, { once: true });
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
return trackFontPromise(target, id, promise);
|
|
102
|
+
}
|
|
39
103
|
|
|
40
104
|
export function loadFont(svg: SVGSVGElement, font: string) {
|
|
41
105
|
const doc = svg.ownerDocument;
|
|
@@ -45,18 +109,27 @@ export function loadFont(svg: SVGSVGElement, font: string) {
|
|
|
45
109
|
if (!FONT_LOAD_MAP.has(target)) FONT_LOAD_MAP.set(target, new Map());
|
|
46
110
|
const map = FONT_LOAD_MAP.get(target)!;
|
|
47
111
|
|
|
48
|
-
const links: HTMLLinkElement[] = [];
|
|
49
112
|
const urls = getFontURLs(font);
|
|
113
|
+
if (!urls.length) return;
|
|
114
|
+
|
|
115
|
+
const links: HTMLLinkElement[] = [];
|
|
50
116
|
urls.forEach((url) => {
|
|
51
117
|
const id = `${font}-${url}`;
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
link
|
|
56
|
-
link
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
118
|
+
const promiseKey = `font:${id}`;
|
|
119
|
+
if (getSvgLoadPromise<void>(svg, promiseKey)) return;
|
|
120
|
+
|
|
121
|
+
let link = map.get(id);
|
|
122
|
+
if (!link) {
|
|
123
|
+
link = doc.createElement('link');
|
|
124
|
+
link.id = id;
|
|
125
|
+
link.rel = 'stylesheet';
|
|
126
|
+
link.href = url;
|
|
127
|
+
links.push(link);
|
|
128
|
+
map.set(id, link);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const promise = getFontLoadPromise(target, id, link);
|
|
132
|
+
trackSvgLoadPromise(svg, promiseKey, promise);
|
|
60
133
|
});
|
|
61
134
|
|
|
62
135
|
if (!links.length) return;
|
package/src/resource/index.ts
CHANGED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
type SvgLoadPromise = Promise<unknown>;
|
|
2
|
+
|
|
3
|
+
const SVG_LOAD_PROMISE_MAP = new WeakMap<
|
|
4
|
+
SVGSVGElement,
|
|
5
|
+
Map<string, SvgLoadPromise>
|
|
6
|
+
>();
|
|
7
|
+
|
|
8
|
+
export function getSvgLoadPromises(svg: SVGSVGElement): SvgLoadPromise[] {
|
|
9
|
+
const map = SVG_LOAD_PROMISE_MAP.get(svg);
|
|
10
|
+
return map ? Array.from(map.values()) : [];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function getSvgLoadPromise<T = unknown>(
|
|
14
|
+
svg: SVGSVGElement,
|
|
15
|
+
key: string,
|
|
16
|
+
): Promise<T> | undefined {
|
|
17
|
+
return SVG_LOAD_PROMISE_MAP.get(svg)?.get(key) as Promise<T> | undefined;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function trackSvgLoadPromise<T>(
|
|
21
|
+
svg: SVGSVGElement,
|
|
22
|
+
key: string,
|
|
23
|
+
promise: Promise<T>,
|
|
24
|
+
): Promise<T> {
|
|
25
|
+
let map = SVG_LOAD_PROMISE_MAP.get(svg);
|
|
26
|
+
if (!map) {
|
|
27
|
+
map = new Map();
|
|
28
|
+
SVG_LOAD_PROMISE_MAP.set(svg, map);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
map.set(key, promise as SvgLoadPromise);
|
|
32
|
+
|
|
33
|
+
promise.finally(() => {
|
|
34
|
+
const map = SVG_LOAD_PROMISE_MAP.get(svg);
|
|
35
|
+
if (!map) return;
|
|
36
|
+
if (map.get(key) === promise) map.delete(key);
|
|
37
|
+
if (map.size === 0) SVG_LOAD_PROMISE_MAP.delete(svg);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
return promise;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export async function waitForSvgLoads(svg: SVGSVGElement): Promise<void> {
|
|
44
|
+
await Promise.resolve();
|
|
45
|
+
while (true) {
|
|
46
|
+
const promises = getSvgLoadPromises(svg);
|
|
47
|
+
if (!promises.length) break;
|
|
48
|
+
await Promise.allSettled(promises);
|
|
49
|
+
await Promise.resolve();
|
|
50
|
+
}
|
|
51
|
+
}
|