@hirokisakabe/pom 0.1.4 → 0.1.6

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
@@ -1,13 +1,63 @@
1
1
  # pom
2
2
 
3
- **pom (PowerPoint Object Model)** は、PowerPoint プレゼンテーション(pptx)を TypeScript で宣言的に記述するためのライブラリです。
3
+ **pom (PowerPoint Object Model)** は、PowerPoint プレゼンテーション(pptx)を TypeScript で宣言的に記述するためのライブラリです。生成 AI に出力させた POM 形式の JSON を、PowerPoint ファイルに変換するユースケースを想定しています。
4
+
5
+ ## 目次
6
+
7
+ - [インストール](#インストール)
8
+ - [クイックスタート](#クイックスタート)
9
+ - [特徴](#特徴)
10
+ - [ノード](#ノード)
11
+ - [マスタースライド](#マスタースライド)
12
+ - [LLM 連携](#llm-連携)
13
+ - [ライセンス](#ライセンス)
14
+
15
+ ## インストール
16
+
17
+ ```bash
18
+ npm install @hirokisakabe/pom
19
+ ```
20
+
21
+ ## クイックスタート
22
+
23
+ ```typescript
24
+ import { buildPptx, POMNode } from "@hirokisakabe/pom";
25
+
26
+ const slide: POMNode = {
27
+ type: "vstack",
28
+ w: "100%",
29
+ h: "max",
30
+ padding: 48,
31
+ gap: 24,
32
+ alignItems: "start",
33
+ children: [
34
+ {
35
+ type: "text",
36
+ text: "プレゼンテーションタイトル",
37
+ fontPx: 48,
38
+ bold: true,
39
+ },
40
+ {
41
+ type: "text",
42
+ text: "サブタイトル",
43
+ fontPx: 24,
44
+ color: "666666",
45
+ },
46
+ ],
47
+ };
48
+
49
+ const pptx = await buildPptx([slide], { w: 1280, h: 720 });
50
+ await pptx.writeFile({ fileName: "presentation.pptx" });
51
+ ```
4
52
 
5
53
  ## 特徴
6
54
 
7
55
  - **型安全**: TypeScript による厳密な型定義
8
56
  - **宣言的**: JSON ライクなオブジェクトでスライドを記述
57
+ - **PowerPoint ファースト**: Shape 機能をネイティブサポート
9
58
  - **柔軟なレイアウト**: VStack/HStack/Box による自動レイアウト
10
59
  - **ピクセル単位**: 直感的なピクセル単位での指定(内部でインチに変換)
60
+ - **マスタースライド**: 全ページ共通のヘッダー・フッター・ページ番号を自動挿入
11
61
  - **AI フレンドリー**: LLM がコード生成しやすいシンプルな構造
12
62
 
13
63
  ## ノード
@@ -48,7 +98,11 @@
48
98
  type: "text";
49
99
  text: string;
50
100
  fontPx?: number;
101
+ color?: string;
51
102
  alignText?: "left" | "center" | "right";
103
+ bold?: boolean;
104
+ fontFamily?: string;
105
+ lineSpacingMultiple?: number;
52
106
 
53
107
  // 共通プロパティ
54
108
  w?: number | "max" | `${number}%`;
@@ -57,6 +111,11 @@
57
111
  }
58
112
  ```
59
113
 
114
+ - `color` で文字色を 16 進カラーコード(例: `"FF0000"`)として指定できます。
115
+ - `bold` で太字を指定できます。
116
+ - `fontFamily` でフォントファミリーを指定できます(デフォルト: `"Noto Sans JP"`)。
117
+ - `lineSpacingMultiple` で行間倍率を指定できます(デフォルト: `1.3`)。
118
+
60
119
  #### 2. Image
61
120
 
62
121
  画像を表示するノード。
@@ -108,7 +167,54 @@
108
167
  - `rows` の `height` を省略すると `defaultRowHeight`(未指定なら32px)が適用されます。
109
168
  - セル背景やフォント装飾を `cells` の各要素で個別に指定できます。
110
169
 
111
- #### 4. Box
170
+ #### 4. Shape
171
+
172
+ 図形を描画するノード。テキスト付き/なしで異なる表現が可能で、複雑なビジュアル効果をサポートしています。
173
+
174
+ ```typescript
175
+ {
176
+ type: "shape";
177
+ shapeType: PptxGenJS.SHAPE_NAME; // 例: "roundRect", "ellipse", "cloud", "star5" など
178
+ text?: string; // 図形内に表示するテキスト(オプション)
179
+ fill?: {
180
+ color?: string;
181
+ transparency?: number;
182
+ };
183
+ line?: {
184
+ color?: string;
185
+ width?: number;
186
+ dashType?: "solid" | "dash" | "dashDot" | "lgDash" | "lgDashDot" | "lgDashDotDot" | "sysDash" | "sysDot";
187
+ };
188
+ shadow?: {
189
+ type: "outer" | "inner";
190
+ opacity?: number;
191
+ blur?: number;
192
+ angle?: number;
193
+ offset?: number;
194
+ color?: string;
195
+ };
196
+ fontPx?: number;
197
+ fontColor?: string;
198
+ alignText?: "left" | "center" | "right";
199
+
200
+ // 共通プロパティ
201
+ w?: number | "max" | `${number}%`;
202
+ h?: number | "max" | `${number}%`;
203
+ ...
204
+ }
205
+ ```
206
+
207
+ **主な図形タイプの例:**
208
+
209
+ - `roundRect`: 角丸長方形(タイトルボックス、カテゴリ表示)
210
+ - `ellipse`: 楕円/円(ステップ番号、バッジ)
211
+ - `cloud`: 雲型(コメント、重要ポイント)
212
+ - `wedgeRectCallout`: 矢印付き吹き出し(注記)
213
+ - `cloudCallout`: 雲吹き出し(コメント)
214
+ - `star5`: 5つ星(強調、デコレーション)
215
+ - `downArrow`: 下矢印(フロー図)
216
+
217
+ #### 5. Box
112
218
 
113
219
  単一の子要素をラップする汎用コンテナ。
114
220
 
@@ -127,7 +233,7 @@
127
233
  }
128
234
  ```
129
235
 
130
- #### 5. VStack
236
+ #### 6. VStack
131
237
 
132
238
  子要素を **縦方向** に並べる。
133
239
 
@@ -146,7 +252,7 @@
146
252
  }
147
253
  ```
148
254
 
149
- #### 6. HStack
255
+ #### 7. HStack
150
256
 
151
257
  子要素を **横方向** に並べる。
152
258
 
@@ -165,49 +271,184 @@
165
271
  }
166
272
  ```
167
273
 
168
- #### 7. Shape
274
+ ## マスタースライド
169
275
 
170
- 図形を描画するノード。テキスト付き/なしで異なる表現が可能で、複雑なビジュアル効果をサポートしています。
276
+ 全ページに共通のヘッダー・フッター・ページ番号を自動挿入できます。
277
+
278
+ ### 基本的な使い方
171
279
 
172
280
  ```typescript
173
- {
174
- type: "shape";
175
- shapeType: PptxGenJS.SHAPE_NAME; // 例: "roundRect", "ellipse", "cloud", "star5" など
176
- text?: string; // 図形内に表示するテキスト(オプション)
177
- fill?: {
178
- color?: string;
179
- transparency?: number;
180
- };
181
- line?: {
182
- color?: string;
183
- width?: number;
184
- dashType?: "solid" | "dash" | "dashDot" | "lgDash" | "lgDashDot" | "lgDashDotDot" | "sysDash" | "sysDot";
281
+ import { buildPptx } from "@hirokisakabe/pom";
282
+
283
+ const pptx = await buildPptx(
284
+ [page1, page2, page3],
285
+ { w: 1280, h: 720 },
286
+ {
287
+ master: {
288
+ header: {
289
+ type: "hstack",
290
+ h: 40,
291
+ padding: { left: 48, right: 48, top: 12, bottom: 0 },
292
+ justifyContent: "spaceBetween",
293
+ alignItems: "center",
294
+ backgroundColor: "0F172A",
295
+ children: [
296
+ {
297
+ type: "text",
298
+ text: "会社名",
299
+ fontPx: 14,
300
+ color: "FFFFFF",
301
+ },
302
+ {
303
+ type: "text",
304
+ text: "{{date}}",
305
+ fontPx: 12,
306
+ color: "E2E8F0",
307
+ },
308
+ ],
309
+ },
310
+ footer: {
311
+ type: "hstack",
312
+ h: 30,
313
+ padding: { left: 48, right: 48, top: 0, bottom: 8 },
314
+ justifyContent: "spaceBetween",
315
+ alignItems: "center",
316
+ children: [
317
+ {
318
+ type: "text",
319
+ text: "Confidential",
320
+ fontPx: 10,
321
+ color: "1E293B",
322
+ },
323
+ {
324
+ type: "text",
325
+ text: "Page {{page}} / {{totalPages}}",
326
+ fontPx: 10,
327
+ color: "1E293B",
328
+ alignText: "right",
329
+ },
330
+ ],
331
+ },
332
+ date: {
333
+ format: "YYYY/MM/DD", // または "locale"
334
+ },
335
+ },
336
+ },
337
+ );
338
+ ```
339
+
340
+ ### マスタースライドのオプション
341
+
342
+ ```typescript
343
+ type MasterSlideOptions = {
344
+ header?: POMNode; // ヘッダー(任意の POMNode を指定可能)
345
+ footer?: POMNode; // フッター(任意の POMNode を指定可能)
346
+ pageNumber?: {
347
+ position: "left" | "center" | "right"; // ページ番号の位置
185
348
  };
186
- shadow?: {
187
- type: "outer" | "inner";
188
- opacity?: number;
189
- blur?: number;
190
- angle?: number;
191
- offset?: number;
192
- color?: string;
349
+ date?: {
350
+ format: "YYYY/MM/DD" | "locale"; // 日付のフォーマット
193
351
  };
194
- fontPx?: number;
195
- fontColor?: string;
196
- alignText?: "left" | "center" | "right";
352
+ };
353
+ ```
197
354
 
198
- // 共通プロパティ
199
- w?: number | "max" | `${number}%`;
200
- h?: number | "max" | `${number}%`;
201
- ...
355
+ ### プレースホルダー
356
+
357
+ ヘッダー・フッター内のテキストで以下のプレースホルダーが使用できます:
358
+
359
+ - `{{page}}`: 現在のページ番号
360
+ - `{{totalPages}}`: 総ページ数
361
+ - `{{date}}`: 日付(`date.format` で指定した形式)
362
+
363
+ ### 特徴
364
+
365
+ - **柔軟性**: ヘッダー・フッターには任意の POMNode(VStack、HStack、Box など)を使用可能
366
+ - **自動合成**: 各ページのコンテンツに自動的にヘッダー・フッターが追加されます
367
+ - **動的置換**: プレースホルダーはページごとに自動的に置換されます
368
+ - **後方互換性**: master オプションは省略可能で、既存コードへの影響はありません
369
+
370
+ ## LLM 連携
371
+
372
+ pom は LLM(GPT-4o、Claude など)で生成した JSON からスライドを作成するユースケースに対応しています。
373
+
374
+ ### 入力用スキーマ
375
+
376
+ `inputPomNodeSchema` を使って、LLM が生成した JSON を検証できます。
377
+
378
+ ```typescript
379
+ import { inputPomNodeSchema, buildPptx, InputPOMNode } from "@hirokisakabe/pom";
380
+
381
+ // LLMからのJSON出力を検証
382
+ const jsonFromLLM = `{
383
+ "type": "vstack",
384
+ "padding": 48,
385
+ "gap": 24,
386
+ "children": [
387
+ { "type": "text", "text": "タイトル", "fontPx": 32, "bold": true },
388
+ { "type": "text", "text": "本文テキスト", "fontPx": 16 }
389
+ ]
390
+ }`;
391
+
392
+ const parsed = JSON.parse(jsonFromLLM);
393
+ const result = inputPomNodeSchema.safeParse(parsed);
394
+
395
+ if (result.success) {
396
+ // 検証成功 - PPTXを生成
397
+ const pptx = await buildPptx([result.data], { w: 1280, h: 720 });
398
+ await pptx.writeFile({ fileName: "output.pptx" });
399
+ } else {
400
+ // 検証失敗 - エラー内容を確認
401
+ console.error("Validation failed:", result.error.format());
202
402
  }
203
403
  ```
204
404
 
205
- **主な図形タイプの例:**
405
+ ### OpenAI Structured Outputs との連携
206
406
 
207
- - `roundRect`: 角丸長方形(タイトルボックス、カテゴリ表示)
208
- - `ellipse`: 楕円/円(ステップ番号、バッジ)
209
- - `cloud`: 雲型(コメント、重要ポイント)
210
- - `wedgeRectCallout`: 矢印付き吹き出し(注記)
211
- - `cloudCallout`: 雲吹き出し(コメント)
212
- - `star5`: 5つ星(強調、デコレーション)
213
- - `downArrow`: 下矢印(フロー図)
407
+ OpenAI SDK の `zodResponseFormat` を使用して、LLM に直接スキーマ準拠の JSON を生成させることができます。
408
+
409
+ ```typescript
410
+ import { inputPomNodeSchema, buildPptx } from "@hirokisakabe/pom";
411
+ import OpenAI from "openai";
412
+ import { zodResponseFormat } from "openai/helpers/zod";
413
+
414
+ const openai = new OpenAI();
415
+
416
+ const response = await openai.chat.completions.create({
417
+ model: "gpt-4o",
418
+ messages: [
419
+ {
420
+ role: "system",
421
+ content:
422
+ "あなたはプレゼンテーション作成アシスタントです。指定されたスキーマに従ってスライドのJSONを生成してください。",
423
+ },
424
+ {
425
+ role: "user",
426
+ content:
427
+ "売上報告のスライドを作成して。タイトルと3つの箇条書きを含めて。",
428
+ },
429
+ ],
430
+ response_format: zodResponseFormat(inputPomNodeSchema, "slide"),
431
+ });
432
+
433
+ const slideData = JSON.parse(response.choices[0].message.content!);
434
+ const pptx = await buildPptx([slideData], { w: 1280, h: 720 });
435
+ await pptx.writeFile({ fileName: "sales-report.pptx" });
436
+ ```
437
+
438
+ ### 利用可能な入力用スキーマ
439
+
440
+ | スキーマ | 説明 |
441
+ | ------------------------------- | ---------------------------------------------- |
442
+ | `inputPomNodeSchema` | メインのノードスキーマ(全ノードタイプを含む) |
443
+ | `inputTextNodeSchema` | テキストノード用 |
444
+ | `inputImageNodeSchema` | 画像ノード用 |
445
+ | `inputTableNodeSchema` | テーブルノード用 |
446
+ | `inputShapeNodeSchema` | 図形ノード用 |
447
+ | `inputBoxNodeSchema` | Boxノード用 |
448
+ | `inputVStackNodeSchema` | VStackノード用 |
449
+ | `inputHStackNodeSchema` | HStackノード用 |
450
+ | `inputMasterSlideOptionsSchema` | マスタースライド設定用 |
451
+
452
+ ## ライセンス
453
+
454
+ MIT
@@ -1,6 +1,8 @@
1
- import { POMNode } from "./types";
1
+ import { POMNode, MasterSlideOptions } from "./types";
2
2
  export declare function buildPptx(nodes: POMNode[], slideSize: {
3
3
  w: number;
4
4
  h: number;
5
+ }, options?: {
6
+ master?: MasterSlideOptions;
5
7
  }): Promise<import("pptxgenjs").default>;
6
8
  //# sourceMappingURL=buildPptx.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"buildPptx.d.ts","sourceRoot":"","sources":["../src/buildPptx.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,OAAO,EAAkB,MAAM,SAAS,CAAC;AAElD,wBAAsB,SAAS,CAC7B,KAAK,EAAE,OAAO,EAAE,EAChB,SAAS,EAAE;IAAE,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAA;CAAE,wCAapC"}
1
+ {"version":3,"file":"buildPptx.d.ts","sourceRoot":"","sources":["../src/buildPptx.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,OAAO,EAAkB,kBAAkB,EAAE,MAAM,SAAS,CAAC;AAiGtE,wBAAsB,SAAS,CAC7B,KAAK,EAAE,OAAO,EAAE,EAChB,SAAS,EAAE;IAAE,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAA;CAAE,EACnC,OAAO,CAAC,EAAE;IAAE,MAAM,CAAC,EAAE,kBAAkB,CAAA;CAAE,wCAgB1C"}
package/dist/buildPptx.js CHANGED
@@ -1,11 +1,75 @@
1
1
  import { calcYogaLayout } from "./calcYogaLayout/calcYogaLayout";
2
2
  import { renderPptx } from "./renderPptx/renderPptx";
3
3
  import { toPositioned } from "./toPositioned/toPositioned";
4
- export async function buildPptx(nodes, slideSize) {
4
+ function replacePlaceholders(node, pageNumber, totalPages, date) {
5
+ if (node.type === "text") {
6
+ return {
7
+ ...node,
8
+ text: node.text
9
+ .replace(/\{\{page\}\}/g, String(pageNumber))
10
+ .replace(/\{\{totalPages\}\}/g, String(totalPages))
11
+ .replace(/\{\{date\}\}/g, date),
12
+ };
13
+ }
14
+ if (node.type === "box") {
15
+ return {
16
+ ...node,
17
+ children: replacePlaceholders(node.children, pageNumber, totalPages, date),
18
+ };
19
+ }
20
+ if (node.type === "vstack" || node.type === "hstack") {
21
+ return {
22
+ ...node,
23
+ children: node.children.map((child) => replacePlaceholders(child, pageNumber, totalPages, date)),
24
+ };
25
+ }
26
+ return node;
27
+ }
28
+ function composePage(content, master, pageNumber, totalPages) {
29
+ if (!master) {
30
+ return content;
31
+ }
32
+ const date = master.date?.format === "locale"
33
+ ? new Date().toLocaleDateString()
34
+ : new Date().toISOString().split("T")[0].replace(/-/g, "/");
35
+ const children = [];
36
+ // ヘッダーを追加
37
+ if (master.header) {
38
+ children.push(replacePlaceholders(master.header, pageNumber, totalPages, date));
39
+ }
40
+ // コンテンツを追加
41
+ children.push(content);
42
+ // フッターを追加
43
+ if (master.footer) {
44
+ children.push(replacePlaceholders(master.footer, pageNumber, totalPages, date));
45
+ }
46
+ // ページ番号を追加
47
+ if (master.pageNumber) {
48
+ const pageNumberNode = {
49
+ type: "text",
50
+ text: String(pageNumber),
51
+ fontPx: 12,
52
+ alignText: master.pageNumber.position,
53
+ };
54
+ children.push(pageNumberNode);
55
+ }
56
+ return {
57
+ type: "vstack",
58
+ w: "100%",
59
+ h: "100%",
60
+ alignItems: "stretch",
61
+ justifyContent: "start",
62
+ children,
63
+ };
64
+ }
65
+ export async function buildPptx(nodes, slideSize, options) {
5
66
  const positionedPages = [];
6
- for (const node of nodes) {
7
- await calcYogaLayout(node, slideSize);
8
- const positioned = toPositioned(node);
67
+ const totalPages = nodes.length;
68
+ for (let i = 0; i < nodes.length; i++) {
69
+ const node = nodes[i];
70
+ const composedNode = composePage(node, options?.master, i + 1, totalPages);
71
+ await calcYogaLayout(composedNode, slideSize);
72
+ const positioned = toPositioned(composedNode);
9
73
  positionedPages.push(positioned);
10
74
  }
11
75
  const pptx = renderPptx(positionedPages, slideSize);
package/dist/index.d.ts CHANGED
@@ -1,3 +1,4 @@
1
1
  export * from "./types";
2
+ export * from "./inputSchema";
2
3
  export { buildPptx } from "./buildPptx";
3
4
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,SAAS,CAAC;AACxB,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,SAAS,CAAC;AACxB,cAAc,eAAe,CAAC;AAC9B,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC"}
package/dist/index.js CHANGED
@@ -1,2 +1,3 @@
1
1
  export * from "./types";
2
+ export * from "./inputSchema";
2
3
  export { buildPptx } from "./buildPptx";