@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.
Files changed (131) hide show
  1. package/README.md +1 -1
  2. package/README.zh-CN.md +1 -1
  3. package/dist/infographic.min.js +121 -120
  4. package/dist/infographic.min.js.map +1 -1
  5. package/esm/constants/service.d.ts +1 -1
  6. package/esm/constants/service.js +1 -1
  7. package/esm/designs/structures/chart-line.js +7 -4
  8. package/esm/editor/interactions/dblclick-edit-text.js +3 -3
  9. package/esm/editor/managers/interaction.js +6 -4
  10. package/esm/editor/plugins/components/button.d.ts +2 -1
  11. package/esm/editor/plugins/components/button.js +4 -4
  12. package/esm/editor/plugins/components/color-picker.d.ts +1 -0
  13. package/esm/editor/plugins/components/color-picker.js +3 -3
  14. package/esm/editor/plugins/components/popover.d.ts +3 -1
  15. package/esm/editor/plugins/components/popover.js +29 -9
  16. package/esm/editor/plugins/edit-bar/edit-bar.d.ts +3 -1
  17. package/esm/editor/plugins/edit-bar/edit-bar.js +17 -7
  18. package/esm/editor/plugins/edit-bar/edit-items/align-elements.js +6 -4
  19. package/esm/editor/plugins/edit-bar/edit-items/font-align.js +8 -5
  20. package/esm/editor/plugins/edit-bar/edit-items/font-color.js +7 -4
  21. package/esm/editor/plugins/edit-bar/edit-items/font-family.js +11 -9
  22. package/esm/editor/plugins/edit-bar/edit-items/font-size.js +8 -5
  23. package/esm/editor/plugins/edit-bar/edit-items/icon-color.js +7 -4
  24. package/esm/editor/plugins/edit-bar/edit-items/types.d.ts +5 -1
  25. package/esm/editor/plugins/reset-viewbox.d.ts +4 -1
  26. package/esm/editor/plugins/reset-viewbox.js +12 -6
  27. package/esm/editor/utils/index.d.ts +1 -0
  28. package/esm/editor/utils/index.js +1 -0
  29. package/esm/editor/utils/root.d.ts +3 -0
  30. package/esm/editor/utils/root.js +18 -0
  31. package/esm/exporter/svg.js +229 -3
  32. package/esm/options/parser.js +8 -6
  33. package/esm/options/types.d.ts +3 -3
  34. package/esm/renderer/renderer.js +1 -1
  35. package/esm/resource/loaders/search.js +2 -6
  36. package/esm/runtime/options.js +1 -1
  37. package/esm/syntax/index.js +56 -10
  38. package/esm/syntax/mapper.js +20 -6
  39. package/esm/syntax/parser.js +9 -0
  40. package/esm/syntax/types.d.ts +1 -1
  41. package/esm/templates/registry.d.ts +1 -0
  42. package/esm/templates/registry.js +6 -0
  43. package/esm/templates/utils.d.ts +1 -0
  44. package/esm/templates/utils.js +68 -0
  45. package/esm/themes/built-in.js +3 -0
  46. package/esm/utils/padding.js +1 -1
  47. package/esm/utils/style.d.ts +3 -1
  48. package/esm/utils/style.js +27 -4
  49. package/esm/version.d.ts +1 -1
  50. package/esm/version.js +1 -1
  51. package/lib/constants/service.d.ts +1 -1
  52. package/lib/constants/service.js +1 -1
  53. package/lib/designs/structures/chart-line.js +7 -4
  54. package/lib/editor/interactions/dblclick-edit-text.js +3 -3
  55. package/lib/editor/managers/interaction.js +7 -5
  56. package/lib/editor/plugins/components/button.d.ts +2 -1
  57. package/lib/editor/plugins/components/button.js +4 -4
  58. package/lib/editor/plugins/components/color-picker.d.ts +1 -0
  59. package/lib/editor/plugins/components/color-picker.js +3 -3
  60. package/lib/editor/plugins/components/popover.d.ts +3 -1
  61. package/lib/editor/plugins/components/popover.js +32 -12
  62. package/lib/editor/plugins/edit-bar/edit-bar.d.ts +3 -1
  63. package/lib/editor/plugins/edit-bar/edit-bar.js +17 -7
  64. package/lib/editor/plugins/edit-bar/edit-items/align-elements.js +6 -4
  65. package/lib/editor/plugins/edit-bar/edit-items/font-align.js +8 -5
  66. package/lib/editor/plugins/edit-bar/edit-items/font-color.js +7 -4
  67. package/lib/editor/plugins/edit-bar/edit-items/font-family.js +11 -9
  68. package/lib/editor/plugins/edit-bar/edit-items/font-size.js +8 -5
  69. package/lib/editor/plugins/edit-bar/edit-items/icon-color.js +7 -4
  70. package/lib/editor/plugins/edit-bar/edit-items/types.d.ts +5 -1
  71. package/lib/editor/plugins/reset-viewbox.d.ts +4 -1
  72. package/lib/editor/plugins/reset-viewbox.js +12 -6
  73. package/lib/editor/utils/index.d.ts +1 -0
  74. package/lib/editor/utils/index.js +1 -0
  75. package/lib/editor/utils/root.d.ts +3 -0
  76. package/lib/editor/utils/root.js +22 -0
  77. package/lib/exporter/svg.js +229 -3
  78. package/lib/options/parser.js +7 -5
  79. package/lib/options/types.d.ts +3 -3
  80. package/lib/renderer/renderer.js +1 -1
  81. package/lib/resource/loaders/search.js +2 -6
  82. package/lib/runtime/options.js +1 -1
  83. package/lib/syntax/index.js +56 -10
  84. package/lib/syntax/mapper.js +20 -6
  85. package/lib/syntax/parser.js +9 -0
  86. package/lib/syntax/types.d.ts +1 -1
  87. package/lib/templates/registry.d.ts +1 -0
  88. package/lib/templates/registry.js +7 -0
  89. package/lib/templates/utils.d.ts +1 -0
  90. package/lib/templates/utils.js +71 -0
  91. package/lib/themes/built-in.js +3 -0
  92. package/lib/utils/padding.js +1 -1
  93. package/lib/utils/style.d.ts +3 -1
  94. package/lib/utils/style.js +27 -4
  95. package/lib/version.d.ts +1 -1
  96. package/lib/version.js +1 -1
  97. package/package.json +1 -1
  98. package/src/constants/service.ts +1 -1
  99. package/src/designs/structures/chart-line.tsx +8 -4
  100. package/src/editor/interactions/dblclick-edit-text.ts +3 -2
  101. package/src/editor/managers/interaction.ts +9 -7
  102. package/src/editor/plugins/components/button.ts +5 -2
  103. package/src/editor/plugins/components/color-picker.ts +4 -2
  104. package/src/editor/plugins/components/popover.ts +31 -12
  105. package/src/editor/plugins/edit-bar/edit-bar.ts +26 -11
  106. package/src/editor/plugins/edit-bar/edit-items/align-elements.ts +7 -2
  107. package/src/editor/plugins/edit-bar/edit-items/font-align.ts +8 -3
  108. package/src/editor/plugins/edit-bar/edit-items/font-color.ts +7 -2
  109. package/src/editor/plugins/edit-bar/edit-items/font-family.ts +11 -7
  110. package/src/editor/plugins/edit-bar/edit-items/font-size.ts +8 -3
  111. package/src/editor/plugins/edit-bar/edit-items/icon-color.ts +7 -2
  112. package/src/editor/plugins/edit-bar/edit-items/types.ts +6 -1
  113. package/src/editor/plugins/reset-viewbox.ts +17 -8
  114. package/src/editor/utils/index.ts +1 -0
  115. package/src/editor/utils/root.ts +26 -0
  116. package/src/exporter/svg.ts +274 -3
  117. package/src/options/parser.ts +7 -6
  118. package/src/options/types.ts +3 -3
  119. package/src/renderer/renderer.ts +1 -1
  120. package/src/resource/loaders/search.ts +2 -5
  121. package/src/runtime/options.ts +1 -1
  122. package/src/syntax/index.ts +71 -10
  123. package/src/syntax/mapper.ts +20 -6
  124. package/src/syntax/parser.ts +10 -0
  125. package/src/syntax/types.ts +1 -0
  126. package/src/templates/registry.ts +6 -0
  127. package/src/templates/utils.ts +111 -0
  128. package/src/themes/built-in.ts +4 -0
  129. package/src/utils/padding.ts +1 -1
  130. package/src/utils/style.ts +31 -4
  131. package/src/version.ts +1 -1
