@antv/infographic 0.2.14 → 0.2.16

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 (101) hide show
  1. package/README.md +39 -5
  2. package/README.zh-CN.md +39 -5
  3. package/dist/infographic.min.js +168 -166
  4. package/dist/infographic.min.js.map +1 -1
  5. package/esm/designs/structures/index.d.ts +1 -0
  6. package/esm/designs/structures/index.js +1 -0
  7. package/esm/designs/structures/relation-dagre-flow.js +4 -139
  8. package/esm/designs/structures/sequence-interaction.d.ts +54 -0
  9. package/esm/designs/structures/sequence-interaction.js +461 -0
  10. package/esm/designs/structures/sequence-timeline.d.ts +1 -0
  11. package/esm/designs/structures/sequence-timeline.js +4 -2
  12. package/esm/designs/utils/geometry.d.ts +44 -0
  13. package/esm/designs/utils/geometry.js +244 -0
  14. package/esm/designs/utils/index.d.ts +1 -0
  15. package/esm/designs/utils/index.js +1 -0
  16. package/esm/editor/managers/sync-registry.d.ts +2 -1
  17. package/esm/editor/types/editor.d.ts +2 -1
  18. package/esm/editor/types/sync.d.ts +2 -1
  19. package/esm/editor/utils/object.js +46 -39
  20. package/esm/exporter/png.js +2 -2
  21. package/esm/exporter/svg.js +9 -1
  22. package/esm/exporter/types.d.ts +10 -0
  23. package/esm/options/types.d.ts +6 -0
  24. package/esm/runtime/Infographic.js +20 -7
  25. package/esm/syntax/index.js +40 -20
  26. package/esm/syntax/parser.js +80 -3
  27. package/esm/syntax/relations.js +26 -2
  28. package/esm/syntax/schema.js +1 -0
  29. package/esm/templates/built-in.js +5 -4
  30. package/esm/templates/sequence-interaction.d.ts +2 -0
  31. package/esm/templates/sequence-interaction.js +76 -0
  32. package/esm/types/data.d.ts +1 -0
  33. package/esm/utils/index.d.ts +1 -0
  34. package/esm/utils/index.js +1 -0
  35. package/esm/utils/measure-text.js +31 -3
  36. package/esm/utils/types.d.ts +16 -0
  37. package/esm/utils/types.js +12 -0
  38. package/esm/version.d.ts +1 -1
  39. package/esm/version.js +1 -1
  40. package/lib/designs/structures/index.d.ts +1 -0
  41. package/lib/designs/structures/index.js +1 -0
  42. package/lib/designs/structures/relation-dagre-flow.js +5 -140
  43. package/lib/designs/structures/sequence-interaction.d.ts +54 -0
  44. package/lib/designs/structures/sequence-interaction.js +465 -0
  45. package/lib/designs/structures/sequence-timeline.d.ts +1 -0
  46. package/lib/designs/structures/sequence-timeline.js +4 -2
  47. package/lib/designs/utils/geometry.d.ts +44 -0
  48. package/lib/designs/utils/geometry.js +256 -0
  49. package/lib/designs/utils/index.d.ts +1 -0
  50. package/lib/designs/utils/index.js +1 -0
  51. package/lib/editor/managers/sync-registry.d.ts +2 -1
  52. package/lib/editor/types/editor.d.ts +2 -1
  53. package/lib/editor/types/sync.d.ts +2 -1
  54. package/lib/editor/utils/object.js +45 -38
  55. package/lib/exporter/png.js +2 -2
  56. package/lib/exporter/svg.js +9 -1
  57. package/lib/exporter/types.d.ts +10 -0
  58. package/lib/options/types.d.ts +6 -0
  59. package/lib/runtime/Infographic.js +19 -6
  60. package/lib/syntax/index.js +40 -20
  61. package/lib/syntax/parser.js +80 -3
  62. package/lib/syntax/relations.js +26 -2
  63. package/lib/syntax/schema.js +1 -0
  64. package/lib/templates/built-in.js +5 -4
  65. package/lib/templates/sequence-interaction.d.ts +2 -0
  66. package/lib/templates/sequence-interaction.js +79 -0
  67. package/lib/types/data.d.ts +1 -0
  68. package/lib/utils/index.d.ts +1 -0
  69. package/lib/utils/index.js +1 -0
  70. package/lib/utils/measure-text.js +30 -2
  71. package/lib/utils/types.d.ts +16 -0
  72. package/lib/utils/types.js +13 -0
  73. package/lib/version.d.ts +1 -1
  74. package/lib/version.js +1 -1
  75. package/package.json +1 -1
  76. package/src/designs/structures/index.ts +1 -0
  77. package/src/designs/structures/relation-dagre-flow.tsx +14 -178
  78. package/src/designs/structures/sequence-interaction.tsx +931 -0
  79. package/src/designs/structures/sequence-timeline.tsx +18 -15
  80. package/src/designs/utils/geometry.tsx +315 -0
  81. package/src/designs/utils/index.ts +1 -0
  82. package/src/editor/managers/sync-registry.ts +2 -1
  83. package/src/editor/types/editor.ts +2 -1
  84. package/src/editor/types/sync.ts +3 -1
  85. package/src/editor/utils/object.ts +50 -40
  86. package/src/exporter/png.ts +3 -2
  87. package/src/exporter/svg.ts +14 -1
  88. package/src/exporter/types.ts +10 -0
  89. package/src/options/types.ts +7 -0
  90. package/src/runtime/Infographic.tsx +27 -17
  91. package/src/syntax/index.ts +51 -18
  92. package/src/syntax/parser.ts +101 -3
  93. package/src/syntax/relations.ts +29 -2
  94. package/src/syntax/schema.ts +1 -0
  95. package/src/templates/built-in.ts +4 -2
  96. package/src/templates/sequence-interaction.ts +101 -0
  97. package/src/types/data.ts +1 -0
  98. package/src/utils/index.ts +1 -0
  99. package/src/utils/measure-text.ts +35 -3
  100. package/src/utils/types.ts +61 -0
  101. package/src/version.ts +1 -1
