@antv/infographic 0.2.16 → 0.2.18
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 +1 -1
- package/README.zh-CN.md +1 -1
- package/dist/infographic.min.js +121 -120
- package/dist/infographic.min.js.map +1 -1
- package/esm/constants/service.d.ts +1 -1
- package/esm/constants/service.js +1 -1
- package/esm/designs/structures/chart-line.js +7 -4
- package/esm/editor/interactions/dblclick-edit-text.js +3 -3
- package/esm/editor/managers/interaction.js +6 -4
- package/esm/editor/plugins/components/button.d.ts +2 -1
- package/esm/editor/plugins/components/button.js +4 -4
- package/esm/editor/plugins/components/color-picker.d.ts +1 -0
- package/esm/editor/plugins/components/color-picker.js +3 -3
- package/esm/editor/plugins/components/popover.d.ts +3 -1
- package/esm/editor/plugins/components/popover.js +29 -9
- package/esm/editor/plugins/edit-bar/edit-bar.d.ts +3 -1
- package/esm/editor/plugins/edit-bar/edit-bar.js +17 -7
- package/esm/editor/plugins/edit-bar/edit-items/align-elements.js +6 -4
- package/esm/editor/plugins/edit-bar/edit-items/font-align.js +8 -5
- package/esm/editor/plugins/edit-bar/edit-items/font-color.js +7 -4
- package/esm/editor/plugins/edit-bar/edit-items/font-family.js +11 -9
- package/esm/editor/plugins/edit-bar/edit-items/font-size.js +8 -5
- package/esm/editor/plugins/edit-bar/edit-items/icon-color.js +7 -4
- package/esm/editor/plugins/edit-bar/edit-items/types.d.ts +5 -1
- package/esm/editor/plugins/reset-viewbox.d.ts +4 -1
- package/esm/editor/plugins/reset-viewbox.js +12 -6
- package/esm/editor/utils/index.d.ts +1 -0
- package/esm/editor/utils/index.js +1 -0
- package/esm/editor/utils/root.d.ts +3 -0
- package/esm/editor/utils/root.js +18 -0
- package/esm/exporter/svg.js +229 -3
- package/esm/options/parser.js +8 -6
- package/esm/options/types.d.ts +3 -3
- package/esm/renderer/renderer.js +1 -1
- package/esm/resource/loaders/search.js +2 -6
- package/esm/runtime/options.js +1 -1
- package/esm/syntax/index.js +56 -10
- package/esm/syntax/mapper.js +20 -6
- package/esm/syntax/parser.js +9 -0
- package/esm/syntax/types.d.ts +1 -1
- package/esm/templates/registry.d.ts +1 -0
- package/esm/templates/registry.js +6 -0
- package/esm/templates/utils.d.ts +1 -0
- package/esm/templates/utils.js +68 -0
- package/esm/themes/built-in.js +3 -0
- package/esm/utils/padding.js +1 -1
- package/esm/utils/style.d.ts +3 -1
- package/esm/utils/style.js +27 -4
- package/esm/version.d.ts +1 -1
- package/esm/version.js +1 -1
- package/lib/constants/service.d.ts +1 -1
- package/lib/constants/service.js +1 -1
- package/lib/designs/structures/chart-line.js +7 -4
- package/lib/editor/interactions/dblclick-edit-text.js +3 -3
- package/lib/editor/managers/interaction.js +7 -5
- package/lib/editor/plugins/components/button.d.ts +2 -1
- package/lib/editor/plugins/components/button.js +4 -4
- package/lib/editor/plugins/components/color-picker.d.ts +1 -0
- package/lib/editor/plugins/components/color-picker.js +3 -3
- package/lib/editor/plugins/components/popover.d.ts +3 -1
- package/lib/editor/plugins/components/popover.js +32 -12
- package/lib/editor/plugins/edit-bar/edit-bar.d.ts +3 -1
- package/lib/editor/plugins/edit-bar/edit-bar.js +17 -7
- package/lib/editor/plugins/edit-bar/edit-items/align-elements.js +6 -4
- package/lib/editor/plugins/edit-bar/edit-items/font-align.js +8 -5
- package/lib/editor/plugins/edit-bar/edit-items/font-color.js +7 -4
- package/lib/editor/plugins/edit-bar/edit-items/font-family.js +11 -9
- package/lib/editor/plugins/edit-bar/edit-items/font-size.js +8 -5
- package/lib/editor/plugins/edit-bar/edit-items/icon-color.js +7 -4
- package/lib/editor/plugins/edit-bar/edit-items/types.d.ts +5 -1
- package/lib/editor/plugins/reset-viewbox.d.ts +4 -1
- package/lib/editor/plugins/reset-viewbox.js +12 -6
- package/lib/editor/utils/index.d.ts +1 -0
- package/lib/editor/utils/index.js +1 -0
- package/lib/editor/utils/root.d.ts +3 -0
- package/lib/editor/utils/root.js +22 -0
- package/lib/exporter/svg.js +229 -3
- package/lib/options/parser.js +7 -5
- package/lib/options/types.d.ts +3 -3
- package/lib/renderer/renderer.js +1 -1
- package/lib/resource/loaders/search.js +2 -6
- package/lib/runtime/options.js +1 -1
- package/lib/syntax/index.js +56 -10
- package/lib/syntax/mapper.js +20 -6
- package/lib/syntax/parser.js +9 -0
- package/lib/syntax/types.d.ts +1 -1
- package/lib/templates/registry.d.ts +1 -0
- package/lib/templates/registry.js +7 -0
- package/lib/templates/utils.d.ts +1 -0
- package/lib/templates/utils.js +71 -0
- package/lib/themes/built-in.js +3 -0
- package/lib/utils/padding.js +1 -1
- package/lib/utils/style.d.ts +3 -1
- package/lib/utils/style.js +27 -4
- package/lib/version.d.ts +1 -1
- package/lib/version.js +1 -1
- package/package.json +1 -1
- package/src/constants/service.ts +1 -1
- package/src/designs/structures/chart-line.tsx +8 -4
- package/src/editor/interactions/dblclick-edit-text.ts +3 -2
- package/src/editor/managers/interaction.ts +9 -7
- package/src/editor/plugins/components/button.ts +5 -2
- package/src/editor/plugins/components/color-picker.ts +4 -2
- package/src/editor/plugins/components/popover.ts +31 -12
- package/src/editor/plugins/edit-bar/edit-bar.ts +26 -11
- package/src/editor/plugins/edit-bar/edit-items/align-elements.ts +7 -2
- package/src/editor/plugins/edit-bar/edit-items/font-align.ts +8 -3
- package/src/editor/plugins/edit-bar/edit-items/font-color.ts +7 -2
- package/src/editor/plugins/edit-bar/edit-items/font-family.ts +11 -7
- package/src/editor/plugins/edit-bar/edit-items/font-size.ts +8 -3
- package/src/editor/plugins/edit-bar/edit-items/icon-color.ts +7 -2
- package/src/editor/plugins/edit-bar/edit-items/types.ts +6 -1
- package/src/editor/plugins/reset-viewbox.ts +17 -8
- package/src/editor/utils/index.ts +1 -0
- package/src/editor/utils/root.ts +26 -0
- package/src/exporter/svg.ts +274 -3
- package/src/options/parser.ts +7 -6
- package/src/options/types.ts +3 -3
- package/src/renderer/renderer.ts +1 -1
- package/src/resource/loaders/search.ts +2 -5
- package/src/runtime/options.ts +1 -1
- package/src/syntax/index.ts +71 -10
- package/src/syntax/mapper.ts +20 -6
- package/src/syntax/parser.ts +10 -0
- package/src/syntax/types.ts +1 -0
- package/src/templates/registry.ts +6 -0
- package/src/templates/utils.ts +111 -0
- package/src/themes/built-in.ts +4 -0
- package/src/utils/padding.ts +1 -1
- package/src/utils/style.ts +31 -4
- package/src/version.ts +1 -1
package/src/exporter/svg.ts
CHANGED
|
@@ -10,6 +10,8 @@ import {
|
|
|
10
10
|
import { embedFonts } from './font';
|
|
11
11
|
import type { SVGExportOptions } from './types';
|
|
12
12
|
|
|
13
|
+
const VIEWBOX_CHANGE_TOLERANCE = 0.5;
|
|
14
|
+
|
|
13
15
|
export async function exportToSVGString(
|
|
14
16
|
svg: SVGSVGElement,
|
|
15
17
|
options: Omit<SVGExportOptions, 'type'> = {},
|
|
@@ -19,6 +21,195 @@ export async function exportToSVGString(
|
|
|
19
21
|
return 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(str);
|
|
20
22
|
}
|
|
21
23
|
|
|
24
|
+
function getExportViewBox(svg: SVGSVGElement) {
|
|
25
|
+
if (svg.hasAttribute('viewBox')) return getViewBox(svg);
|
|
26
|
+
|
|
27
|
+
const width = parseAbsoluteLength(svg.getAttribute('width'));
|
|
28
|
+
const height = parseAbsoluteLength(svg.getAttribute('height'));
|
|
29
|
+
if (
|
|
30
|
+
!Number.isNaN(width) &&
|
|
31
|
+
width > 0 &&
|
|
32
|
+
!Number.isNaN(height) &&
|
|
33
|
+
height > 0
|
|
34
|
+
) {
|
|
35
|
+
return { x: 0, y: 0, width, height };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const rect = svg.getBoundingClientRect();
|
|
39
|
+
if (rect.width > 0 && rect.height > 0) {
|
|
40
|
+
return { x: 0, y: 0, width: rect.width, height: rect.height };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function parseAbsoluteLength(value: string | null): number {
|
|
47
|
+
if (!value) return Number.NaN;
|
|
48
|
+
const trimmed = value.trim();
|
|
49
|
+
if (!trimmed) return Number.NaN;
|
|
50
|
+
if (!/^[-+]?(?:\d+\.?\d*|\.\d+)(?:px)?$/.test(trimmed)) return Number.NaN;
|
|
51
|
+
return Number.parseFloat(trimmed);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function measureSpanContentHeight(span: HTMLElement): number {
|
|
55
|
+
const prevHeight = span.style.height;
|
|
56
|
+
const prevOverflow = span.style.overflow;
|
|
57
|
+
try {
|
|
58
|
+
span.style.height = 'max-content';
|
|
59
|
+
span.style.overflow = 'hidden';
|
|
60
|
+
void span.offsetHeight; // force reflow
|
|
61
|
+
return span.scrollHeight;
|
|
62
|
+
} finally {
|
|
63
|
+
span.style.height = prevHeight;
|
|
64
|
+
span.style.overflow = prevOverflow;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function measureSpanContentWidth(span: HTMLElement): number {
|
|
69
|
+
const prevWidth = span.style.width;
|
|
70
|
+
const prevOverflow = span.style.overflow;
|
|
71
|
+
try {
|
|
72
|
+
span.style.width = 'max-content';
|
|
73
|
+
span.style.overflow = 'hidden';
|
|
74
|
+
void span.offsetWidth; // force reflow
|
|
75
|
+
return span.scrollWidth;
|
|
76
|
+
} finally {
|
|
77
|
+
span.style.width = prevWidth;
|
|
78
|
+
span.style.overflow = prevOverflow;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Returns [left, top, right, bottom] in SVG coordinates for a foreignObject,
|
|
83
|
+
// accounting for flex alignment: bottom/center-aligned content can overflow,
|
|
84
|
+
// and horizontally aligned content can overflow as well.
|
|
85
|
+
function getFOContentBoundsInSVG(
|
|
86
|
+
fo: SVGForeignObjectElement,
|
|
87
|
+
content: HTMLElement,
|
|
88
|
+
toSVGCoord: (x: number, y: number) => SVGPoint,
|
|
89
|
+
): [number, number, number, number] {
|
|
90
|
+
const foRect = fo.getBoundingClientRect();
|
|
91
|
+
const foTopLeft = toSVGCoord(foRect.left, foRect.top);
|
|
92
|
+
const foBottomRight = toSVGCoord(foRect.right, foRect.bottom);
|
|
93
|
+
|
|
94
|
+
const foLeftSVG = foTopLeft.x;
|
|
95
|
+
const foTopSVG = foTopLeft.y;
|
|
96
|
+
const foRightSVG = foBottomRight.x;
|
|
97
|
+
const foBottomSVG = foBottomRight.y;
|
|
98
|
+
|
|
99
|
+
const foWidthSVG = foRightSVG - foLeftSVG;
|
|
100
|
+
const foHeightSVG = foBottomSVG - foTopSVG;
|
|
101
|
+
|
|
102
|
+
const svgUnitsPerClientPxY =
|
|
103
|
+
foRect.height > 0 ? foHeightSVG / foRect.height : 1;
|
|
104
|
+
const svgUnitsPerClientPxX = foRect.width > 0 ? foWidthSVG / foRect.width : 1;
|
|
105
|
+
|
|
106
|
+
// Measure actual content dimensions
|
|
107
|
+
const realScrollHeight = measureSpanContentHeight(content);
|
|
108
|
+
const contentHeightSVG =
|
|
109
|
+
realScrollHeight > 0
|
|
110
|
+
? realScrollHeight * svgUnitsPerClientPxY
|
|
111
|
+
: foHeightSVG;
|
|
112
|
+
|
|
113
|
+
const realScrollWidth = measureSpanContentWidth(content);
|
|
114
|
+
const contentWidthSVG =
|
|
115
|
+
realScrollWidth > 0 ? realScrollWidth * svgUnitsPerClientPxX : foWidthSVG;
|
|
116
|
+
|
|
117
|
+
const computedStyle = window.getComputedStyle(content);
|
|
118
|
+
const alignItems = computedStyle.alignItems;
|
|
119
|
+
const justifyContent = computedStyle.justifyContent;
|
|
120
|
+
|
|
121
|
+
// Calculate vertical bounds
|
|
122
|
+
let top: number, bottom: number;
|
|
123
|
+
if (alignItems === 'flex-end' || alignItems === 'end') {
|
|
124
|
+
top = foBottomSVG - contentHeightSVG;
|
|
125
|
+
bottom = foBottomSVG;
|
|
126
|
+
} else if (alignItems === 'center') {
|
|
127
|
+
const overflowY = contentHeightSVG - foHeightSVG;
|
|
128
|
+
top = foTopSVG - overflowY / 2;
|
|
129
|
+
bottom = foBottomSVG + overflowY / 2;
|
|
130
|
+
} else {
|
|
131
|
+
top = foTopSVG;
|
|
132
|
+
bottom = foTopSVG + contentHeightSVG;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Calculate horizontal bounds
|
|
136
|
+
let left: number, right: number;
|
|
137
|
+
if (
|
|
138
|
+
justifyContent === 'flex-end' ||
|
|
139
|
+
justifyContent === 'end' ||
|
|
140
|
+
justifyContent === 'right'
|
|
141
|
+
) {
|
|
142
|
+
left = foRightSVG - contentWidthSVG;
|
|
143
|
+
right = foRightSVG;
|
|
144
|
+
} else if (justifyContent === 'center') {
|
|
145
|
+
const overflowX = contentWidthSVG - foWidthSVG;
|
|
146
|
+
left = foLeftSVG - overflowX / 2;
|
|
147
|
+
right = foRightSVG + overflowX / 2;
|
|
148
|
+
} else {
|
|
149
|
+
left = foLeftSVG;
|
|
150
|
+
right = foLeftSVG + contentWidthSVG;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return [left, top, right, bottom];
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Computes a viewBox that fully covers all foreignObject text content,
|
|
158
|
+
* accounting for overflow caused by flex alignment (bottom/center align
|
|
159
|
+
* can push content outside the foreignObject bounds).
|
|
160
|
+
*/
|
|
161
|
+
function computeFullViewBox(svg: SVGSVGElement): string | null {
|
|
162
|
+
const viewBox = getExportViewBox(svg);
|
|
163
|
+
if (!viewBox) return null;
|
|
164
|
+
|
|
165
|
+
if (typeof svg.getScreenCTM !== 'function') return null;
|
|
166
|
+
const screenCTM = svg.getScreenCTM();
|
|
167
|
+
if (!screenCTM) return null;
|
|
168
|
+
const inverseCTM = screenCTM.inverse();
|
|
169
|
+
|
|
170
|
+
const toSVGCoord = (clientX: number, clientY: number) => {
|
|
171
|
+
const pt = svg.createSVGPoint();
|
|
172
|
+
pt.x = clientX;
|
|
173
|
+
pt.y = clientY;
|
|
174
|
+
return pt.matrixTransform(inverseCTM);
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
let minX = viewBox.x;
|
|
178
|
+
let minY = viewBox.y;
|
|
179
|
+
let maxX = viewBox.x + viewBox.width;
|
|
180
|
+
let maxY = viewBox.y + viewBox.height;
|
|
181
|
+
|
|
182
|
+
svg
|
|
183
|
+
.querySelectorAll<SVGForeignObjectElement>('foreignObject')
|
|
184
|
+
.forEach((fo) => {
|
|
185
|
+
const content = fo.firstElementChild as HTMLElement;
|
|
186
|
+
if (!content) return;
|
|
187
|
+
const [left, top, right, bottom] = getFOContentBoundsInSVG(
|
|
188
|
+
fo,
|
|
189
|
+
content,
|
|
190
|
+
toSVGCoord,
|
|
191
|
+
);
|
|
192
|
+
minX = Math.min(minX, left);
|
|
193
|
+
minY = Math.min(minY, top);
|
|
194
|
+
maxX = Math.max(maxX, right);
|
|
195
|
+
maxY = Math.max(maxY, bottom);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
const newX = minX;
|
|
199
|
+
const newY = minY;
|
|
200
|
+
const newWidth = maxX - newX;
|
|
201
|
+
const newHeight = maxY - newY;
|
|
202
|
+
if (
|
|
203
|
+
newWidth <= viewBox.width + VIEWBOX_CHANGE_TOLERANCE &&
|
|
204
|
+
newHeight <= viewBox.height + VIEWBOX_CHANGE_TOLERANCE &&
|
|
205
|
+
newX >= viewBox.x - VIEWBOX_CHANGE_TOLERANCE &&
|
|
206
|
+
newY >= viewBox.y - VIEWBOX_CHANGE_TOLERANCE
|
|
207
|
+
)
|
|
208
|
+
return null;
|
|
209
|
+
|
|
210
|
+
return `${newX} ${newY} ${newWidth} ${newHeight}`;
|
|
211
|
+
}
|
|
212
|
+
|
|
22
213
|
export async function exportToSVG(
|
|
23
214
|
svg: SVGSVGElement,
|
|
24
215
|
options: Omit<SVGExportOptions, 'type'> = {},
|
|
@@ -29,7 +220,15 @@ export async function exportToSVG(
|
|
|
29
220
|
removeIds = false,
|
|
30
221
|
} = options;
|
|
31
222
|
const clonedSVG = svg.cloneNode(true) as SVGSVGElement;
|
|
32
|
-
|
|
223
|
+
|
|
224
|
+
if (typeof document !== 'undefined') {
|
|
225
|
+
const fullViewBox = computeFullViewBox(svg);
|
|
226
|
+
if (fullViewBox) {
|
|
227
|
+
clonedSVG.setAttribute('viewBox', fullViewBox);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const { width, height } = getViewBox(clonedSVG);
|
|
33
232
|
setAttributes(clonedSVG, { width, height });
|
|
34
233
|
|
|
35
234
|
if (removeIds) {
|
|
@@ -59,7 +258,9 @@ async function embedIcons(svg: SVGSVGElement) {
|
|
|
59
258
|
|
|
60
259
|
if (!existsSymbol) {
|
|
61
260
|
const symbolElement = document.querySelector(href);
|
|
62
|
-
if (symbolElement)
|
|
261
|
+
if (symbolElement) {
|
|
262
|
+
defs.appendChild(symbolElement.cloneNode(true));
|
|
263
|
+
}
|
|
63
264
|
}
|
|
64
265
|
});
|
|
65
266
|
}
|
|
@@ -293,6 +494,7 @@ function collectDefElements(svg: SVGSVGElement, ids: Set<string>) {
|
|
|
293
494
|
|
|
294
495
|
while (queue.length) {
|
|
295
496
|
const id = queue.shift()!;
|
|
497
|
+
if (!id) continue;
|
|
296
498
|
if (visited.has(id)) continue;
|
|
297
499
|
visited.add(id);
|
|
298
500
|
|
|
@@ -309,11 +511,80 @@ function collectDefElements(svg: SVGSVGElement, ids: Set<string>) {
|
|
|
309
511
|
return collected;
|
|
310
512
|
}
|
|
311
513
|
|
|
514
|
+
// Fallback implementation based on the CSS.escape algorithm
|
|
515
|
+
function cssEscape(value: string): string {
|
|
516
|
+
const string = String(value);
|
|
517
|
+
const length = string.length;
|
|
518
|
+
let result = '';
|
|
519
|
+
|
|
520
|
+
if (length === 0) {
|
|
521
|
+
return '';
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
for (let i = 0; i < length; i++) {
|
|
525
|
+
const codeUnit = string.charCodeAt(i);
|
|
526
|
+
|
|
527
|
+
// Null character
|
|
528
|
+
if (codeUnit === 0x0000) {
|
|
529
|
+
result += '\uFFFD';
|
|
530
|
+
continue;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
// Control characters or DEL
|
|
534
|
+
if ((codeUnit >= 0x0001 && codeUnit <= 0x001f) || codeUnit === 0x007f) {
|
|
535
|
+
result += '\\' + codeUnit.toString(16) + ' ';
|
|
536
|
+
continue;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
// Escape if first character is a digit
|
|
540
|
+
if (i === 0 && codeUnit >= 0x0030 && codeUnit <= 0x0039) {
|
|
541
|
+
result += '\\' + codeUnit.toString(16) + ' ';
|
|
542
|
+
continue;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
// Escape if second character is a digit and first is a hyphen
|
|
546
|
+
if (
|
|
547
|
+
i === 1 &&
|
|
548
|
+
codeUnit >= 0x0030 &&
|
|
549
|
+
codeUnit <= 0x0039 &&
|
|
550
|
+
string.charCodeAt(0) === 0x002d
|
|
551
|
+
) {
|
|
552
|
+
result += '\\' + codeUnit.toString(16) + ' ';
|
|
553
|
+
continue;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
// If the character is the first and is a hyphen followed by end of string, escape it
|
|
557
|
+
if (i === 0 && length === 1 && codeUnit === 0x002d) {
|
|
558
|
+
result += '\\' + string.charAt(i);
|
|
559
|
+
continue;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
// Characters that are safe to use unescaped
|
|
563
|
+
if (
|
|
564
|
+
codeUnit >= 0x0080 ||
|
|
565
|
+
(codeUnit >= 0x0030 && codeUnit <= 0x0039) || // 0-9
|
|
566
|
+
(codeUnit >= 0x0041 && codeUnit <= 0x005a) || // A-Z
|
|
567
|
+
(codeUnit >= 0x0061 && codeUnit <= 0x007a) || // a-z
|
|
568
|
+
codeUnit === 0x002d || // -
|
|
569
|
+
codeUnit === 0x005f // _
|
|
570
|
+
) {
|
|
571
|
+
result += string.charAt(i);
|
|
572
|
+
continue;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
// All other characters
|
|
576
|
+
result += '\\' + string.charAt(i);
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
return result;
|
|
580
|
+
}
|
|
581
|
+
|
|
312
582
|
function escapeCssId(id: string) {
|
|
313
583
|
if (globalThis.CSS && typeof globalThis.CSS.escape === 'function') {
|
|
314
584
|
return globalThis.CSS.escape(id);
|
|
315
585
|
}
|
|
316
|
-
|
|
586
|
+
|
|
587
|
+
return cssEscape(id);
|
|
317
588
|
}
|
|
318
589
|
|
|
319
590
|
function removeDefs(svg: SVGSVGElement) {
|
package/src/options/parser.ts
CHANGED
|
@@ -3,13 +3,13 @@ import {
|
|
|
3
3
|
DesignOptions,
|
|
4
4
|
getItem,
|
|
5
5
|
getStructure,
|
|
6
|
-
getTemplate,
|
|
7
6
|
NullableParsedDesignsOptions,
|
|
8
7
|
ParsedDesignsOptions,
|
|
9
8
|
Title,
|
|
10
9
|
} from '../designs';
|
|
11
10
|
import { getPaletteColor } from '../renderer';
|
|
12
11
|
import type { TemplateOptions } from '../templates';
|
|
12
|
+
import { getTemplate, resolveTemplateKey } from '../templates/registry';
|
|
13
13
|
import { generateThemeColors, getTheme, type ThemeConfig } from '../themes';
|
|
14
14
|
import type { Data, ItemDatum, ParsedData } from '../types';
|
|
15
15
|
import {
|
|
@@ -32,14 +32,15 @@ export function parseOptions(
|
|
|
32
32
|
data,
|
|
33
33
|
...restOptions
|
|
34
34
|
} = options;
|
|
35
|
+
const resolvedTemplate = template ? resolveTemplateKey(template) : undefined;
|
|
35
36
|
|
|
36
37
|
const parsedContainer =
|
|
37
38
|
typeof container === 'string'
|
|
38
39
|
? document.querySelector(container) || document.createElement('div')
|
|
39
40
|
: container;
|
|
40
41
|
|
|
41
|
-
const templateOptions: TemplateOptions | undefined =
|
|
42
|
-
? getTemplate(
|
|
42
|
+
const templateOptions: TemplateOptions | undefined = resolvedTemplate
|
|
43
|
+
? getTemplate(resolvedTemplate)
|
|
43
44
|
: undefined;
|
|
44
45
|
const mergedThemeConfig = merge(
|
|
45
46
|
{},
|
|
@@ -52,7 +53,7 @@ export function parseOptions(
|
|
|
52
53
|
: undefined;
|
|
53
54
|
|
|
54
55
|
const parsed: Partial<ParsedInfographicOptions> = {
|
|
55
|
-
container: parsedContainer as
|
|
56
|
+
container: parsedContainer as Element | ShadowRoot,
|
|
56
57
|
padding: parsePadding(padding),
|
|
57
58
|
};
|
|
58
59
|
|
|
@@ -63,10 +64,10 @@ export function parseOptions(
|
|
|
63
64
|
|
|
64
65
|
Object.assign(parsed, restOptions);
|
|
65
66
|
|
|
66
|
-
const parsedData = parseData(data,
|
|
67
|
+
const parsedData = parseData(data, resolvedTemplate);
|
|
67
68
|
if (parsedData) parsed.data = parsedData;
|
|
68
69
|
|
|
69
|
-
if (
|
|
70
|
+
if (resolvedTemplate) parsed.template = resolvedTemplate;
|
|
70
71
|
if (templateOptions?.design || design) {
|
|
71
72
|
const designOptions = {
|
|
72
73
|
...(resolvedThemeConfig
|
package/src/options/types.ts
CHANGED
|
@@ -5,8 +5,8 @@ import type { Data, Padding, ParsedData } from '../types';
|
|
|
5
5
|
import type { Path } from '../utils';
|
|
6
6
|
|
|
7
7
|
export interface InfographicOptions {
|
|
8
|
-
/**
|
|
9
|
-
container?: string |
|
|
8
|
+
/** 容器,可以是选择器、Element 或 ShadowRoot */
|
|
9
|
+
container?: string | Element | ShadowRoot;
|
|
10
10
|
/** 宽度 */
|
|
11
11
|
width?: number | string;
|
|
12
12
|
/** 高度 */
|
|
@@ -37,7 +37,7 @@ export interface InfographicOptions {
|
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
export interface ParsedInfographicOptions {
|
|
40
|
-
container:
|
|
40
|
+
container: Element | ShadowRoot;
|
|
41
41
|
width?: number | string;
|
|
42
42
|
height?: number | string;
|
|
43
43
|
padding?: Padding;
|
package/src/renderer/renderer.ts
CHANGED
|
@@ -11,8 +11,8 @@ const queryIcon = async (query: string): Promise<string | null> => {
|
|
|
11
11
|
const response = await fetchWithCache(url);
|
|
12
12
|
if (!response.ok) return null;
|
|
13
13
|
const result = await response.json();
|
|
14
|
-
if (!result?.
|
|
15
|
-
return (result.data
|
|
14
|
+
if (!result?.success || !Array.isArray(result.data)) return null;
|
|
15
|
+
return (result.data[0] as string) || null;
|
|
16
16
|
} catch (error) {
|
|
17
17
|
console.error(`Failed to query icon for "${query}":`, error);
|
|
18
18
|
return null;
|
|
@@ -43,9 +43,6 @@ export async function loadSearchResource(query: string, format?: string) {
|
|
|
43
43
|
const svgText = commaIndex >= 0 ? result.slice(commaIndex + 1) : result;
|
|
44
44
|
return loadSVGResource(svgText);
|
|
45
45
|
}
|
|
46
|
-
if (mimeType === 'image/svg+xml' && format === 'svg' && isBase64) {
|
|
47
|
-
return loadImageBase64Resource(result);
|
|
48
|
-
}
|
|
49
46
|
return loadImageBase64Resource(result);
|
|
50
47
|
}
|
|
51
48
|
|
package/src/runtime/options.ts
CHANGED
package/src/syntax/index.ts
CHANGED
|
@@ -12,6 +12,16 @@ import {
|
|
|
12
12
|
} from './schema';
|
|
13
13
|
import type { ObjectSchema, SyntaxNode, SyntaxParseResult } from './types';
|
|
14
14
|
|
|
15
|
+
const ALLOWED_ROOT_KEYS = new Set([
|
|
16
|
+
'infographic',
|
|
17
|
+
'template',
|
|
18
|
+
'design',
|
|
19
|
+
'data',
|
|
20
|
+
'theme',
|
|
21
|
+
'width',
|
|
22
|
+
'height',
|
|
23
|
+
]);
|
|
24
|
+
|
|
15
25
|
function normalizeItems(items: ItemDatum[]) {
|
|
16
26
|
const seen = new Set<string>();
|
|
17
27
|
const normalized: ItemDatum[] = [];
|
|
@@ -30,6 +40,14 @@ function normalizeItems(items: ItemDatum[]) {
|
|
|
30
40
|
return normalized.reverse();
|
|
31
41
|
}
|
|
32
42
|
|
|
43
|
+
function assignMissingNodeIds(items: ItemDatum[]) {
|
|
44
|
+
items.forEach((item) => {
|
|
45
|
+
if (!item.id && item.label) {
|
|
46
|
+
item.id = item.label;
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
33
51
|
function resolveTemplate(
|
|
34
52
|
node: SyntaxNode | undefined,
|
|
35
53
|
errors: SyntaxParseResult['errors'],
|
|
@@ -44,12 +62,54 @@ function resolveTemplate(
|
|
|
44
62
|
return undefined;
|
|
45
63
|
}
|
|
46
64
|
|
|
65
|
+
function inferTemplateFromBareFirstLine(
|
|
66
|
+
entries: Record<string, SyntaxNode>,
|
|
67
|
+
warnings: SyntaxParseResult['warnings'],
|
|
68
|
+
) {
|
|
69
|
+
if ('infographic' in entries || 'template' in entries) {
|
|
70
|
+
return undefined;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const [firstEntry] = Object.entries(entries);
|
|
74
|
+
if (!firstEntry) return undefined;
|
|
75
|
+
|
|
76
|
+
const [key, node] = firstEntry;
|
|
77
|
+
if (ALLOWED_ROOT_KEYS.has(key) || node.kind !== 'object') {
|
|
78
|
+
return undefined;
|
|
79
|
+
}
|
|
80
|
+
if (node.value !== undefined || Object.keys(node.entries).length > 0) {
|
|
81
|
+
return undefined;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
warnings.push({
|
|
85
|
+
path: 'template',
|
|
86
|
+
line: node.line,
|
|
87
|
+
code: 'implicit_template',
|
|
88
|
+
message:
|
|
89
|
+
'Inferred template from a bare first line. Prefix it with "infographic" or "template" to make the syntax explicit.',
|
|
90
|
+
raw: key,
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
template: key,
|
|
95
|
+
inferredKey: key,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
47
99
|
export function parseSyntax(input: string): SyntaxParseResult {
|
|
48
100
|
const { ast, errors } = parseSyntaxToAst(input);
|
|
49
101
|
const warnings: SyntaxParseResult['warnings'] = [];
|
|
50
102
|
const options: Partial<InfographicOptions> = {};
|
|
51
103
|
|
|
52
104
|
const mergedEntries = { ...ast.entries };
|
|
105
|
+
const inferredTemplate = inferTemplateFromBareFirstLine(
|
|
106
|
+
ast.entries,
|
|
107
|
+
warnings,
|
|
108
|
+
);
|
|
109
|
+
if (inferredTemplate) {
|
|
110
|
+
delete mergedEntries[inferredTemplate.inferredKey];
|
|
111
|
+
}
|
|
112
|
+
|
|
53
113
|
const infographicNode = ast.entries.infographic;
|
|
54
114
|
let templateFromInfographic: string | undefined;
|
|
55
115
|
if (infographicNode && infographicNode.kind === 'object') {
|
|
@@ -59,17 +119,8 @@ export function parseSyntax(input: string): SyntaxParseResult {
|
|
|
59
119
|
});
|
|
60
120
|
}
|
|
61
121
|
|
|
62
|
-
const allowedRootKeys = new Set([
|
|
63
|
-
'infographic',
|
|
64
|
-
'template',
|
|
65
|
-
'design',
|
|
66
|
-
'data',
|
|
67
|
-
'theme',
|
|
68
|
-
'width',
|
|
69
|
-
'height',
|
|
70
|
-
]);
|
|
71
122
|
Object.keys(mergedEntries).forEach((key) => {
|
|
72
|
-
if (!
|
|
123
|
+
if (!ALLOWED_ROOT_KEYS.has(key)) {
|
|
73
124
|
errors.push({
|
|
74
125
|
path: key,
|
|
75
126
|
line: (mergedEntries[key] as SyntaxNode).line,
|
|
@@ -86,6 +137,9 @@ export function parseSyntax(input: string): SyntaxParseResult {
|
|
|
86
137
|
if (!options.template && templateFromInfographic) {
|
|
87
138
|
options.template = templateFromInfographic;
|
|
88
139
|
}
|
|
140
|
+
if (!options.template && inferredTemplate) {
|
|
141
|
+
options.template = inferredTemplate.template;
|
|
142
|
+
}
|
|
89
143
|
|
|
90
144
|
const designNode = mergedEntries.design as SyntaxNode | undefined;
|
|
91
145
|
if (designNode) {
|
|
@@ -113,6 +167,13 @@ export function parseSyntax(input: string): SyntaxParseResult {
|
|
|
113
167
|
if (parsed.relations.length > 0 || parsed.items.length > 0) {
|
|
114
168
|
const current = (options.data ?? {}) as Record<string, any>;
|
|
115
169
|
|
|
170
|
+
if (Array.isArray(current.items)) {
|
|
171
|
+
assignMissingNodeIds(current.items as ItemDatum[]);
|
|
172
|
+
}
|
|
173
|
+
if (Array.isArray(current.nodes)) {
|
|
174
|
+
assignMissingNodeIds(current.nodes as ItemDatum[]);
|
|
175
|
+
}
|
|
176
|
+
|
|
116
177
|
// 优先使用已存在的数据列表 (sequences, lists, etc.)
|
|
117
178
|
const dataKeys = Object.keys(
|
|
118
179
|
(DataSchema as ObjectSchema).fields,
|
package/src/syntax/mapper.ts
CHANGED
|
@@ -16,10 +16,18 @@ const HEX_COLOR_PATTERN =
|
|
|
16
16
|
/^(#[0-9a-f]{8}|#[0-9a-f]{6}|#[0-9a-f]{4}|#[0-9a-f]{3})/i;
|
|
17
17
|
const FUNCTION_COLOR_PATTERN = /^((?:rgb|rgba|hsl|hsla)\([^)]*\))/i;
|
|
18
18
|
|
|
19
|
+
function normalizeBooleanLiteral(value: string) {
|
|
20
|
+
const trimmed = value.trim();
|
|
21
|
+
if (/^true$/i.test(trimmed)) return 'true';
|
|
22
|
+
if (/^false$/i.test(trimmed)) return 'false';
|
|
23
|
+
return undefined;
|
|
24
|
+
}
|
|
25
|
+
|
|
19
26
|
function parseScalar(value: string) {
|
|
20
27
|
const trimmed = value.trim();
|
|
21
|
-
|
|
22
|
-
if (
|
|
28
|
+
const normalizedBoolean = normalizeBooleanLiteral(trimmed);
|
|
29
|
+
if (normalizedBoolean === 'true') return true;
|
|
30
|
+
if (normalizedBoolean === 'false') return false;
|
|
23
31
|
if (/^-?\d+(\.\d+)?$/.test(trimmed)) return parseFloat(trimmed);
|
|
24
32
|
return trimmed;
|
|
25
33
|
}
|
|
@@ -240,7 +248,8 @@ export function mapWithSchema(
|
|
|
240
248
|
);
|
|
241
249
|
return undefined;
|
|
242
250
|
}
|
|
243
|
-
|
|
251
|
+
const normalizedBoolean = normalizeBooleanLiteral(value);
|
|
252
|
+
if (!normalizedBoolean) {
|
|
244
253
|
addError(
|
|
245
254
|
errors,
|
|
246
255
|
node,
|
|
@@ -251,7 +260,7 @@ export function mapWithSchema(
|
|
|
251
260
|
);
|
|
252
261
|
return undefined;
|
|
253
262
|
}
|
|
254
|
-
return
|
|
263
|
+
return normalizedBoolean === 'true';
|
|
255
264
|
}
|
|
256
265
|
case 'enum': {
|
|
257
266
|
const value = readScalar(node);
|
|
@@ -259,7 +268,12 @@ export function mapWithSchema(
|
|
|
259
268
|
addError(errors, node, path, 'schema_mismatch', 'Expected enum value.');
|
|
260
269
|
return undefined;
|
|
261
270
|
}
|
|
262
|
-
|
|
271
|
+
const normalizedBoolean = normalizeBooleanLiteral(value);
|
|
272
|
+
const enumValue =
|
|
273
|
+
normalizedBoolean && schema.values.includes(normalizedBoolean)
|
|
274
|
+
? normalizedBoolean
|
|
275
|
+
: value;
|
|
276
|
+
if (!schema.values.includes(enumValue)) {
|
|
263
277
|
addError(
|
|
264
278
|
errors,
|
|
265
279
|
node,
|
|
@@ -270,7 +284,7 @@ export function mapWithSchema(
|
|
|
270
284
|
);
|
|
271
285
|
return undefined;
|
|
272
286
|
}
|
|
273
|
-
return
|
|
287
|
+
return enumValue;
|
|
274
288
|
}
|
|
275
289
|
case 'array': {
|
|
276
290
|
if (node.kind === 'array') {
|
package/src/syntax/parser.ts
CHANGED
|
@@ -40,6 +40,15 @@ function stripComments(content: string) {
|
|
|
40
40
|
return content.trimEnd();
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
function isCommentLine(content: string) {
|
|
44
|
+
const trimmed = content.trimStart();
|
|
45
|
+
return trimmed.startsWith('#') || trimmed.startsWith('//');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function isCodeFenceLine(content: string) {
|
|
49
|
+
return /^```[\w-]*\s*$/.test(content.trim());
|
|
50
|
+
}
|
|
51
|
+
|
|
43
52
|
function looksLikeRelationExpression(text: string) {
|
|
44
53
|
return /[<>=o.x-]{2,}/.test(text);
|
|
45
54
|
}
|
|
@@ -169,6 +178,7 @@ export function parseSyntaxToAst(input: string): ParseResult {
|
|
|
169
178
|
const lineNumber = index + 1;
|
|
170
179
|
if (!line.trim()) return;
|
|
171
180
|
const { indent, content } = getIndentInfo(line);
|
|
181
|
+
if (isCommentLine(content) || isCodeFenceLine(content)) return;
|
|
172
182
|
const stripped = stripComments(content);
|
|
173
183
|
if (!stripped.trim()) return;
|
|
174
184
|
|
package/src/syntax/types.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { TemplateOptions } from './types';
|
|
2
|
+
import { findClosestTemplateKey } from './utils';
|
|
2
3
|
|
|
3
4
|
const TEMPLATE_REGISTRY = new Map<string, TemplateOptions>();
|
|
4
5
|
|
|
@@ -6,6 +7,11 @@ export function registerTemplate(type: string, template: TemplateOptions) {
|
|
|
6
7
|
TEMPLATE_REGISTRY.set(type, template);
|
|
7
8
|
}
|
|
8
9
|
|
|
10
|
+
export function resolveTemplateKey(type: string): string | undefined {
|
|
11
|
+
if (TEMPLATE_REGISTRY.has(type)) return type;
|
|
12
|
+
return findClosestTemplateKey(type, TEMPLATE_REGISTRY.keys());
|
|
13
|
+
}
|
|
14
|
+
|
|
9
15
|
export function getTemplate(type: string): TemplateOptions | undefined {
|
|
10
16
|
return TEMPLATE_REGISTRY.get(type);
|
|
11
17
|
}
|