@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 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
@@ -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
@@ -2,3 +2,4 @@ export * from "./types.js";
2
2
  export * from "./inputSchema.js";
3
3
  export { buildPptx } from "./buildPptx.js";
4
4
  export { defineComponent, defaultTheme, mergeTheme, expandComponents, expandComponentSlides, } from "./component.js";
5
+ export { parseXml } from "./parseXml.js";
@@ -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"}
@@ -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.0",
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",