@hirokisakabe/pom 1.3.0 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +27 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/parseXml.d.ts +24 -0
- package/dist/parseXml.d.ts.map +1 -0
- package/dist/parseXml.js +332 -0
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -192,6 +192,33 @@ const slides = expandComponentSlides(llmOutput, registry);
|
|
|
192
192
|
const pptx = await buildPptx(slides, { w: 1280, h: 720 });
|
|
193
193
|
```
|
|
194
194
|
|
|
195
|
+
## XML Input
|
|
196
|
+
|
|
197
|
+
`parseXml` allows you to describe slides in XML instead of JSON. XML tags (PascalCase) are mapped to POM node types, and attribute values are automatically type-coerced using Zod schemas.
|
|
198
|
+
|
|
199
|
+
```typescript
|
|
200
|
+
import { parseXml, buildPptx } from "@hirokisakabe/pom";
|
|
201
|
+
|
|
202
|
+
const xml = `
|
|
203
|
+
<VStack gap="16" padding="32">
|
|
204
|
+
<Text fontPx="32" bold="true">売上レポート</Text>
|
|
205
|
+
<HStack gap="16">
|
|
206
|
+
<Chart chartType="bar" w="400" h="300"
|
|
207
|
+
data='[{ "name": "Q1", "labels": ["1月","2月","3月"], "values": [100,120,90] }]'
|
|
208
|
+
/>
|
|
209
|
+
<Text fontPx="18" color="00AA00">前年比 +15%</Text>
|
|
210
|
+
</HStack>
|
|
211
|
+
</VStack>
|
|
212
|
+
`;
|
|
213
|
+
|
|
214
|
+
const nodes = parseXml(xml);
|
|
215
|
+
const pptx = await buildPptx(nodes, { w: 1280, h: 720 });
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
Unknown tags are treated as component nodes (`{ type: "component", name: tagName, props: {...} }`), which can be resolved with `expandComponents()`.
|
|
219
|
+
|
|
220
|
+
For more details, see [LLM Integration - XML Format](./docs/llm-integration.md#xml-format).
|
|
221
|
+
|
|
195
222
|
## Documentation
|
|
196
223
|
|
|
197
224
|
| Document | Description |
|
package/dist/index.d.ts
CHANGED
|
@@ -4,4 +4,5 @@ export { buildPptx } from "./buildPptx.ts";
|
|
|
4
4
|
export type { TextMeasurementMode } from "./buildPptx.ts";
|
|
5
5
|
export { defineComponent, defaultTheme, mergeTheme, expandComponents, expandComponentSlides, } from "./component.ts";
|
|
6
6
|
export type { Theme, ComponentRegistry } from "./component.ts";
|
|
7
|
+
export { parseXml } from "./parseXml.ts";
|
|
7
8
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAC;AAC3B,cAAc,kBAAkB,CAAC;AACjC,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAC3C,YAAY,EAAE,mBAAmB,EAAE,MAAM,gBAAgB,CAAC;AAC1D,OAAO,EACL,eAAe,EACf,YAAY,EACZ,UAAU,EACV,gBAAgB,EAChB,qBAAqB,GACtB,MAAM,gBAAgB,CAAC;AACxB,YAAY,EAAE,KAAK,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAC;AAC3B,cAAc,kBAAkB,CAAC;AACjC,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAC3C,YAAY,EAAE,mBAAmB,EAAE,MAAM,gBAAgB,CAAC;AAC1D,OAAO,EACL,eAAe,EACf,YAAY,EACZ,UAAU,EACV,gBAAgB,EAChB,qBAAqB,GACtB,MAAM,gBAAgB,CAAC;AACxB,YAAY,EAAE,KAAK,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AAC/D,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { POMNode } from "./types.ts";
|
|
2
|
+
/**
|
|
3
|
+
* XML 文字列を POMNode 配列に変換する。
|
|
4
|
+
*
|
|
5
|
+
* XML タグは POM ノードタイプにマッピングされ、属性値は Zod スキーマを参照して
|
|
6
|
+
* 適切な型(number, boolean, array, object)に変換される。
|
|
7
|
+
* 組み込みノード以外のタグ名はカスタムコンポーネントとして扱われる。
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```typescript
|
|
11
|
+
* import { parseXml, buildPptx } from "@hirokisakabe/pom";
|
|
12
|
+
*
|
|
13
|
+
* const xml = `
|
|
14
|
+
* <VStack gap="16" padding="32">
|
|
15
|
+
* <Text fontPx="32" bold="true">売上レポート</Text>
|
|
16
|
+
* </VStack>
|
|
17
|
+
* `;
|
|
18
|
+
*
|
|
19
|
+
* const nodes = parseXml(xml);
|
|
20
|
+
* const pptx = await buildPptx(nodes, { w: 1280, h: 720 });
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
export declare function parseXml(xmlString: string): POMNode[];
|
|
24
|
+
//# sourceMappingURL=parseXml.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"parseXml.d.ts","sourceRoot":"","sources":["../src/parseXml.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AAoX1C;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,QAAQ,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,EAAE,CAuBrD"}
|
package/dist/parseXml.js
ADDED
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
import { XMLParser } from "fast-xml-parser";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { inputTextNodeSchema, inputImageNodeSchema, inputTableNodeSchema, inputShapeNodeSchema, inputChartNodeSchema, inputTimelineNodeSchema, inputMatrixNodeSchema, inputTreeNodeSchema, inputFlowNodeSchema, inputProcessArrowNodeSchema, inputLineNodeSchema, inputBaseNodeSchema, } from "./inputSchema.js";
|
|
4
|
+
import { alignItemsSchema, justifyContentSchema, shadowStyleSchema, } from "./types.js";
|
|
5
|
+
// ===== Tag name → POM node type mapping =====
|
|
6
|
+
const TAG_TO_TYPE = {
|
|
7
|
+
Text: "text",
|
|
8
|
+
Image: "image",
|
|
9
|
+
Table: "table",
|
|
10
|
+
Shape: "shape",
|
|
11
|
+
Chart: "chart",
|
|
12
|
+
Timeline: "timeline",
|
|
13
|
+
Matrix: "matrix",
|
|
14
|
+
Tree: "tree",
|
|
15
|
+
Flow: "flow",
|
|
16
|
+
ProcessArrow: "processArrow",
|
|
17
|
+
Line: "line",
|
|
18
|
+
Box: "box",
|
|
19
|
+
VStack: "vstack",
|
|
20
|
+
HStack: "hstack",
|
|
21
|
+
Layer: "layer",
|
|
22
|
+
};
|
|
23
|
+
function extractShape(schema) {
|
|
24
|
+
return schema.shape;
|
|
25
|
+
}
|
|
26
|
+
const leafNodeShapes = {
|
|
27
|
+
text: extractShape(inputTextNodeSchema),
|
|
28
|
+
image: extractShape(inputImageNodeSchema),
|
|
29
|
+
table: extractShape(inputTableNodeSchema),
|
|
30
|
+
shape: extractShape(inputShapeNodeSchema),
|
|
31
|
+
chart: extractShape(inputChartNodeSchema),
|
|
32
|
+
timeline: extractShape(inputTimelineNodeSchema),
|
|
33
|
+
matrix: extractShape(inputMatrixNodeSchema),
|
|
34
|
+
tree: extractShape(inputTreeNodeSchema),
|
|
35
|
+
flow: extractShape(inputFlowNodeSchema),
|
|
36
|
+
processArrow: extractShape(inputProcessArrowNodeSchema),
|
|
37
|
+
line: extractShape(inputLineNodeSchema),
|
|
38
|
+
};
|
|
39
|
+
const containerShapes = {
|
|
40
|
+
box: extractShape(inputBaseNodeSchema.extend({ shadow: shadowStyleSchema.optional() })),
|
|
41
|
+
vstack: extractShape(inputBaseNodeSchema.extend({
|
|
42
|
+
gap: z.number().optional(),
|
|
43
|
+
alignItems: alignItemsSchema.optional(),
|
|
44
|
+
justifyContent: justifyContentSchema.optional(),
|
|
45
|
+
})),
|
|
46
|
+
hstack: extractShape(inputBaseNodeSchema.extend({
|
|
47
|
+
gap: z.number().optional(),
|
|
48
|
+
alignItems: alignItemsSchema.optional(),
|
|
49
|
+
justifyContent: justifyContentSchema.optional(),
|
|
50
|
+
})),
|
|
51
|
+
layer: extractShape(inputBaseNodeSchema),
|
|
52
|
+
};
|
|
53
|
+
const CONTAINER_TYPES = new Set(["box", "vstack", "hstack", "layer"]);
|
|
54
|
+
const TEXT_CONTENT_NODES = new Set(["text", "shape"]);
|
|
55
|
+
function getDef(schema) {
|
|
56
|
+
return schema._def;
|
|
57
|
+
}
|
|
58
|
+
function getPropertySchema(nodeType, propertyName) {
|
|
59
|
+
const shape = leafNodeShapes[nodeType] ?? containerShapes[nodeType];
|
|
60
|
+
if (!shape)
|
|
61
|
+
return undefined;
|
|
62
|
+
return shape[propertyName];
|
|
63
|
+
}
|
|
64
|
+
function getZodType(schema) {
|
|
65
|
+
const def = getDef(schema);
|
|
66
|
+
return (def.type ?? def.typeName ?? "");
|
|
67
|
+
}
|
|
68
|
+
function unwrapSchema(schema) {
|
|
69
|
+
const typeName = getZodType(schema);
|
|
70
|
+
const def = getDef(schema);
|
|
71
|
+
switch (typeName) {
|
|
72
|
+
case "optional":
|
|
73
|
+
case "default":
|
|
74
|
+
case "nullable":
|
|
75
|
+
return unwrapSchema(def.innerType);
|
|
76
|
+
case "lazy":
|
|
77
|
+
return unwrapSchema(def.getter());
|
|
78
|
+
case "pipe":
|
|
79
|
+
return unwrapSchema(def.in);
|
|
80
|
+
default:
|
|
81
|
+
return schema;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
function resolveZodTypeName(schema) {
|
|
85
|
+
return getZodType(unwrapSchema(schema));
|
|
86
|
+
}
|
|
87
|
+
// ===== Value coercion =====
|
|
88
|
+
function coerceValue(value, schema) {
|
|
89
|
+
const unwrapped = unwrapSchema(schema);
|
|
90
|
+
const typeName = getZodType(unwrapped);
|
|
91
|
+
const def = getDef(unwrapped);
|
|
92
|
+
switch (typeName) {
|
|
93
|
+
case "number": {
|
|
94
|
+
const num = Number(value);
|
|
95
|
+
if (isNaN(num)) {
|
|
96
|
+
throw new Error(`Cannot convert "${value}" to number`);
|
|
97
|
+
}
|
|
98
|
+
return num;
|
|
99
|
+
}
|
|
100
|
+
case "boolean":
|
|
101
|
+
if (value !== "true" && value !== "false") {
|
|
102
|
+
throw new Error(`Cannot convert "${value}" to boolean (expected "true" or "false")`);
|
|
103
|
+
}
|
|
104
|
+
return value === "true";
|
|
105
|
+
case "string":
|
|
106
|
+
case "enum":
|
|
107
|
+
return value;
|
|
108
|
+
case "literal": {
|
|
109
|
+
const values = def.values;
|
|
110
|
+
const singleValue = def.value;
|
|
111
|
+
return values?.[0] ?? singleValue;
|
|
112
|
+
}
|
|
113
|
+
case "array":
|
|
114
|
+
case "object":
|
|
115
|
+
case "record":
|
|
116
|
+
case "tuple":
|
|
117
|
+
return JSON.parse(value);
|
|
118
|
+
case "union": {
|
|
119
|
+
const options = def.options;
|
|
120
|
+
return coerceUnionValue(value, options);
|
|
121
|
+
}
|
|
122
|
+
default:
|
|
123
|
+
return coerceFallback(value);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
function coerceUnionValue(value, options) {
|
|
127
|
+
const typeNames = options.map((opt) => resolveZodTypeName(opt));
|
|
128
|
+
// Try boolean
|
|
129
|
+
if ((value === "true" || value === "false") &&
|
|
130
|
+
typeNames.includes("boolean")) {
|
|
131
|
+
return value === "true";
|
|
132
|
+
}
|
|
133
|
+
// Try number
|
|
134
|
+
if (typeNames.includes("number")) {
|
|
135
|
+
const num = Number(value);
|
|
136
|
+
if (!isNaN(num) && value !== "") {
|
|
137
|
+
return num;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
// Try literal
|
|
141
|
+
for (let i = 0; i < options.length; i++) {
|
|
142
|
+
if (typeNames[i] === "literal") {
|
|
143
|
+
const unwrapped = unwrapSchema(options[i]);
|
|
144
|
+
const def = getDef(unwrapped);
|
|
145
|
+
const values = def.values;
|
|
146
|
+
const singleValue = def.value;
|
|
147
|
+
const litVal = values?.[0] ?? singleValue;
|
|
148
|
+
if (litVal != null && `${litVal}` === value)
|
|
149
|
+
return litVal;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
// Try JSON parse for objects/arrays
|
|
153
|
+
if (typeNames.some((t) => ["array", "object", "record", "tuple"].includes(t))) {
|
|
154
|
+
if (value.startsWith("{") || value.startsWith("[")) {
|
|
155
|
+
try {
|
|
156
|
+
return JSON.parse(value);
|
|
157
|
+
}
|
|
158
|
+
catch {
|
|
159
|
+
/* ignore */
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
// Fallback to string
|
|
164
|
+
return value;
|
|
165
|
+
}
|
|
166
|
+
function coerceFallback(value) {
|
|
167
|
+
if (value === "true")
|
|
168
|
+
return true;
|
|
169
|
+
if (value === "false")
|
|
170
|
+
return false;
|
|
171
|
+
const num = Number(value);
|
|
172
|
+
if (value !== "" && !isNaN(num))
|
|
173
|
+
return num;
|
|
174
|
+
if (value.startsWith("{") || value.startsWith("[")) {
|
|
175
|
+
try {
|
|
176
|
+
return JSON.parse(value);
|
|
177
|
+
}
|
|
178
|
+
catch {
|
|
179
|
+
/* ignore */
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return value;
|
|
183
|
+
}
|
|
184
|
+
// ===== XML node helpers =====
|
|
185
|
+
function isTextNode(node) {
|
|
186
|
+
return "#text" in node;
|
|
187
|
+
}
|
|
188
|
+
function getTagName(node) {
|
|
189
|
+
for (const key of Object.keys(node)) {
|
|
190
|
+
if (key !== ":@")
|
|
191
|
+
return key;
|
|
192
|
+
}
|
|
193
|
+
throw new Error("No tag name found in XML element");
|
|
194
|
+
}
|
|
195
|
+
function getAttributes(node) {
|
|
196
|
+
const attrs = {};
|
|
197
|
+
const rawAttrs = node[":@"];
|
|
198
|
+
if (rawAttrs) {
|
|
199
|
+
for (const [key, value] of Object.entries(rawAttrs)) {
|
|
200
|
+
const attrName = key.startsWith("@_") ? key.slice(2) : key;
|
|
201
|
+
attrs[attrName] = value;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return attrs;
|
|
205
|
+
}
|
|
206
|
+
function getChildElements(node) {
|
|
207
|
+
const tagName = getTagName(node);
|
|
208
|
+
const children = node[tagName];
|
|
209
|
+
if (!children)
|
|
210
|
+
return [];
|
|
211
|
+
return children.filter((child) => !isTextNode(child));
|
|
212
|
+
}
|
|
213
|
+
function getTextContent(node) {
|
|
214
|
+
const tagName = getTagName(node);
|
|
215
|
+
const children = node[tagName];
|
|
216
|
+
if (!children)
|
|
217
|
+
return undefined;
|
|
218
|
+
const textParts = [];
|
|
219
|
+
for (const child of children) {
|
|
220
|
+
if (isTextNode(child)) {
|
|
221
|
+
textParts.push(child["#text"]);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
return textParts.length > 0 ? textParts.join("") : undefined;
|
|
225
|
+
}
|
|
226
|
+
// ===== Node conversion =====
|
|
227
|
+
function convertElement(node) {
|
|
228
|
+
const tagName = getTagName(node);
|
|
229
|
+
const nodeType = TAG_TO_TYPE[tagName];
|
|
230
|
+
const attrs = getAttributes(node);
|
|
231
|
+
const childElements = getChildElements(node);
|
|
232
|
+
const textContent = getTextContent(node);
|
|
233
|
+
if (nodeType) {
|
|
234
|
+
return convertPomNode(nodeType, attrs, childElements, textContent);
|
|
235
|
+
}
|
|
236
|
+
else {
|
|
237
|
+
return convertComponentNode(tagName, attrs, childElements, textContent);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
function convertPomNode(nodeType, attrs, childElements, textContent) {
|
|
241
|
+
const result = { type: nodeType };
|
|
242
|
+
for (const [key, value] of Object.entries(attrs)) {
|
|
243
|
+
if (key === "type")
|
|
244
|
+
continue;
|
|
245
|
+
const propSchema = getPropertySchema(nodeType, key);
|
|
246
|
+
if (propSchema) {
|
|
247
|
+
result[key] = coerceValue(value, propSchema);
|
|
248
|
+
}
|
|
249
|
+
else {
|
|
250
|
+
result[key] = coerceFallback(value);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
// Text content → text property for nodes that support it
|
|
254
|
+
if (textContent !== undefined && TEXT_CONTENT_NODES.has(nodeType)) {
|
|
255
|
+
if (!("text" in result)) {
|
|
256
|
+
result.text = textContent;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
// Children for container nodes
|
|
260
|
+
if (CONTAINER_TYPES.has(nodeType) && childElements.length > 0) {
|
|
261
|
+
const convertedChildren = childElements.map(convertElement);
|
|
262
|
+
if (nodeType === "box") {
|
|
263
|
+
if (childElements.length !== 1) {
|
|
264
|
+
throw new Error(`<Box> must have exactly 1 child element, but got ${childElements.length}`);
|
|
265
|
+
}
|
|
266
|
+
result.children = convertedChildren[0];
|
|
267
|
+
}
|
|
268
|
+
else {
|
|
269
|
+
result.children = convertedChildren;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
return result;
|
|
273
|
+
}
|
|
274
|
+
function convertComponentNode(tagName, attrs, childElements, textContent) {
|
|
275
|
+
const props = {};
|
|
276
|
+
for (const [key, value] of Object.entries(attrs)) {
|
|
277
|
+
props[key] = coerceFallback(value);
|
|
278
|
+
}
|
|
279
|
+
if (childElements.length > 0) {
|
|
280
|
+
props.children = childElements.map(convertElement);
|
|
281
|
+
}
|
|
282
|
+
else if (textContent !== undefined) {
|
|
283
|
+
props.children = textContent;
|
|
284
|
+
}
|
|
285
|
+
return {
|
|
286
|
+
type: "component",
|
|
287
|
+
name: tagName,
|
|
288
|
+
props,
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* XML 文字列を POMNode 配列に変換する。
|
|
293
|
+
*
|
|
294
|
+
* XML タグは POM ノードタイプにマッピングされ、属性値は Zod スキーマを参照して
|
|
295
|
+
* 適切な型(number, boolean, array, object)に変換される。
|
|
296
|
+
* 組み込みノード以外のタグ名はカスタムコンポーネントとして扱われる。
|
|
297
|
+
*
|
|
298
|
+
* @example
|
|
299
|
+
* ```typescript
|
|
300
|
+
* import { parseXml, buildPptx } from "@hirokisakabe/pom";
|
|
301
|
+
*
|
|
302
|
+
* const xml = `
|
|
303
|
+
* <VStack gap="16" padding="32">
|
|
304
|
+
* <Text fontPx="32" bold="true">売上レポート</Text>
|
|
305
|
+
* </VStack>
|
|
306
|
+
* `;
|
|
307
|
+
*
|
|
308
|
+
* const nodes = parseXml(xml);
|
|
309
|
+
* const pptx = await buildPptx(nodes, { w: 1280, h: 720 });
|
|
310
|
+
* ```
|
|
311
|
+
*/
|
|
312
|
+
export function parseXml(xmlString) {
|
|
313
|
+
if (!xmlString.trim())
|
|
314
|
+
return [];
|
|
315
|
+
const parser = new XMLParser({
|
|
316
|
+
preserveOrder: true,
|
|
317
|
+
ignoreAttributes: false,
|
|
318
|
+
attributeNamePrefix: "@_",
|
|
319
|
+
parseAttributeValue: false,
|
|
320
|
+
parseTagValue: false,
|
|
321
|
+
trimValues: true,
|
|
322
|
+
});
|
|
323
|
+
const wrappedXml = `<__root__>${xmlString}</__root__>`;
|
|
324
|
+
const parsed = parser.parse(wrappedXml);
|
|
325
|
+
if (!parsed || parsed.length === 0)
|
|
326
|
+
return [];
|
|
327
|
+
const rootElement = parsed[0];
|
|
328
|
+
const rootChildren = (rootElement["__root__"] ?? []);
|
|
329
|
+
return rootChildren
|
|
330
|
+
.filter((child) => !isTextNode(child))
|
|
331
|
+
.map((child) => convertElement(child));
|
|
332
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hirokisakabe/pom",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
4
4
|
"description": "PowerPoint Object Model - A declarative TypeScript library for creating PowerPoint presentations",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -88,6 +88,7 @@
|
|
|
88
88
|
"vitest": "^4.0.8"
|
|
89
89
|
},
|
|
90
90
|
"dependencies": {
|
|
91
|
+
"fast-xml-parser": "^5.3.7",
|
|
91
92
|
"image-size": "2.0.2",
|
|
92
93
|
"opentype.js": "^1.3.4",
|
|
93
94
|
"pptxgenjs": "4.0.1",
|