@@ -11,7 +11,7 @@ import {
11
11
  ParsedInfographicOptions,
12
12
  parseOptions,
13
13
  } from '../options';
14
- import { Renderer } from '../renderer';
14
+ import { DEFAULT_FONT, Renderer, setDefaultFont } from '../renderer';
15
15
  import { waitForSvgLoads } from '../resource';
16
16
  import { parseSyntax, type SyntaxError } from '../syntax';
17
17
  import { IEventEmitter } from '../types';
@@ -133,29 +133,39 @@ export class Infographic {
133
133
  * Compose the SVG template
134
134
  */
135
135
  compose(parsedOptions: ParsedInfographicOptions): SVGSVGElement {
136
- const { design, data } = parsedOptions;
136
+ const { design, data, themeConfig } = parsedOptions;
137
137
  const { title, item, items, structure } = design;
138
138
  const { component: Structure, props: structureProps } = structure;
139
139
  const Title = title.component;
140
140
  const Item = item.component;
141
141
  const Items = items.map((it) => it.component);
142
142
 
143
- const svg = renderSVG(
144
- <Structure
145
- data={data}
146
- Title={Title}
147
- Item={Item}
148
- Items={Items}
149
- options={parsedOptions}
150
- {...structureProps}
151
- />,
152
- );
153
-
154
- const template = parseSVG(svg);
155
- if (!template) {
156
- throw new Error('Failed to parse SVG template');
143
+ // Apply theme font-family before measurement so measureText uses the correct font
144
+ const themeFontFamily = themeConfig?.base?.text?.['font-family'];
145
+ const previousDefaultFont = DEFAULT_FONT;
146
+ if (themeFontFamily) setDefaultFont(themeFontFamily);
147
+
148
+ try {
149
+ const svg = renderSVG(
150
+ <Structure
151
+ data={data}
152
+ Title={Title}
153
+ Item={Item}
154
+ Items={Items}
155
+ options={parsedOptions}
156
+ {...structureProps}
157
+ />,
158
+ );
159
+
160
+ const template = parseSVG(svg);
161
+ if (!template) {
162
+ throw new Error('Failed to parse SVG template');
163
+ }
164
+ return template;
165
+ } finally {
166
+ // Restore previous default font
167
+ if (themeFontFamily) setDefaultFont(previousDefaultFont);
157
168
  }
158
- return template;
159
169
  }
160
170
 
161
171
  getTypes() {
@@ -10,7 +10,7 @@ import {
10
10
  TemplateSchema,
11
11
  ThemeSchema,
12
12
  } from './schema';
13
- import type { SyntaxNode, SyntaxParseResult } from './types';
13
+ import type { ObjectSchema, SyntaxNode, SyntaxParseResult } from './types';
14
14
 
15
15
  function normalizeItems(items: ItemDatum[]) {
16
16
  const seen = new Set<string>();
@@ -112,24 +112,57 @@ export function parseSyntax(input: string): SyntaxParseResult {
112
112
  );
113
113
  if (parsed.relations.length > 0 || parsed.items.length > 0) {
114
114
  const current = (options.data ?? {}) as Record<string, any>;
115
- const existingItems = Array.isArray(current.items)
116
- ? (current.items as ItemDatum[])
117
- : [];
118
- const normalizedItems = normalizeItems(existingItems);
119
- const itemMap = new Map<string, ItemDatum>();
120
- normalizedItems.forEach((item) => {
121
- if (item.id) itemMap.set(item.id, item);
122
- });
123
- parsed.items.forEach((item) => {
124
- const existing = itemMap.get(item.id as string);
125
- if (existing) {
126
- if (!existing.label && item.label) existing.label = item.label;
127
- } else {
128
- normalizedItems.push(item);
129
- itemMap.set(item.id as string, item);
115
+
116
+ // 优先使用已存在的数据列表 (sequences, lists, etc.)
117
+ const dataKeys = Object.keys(
118
+ (DataSchema as ObjectSchema).fields,
119
+ ).filter((key) => key !== 'items' && key !== 'relations');
120
+ let hasStructuredData = false;
121
+
122
+ // 尝试找到一个非空的数据源
123
+ for (const key of dataKeys) {
124
+ if (Array.isArray(current[key]) && current[key].length > 0) {
125
+ hasStructuredData = true;
126
+ break;
130
127
  }
131
- });
132
- current.items = normalizedItems;
128
+ }
129
+
130
+ // 如果 items 已包含层级结构数据(带 children),也视为结构化数据
131
+ if (
132
+ !hasStructuredData &&
133
+ Array.isArray(current.items) &&
134
+ current.items.length > 0 &&
135
+ current.items.some(
136
+ (item: any) =>
137
+ Array.isArray(item.children) && item.children.length > 0,
138
+ )
139
+ ) {
140
+ hasStructuredData = true;
141
+ }
142
+
143
+ // 如果没有找到其他数据源,才尝试合并 items
144
+ if (!hasStructuredData) {
145
+ const existingItems = Array.isArray(current.items)
146
+ ? (current.items as ItemDatum[])
147
+ : [];
148
+
149
+ const normalizedItems = normalizeItems(existingItems);
150
+ const itemMap = new Map<string, ItemDatum>();
151
+ normalizedItems.forEach((item) => {
152
+ if (item.id) itemMap.set(item.id, item);
153
+ });
154
+ parsed.items.forEach((item) => {
155
+ const existing = itemMap.get(item.id as string);
156
+ if (existing) {
157
+ if (!existing.label && item.label) existing.label = item.label;
158
+ } else {
159
+ normalizedItems.push(item);
160
+ itemMap.set(item.id as string, item);
161
+ }
162
+ });
163
+ current.items = normalizedItems;
164
+ }
165
+
133
166
  current.relations = parsed.relations;
134
167
  options.data = current as any;
135
168
  }
@@ -58,6 +58,97 @@ function parseKeyValue(raw: string) {
58
58
  return { key: text, value: undefined };
59
59
  }
60
60
 
61
+ interface AssignEntryResult {
62
+ parent: ObjectNode;
63
+ key: string;
64
+ }
65
+
66
+ function isUnsafeObjectKey(key: string) {
67
+ return key === '__proto__' || key === 'constructor' || key === 'prototype';
68
+ }
69
+
70
+ function assignObjectEntry(
71
+ parent: ObjectNode,
72
+ rawKey: string,
73
+ node: ObjectNode,
74
+ line: number,
75
+ errors: SyntaxError[],
76
+ ): AssignEntryResult | null {
77
+ if (!rawKey.includes('.')) {
78
+ if (isUnsafeObjectKey(rawKey)) {
79
+ errors.push({
80
+ path: rawKey,
81
+ line,
82
+ code: 'bad_syntax',
83
+ message: `Invalid key part: ${rawKey}`,
84
+ raw: rawKey,
85
+ });
86
+ return null;
87
+ }
88
+ parent.entries[rawKey] = node;
89
+ return { parent, key: rawKey };
90
+ }
91
+
92
+ const parts = rawKey.split('.');
93
+ if (parts.some((part) => !part)) {
94
+ errors.push({
95
+ path: rawKey,
96
+ line,
97
+ code: 'bad_syntax',
98
+ message: 'Invalid dotted key path.',
99
+ raw: rawKey,
100
+ });
101
+ return null;
102
+ }
103
+
104
+ let current = parent;
105
+ for (let index = 0; index < parts.length - 1; index += 1) {
106
+ const part = parts[index];
107
+ if (isUnsafeObjectKey(part)) {
108
+ errors.push({
109
+ path: rawKey,
110
+ line,
111
+ code: 'bad_syntax',
112
+ message: `Invalid key part in dotted path: ${part}`,
113
+ raw: rawKey,
114
+ });
115
+ return null;
116
+ }
117
+ const existing = current.entries[part];
118
+ if (!existing) {
119
+ const container = createObjectNode(line);
120
+ current.entries[part] = container;
121
+ current = container;
122
+ continue;
123
+ }
124
+ if (existing.kind !== 'object') {
125
+ errors.push({
126
+ path: parts.slice(0, index + 1).join('.'),
127
+ line,
128
+ code: 'bad_syntax',
129
+ message: 'Cannot assign dotted key under a list value.',
130
+ raw: rawKey,
131
+ });
132
+ return null;
133
+ }
134
+ current = existing;
135
+ }
136
+
137
+ const finalKey = parts[parts.length - 1];
138
+ if (isUnsafeObjectKey(finalKey)) {
139
+ errors.push({
140
+ path: rawKey,
141
+ line,
142
+ code: 'bad_syntax',
143
+ message: `Invalid key part in dotted path: ${finalKey}`,
144
+ raw: rawKey,
145
+ });
146
+ return null;
147
+ }
148
+ current.entries[finalKey] = node;
149
+ return { parent: current, key: finalKey };
150
+ }
151
+
61
152
  function createObjectNode(line: number, value?: string): ObjectNode {
62
153
  return { kind: 'object', line, value, entries: {} };
63
154
  }
@@ -203,12 +294,19 @@ export function parseSyntaxToAst(input: string): ParseResult {
203
294
  }
204
295
 
205
296
  const node = createObjectNode(lineNumber, parsed.value);
206
- parentNode.entries[parsed.key] = node;
297
+ const assigned = assignObjectEntry(
298
+ parentNode,
299
+ parsed.key,
300
+ node,
301
+ lineNumber,
302
+ errors,
303
+ );
304
+ if (!assigned) return;
207
305
  stack.push({
208
306
  indent,
209
307
  node,
210
- parent: parentNode,
211
- key: parsed.key,
308
+ parent: assigned.parent,
309
+ key: assigned.key,
212
310
  });
213
311
  });
214
312
 
@@ -3,8 +3,8 @@ import { mapWithSchema } from './mapper';
3
3
  import { RelationSchema } from './schema';
4
4
  import type { SyntaxError, SyntaxNode } from './types';
5
5
 
6
- const RELATION_TOKEN = /[<>=o.x-]{2,}/;
7
- const ARROW_TOKEN = /[<>=o.x-]{2,}/g;
6
+ const RELATION_TOKEN = /(?:[<>o.x-]{2,}|[<>=]{2,})/;
7
+ const ARROW_TOKEN = /(?:[<>o.x-]{2,}|[<>=]{2,})/g;
8
8
 
9
9
  interface ParsedNode {
10
10
  id: string;
@@ -125,6 +125,33 @@ function readEdge(text: string, startIndex: number): ParsedEdge | null {
125
125
  let directionToken = arrowToken;
126
126
  let index = arrowEnd;
127
127
 
128
+ // Detect split bidirectional arrow pattern: <- label ->
129
+ {
130
+ const leftHasLeft = directionToken.includes('<');
131
+ const leftHasRight = directionToken.includes('>');
132
+ if (leftHasLeft && !leftHasRight) {
133
+ const lookahead = new RegExp(ARROW_TOKEN.source, 'g');
134
+ lookahead.lastIndex = arrowEnd;
135
+ const rightMatch = lookahead.exec(text);
136
+ if (
137
+ rightMatch &&
138
+ rightMatch[0].includes('>') &&
139
+ !rightMatch[0].includes('<')
140
+ ) {
141
+ const middleText = text.slice(arrowEnd, rightMatch.index).trim();
142
+ if (middleText) {
143
+ const splitLabel = normalizeLabel(middleText);
144
+ return {
145
+ label: splitLabel || label,
146
+ direction: 'both',
147
+ reverse: false,
148
+ nextIndex: rightMatch.index + rightMatch[0].length,
149
+ };
150
+ }
151
+ }
152
+ }
153
+ }
154
+
128
155
  index = skipSpaces(text, index);
129
156
  if (text[index] === '|') {
130
157
  const pipeEnd = text.indexOf('|', index + 1);
@@ -60,6 +60,7 @@ export const RelationSchema: ObjectSchema = object(
60
60
  direction: enumOf(['forward', 'both', 'none']),
61
61
  showArrow: enumOf(['true', 'false']),
62
62
  arrowType: enumOf(['arrow', 'triangle', 'diamond']),
63
+ lineStyle: enumOf(['solid', 'dashed']),
63
64
  },
64
65
  { allowUnknown: true },
65
66
  );
@@ -6,6 +6,7 @@ import { hierarchyTreeTemplates } from './hierarchy-tree';
6
6
  import { listZigzagTemplates } from './list-zigzag';
7
7
  import { registerTemplate } from './registry';
8
8
  import { relationDagreFlowTemplates } from './relation-dagre-flow';
9
+ import { sequenceInteractionTemplates } from './sequence-interaction';
9
10
  import { sequenceStairsTemplates } from './sequence-stairs';
10
11
  import type { TemplateOptions } from './types';
11
12
  import { wordCloudTemplate } from './word-cloud';
@@ -209,7 +210,7 @@ const BUILT_IN_TEMPLATES: Record<string, TemplateOptions> = {
209
210
  'sequence-timeline-plain-text': {
210
211
  design: {
211
212
  title: 'default',
212
- structure: { type: 'sequence-timeline' },
213
+ structure: { type: 'sequence-timeline', showStepLabels: false },
213
214
  items: [{ type: 'plain-text' }],
214
215
  },
215
216
  },
@@ -230,7 +231,7 @@ const BUILT_IN_TEMPLATES: Record<string, TemplateOptions> = {
230
231
  'sequence-timeline-simple': {
231
232
  design: {
232
233
  title: 'default',
233
- structure: { type: 'sequence-timeline', gap: 20 },
234
+ structure: { type: 'sequence-timeline', gap: 20, showStepLabels: false },
234
235
  items: [{ type: 'simple', positionV: 'middle' }],
235
236
  },
236
237
  },
@@ -752,6 +753,7 @@ const BUILT_IN_TEMPLATES: Record<string, TemplateOptions> = {
752
753
  ...wordCloudTemplate,
753
754
  ...listZigzagTemplates,
754
755
  ...relationDagreFlowTemplates,
756
+ ...sequenceInteractionTemplates,
755
757
  ...hierarchyStructureTemplates,
756
758
  };
757
759
 
@@ -0,0 +1,101 @@
1
+ import type { TemplateOptions } from './types';
2
+
3
+ // 多样化的节点样式
4
+ const items = {
5
+ 'badge-card': {
6
+ type: 'badge-card',
7
+ width: 160,
8
+ height: 60,
9
+ },
10
+ 'compact-card': {
11
+ type: 'compact-card',
12
+ width: 180,
13
+ height: 60,
14
+ },
15
+ 'capsule-item': {
16
+ type: 'capsule-item',
17
+ width: 150,
18
+ height: 60,
19
+ },
20
+ 'rounded-rect-node': {
21
+ type: 'rounded-rect-node',
22
+ width: 140,
23
+ height: 60,
24
+ },
25
+ } as const;
26
+
27
+ // 基础结构属性
28
+ const baseStructureAttrs = {
29
+ type: 'sequence-interaction',
30
+ showLaneHeader: true,
31
+ arrowType: 'triangle',
32
+ } as const;
33
+
34
+ // 箭头样式配置
35
+ const arrowStyles = {
36
+ default: {},
37
+ dashed: {
38
+ edgeStyle: 'dashed',
39
+ },
40
+ animated: {
41
+ edgeStyle: 'dashed',
42
+ animated: true,
43
+ },
44
+ } as const;
45
+
46
+ // 结构布局配置
47
+ const structures = {
48
+ // 默认:带生命线
49
+ default: {
50
+ ...baseStructureAttrs,
51
+ showLifeline: true,
52
+ nodeGap: 40,
53
+ },
54
+ // 紧凑:更小间距
55
+ compact: {
56
+ ...baseStructureAttrs,
57
+ showLifeline: true,
58
+ nodeGap: 20,
59
+ },
60
+ // 宽松:更大间距
61
+ wide: {
62
+ ...baseStructureAttrs,
63
+ showLifeline: true,
64
+ nodeGap: 60,
65
+ },
66
+ } as const;
67
+
68
+ export const sequenceInteractionTemplates: Record<string, TemplateOptions> = {};
69
+
70
+ // 排除某些不合适的组合
71
+ const omit: string[] = [
72
+ // 后续如果有不合适的可以排除掉
73
+ ];
74
+
75
+ Object.entries(structures).forEach(([strKey, strAttrs]) => {
76
+ Object.entries(arrowStyles).forEach(([arrowKey, arrowAttrs]) => {
77
+ Object.entries(items).forEach(([itemKey, itemAttrs]) => {
78
+ const parts = [strKey];
79
+ if (arrowKey !== 'default') {
80
+ parts.push(arrowKey);
81
+ }
82
+ parts.push(itemKey);
83
+
84
+ const appendix = parts.join('-');
85
+ if (omit.includes(appendix)) return;
86
+
87
+ const templateKey = `sequence-interaction-${appendix}`;
88
+
89
+ sequenceInteractionTemplates[templateKey] = {
90
+ design: {
91
+ title: 'default',
92
+ structure: {
93
+ ...strAttrs,
94
+ ...arrowAttrs,
95
+ },
96
+ item: itemAttrs,
97
+ },
98
+ };
99
+ });
100
+ });
101
+ });
package/src/types/data.ts CHANGED
@@ -56,6 +56,7 @@ export interface RelationEdgeDatum extends BaseDatum {
56
56
  direction?: 'forward' | 'both' | 'none';
57
57
  showArrow?: boolean;
58
58
  arrowType?: 'arrow' | 'triangle' | 'diamond';
59
+ lineStyle?: 'solid' | 'dashed';
59
60
  }
60
61
 
61
62
  /**
@@ -17,5 +17,6 @@ export * from './recognizer';
17
17
  export * from './style';
18
18
  export * from './svg';
19
19
  export * from './text';
20
+ export * from './types';
20
21
  export * from './uuid';
21
22
  export * from './viewbox';
@@ -1,10 +1,15 @@
1
1
  import { measureText as measure, registerFont } from 'measury';
2
+ import Tegakizatsu from 'measury/fonts/851tegakizatsu-Regular';
2
3
  import AlibabaPuHuiTi from 'measury/fonts/AlibabaPuHuiTi-Regular';
4
+ import Arial from 'measury/fonts/Arial-Regular';
5
+ import LXGWWenKai from 'measury/fonts/LXGWWenKai-Regular';
6
+ import SourceHanSans from 'measury/fonts/SourceHanSans-Regular';
7
+ import SourceHanSerif from 'measury/fonts/SourceHanSerif-Regular';
3
8
  import { JSXNode, TextProps } from '../jsx';
4
9
  import { DEFAULT_FONT } from '../renderer';
5
- import { encodeFontFamily } from './font';
10
+ import { decodeFontFamily, encodeFontFamily } from './font';
6
11
 
7
- let FONT_EXTEND_FACTOR = 1.01;
12
+ let FONT_EXTEND_FACTOR = 1.015;
8
13
 
9
14
  export const setFontExtendFactor = (factor: number) => {
10
15
  FONT_EXTEND_FACTOR = factor;
@@ -12,6 +17,28 @@ export const setFontExtendFactor = (factor: number) => {
12
17
 
13
18
  registerFont(AlibabaPuHuiTi);
14
19
 
20
+ // Lazy-register extra measury fonts on first use (SSR only needs glyph data).
21
+ const EXTRA_MEASURY_FONTS: Record<string, Parameters<typeof registerFont>[0]> =
22
+ {
23
+ '851tegakizatsu': Tegakizatsu,
24
+ Arial: Arial,
25
+ 'LXGW WenKai': LXGWWenKai,
26
+ 'Source Han Sans': SourceHanSans,
27
+ 'Source Han Serif': SourceHanSerif,
28
+ };
29
+ const registeredMeasuryFonts = new Set<string>();
30
+
31
+ function ensureMeasuryFont(fontFamily: string) {
32
+ // decodeFontFamily: '"851tegakizatsu", sans-serif' → '851tegakizatsu, sans-serif'
33
+ // split by comma and take the first family name
34
+ const primary = decodeFontFamily(fontFamily)?.split(',')[0]?.trim();
35
+ if (!primary || registeredMeasuryFonts.has(primary)) return;
36
+ const data = EXTRA_MEASURY_FONTS[primary];
37
+ if (!data) return;
38
+ registerFont(data);
39
+ registeredMeasuryFonts.add(primary);
40
+ }
41
+
15
42
  let canvasContext: CanvasRenderingContext2D | null | undefined = undefined;
16
43
  let measureSpan: HTMLSpanElement | null = null;
17
44
 
@@ -121,13 +148,18 @@ export function measureText(
121
148
  } = attrs;
122
149
 
123
150
  const content = text.toString();
151
+ ensureMeasuryFont(fontFamily);
124
152
  const options = {
125
153
  fontFamily,
126
154
  fontSize: parseFloat(fontSize.toString()),
127
155
  fontWeight,
128
156
  lineHeight,
129
157
  };
130
- const fallback = () => measure(content, options);
158
+ const fallback = () =>
159
+ measure(content, {
160
+ ...options,
161
+ fontFamily: decodeFontFamily(fontFamily),
162
+ });
131
163
  const metrics = measureTextInBrowser(content, options) ?? fallback();
132
164
 
133
165
  // 额外添加 1% 宽高
@@ -0,0 +1,61 @@
1
+ // =============================================================================
2
+ // Path Helper Types (路径生成工具类型)
3
+ //
4
+ // 作用:递归提取对象的所有可选路径(点语法),用于 SyncRegistry 等场景的类型安全与补全。
5
+ //
6
+ // [Input]:
7
+ // type Obj = { a: { b: { c: string } }, d: number };
8
+ //
9
+ // [Output]:
10
+ // "a" | "a.b" | "a.b.c" | "d"
11
+ // =============================================================================
12
+
13
+ // 1. 递归深度控制:定义一个元组,用于递减深度 (D -> D-1)
14
+ // Prev[3] => 2, Prev[2] => 1, ... Prev[0] => never
15
+ type Prev = [never, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
16
+
17
+ // 2. 终止节点类型:遇到这些类型停止递归
18
+ // 包括基本类型、数组、函数等,只深入遍历普通对象
19
+ type StopType =
20
+ | string
21
+ | number
22
+ | boolean
23
+ | symbol
24
+ | undefined
25
+ | null
26
+ | ((...args: any[]) => any)
27
+ | Array<any>;
28
+
29
+ // 3. 辅助:拼接两个路径片段 (K.P)
30
+ type Join<K, P> = K extends string | number
31
+ ? P extends string | number
32
+ ? `${K}${'' extends P ? '' : '.'}${P}`
33
+ : never
34
+ : never;
35
+
36
+ // 4. 辅助:提取对象中明确定义的 Key (过滤掉索引签名 [key: string]: any)
37
+ // 使用 Key Remapping (TS 4.1+) 技术
38
+ type ValidKey<T> = keyof {
39
+ [K in keyof T as string extends K
40
+ ? never
41
+ : number extends K
42
+ ? never
43
+ : K]: any;
44
+ };
45
+
46
+ /**
47
+ * 递归生成 T 的所有点语法路径 (例如 "design.structure" 或 "data.items")
48
+ * @template T 目标对象类型
49
+ * @template D 递归深度限制,默认 3 层 (足以覆盖大多数配置嵌套)
50
+ */
51
+ export type Path<T, D extends number = 3> = [D] extends [never]
52
+ ? never // A. 达到最大深度,终止
53
+ : T extends StopType
54
+ ? never // B. 遇到终止类型,终止
55
+ : {
56
+ // C. 遍历所有有效的 Key,并移除可选修饰符 (-?) 以处理 undefined
57
+ [K in ValidKey<T>]-?: K extends string | number
58
+ ? // 生成: 当前路径 K 或 "K + 子路径"
59
+ K | Join<K, Path<NonNullable<T[K]>, Prev[D]>>
60
+ : never;
61
+ }[ValidKey<T>]; // D. 提取所有生成路径的联合类型
package/src/version.ts CHANGED
@@ -1 +1 @@
1
- export const VERSION = '0.2.14';
1
+ export const VERSION = '0.2.16';