@@ -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
- const { width, height } = getViewBox(svg);
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) defs.appendChild(symbolElement.cloneNode(true));
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
- return id.replace(/([!"#$%&'()*+,./:;<=>?@[\]^`{|}~])/g, '\\$1');
586
+
587
+ return cssEscape(id);
317
588
  }
318
589
 
319
590
  function removeDefs(svg: SVGSVGElement) {
@@ -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 = template
42
- ? getTemplate(template)
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 HTMLElement,
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, options.template);
67
+ const parsedData = parseData(data, resolvedTemplate);
67
68
  if (parsedData) parsed.data = parsedData;
68
69
 
69
- if (template) parsed.template = template;
70
+ if (resolvedTemplate) parsed.template = resolvedTemplate;
70
71
  if (templateOptions?.design || design) {
71
72
  const designOptions = {
72
73
  ...(resolvedThemeConfig
@@ -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
- /** 容器,可以是选择器或者 HTMLElement */
9
- container?: string | HTMLElement;
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: HTMLElement;
40
+ container: Element | ShadowRoot;
41
41
  width?: number | string;
42
42
  height?: number | string;
43
43
  padding?: Padding;
@@ -86,7 +86,7 @@ export class Renderer implements IRenderer {
86
86
  });
87
87
 
88
88
  try {
89
- observer.observe(document, {
89
+ observer.observe(this.options.container, {
90
90
  childList: true,
91
91
  subtree: true,
92
92
  });
@@ -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?.status || !Array.isArray(result.data?.data)) return null;
15
- return (result.data.data[0] as string) || null;
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
 
@@ -31,7 +31,7 @@ const createDefaultInteractions = () => [
31
31
 
32
32
  export const DEFAULT_OPTIONS: Partial<InfographicOptions> = {
33
33
  padding: 20,
34
- theme: 'light',
34
+ theme: 'default',
35
35
  themeConfig: {
36
36
  palette: 'antv',
37
37
  },
@@ -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 (!allowedRootKeys.has(key)) {
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,
@@ -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
- if (trimmed === 'true') return true;
22
- if (trimmed === 'false') return false;
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
- if (value !== 'true' && value !== 'false') {
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 value === 'true';
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
- if (!schema.values.includes(value)) {
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 value;
287
+ return enumValue;
274
288
  }
275
289
  case 'array': {
276
290
  if (node.kind === 'array') {
@@ -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
 
@@ -22,6 +22,7 @@ export interface ArrayNode {
22
22
  }
23
23
 
24
24
  export type SyntaxErrorCode =
25
+ | 'implicit_template'
25
26
  | 'unknown_key'
26
27
  | 'schema_mismatch'
27
28
  | 'invalid_value'
@@ -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
  }