@hirokisakabe/pom 8.2.1 → 8.3.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.
Files changed (51) hide show
  1. package/README.md +24 -24
  2. package/dist/autoFit/autoFit.js.map +1 -1
  3. package/dist/buildContext.js +3 -1
  4. package/dist/buildContext.js.map +1 -1
  5. package/dist/buildPptx.d.ts.map +1 -1
  6. package/dist/buildPptx.js +4 -0
  7. package/dist/buildPptx.js.map +1 -1
  8. package/dist/calcYogaLayout/calcYogaLayout.js +2 -1
  9. package/dist/calcYogaLayout/calcYogaLayout.js.map +1 -1
  10. package/dist/calcYogaLayout/fontLoader.js.map +1 -1
  11. package/dist/calcYogaLayout/measureText.d.ts.map +1 -1
  12. package/dist/calcYogaLayout/measureText.js +9 -2
  13. package/dist/calcYogaLayout/measureText.js.map +1 -1
  14. package/dist/diagnostics.js.map +1 -1
  15. package/dist/icons/renderIcon.js.map +1 -1
  16. package/dist/parseMasterPptx.js.map +1 -1
  17. package/dist/parseXml/coercionRules.js +6 -2
  18. package/dist/parseXml/coercionRules.js.map +1 -1
  19. package/dist/parseXml/parseXml.d.ts.map +1 -1
  20. package/dist/parseXml/parseXml.js +11 -8
  21. package/dist/parseXml/parseXml.js.map +1 -1
  22. package/dist/parseXml/serializeXml.d.ts.map +1 -1
  23. package/dist/parseXml/serializeXml.js +1 -0
  24. package/dist/parseXml/serializeXml.js.map +1 -1
  25. package/dist/registry/definitions/list.js.map +1 -1
  26. package/dist/registry/definitions/shape.js.map +1 -1
  27. package/dist/registry/definitions/text.js +3 -1
  28. package/dist/registry/definitions/text.js.map +1 -1
  29. package/dist/renderPptx/gradientFills.js +139 -0
  30. package/dist/renderPptx/gradientFills.js.map +1 -0
  31. package/dist/renderPptx/nodes/icon.js.map +1 -1
  32. package/dist/renderPptx/nodes/list.js.map +1 -1
  33. package/dist/renderPptx/nodes/table.js.map +1 -1
  34. package/dist/renderPptx/nodes/text.js +18 -14
  35. package/dist/renderPptx/nodes/text.js.map +1 -1
  36. package/dist/renderPptx/nodes/tree.js.map +1 -1
  37. package/dist/renderPptx/renderPptx.js +6 -2
  38. package/dist/renderPptx/renderPptx.js.map +1 -1
  39. package/dist/renderPptx/textOptions.js +2 -1
  40. package/dist/renderPptx/textOptions.js.map +1 -1
  41. package/dist/renderPptx/utils/backgroundBorder.js +6 -4
  42. package/dist/renderPptx/utils/backgroundBorder.js.map +1 -1
  43. package/dist/shared/gradient.js +103 -0
  44. package/dist/shared/gradient.js.map +1 -0
  45. package/dist/shared/measureImage.js.map +1 -1
  46. package/dist/shared/tableUtils.js.map +1 -1
  47. package/dist/types.d.ts +41 -0
  48. package/dist/types.d.ts.map +1 -1
  49. package/dist/types.js +8 -2
  50. package/dist/types.js.map +1 -1
  51. package/package.json +9 -9
package/README.md CHANGED
@@ -37,11 +37,11 @@
37
37
 
38
38
  - **AI Friendly** — Simple XML structure designed for LLM code generation. Include [llm.txt](./website/public/llm.txt) in your system prompt for XML reference. Also available at `https://pom.pptx.app/llm.txt`.
39
39
  - **Declarative** — Describe slides as XML. No imperative API calls needed — just data in, PPTX out.
40
- - **Flexible Layout** — Flexbox-style layout with VStack / HStack, powered by yoga-layout.
40
+ - **Flexible Layout** — Flexbox-style layout with VStack / HStack, powered by yoga-layout. Ratio-based layouts (e.g., 2:1 columns) via the `grow` attribute (CSS `flex-grow`).
41
41
  - **Shorthand + Dot Notation** — Layout/style attributes (e.g. `padding`, `margin`, `border`, `fill`, `shadow`) can mix shorthand and dot notation on the same node. Shorthand sets defaults and dot notation overrides specific keys.
42
42
  - **Rich Nodes** — 18 built-in node types: charts, flowcharts, tables, timelines, org trees, and more.
43
43
  - **Schema-validated** — XML input is validated with Zod schemas at runtime with clear error messages.
44
- - **PowerPoint Native** — Generates real editable PowerPoint shapes — not images. Recipients can modify everything.
44
+ - **PowerPoint Native** — Generates real editable PowerPoint shapes — not images. Recipients can modify everything. Linear gradient backgrounds (`backgroundGradient="linear-gradient(135deg, #667EEA 0%, #764BA2 100%)"`) are exported as native gradient fills.
45
45
  - **Pixel Units** — Intuitive pixel-based sizing (internally converted to inches at 96 DPI).
46
46
  - **Master Slide** — Define headers, footers, and page numbers once — applied to all slides automatically.
47
47
  - **Accurate Text Measurement** — Text width measured with opentype.js and bundled Noto Sans JP fonts for consistent layout.
@@ -74,28 +74,28 @@ Each slide must be wrapped in a `<Slide>` element. To produce multiple slides, l
74
74
 
75
75
  ## Available Nodes
76
76
 
77
- | Node | Description |
78
- | ------------ | ------------------------------------------------------------------------------------------------------- |
79
- | Text | Text with font styling, decoration, inline bold/italic/underline/strike/highlight/color, and hyperlinks |
80
- | Ul | Unordered (bullet) list with Li items |
81
- | Ol | Ordered (numbered) list with Li items |
82
- | Image | Images from file path, URL, or base64 |
83
- | Table | Tables with customizable columns and rows |
84
- | Shape | PowerPoint shapes (roundRect, ellipse, etc.) |
85
- | Chart | Charts (bar, line, pie, area, doughnut, radar) |
86
- | Timeline | Timeline / roadmap visualizations |
87
- | Matrix | 2x2 positioning maps |
88
- | Tree | Organization charts and decision trees |
89
- | Flow | Flowcharts with nodes and edges |
90
- | ProcessArrow | Chevron-style process diagrams |
91
- | Pyramid | Pyramid diagrams for hierarchies |
92
- | Line | Horizontal / vertical lines |
93
- | Arrow | Connectors between nodes referenced by ID |
94
- | Layer | Absolute-positioned overlay container |
95
- | VStack | Vertical stack layout |
96
- | HStack | Horizontal stack layout |
97
- | Icon | Lucide icons |
98
- | Svg | Inline SVG graphics |
77
+ | Node | Description |
78
+ | ------------ | ----------------------------------------------------------------------------------------------------------------------- |
79
+ | Text | Text with font styling, decoration, letter spacing, inline bold/italic/underline/strike/highlight/color, and hyperlinks |
80
+ | Ul | Unordered (bullet) list with Li items |
81
+ | Ol | Ordered (numbered) list with Li items |
82
+ | Image | Images from file path, URL, or base64 |
83
+ | Table | Tables with customizable columns and rows |
84
+ | Shape | PowerPoint shapes (roundRect, ellipse, etc.) |
85
+ | Chart | Charts (bar, line, pie, area, doughnut, radar) |
86
+ | Timeline | Timeline / roadmap visualizations |
87
+ | Matrix | 2x2 positioning maps |
88
+ | Tree | Organization charts and decision trees |
89
+ | Flow | Flowcharts with nodes and edges |
90
+ | ProcessArrow | Chevron-style process diagrams |
91
+ | Pyramid | Pyramid diagrams for hierarchies |
92
+ | Line | Horizontal / vertical lines |
93
+ | Arrow | Connectors between nodes referenced by ID |
94
+ | Layer | Absolute-positioned overlay container |
95
+ | VStack | Vertical stack layout |
96
+ | HStack | Horizontal stack layout |
97
+ | Icon | Lucide icons |
98
+ | Svg | Inline SVG graphics |
99
99
 
100
100
  For detailed node documentation, see [Nodes](./docs/nodes.md).
101
101
 
@@ -1 +1 @@
1
- {"version":3,"file":"autoFit.js","names":[],"sources":["../../src/autoFit/autoFit.ts"],"sourcesContent":["import type { POMNode } from \"../types.ts\";\nimport type { BuildContext } from \"../buildContext.ts\";\nimport type { YogaNodeMap } from \"../calcYogaLayout/types.ts\";\nimport { calcYogaLayout } from \"../calcYogaLayout/calcYogaLayout.ts\";\nimport { freeYogaTree } from \"../shared/freeYogaTree.ts\";\nimport { reduceTableRowHeight } from \"./strategies/reduceTableRowHeight.ts\";\nimport { reduceFontSize } from \"./strategies/reduceFontSize.ts\";\nimport { reduceGapAndPadding } from \"./strategies/reduceGapAndPadding.ts\";\nimport { uniformScale } from \"./strategies/uniformScale.ts\";\n\n/** オーバーフロー判定の許容マージン(0.5%) */\nconst OVERFLOW_TOLERANCE = 1.005;\n\ntype Strategy = (node: POMNode, targetRatio: number) => boolean;\n\nconst strategies: Strategy[] = [\n reduceTableRowHeight,\n reduceFontSize,\n reduceGapAndPadding,\n uniformScale,\n];\n\n/** オーバーフロー測定結果 */\ninterface OverflowResult {\n contentHeight: number;\n isOverflowing: boolean;\n /** スライド高さ / コンテンツ高さ(オーバーフロー時 < 1) */\n targetRatio: number;\n map: YogaNodeMap;\n}\n\n/**\n * レイアウト計算を実行し、コンテンツのオーバーフロー状態を測定する。\n */\nasync function measureOverflow(\n node: POMNode,\n slideSize: { w: number; h: number },\n ctx: BuildContext,\n): Promise<OverflowResult> {\n const map = await calcYogaLayout(node, slideSize, ctx);\n const contentHeight = calcContentHeight(map, node);\n const isOverflowing = contentHeight > slideSize.h * OVERFLOW_TOLERANCE;\n const targetRatio = isOverflowing ? slideSize.h / contentHeight : 1;\n return { contentHeight, isOverflowing, targetRatio, map };\n}\n\n/**\n * Yoga レイアウト結果からコンテンツの占有高さを算出する。\n *\n * ルートの yogaNode の子要素の (top + height) の最大値を計算し、\n * ルートの padding.bottom を加算してコンテンツの占有高さとする。\n * h=\"max\" や flexGrow の影響を受けず、正確なコンテンツ高さを返す。\n */\nfunction calcContentHeight(map: YogaNodeMap, node: POMNode): number {\n const rootYoga = map.get(node);\n if (!rootYoga) {\n throw new Error(\"YogaNode not found in map for root node\");\n }\n\n const childCount = rootYoga.getChildCount();\n if (childCount === 0) {\n return rootYoga.getComputedHeight();\n }\n\n let maxBottom = 0;\n for (let i = 0; i < childCount; i++) {\n const child = rootYoga.getChild(i);\n const childLayout = child.getComputedLayout();\n const bottom = childLayout.top + childLayout.height;\n if (bottom > maxBottom) {\n maxBottom = bottom;\n }\n }\n\n // ルートの paddingBottom を加算\n const paddingBottom = rootYoga.getComputedPadding(2); // EDGE_BOTTOM = 2\n return maxBottom + paddingBottom;\n}\n\n/**\n * スライドのオーバーフローを検出し、段階的に調整してスライド内に収める。\n *\n * 調整の優先順:\n * 1. テーブル行高さ縮小\n * 2. フォントサイズ縮小\n * 3. gap/padding 縮小\n * 4. 全体スケーリング(フォールバック)\n */\nexport async function autoFitSlide(\n node: POMNode,\n slideSize: { w: number; h: number },\n ctx: BuildContext,\n): Promise<YogaNodeMap> {\n // Phase 1: 戦略を順次適用してオーバーフローを解消\n for (const strategy of strategies) {\n const result = await measureOverflow(node, slideSize, ctx);\n freeYogaTree(result.map);\n\n if (!result.isOverflowing) {\n break;\n }\n\n const changed = strategy(node, result.targetRatio);\n if (!changed) {\n continue;\n }\n }\n\n // Phase 2: 最終レイアウト計算とオーバーフロー検証\n return finalizeLayout(node, slideSize, ctx);\n}\n\n/**\n * 最終レイアウトを計算し、オーバーフローが残っていれば警告を出力する。\n */\nasync function finalizeLayout(\n node: POMNode,\n slideSize: { w: number; h: number },\n ctx: BuildContext,\n): Promise<YogaNodeMap> {\n const result = await measureOverflow(node, slideSize, ctx);\n if (result.isOverflowing) {\n ctx.diagnostics.add(\n \"AUTOFIT_OVERFLOW\",\n `autoFit: content height (${Math.round(result.contentHeight)}px) exceeds slide height (${slideSize.h}px) after all adjustments.`,\n );\n }\n freeYogaTree(result.map);\n\n return calcYogaLayout(node, slideSize, ctx);\n}\n"],"mappings":";;;;;;;;AAWA,MAAM,qBAAqB;AAI3B,MAAM,aAAyB;CAC7B;CACA;CACA;CACA;AACF;;;;AAcA,eAAe,gBACb,MACA,WACA,KACyB;CACzB,MAAM,MAAM,MAAM,eAAe,MAAM,WAAW,GAAG;CACrD,MAAM,gBAAgB,kBAAkB,KAAK,IAAI;CACjD,MAAM,gBAAgB,gBAAgB,UAAU,IAAI;CAEpD,OAAO;EAAE;EAAe;EAAe,aADnB,gBAAgB,UAAU,IAAI,gBAAgB;EACd;CAAI;AAC1D;;;;;;;;AASA,SAAS,kBAAkB,KAAkB,MAAuB;CAClE,MAAM,WAAW,IAAI,IAAI,IAAI;CAC7B,IAAI,CAAC,UACH,MAAM,IAAI,MAAM,yCAAyC;CAG3D,MAAM,aAAa,SAAS,cAAc;CAC1C,IAAI,eAAe,GACjB,OAAO,SAAS,kBAAkB;CAGpC,IAAI,YAAY;CAChB,KAAK,IAAI,IAAI,GAAG,IAAI,YAAY,KAAK;EAEnC,MAAM,cADQ,SAAS,SAAS,CACR,EAAE,kBAAkB;EAC5C,MAAM,SAAS,YAAY,MAAM,YAAY;EAC7C,IAAI,SAAS,WACX,YAAY;CAEhB;CAGA,MAAM,gBAAgB,SAAS,mBAAmB,CAAC;CACnD,OAAO,YAAY;AACrB;;;;;;;;;;AAWA,eAAsB,aACpB,MACA,WACA,KACsB;CAEtB,KAAK,MAAM,YAAY,YAAY;EACjC,MAAM,SAAS,MAAM,gBAAgB,MAAM,WAAW,GAAG;EACzD,aAAa,OAAO,GAAG;EAEvB,IAAI,CAAC,OAAO,eACV;EAIF,IAAI,CADY,SAAS,MAAM,OAAO,WAC3B,GACT;CAEJ;CAGA,OAAO,eAAe,MAAM,WAAW,GAAG;AAC5C;;;;AAKA,eAAe,eACb,MACA,WACA,KACsB;CACtB,MAAM,SAAS,MAAM,gBAAgB,MAAM,WAAW,GAAG;CACzD,IAAI,OAAO,eACT,IAAI,YAAY,IACd,oBACA,4BAA4B,KAAK,MAAM,OAAO,aAAa,EAAE,4BAA4B,UAAU,EAAE,2BACvG;CAEF,aAAa,OAAO,GAAG;CAEvB,OAAO,eAAe,MAAM,WAAW,GAAG;AAC5C"}
1
+ {"version":3,"file":"autoFit.js","names":[],"sources":["../../src/autoFit/autoFit.ts"],"sourcesContent":["import type { POMNode } from \"../types.ts\";\nimport type { BuildContext } from \"../buildContext.ts\";\nimport type { YogaNodeMap } from \"../calcYogaLayout/types.ts\";\nimport { calcYogaLayout } from \"../calcYogaLayout/calcYogaLayout.ts\";\nimport { freeYogaTree } from \"../shared/freeYogaTree.ts\";\nimport { reduceTableRowHeight } from \"./strategies/reduceTableRowHeight.ts\";\nimport { reduceFontSize } from \"./strategies/reduceFontSize.ts\";\nimport { reduceGapAndPadding } from \"./strategies/reduceGapAndPadding.ts\";\nimport { uniformScale } from \"./strategies/uniformScale.ts\";\n\n/** オーバーフロー判定の許容マージン(0.5%) */\nconst OVERFLOW_TOLERANCE = 1.005;\n\ntype Strategy = (node: POMNode, targetRatio: number) => boolean;\n\nconst strategies: Strategy[] = [\n reduceTableRowHeight,\n reduceFontSize,\n reduceGapAndPadding,\n uniformScale,\n];\n\n/** オーバーフロー測定結果 */\ninterface OverflowResult {\n contentHeight: number;\n isOverflowing: boolean;\n /** スライド高さ / コンテンツ高さ(オーバーフロー時 < 1) */\n targetRatio: number;\n map: YogaNodeMap;\n}\n\n/**\n * レイアウト計算を実行し、コンテンツのオーバーフロー状態を測定する。\n */\nasync function measureOverflow(\n node: POMNode,\n slideSize: { w: number; h: number },\n ctx: BuildContext,\n): Promise<OverflowResult> {\n const map = await calcYogaLayout(node, slideSize, ctx);\n const contentHeight = calcContentHeight(map, node);\n const isOverflowing = contentHeight > slideSize.h * OVERFLOW_TOLERANCE;\n const targetRatio = isOverflowing ? slideSize.h / contentHeight : 1;\n return { contentHeight, isOverflowing, targetRatio, map };\n}\n\n/**\n * Yoga レイアウト結果からコンテンツの占有高さを算出する。\n *\n * ルートの yogaNode の子要素の (top + height) の最大値を計算し、\n * ルートの padding.bottom を加算してコンテンツの占有高さとする。\n * h=\"max\" や flexGrow の影響を受けず、正確なコンテンツ高さを返す。\n */\nfunction calcContentHeight(map: YogaNodeMap, node: POMNode): number {\n const rootYoga = map.get(node);\n if (!rootYoga) {\n throw new Error(\"YogaNode not found in map for root node\");\n }\n\n const childCount = rootYoga.getChildCount();\n if (childCount === 0) {\n return rootYoga.getComputedHeight();\n }\n\n let maxBottom = 0;\n for (let i = 0; i < childCount; i++) {\n const child = rootYoga.getChild(i);\n const childLayout = child.getComputedLayout();\n const bottom = childLayout.top + childLayout.height;\n if (bottom > maxBottom) {\n maxBottom = bottom;\n }\n }\n\n // ルートの paddingBottom を加算\n const paddingBottom = rootYoga.getComputedPadding(2); // EDGE_BOTTOM = 2\n return maxBottom + paddingBottom;\n}\n\n/**\n * スライドのオーバーフローを検出し、段階的に調整してスライド内に収める。\n *\n * 調整の優先順:\n * 1. テーブル行高さ縮小\n * 2. フォントサイズ縮小\n * 3. gap/padding 縮小\n * 4. 全体スケーリング(フォールバック)\n */\nexport async function autoFitSlide(\n node: POMNode,\n slideSize: { w: number; h: number },\n ctx: BuildContext,\n): Promise<YogaNodeMap> {\n // Phase 1: 戦略を順次適用してオーバーフローを解消\n for (const strategy of strategies) {\n const result = await measureOverflow(node, slideSize, ctx);\n freeYogaTree(result.map);\n\n if (!result.isOverflowing) {\n break;\n }\n\n const changed = strategy(node, result.targetRatio);\n if (!changed) {\n continue;\n }\n }\n\n // Phase 2: 最終レイアウト計算とオーバーフロー検証\n return finalizeLayout(node, slideSize, ctx);\n}\n\n/**\n * 最終レイアウトを計算し、オーバーフローが残っていれば警告を出力する。\n */\nasync function finalizeLayout(\n node: POMNode,\n slideSize: { w: number; h: number },\n ctx: BuildContext,\n): Promise<YogaNodeMap> {\n const result = await measureOverflow(node, slideSize, ctx);\n if (result.isOverflowing) {\n ctx.diagnostics.add(\n \"AUTOFIT_OVERFLOW\",\n `autoFit: content height (${Math.round(result.contentHeight)}px) exceeds slide height (${slideSize.h}px) after all adjustments.`,\n );\n }\n freeYogaTree(result.map);\n\n return calcYogaLayout(node, slideSize, ctx);\n}\n"],"mappings":";;;;;;;;AAWA,MAAM,qBAAqB;AAI3B,MAAM,aAAyB;CAC7B;CACA;CACA;CACA;AACF;;;;AAcA,eAAe,gBACb,MACA,WACA,KACyB;CACzB,MAAM,MAAM,MAAM,eAAe,MAAM,WAAW,GAAG;CACrD,MAAM,gBAAgB,kBAAkB,KAAK,IAAI;CACjD,MAAM,gBAAgB,gBAAgB,UAAU,IAAI;CAEpD,OAAO;EAAE;EAAe;EAAe,aADnB,gBAAgB,UAAU,IAAI,gBAAgB;EACd;CAAI;AAC1D;;;;;;;;AASA,SAAS,kBAAkB,KAAkB,MAAuB;CAClE,MAAM,WAAW,IAAI,IAAI,IAAI;CAC7B,IAAI,CAAC,UACH,MAAM,IAAI,MAAM,yCAAyC;CAG3D,MAAM,aAAa,SAAS,cAAc;CAC1C,IAAI,eAAe,GACjB,OAAO,SAAS,kBAAkB;CAGpC,IAAI,YAAY;CAChB,KAAK,IAAI,IAAI,GAAG,IAAI,YAAY,KAAK;EAEnC,MAAM,cADQ,SAAS,SAAS,CACR,CAAC,CAAC,kBAAkB;EAC5C,MAAM,SAAS,YAAY,MAAM,YAAY;EAC7C,IAAI,SAAS,WACX,YAAY;CAEhB;CAGA,MAAM,gBAAgB,SAAS,mBAAmB,CAAC;CACnD,OAAO,YAAY;AACrB;;;;;;;;;;AAWA,eAAsB,aACpB,MACA,WACA,KACsB;CAEtB,KAAK,MAAM,YAAY,YAAY;EACjC,MAAM,SAAS,MAAM,gBAAgB,MAAM,WAAW,GAAG;EACzD,aAAa,OAAO,GAAG;EAEvB,IAAI,CAAC,OAAO,eACV;EAIF,IAAI,CADY,SAAS,MAAM,OAAO,WAC3B,GACT;CAEJ;CAGA,OAAO,eAAe,MAAM,WAAW,GAAG;AAC5C;;;;AAKA,eAAe,eACb,MACA,WACA,KACsB;CACtB,MAAM,SAAS,MAAM,gBAAgB,MAAM,WAAW,GAAG;CACzD,IAAI,OAAO,eACT,IAAI,YAAY,IACd,oBACA,4BAA4B,KAAK,MAAM,OAAO,aAAa,EAAE,4BAA4B,UAAU,EAAE,2BACvG;CAEF,aAAa,OAAO,GAAG;CAEvB,OAAO,eAAe,MAAM,WAAW,GAAG;AAC5C"}
@@ -1,4 +1,5 @@
1
1
  import { DiagnosticCollector } from "./diagnostics.js";
2
+ import { GradientFillRegistry } from "./renderPptx/gradientFills.js";
2
3
  //#region src/buildContext.ts
3
4
  function createBuildContext(textMeasurementMode = "auto") {
4
5
  return {
@@ -6,7 +7,8 @@ function createBuildContext(textMeasurementMode = "auto") {
6
7
  imageSizeCache: /* @__PURE__ */ new Map(),
7
8
  imageDataCache: /* @__PURE__ */ new Map(),
8
9
  iconRasterCache: /* @__PURE__ */ new Map(),
9
- diagnostics: new DiagnosticCollector()
10
+ diagnostics: new DiagnosticCollector(),
11
+ gradientFills: new GradientFillRegistry()
10
12
  };
11
13
  }
12
14
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"buildContext.js","names":[],"sources":["../src/buildContext.ts"],"sourcesContent":["import type { TextMeasurementMode } from \"./calcYogaLayout/measureText.ts\";\nimport { DiagnosticCollector } from \"./diagnostics.ts\";\n\nexport interface BuildContext {\n textMeasurementMode: TextMeasurementMode;\n imageSizeCache: Map<string, { widthPx: number; heightPx: number }>;\n imageDataCache: Map<string, string>;\n iconRasterCache: Map<string, string>;\n diagnostics: DiagnosticCollector;\n}\n\nexport function createBuildContext(\n textMeasurementMode: TextMeasurementMode = \"auto\",\n): BuildContext {\n return {\n textMeasurementMode,\n imageSizeCache: new Map(),\n imageDataCache: new Map(),\n iconRasterCache: new Map(),\n diagnostics: new DiagnosticCollector(),\n };\n}\n"],"mappings":";;AAWA,SAAgB,mBACd,sBAA2C,QAC7B;CACd,OAAO;EACL;EACA,gCAAgB,IAAI,IAAI;EACxB,gCAAgB,IAAI,IAAI;EACxB,iCAAiB,IAAI,IAAI;EACzB,aAAa,IAAI,oBAAoB;CACvC;AACF"}
1
+ {"version":3,"file":"buildContext.js","names":[],"sources":["../src/buildContext.ts"],"sourcesContent":["import type { TextMeasurementMode } from \"./calcYogaLayout/measureText.ts\";\nimport { DiagnosticCollector } from \"./diagnostics.ts\";\nimport { GradientFillRegistry } from \"./renderPptx/gradientFills.ts\";\n\nexport interface BuildContext {\n textMeasurementMode: TextMeasurementMode;\n imageSizeCache: Map<string, { widthPx: number; heightPx: number }>;\n imageDataCache: Map<string, string>;\n iconRasterCache: Map<string, string>;\n diagnostics: DiagnosticCollector;\n gradientFills: GradientFillRegistry;\n}\n\nexport function createBuildContext(\n textMeasurementMode: TextMeasurementMode = \"auto\",\n): BuildContext {\n return {\n textMeasurementMode,\n imageSizeCache: new Map(),\n imageDataCache: new Map(),\n iconRasterCache: new Map(),\n diagnostics: new DiagnosticCollector(),\n gradientFills: new GradientFillRegistry(),\n };\n}\n"],"mappings":";;;AAaA,SAAgB,mBACd,sBAA2C,QAC7B;CACd,OAAO;EACL;EACA,gCAAgB,IAAI,IAAI;EACxB,gCAAgB,IAAI,IAAI;EACxB,iCAAiB,IAAI,IAAI;EACzB,aAAa,IAAI,oBAAoB;EACrC,eAAe,IAAI,qBAAqB;CAC1C;AACF"}
@@ -1 +1 @@
1
- {"version":3,"file":"buildPptx.d.ts","names":[],"sources":["../src/buildPptx.ts"],"mappings":";;;;;UAiBiB,eAAA;EACf,IAAA,sBAA0B,OAAA;EAC1B,WAAA,EAAa,UAAU;AAAA;AAAA,iBAGH,SAAA,CACpB,GAAA,UACA,SAAA;EAAa,CAAA;EAAW,CAAA;AAAA,GACxB,OAAA;EACE,MAAA,GAAS,kBAAA;EACT,UAAA,GAAa,WAAA,GAAc,UAAA;EAC3B,eAAA,GAAkB,mBAAA;EAClB,OAAA;EACA,MAAA;AAAA,IAED,OAAA,CAAQ,eAAA"}
1
+ {"version":3,"file":"buildPptx.d.ts","names":[],"sources":["../src/buildPptx.ts"],"mappings":";;;;;UAkBiB,eAAA;EACf,IAAA,sBAA0B,OAAA;EAC1B,WAAA,EAAa,UAAU;AAAA;AAAA,iBAGH,SAAA,CACpB,GAAA,UACA,SAAA;EAAa,CAAA;EAAW,CAAA;AAAA,GACxB,OAAA;EACE,MAAA,GAAS,kBAAA;EACT,UAAA,GAAa,WAAA,GAAc,UAAA;EAC3B,eAAA,GAAkB,mBAAA;EAClB,OAAA;EACA,MAAA;AAAA,IAED,OAAA,CAAQ,eAAA"}
package/dist/buildPptx.js CHANGED
@@ -3,6 +3,7 @@ import { toPositioned } from "./toPositioned/toPositioned.js";
3
3
  import { calcYogaLayout } from "./calcYogaLayout/calcYogaLayout.js";
4
4
  import { autoFitSlide } from "./autoFit/autoFit.js";
5
5
  import { DiagnosticsError } from "./diagnostics.js";
6
+ import { patchPptxWriteForGradientFills } from "./renderPptx/gradientFills.js";
6
7
  import { createBuildContext } from "./buildContext.js";
7
8
  import { extractLayoutResults } from "./calcYogaLayout/types.js";
8
9
  import { parseMasterPptx } from "./parseMasterPptx.js";
@@ -11,6 +12,8 @@ import { renderPptx } from "./renderPptx/renderPptx.js";
11
12
  //#region src/buildPptx.ts
12
13
  async function buildPptx(xml, slideSize, options) {
13
14
  const ctx = createBuildContext(options?.textMeasurement ?? "auto");
15
+ ctx.gradientFills.reserveColors(xml);
16
+ if (options?.master) ctx.gradientFills.reserveColors(JSON.stringify(options.master));
14
17
  const nodes = parseXml(xml);
15
18
  const positionedPages = [];
16
19
  for (const node of nodes) {
@@ -38,6 +41,7 @@ async function buildPptx(xml, slideSize, options) {
38
41
  ctx.diagnostics.add("MASTER_PPTX_PARSE_FAILED", message);
39
42
  }
40
43
  const pptx = await renderPptx(positionedPages, slideSize, ctx, master);
44
+ patchPptxWriteForGradientFills(pptx, ctx.gradientFills);
41
45
  const diagnostics = ctx.diagnostics.items;
42
46
  if (options?.strict && diagnostics.length > 0) throw new DiagnosticsError(diagnostics);
43
47
  return {
@@ -1 +1 @@
1
- {"version":3,"file":"buildPptx.js","names":[],"sources":["../src/buildPptx.ts"],"sourcesContent":["import { autoFitSlide } from \"./autoFit/autoFit.ts\";\nimport { createBuildContext } from \"./buildContext.ts\";\nimport { calcYogaLayout } from \"./calcYogaLayout/calcYogaLayout.ts\";\nimport type { TextMeasurementMode } from \"./calcYogaLayout/measureText.ts\";\nimport type { YogaNodeMap } from \"./calcYogaLayout/types.ts\";\nimport { extractLayoutResults } from \"./calcYogaLayout/types.ts\";\nimport type { Diagnostic } from \"./diagnostics.ts\";\nimport { DiagnosticsError } from \"./diagnostics.ts\";\nimport { parseMasterPptx } from \"./parseMasterPptx.ts\";\nimport { parseXml } from \"./parseXml/parseXml.ts\";\nimport { renderPptx } from \"./renderPptx/renderPptx.ts\";\nimport { freeYogaTree } from \"./shared/freeYogaTree.ts\";\nimport { toPositioned } from \"./toPositioned/toPositioned.ts\";\nimport { PositionedNode, SlideMasterOptions } from \"./types.ts\";\n\nexport type { TextMeasurementMode };\n\nexport interface BuildPptxResult {\n pptx: import(\"pptxgenjs\").default;\n diagnostics: Diagnostic[];\n}\n\nexport async function buildPptx(\n xml: string,\n slideSize: { w: number; h: number },\n options?: {\n master?: SlideMasterOptions;\n masterPptx?: ArrayBuffer | Uint8Array;\n textMeasurement?: TextMeasurementMode;\n autoFit?: boolean;\n strict?: boolean;\n },\n): Promise<BuildPptxResult> {\n const ctx = createBuildContext(options?.textMeasurement ?? \"auto\");\n\n const nodes = parseXml(xml);\n const positionedPages: PositionedNode[] = [];\n\n for (const node of nodes) {\n let map: YogaNodeMap | undefined;\n try {\n if (options?.autoFit !== false) {\n map = await autoFitSlide(node, slideSize, ctx);\n } else {\n map = await calcYogaLayout(node, slideSize, ctx);\n }\n const layoutMap = extractLayoutResults(map);\n const positioned = await toPositioned(node, ctx, layoutMap);\n positionedPages.push(positioned);\n } finally {\n if (map) freeYogaTree(map);\n }\n }\n\n // masterPptx から背景を抽出し、master オプションにマージ\n let master = options?.master;\n if (options?.masterPptx) {\n try {\n const bg = await parseMasterPptx(options.masterPptx);\n if (bg) {\n if (master) {\n // 明示的に background が指定されていない場合のみ、masterPptx の背景を使用\n if (!master.background) {\n master = { ...master, background: bg };\n }\n } else {\n master = { background: bg };\n }\n }\n } catch (e) {\n const message =\n e instanceof Error ? e.message : \"Unknown error parsing masterPptx\";\n ctx.diagnostics.add(\"MASTER_PPTX_PARSE_FAILED\", message);\n }\n }\n\n const pptx = await renderPptx(positionedPages, slideSize, ctx, master);\n const diagnostics = ctx.diagnostics.items;\n\n if (options?.strict && diagnostics.length > 0) {\n throw new DiagnosticsError(diagnostics);\n }\n\n return { pptx, diagnostics };\n}\n"],"mappings":";;;;;;;;;;;AAsBA,eAAsB,UACpB,KACA,WACA,SAO0B;CAC1B,MAAM,MAAM,mBAAmB,SAAS,mBAAmB,MAAM;CAEjE,MAAM,QAAQ,SAAS,GAAG;CAC1B,MAAM,kBAAoC,CAAC;CAE3C,KAAK,MAAM,QAAQ,OAAO;EACxB,IAAI;EACJ,IAAI;GACF,IAAI,SAAS,YAAY,OACvB,MAAM,MAAM,aAAa,MAAM,WAAW,GAAG;QAE7C,MAAM,MAAM,eAAe,MAAM,WAAW,GAAG;GAGjD,MAAM,aAAa,MAAM,aAAa,MAAM,KAD1B,qBAAqB,GACkB,CAAC;GAC1D,gBAAgB,KAAK,UAAU;EACjC,UAAU;GACR,IAAI,KAAK,aAAa,GAAG;EAC3B;CACF;CAGA,IAAI,SAAS,SAAS;CACtB,IAAI,SAAS,YACX,IAAI;EACF,MAAM,KAAK,MAAM,gBAAgB,QAAQ,UAAU;EACnD,IAAI,IACF,IAAI;OAEE,CAAC,OAAO,YACV,SAAS;IAAE,GAAG;IAAQ,YAAY;GAAG;EAAA,OAGvC,SAAS,EAAE,YAAY,GAAG;CAGhC,SAAS,GAAG;EACV,MAAM,UACJ,aAAa,QAAQ,EAAE,UAAU;EACnC,IAAI,YAAY,IAAI,4BAA4B,OAAO;CACzD;CAGF,MAAM,OAAO,MAAM,WAAW,iBAAiB,WAAW,KAAK,MAAM;CACrE,MAAM,cAAc,IAAI,YAAY;CAEpC,IAAI,SAAS,UAAU,YAAY,SAAS,GAC1C,MAAM,IAAI,iBAAiB,WAAW;CAGxC,OAAO;EAAE;EAAM;CAAY;AAC7B"}
1
+ {"version":3,"file":"buildPptx.js","names":[],"sources":["../src/buildPptx.ts"],"sourcesContent":["import { autoFitSlide } from \"./autoFit/autoFit.ts\";\nimport { createBuildContext } from \"./buildContext.ts\";\nimport { calcYogaLayout } from \"./calcYogaLayout/calcYogaLayout.ts\";\nimport type { TextMeasurementMode } from \"./calcYogaLayout/measureText.ts\";\nimport type { YogaNodeMap } from \"./calcYogaLayout/types.ts\";\nimport { extractLayoutResults } from \"./calcYogaLayout/types.ts\";\nimport type { Diagnostic } from \"./diagnostics.ts\";\nimport { DiagnosticsError } from \"./diagnostics.ts\";\nimport { parseMasterPptx } from \"./parseMasterPptx.ts\";\nimport { parseXml } from \"./parseXml/parseXml.ts\";\nimport { patchPptxWriteForGradientFills } from \"./renderPptx/gradientFills.ts\";\nimport { renderPptx } from \"./renderPptx/renderPptx.ts\";\nimport { freeYogaTree } from \"./shared/freeYogaTree.ts\";\nimport { toPositioned } from \"./toPositioned/toPositioned.ts\";\nimport { PositionedNode, SlideMasterOptions } from \"./types.ts\";\n\nexport type { TextMeasurementMode };\n\nexport interface BuildPptxResult {\n pptx: import(\"pptxgenjs\").default;\n diagnostics: Diagnostic[];\n}\n\nexport async function buildPptx(\n xml: string,\n slideSize: { w: number; h: number },\n options?: {\n master?: SlideMasterOptions;\n masterPptx?: ArrayBuffer | Uint8Array;\n textMeasurement?: TextMeasurementMode;\n autoFit?: boolean;\n strict?: boolean;\n },\n): Promise<BuildPptxResult> {\n const ctx = createBuildContext(options?.textMeasurement ?? \"auto\");\n\n // グラデーション後処理のマーカー色がユーザー指定色と衝突しないよう、\n // 入力 XML / master オプション中に現れる色を予約しておく\n ctx.gradientFills.reserveColors(xml);\n if (options?.master) {\n ctx.gradientFills.reserveColors(JSON.stringify(options.master));\n }\n\n const nodes = parseXml(xml);\n const positionedPages: PositionedNode[] = [];\n\n for (const node of nodes) {\n let map: YogaNodeMap | undefined;\n try {\n if (options?.autoFit !== false) {\n map = await autoFitSlide(node, slideSize, ctx);\n } else {\n map = await calcYogaLayout(node, slideSize, ctx);\n }\n const layoutMap = extractLayoutResults(map);\n const positioned = await toPositioned(node, ctx, layoutMap);\n positionedPages.push(positioned);\n } finally {\n if (map) freeYogaTree(map);\n }\n }\n\n // masterPptx から背景を抽出し、master オプションにマージ\n let master = options?.master;\n if (options?.masterPptx) {\n try {\n const bg = await parseMasterPptx(options.masterPptx);\n if (bg) {\n if (master) {\n // 明示的に background が指定されていない場合のみ、masterPptx の背景を使用\n if (!master.background) {\n master = { ...master, background: bg };\n }\n } else {\n master = { background: bg };\n }\n }\n } catch (e) {\n const message =\n e instanceof Error ? e.message : \"Unknown error parsing masterPptx\";\n ctx.diagnostics.add(\"MASTER_PPTX_PARSE_FAILED\", message);\n }\n }\n\n const pptx = await renderPptx(positionedPages, slideSize, ctx, master);\n\n // backgroundGradient 使用時は write/writeFile に gradFill 置換の後処理を仕込む\n patchPptxWriteForGradientFills(pptx, ctx.gradientFills);\n\n const diagnostics = ctx.diagnostics.items;\n\n if (options?.strict && diagnostics.length > 0) {\n throw new DiagnosticsError(diagnostics);\n }\n\n return { pptx, diagnostics };\n}\n"],"mappings":";;;;;;;;;;;;AAuBA,eAAsB,UACpB,KACA,WACA,SAO0B;CAC1B,MAAM,MAAM,mBAAmB,SAAS,mBAAmB,MAAM;CAIjE,IAAI,cAAc,cAAc,GAAG;CACnC,IAAI,SAAS,QACX,IAAI,cAAc,cAAc,KAAK,UAAU,QAAQ,MAAM,CAAC;CAGhE,MAAM,QAAQ,SAAS,GAAG;CAC1B,MAAM,kBAAoC,CAAC;CAE3C,KAAK,MAAM,QAAQ,OAAO;EACxB,IAAI;EACJ,IAAI;GACF,IAAI,SAAS,YAAY,OACvB,MAAM,MAAM,aAAa,MAAM,WAAW,GAAG;QAE7C,MAAM,MAAM,eAAe,MAAM,WAAW,GAAG;GAGjD,MAAM,aAAa,MAAM,aAAa,MAAM,KAD1B,qBAAqB,GACkB,CAAC;GAC1D,gBAAgB,KAAK,UAAU;EACjC,UAAU;GACR,IAAI,KAAK,aAAa,GAAG;EAC3B;CACF;CAGA,IAAI,SAAS,SAAS;CACtB,IAAI,SAAS,YACX,IAAI;EACF,MAAM,KAAK,MAAM,gBAAgB,QAAQ,UAAU;EACnD,IAAI,IACF,IAAI;OAEE,CAAC,OAAO,YACV,SAAS;IAAE,GAAG;IAAQ,YAAY;GAAG;EAAA,OAGvC,SAAS,EAAE,YAAY,GAAG;CAGhC,SAAS,GAAG;EACV,MAAM,UACJ,aAAa,QAAQ,EAAE,UAAU;EACnC,IAAI,YAAY,IAAI,4BAA4B,OAAO;CACzD;CAGF,MAAM,OAAO,MAAM,WAAW,iBAAiB,WAAW,KAAK,MAAM;CAGrE,+BAA+B,MAAM,IAAI,aAAa;CAEtD,MAAM,cAAc,IAAI,YAAY;CAEpC,IAAI,SAAS,UAAU,YAAY,SAAS,GAC1C,MAAM,IAAI,iBAAiB,WAAW;CAGxC,OAAO;EAAE;EAAM;CAAY;AAC7B"}
@@ -92,7 +92,7 @@ async function buildPomWithYogaTree(node, parentYoga, ctx, map, parentNode, gran
92
92
  await applyStyleToYogaNode(node, yn, ctx);
93
93
  if (parentNode?.type === "hstack" || parentNode?.type === "vstack") yn.setFlexShrink(node.type === "icon" ? 0 : 1);
94
94
  if (parentNode?.type === "hstack" && node.w === void 0 && node.type !== "table" && node.type !== "icon") {
95
- yn.setFlexGrow(1);
95
+ yn.setFlexGrow(node.grow ?? 1);
96
96
  if (nodeHasDefiniteWidth(parentNode, grandparentNode)) yn.setFlexBasis(0);
97
97
  }
98
98
  parentYoga.insertChild(yn, parentYoga.getChildCount());
@@ -128,6 +128,7 @@ async function applyStyleToYogaNode(node, yn, ctx) {
128
128
  yn.setHeightPercent(percent);
129
129
  }
130
130
  }
131
+ if (node.grow !== void 0) yn.setFlexGrow(node.grow);
131
132
  if (node.minW !== void 0) yn.setMinWidth(node.minW);
132
133
  if (node.maxW !== void 0) yn.setMaxWidth(node.maxW);
133
134
  if (node.minH !== void 0) yn.setMinHeight(node.minH);
@@ -1 +1 @@
1
- {"version":3,"file":"calcYogaLayout.js","names":[],"sources":["../../src/calcYogaLayout/calcYogaLayout.ts"],"sourcesContent":["import type { POMNode, AlignItems } from \"../types.ts\";\nimport type { BuildContext } from \"../buildContext.ts\";\nimport type { YogaNodeMap } from \"./types.ts\";\nimport { Node as YogaNode } from \"yoga-layout\";\nimport { loadYoga } from \"yoga-layout/load\";\nimport { prefetchImageSize } from \"../shared/measureImage.ts\";\nimport { freeYogaTree } from \"../shared/freeYogaTree.ts\";\nimport { getNodeDef } from \"../registry/index.ts\";\n\n/**\n * POMNode ツリーを Yoga でレイアウト計算する\n * POMNode と YogaNode の対応を YogaNodeMap として返す\n *\n * @param root 入力 POMNode ツリーのルート\n * @param slideSize スライド全体のサイズ(px)\n * @param ctx BuildContext\n * @returns YogaNodeMap(POMNode → YogaNode のマッピング)\n */\nexport async function calcYogaLayout(\n root: POMNode,\n slideSize: { w: number; h: number },\n ctx: BuildContext,\n): Promise<YogaNodeMap> {\n const Yoga = await getYoga();\n\n // 事前に全画像のサイズを取得(HTTPS対応のため)\n await prefetchAllImageSizes(root, ctx);\n\n const map: YogaNodeMap = new Map();\n\n try {\n const rootYoga = Yoga.Node.create();\n map.set(root, rootYoga);\n\n await buildPomWithYogaTree(root, rootYoga, ctx, map);\n\n // スライド全体サイズを指定\n rootYoga.setWidth(slideSize.w);\n rootYoga.setHeight(slideSize.h);\n\n rootYoga.calculateLayout(slideSize.w, slideSize.h, Yoga.DIRECTION_LTR);\n } catch (e) {\n // 途中で失敗した場合、作成済みの YogaNode を解放してから再 throw\n freeYogaTree(map);\n throw e;\n }\n\n return map;\n}\n\n/**\n * POMNode ツリー内のすべての画像のサイズを事前取得する\n */\nasync function prefetchAllImageSizes(\n node: POMNode,\n ctx: BuildContext,\n): Promise<void> {\n const imageSources = collectImageSources(node);\n await Promise.all(\n imageSources.map((src) =>\n prefetchImageSize(\n src,\n ctx.imageSizeCache,\n ctx.imageDataCache,\n ctx.diagnostics,\n ),\n ),\n );\n}\n\n/**\n * POMNode ツリー内のすべての画像のsrcを収集する\n */\nfunction collectImageSources(node: POMNode): string[] {\n const sources: string[] = [];\n\n function traverse(n: POMNode) {\n // backgroundImage の src を収集(全ノード共通)\n if (n.backgroundImage) {\n sources.push(n.backgroundImage.src);\n }\n\n const def = getNodeDef(n.type);\n\n // ノード固有の画像ソース収集\n if (def.collectImageSources) {\n sources.push(...def.collectImageSources(n));\n }\n\n // 子要素の再帰\n switch (def.category) {\n case \"multi-child\":\n case \"absolute-child\": {\n const containerNode = n as Extract<\n POMNode,\n { type: \"vstack\" | \"hstack\" | \"layer\" }\n >;\n for (const child of containerNode.children) {\n traverse(child);\n }\n break;\n }\n }\n }\n\n traverse(node);\n return sources;\n}\n\n/**\n * Yogaシングルトン\n */\nlet yogaP: Promise<Yoga> | null = null;\ntype Yoga = Awaited<ReturnType<typeof loadYoga>>;\nasync function getYoga(): Promise<Yoga> {\n if (yogaP === null) yogaP = loadYoga();\n return yogaP;\n}\n\n/**\n * ノードが交差軸方向(幅)で確定サイズを持つかを判定する\n * - 明示的な w がある場合は確定\n * - alignSelf で stretch 以外が指定されている場合は不確定\n * - 親の alignItems で stretch(デフォルト)以外が指定されている場合は不確定\n */\nfunction nodeHasDefiniteWidth(node: POMNode, parentNode?: POMNode): boolean {\n // 明示的な幅がある\n if (node.w !== undefined) return true;\n\n // alignSelf で stretch 以外が明示されている場合は不確定\n if (\n node.alignSelf !== undefined &&\n node.alignSelf !== \"stretch\" &&\n node.alignSelf !== \"auto\"\n ) {\n return false;\n }\n\n // 親がいない場合(ルートノード)は確定\n if (!parentNode) return true;\n\n // 親の alignItems を取得(VStack/HStack のみ持つ)\n let parentAlignItems: AlignItems | undefined;\n if (parentNode.type === \"vstack\") {\n parentAlignItems = parentNode.alignItems;\n } else if (parentNode.type === \"hstack\") {\n parentAlignItems = parentNode.alignItems;\n }\n\n // VStack(column 方向)の子の場合、交差軸は水平方向\n // alignItems が stretch(デフォルト)なら子は親幅に伸長される\n if (parentNode.type === \"vstack\" || parentNode.type === \"layer\") {\n return parentAlignItems === undefined || parentAlignItems === \"stretch\";\n }\n\n // HStack の子の場合、幅は主軸方向で flex により決まるため確定とみなす\n if (parentNode.type === \"hstack\") {\n return true;\n }\n\n return true;\n}\n\n/**\n * POMNode ツリーを再帰的に走査し、YogaNode ツリーを構築する\n */\nasync function buildPomWithYogaTree(\n node: POMNode,\n parentYoga: YogaNode,\n ctx: BuildContext,\n map: YogaNodeMap,\n parentNode?: POMNode,\n grandparentNode?: POMNode,\n) {\n const yoga = await getYoga();\n\n const yn = yoga.Node.create();\n map.set(node, yn); // 対応する YogaNode をマップに登録\n\n await applyStyleToYogaNode(node, yn, ctx);\n\n // HStack/VStack の子要素に flexShrink=1 をデフォルト設定(CSS Flexbox と同じ挙動)\n // 主軸方向で %サイズ + gap がある場合の overflow を防ぐ\n // アイコンは固定サイズのコンテンツなので shrink させない\n if (parentNode?.type === \"hstack\" || parentNode?.type === \"vstack\") {\n yn.setFlexShrink(node.type === \"icon\" ? 0 : 1);\n }\n\n // HStack の子要素で幅が指定されていない場合、デフォルトで均等分割\n // テーブルは setMeasureFunc でカラム幅合計を返すため除外\n // アイコンは固定サイズのコンテンツなので除外\n if (\n parentNode?.type === \"hstack\" &&\n node.w === undefined &&\n node.type !== \"table\" &&\n node.type !== \"icon\"\n ) {\n yn.setFlexGrow(1);\n // HStack が確定幅を持つ場合のみ flexBasis=0 で均等分割\n // HStack が auto-sized(親の alignItems が center/start/end 等)の場合、\n // flexBasis=0 だと子要素の自然な幅が失われてレイアウトが崩れるため、\n // flexBasis=auto(デフォルト)のまま維持する\n if (nodeHasDefiniteWidth(parentNode, grandparentNode)) {\n yn.setFlexBasis(0);\n }\n }\n\n parentYoga.insertChild(yn, parentYoga.getChildCount());\n\n const def = getNodeDef(node.type);\n\n switch (def.category) {\n case \"multi-child\":\n case \"absolute-child\": {\n const containerNode = node as Extract<\n POMNode,\n { type: \"vstack\" | \"hstack\" | \"layer\" }\n >;\n for (const child of containerNode.children) {\n await buildPomWithYogaTree(child, yn, ctx, map, node, parentNode);\n }\n break;\n }\n case \"leaf\":\n // 子要素なし\n break;\n }\n}\n\n/**\n * node のスタイルを YogaNode に適用する\n */\nasync function applyStyleToYogaNode(\n node: POMNode,\n yn: YogaNode,\n ctx: BuildContext,\n) {\n const yoga = await getYoga();\n\n // デフォルト: 縦並び\n yn.setFlexDirection(yoga.FLEX_DIRECTION_COLUMN);\n\n // width\n if (node.w !== undefined) {\n if (typeof node.w === \"number\") {\n yn.setWidth(node.w);\n } else if (node.w === \"max\") {\n yn.setFlexGrow(1);\n } else if (node.w.endsWith(\"%\")) {\n const percent = parseFloat(node.w);\n yn.setWidthPercent(percent);\n }\n }\n\n // height\n if (node.h !== undefined) {\n if (typeof node.h === \"number\") {\n yn.setHeight(node.h);\n } else if (node.h === \"max\") {\n yn.setFlexGrow(1);\n } else if (node.h.endsWith(\"%\")) {\n const percent = parseFloat(node.h);\n yn.setHeightPercent(percent);\n }\n }\n\n // min/max constraints\n if (node.minW !== undefined) {\n yn.setMinWidth(node.minW);\n }\n if (node.maxW !== undefined) {\n yn.setMaxWidth(node.maxW);\n }\n if (node.minH !== undefined) {\n yn.setMinHeight(node.minH);\n }\n if (node.maxH !== undefined) {\n yn.setMaxHeight(node.maxH);\n }\n\n // padding\n if (node.padding !== undefined) {\n if (typeof node.padding === \"number\") {\n yn.setPadding(yoga.EDGE_TOP, node.padding);\n yn.setPadding(yoga.EDGE_RIGHT, node.padding);\n yn.setPadding(yoga.EDGE_BOTTOM, node.padding);\n yn.setPadding(yoga.EDGE_LEFT, node.padding);\n } else {\n if (node.padding.top !== undefined) {\n yn.setPadding(yoga.EDGE_TOP, node.padding.top);\n }\n if (node.padding.right !== undefined) {\n yn.setPadding(yoga.EDGE_RIGHT, node.padding.right);\n }\n if (node.padding.bottom !== undefined) {\n yn.setPadding(yoga.EDGE_BOTTOM, node.padding.bottom);\n }\n if (node.padding.left !== undefined) {\n yn.setPadding(yoga.EDGE_LEFT, node.padding.left);\n }\n }\n }\n\n // margin\n if (node.margin !== undefined) {\n if (typeof node.margin === \"number\") {\n yn.setMargin(yoga.EDGE_TOP, node.margin);\n yn.setMargin(yoga.EDGE_RIGHT, node.margin);\n yn.setMargin(yoga.EDGE_BOTTOM, node.margin);\n yn.setMargin(yoga.EDGE_LEFT, node.margin);\n } else {\n if (node.margin.top !== undefined) {\n yn.setMargin(yoga.EDGE_TOP, node.margin.top);\n }\n if (node.margin.right !== undefined) {\n yn.setMargin(yoga.EDGE_RIGHT, node.margin.right);\n }\n if (node.margin.bottom !== undefined) {\n yn.setMargin(yoga.EDGE_BOTTOM, node.margin.bottom);\n }\n if (node.margin.left !== undefined) {\n yn.setMargin(yoga.EDGE_LEFT, node.margin.left);\n }\n }\n }\n\n // position\n if (node.position === \"absolute\") {\n yn.setPositionType(yoga.POSITION_TYPE_ABSOLUTE);\n }\n if (node.top !== undefined) {\n yn.setPosition(yoga.EDGE_TOP, node.top);\n }\n if (node.right !== undefined) {\n yn.setPosition(yoga.EDGE_RIGHT, node.right);\n }\n if (node.bottom !== undefined) {\n yn.setPosition(yoga.EDGE_BOTTOM, node.bottom);\n }\n if (node.left !== undefined) {\n yn.setPosition(yoga.EDGE_LEFT, node.left);\n }\n\n // alignSelf\n if (node.alignSelf !== undefined) {\n switch (node.alignSelf) {\n case \"auto\":\n yn.setAlignSelf(yoga.ALIGN_AUTO);\n break;\n case \"start\":\n yn.setAlignSelf(yoga.ALIGN_FLEX_START);\n break;\n case \"center\":\n yn.setAlignSelf(yoga.ALIGN_CENTER);\n break;\n case \"end\":\n yn.setAlignSelf(yoga.ALIGN_FLEX_END);\n break;\n case \"stretch\":\n yn.setAlignSelf(yoga.ALIGN_STRETCH);\n break;\n }\n }\n\n // ノード固有のスタイル適用(measureFunc 等)\n const def = getNodeDef(node.type);\n if (def.applyYogaStyle) {\n await def.applyYogaStyle(node, yn, yoga, ctx);\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"calcYogaLayout.js","names":[],"sources":["../../src/calcYogaLayout/calcYogaLayout.ts"],"sourcesContent":["import type { POMNode, AlignItems } from \"../types.ts\";\nimport type { BuildContext } from \"../buildContext.ts\";\nimport type { YogaNodeMap } from \"./types.ts\";\nimport { Node as YogaNode } from \"yoga-layout\";\nimport { loadYoga } from \"yoga-layout/load\";\nimport { prefetchImageSize } from \"../shared/measureImage.ts\";\nimport { freeYogaTree } from \"../shared/freeYogaTree.ts\";\nimport { getNodeDef } from \"../registry/index.ts\";\n\n/**\n * POMNode ツリーを Yoga でレイアウト計算する\n * POMNode と YogaNode の対応を YogaNodeMap として返す\n *\n * @param root 入力 POMNode ツリーのルート\n * @param slideSize スライド全体のサイズ(px)\n * @param ctx BuildContext\n * @returns YogaNodeMap(POMNode → YogaNode のマッピング)\n */\nexport async function calcYogaLayout(\n root: POMNode,\n slideSize: { w: number; h: number },\n ctx: BuildContext,\n): Promise<YogaNodeMap> {\n const Yoga = await getYoga();\n\n // 事前に全画像のサイズを取得(HTTPS対応のため)\n await prefetchAllImageSizes(root, ctx);\n\n const map: YogaNodeMap = new Map();\n\n try {\n const rootYoga = Yoga.Node.create();\n map.set(root, rootYoga);\n\n await buildPomWithYogaTree(root, rootYoga, ctx, map);\n\n // スライド全体サイズを指定\n rootYoga.setWidth(slideSize.w);\n rootYoga.setHeight(slideSize.h);\n\n rootYoga.calculateLayout(slideSize.w, slideSize.h, Yoga.DIRECTION_LTR);\n } catch (e) {\n // 途中で失敗した場合、作成済みの YogaNode を解放してから再 throw\n freeYogaTree(map);\n throw e;\n }\n\n return map;\n}\n\n/**\n * POMNode ツリー内のすべての画像のサイズを事前取得する\n */\nasync function prefetchAllImageSizes(\n node: POMNode,\n ctx: BuildContext,\n): Promise<void> {\n const imageSources = collectImageSources(node);\n await Promise.all(\n imageSources.map((src) =>\n prefetchImageSize(\n src,\n ctx.imageSizeCache,\n ctx.imageDataCache,\n ctx.diagnostics,\n ),\n ),\n );\n}\n\n/**\n * POMNode ツリー内のすべての画像のsrcを収集する\n */\nfunction collectImageSources(node: POMNode): string[] {\n const sources: string[] = [];\n\n function traverse(n: POMNode) {\n // backgroundImage の src を収集(全ノード共通)\n if (n.backgroundImage) {\n sources.push(n.backgroundImage.src);\n }\n\n const def = getNodeDef(n.type);\n\n // ノード固有の画像ソース収集\n if (def.collectImageSources) {\n sources.push(...def.collectImageSources(n));\n }\n\n // 子要素の再帰\n switch (def.category) {\n case \"multi-child\":\n case \"absolute-child\": {\n const containerNode = n as Extract<\n POMNode,\n { type: \"vstack\" | \"hstack\" | \"layer\" }\n >;\n for (const child of containerNode.children) {\n traverse(child);\n }\n break;\n }\n }\n }\n\n traverse(node);\n return sources;\n}\n\n/**\n * Yogaシングルトン\n */\nlet yogaP: Promise<Yoga> | null = null;\ntype Yoga = Awaited<ReturnType<typeof loadYoga>>;\nasync function getYoga(): Promise<Yoga> {\n if (yogaP === null) yogaP = loadYoga();\n return yogaP;\n}\n\n/**\n * ノードが交差軸方向(幅)で確定サイズを持つかを判定する\n * - 明示的な w がある場合は確定\n * - alignSelf で stretch 以外が指定されている場合は不確定\n * - 親の alignItems で stretch(デフォルト)以外が指定されている場合は不確定\n */\nfunction nodeHasDefiniteWidth(node: POMNode, parentNode?: POMNode): boolean {\n // 明示的な幅がある\n if (node.w !== undefined) return true;\n\n // alignSelf で stretch 以外が明示されている場合は不確定\n if (\n node.alignSelf !== undefined &&\n node.alignSelf !== \"stretch\" &&\n node.alignSelf !== \"auto\"\n ) {\n return false;\n }\n\n // 親がいない場合(ルートノード)は確定\n if (!parentNode) return true;\n\n // 親の alignItems を取得(VStack/HStack のみ持つ)\n let parentAlignItems: AlignItems | undefined;\n if (parentNode.type === \"vstack\") {\n parentAlignItems = parentNode.alignItems;\n } else if (parentNode.type === \"hstack\") {\n parentAlignItems = parentNode.alignItems;\n }\n\n // VStack(column 方向)の子の場合、交差軸は水平方向\n // alignItems が stretch(デフォルト)なら子は親幅に伸長される\n if (parentNode.type === \"vstack\" || parentNode.type === \"layer\") {\n return parentAlignItems === undefined || parentAlignItems === \"stretch\";\n }\n\n // HStack の子の場合、幅は主軸方向で flex により決まるため確定とみなす\n if (parentNode.type === \"hstack\") {\n return true;\n }\n\n return true;\n}\n\n/**\n * POMNode ツリーを再帰的に走査し、YogaNode ツリーを構築する\n */\nasync function buildPomWithYogaTree(\n node: POMNode,\n parentYoga: YogaNode,\n ctx: BuildContext,\n map: YogaNodeMap,\n parentNode?: POMNode,\n grandparentNode?: POMNode,\n) {\n const yoga = await getYoga();\n\n const yn = yoga.Node.create();\n map.set(node, yn); // 対応する YogaNode をマップに登録\n\n await applyStyleToYogaNode(node, yn, ctx);\n\n // HStack/VStack の子要素に flexShrink=1 をデフォルト設定(CSS Flexbox と同じ挙動)\n // 主軸方向で %サイズ + gap がある場合の overflow を防ぐ\n // アイコンは固定サイズのコンテンツなので shrink させない\n if (parentNode?.type === \"hstack\" || parentNode?.type === \"vstack\") {\n yn.setFlexShrink(node.type === \"icon\" ? 0 : 1);\n }\n\n // HStack の子要素で幅が指定されていない場合、grow の比率で余白を配分\n // (grow 未指定は 1 として扱い均等分割)\n // テーブルは setMeasureFunc でカラム幅合計を返すため除外\n // アイコンは固定サイズのコンテンツなので除外\n if (\n parentNode?.type === \"hstack\" &&\n node.w === undefined &&\n node.type !== \"table\" &&\n node.type !== \"icon\"\n ) {\n yn.setFlexGrow(node.grow ?? 1);\n // HStack が確定幅を持つ場合のみ flexBasis=0 で均等分割\n // HStack が auto-sized(親の alignItems が center/start/end 等)の場合、\n // flexBasis=0 だと子要素の自然な幅が失われてレイアウトが崩れるため、\n // flexBasis=auto(デフォルト)のまま維持する\n if (nodeHasDefiniteWidth(parentNode, grandparentNode)) {\n yn.setFlexBasis(0);\n }\n }\n\n parentYoga.insertChild(yn, parentYoga.getChildCount());\n\n const def = getNodeDef(node.type);\n\n switch (def.category) {\n case \"multi-child\":\n case \"absolute-child\": {\n const containerNode = node as Extract<\n POMNode,\n { type: \"vstack\" | \"hstack\" | \"layer\" }\n >;\n for (const child of containerNode.children) {\n await buildPomWithYogaTree(child, yn, ctx, map, node, parentNode);\n }\n break;\n }\n case \"leaf\":\n // 子要素なし\n break;\n }\n}\n\n/**\n * node のスタイルを YogaNode に適用する\n */\nasync function applyStyleToYogaNode(\n node: POMNode,\n yn: YogaNode,\n ctx: BuildContext,\n) {\n const yoga = await getYoga();\n\n // デフォルト: 縦並び\n yn.setFlexDirection(yoga.FLEX_DIRECTION_COLUMN);\n\n // width\n if (node.w !== undefined) {\n if (typeof node.w === \"number\") {\n yn.setWidth(node.w);\n } else if (node.w === \"max\") {\n yn.setFlexGrow(1);\n } else if (node.w.endsWith(\"%\")) {\n const percent = parseFloat(node.w);\n yn.setWidthPercent(percent);\n }\n }\n\n // height\n if (node.h !== undefined) {\n if (typeof node.h === \"number\") {\n yn.setHeight(node.h);\n } else if (node.h === \"max\") {\n yn.setFlexGrow(1);\n } else if (node.h.endsWith(\"%\")) {\n const percent = parseFloat(node.h);\n yn.setHeightPercent(percent);\n }\n }\n\n // flex-grow(w=\"max\" / h=\"max\" の setFlexGrow(1) より優先)\n if (node.grow !== undefined) {\n yn.setFlexGrow(node.grow);\n }\n\n // min/max constraints\n if (node.minW !== undefined) {\n yn.setMinWidth(node.minW);\n }\n if (node.maxW !== undefined) {\n yn.setMaxWidth(node.maxW);\n }\n if (node.minH !== undefined) {\n yn.setMinHeight(node.minH);\n }\n if (node.maxH !== undefined) {\n yn.setMaxHeight(node.maxH);\n }\n\n // padding\n if (node.padding !== undefined) {\n if (typeof node.padding === \"number\") {\n yn.setPadding(yoga.EDGE_TOP, node.padding);\n yn.setPadding(yoga.EDGE_RIGHT, node.padding);\n yn.setPadding(yoga.EDGE_BOTTOM, node.padding);\n yn.setPadding(yoga.EDGE_LEFT, node.padding);\n } else {\n if (node.padding.top !== undefined) {\n yn.setPadding(yoga.EDGE_TOP, node.padding.top);\n }\n if (node.padding.right !== undefined) {\n yn.setPadding(yoga.EDGE_RIGHT, node.padding.right);\n }\n if (node.padding.bottom !== undefined) {\n yn.setPadding(yoga.EDGE_BOTTOM, node.padding.bottom);\n }\n if (node.padding.left !== undefined) {\n yn.setPadding(yoga.EDGE_LEFT, node.padding.left);\n }\n }\n }\n\n // margin\n if (node.margin !== undefined) {\n if (typeof node.margin === \"number\") {\n yn.setMargin(yoga.EDGE_TOP, node.margin);\n yn.setMargin(yoga.EDGE_RIGHT, node.margin);\n yn.setMargin(yoga.EDGE_BOTTOM, node.margin);\n yn.setMargin(yoga.EDGE_LEFT, node.margin);\n } else {\n if (node.margin.top !== undefined) {\n yn.setMargin(yoga.EDGE_TOP, node.margin.top);\n }\n if (node.margin.right !== undefined) {\n yn.setMargin(yoga.EDGE_RIGHT, node.margin.right);\n }\n if (node.margin.bottom !== undefined) {\n yn.setMargin(yoga.EDGE_BOTTOM, node.margin.bottom);\n }\n if (node.margin.left !== undefined) {\n yn.setMargin(yoga.EDGE_LEFT, node.margin.left);\n }\n }\n }\n\n // position\n if (node.position === \"absolute\") {\n yn.setPositionType(yoga.POSITION_TYPE_ABSOLUTE);\n }\n if (node.top !== undefined) {\n yn.setPosition(yoga.EDGE_TOP, node.top);\n }\n if (node.right !== undefined) {\n yn.setPosition(yoga.EDGE_RIGHT, node.right);\n }\n if (node.bottom !== undefined) {\n yn.setPosition(yoga.EDGE_BOTTOM, node.bottom);\n }\n if (node.left !== undefined) {\n yn.setPosition(yoga.EDGE_LEFT, node.left);\n }\n\n // alignSelf\n if (node.alignSelf !== undefined) {\n switch (node.alignSelf) {\n case \"auto\":\n yn.setAlignSelf(yoga.ALIGN_AUTO);\n break;\n case \"start\":\n yn.setAlignSelf(yoga.ALIGN_FLEX_START);\n break;\n case \"center\":\n yn.setAlignSelf(yoga.ALIGN_CENTER);\n break;\n case \"end\":\n yn.setAlignSelf(yoga.ALIGN_FLEX_END);\n break;\n case \"stretch\":\n yn.setAlignSelf(yoga.ALIGN_STRETCH);\n break;\n }\n }\n\n // ノード固有のスタイル適用(measureFunc 等)\n const def = getNodeDef(node.type);\n if (def.applyYogaStyle) {\n await def.applyYogaStyle(node, yn, yoga, ctx);\n }\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"fontLoader.js","names":[],"sources":["../../src/calcYogaLayout/fontLoader.ts"],"sourcesContent":["/**\n * opentype.js を使用したフォント読み込みモジュール\n * Node.js とブラウザ両方で動作する\n */\n\nimport type { Font } from \"opentype.js\";\nimport * as opentypeModule from \"opentype.js\";\nimport { NOTO_SANS_JP_REGULAR_BASE64 } from \"./fonts/notoSansJPRegular.ts\";\nimport { NOTO_SANS_JP_BOLD_BASE64 } from \"./fonts/notoSansJPBold.ts\";\n\n// opentype.js 2.0 は ESM ビルドで named export のみを提供する一方、\n// CJS UMD ビルドでは module.exports = factory() の動的構造のため\n// Node ESM から取り込むと named exports が静的解析できない。\n// どちらの形でも動くよう default プロパティを優先して unwrap する。\nconst opentype =\n (opentypeModule as unknown as { default?: typeof opentypeModule }).default ??\n opentypeModule;\n\n// フォントキャッシュ\nconst fontCache = new Map<string, Font>();\n\n/**\n * Base64 文字列を ArrayBuffer に変換する\n * Node.js とブラウザ両方で動作する\n */\nfunction base64ToArrayBuffer(base64: string): ArrayBuffer {\n // Node.js 環境\n if (typeof Buffer !== \"undefined\") {\n const buffer = Buffer.from(base64, \"base64\");\n return buffer.buffer.slice(\n buffer.byteOffset,\n buffer.byteOffset + buffer.byteLength,\n );\n }\n // ブラウザ環境\n const binaryString = atob(base64);\n const bytes = new Uint8Array(binaryString.length);\n for (let i = 0; i < binaryString.length; i++) {\n bytes[i] = binaryString.charCodeAt(i);\n }\n return bytes.buffer;\n}\n\n/**\n * フォントを取得する(キャッシュ付き)\n * @param weight フォントウェイト (\"normal\" or \"bold\")\n * @returns opentype.js の Font オブジェクト\n */\nfunction getFont(weight: \"normal\" | \"bold\"): Font {\n const cacheKey = weight;\n\n // キャッシュがあればそれを返す\n const cached = fontCache.get(cacheKey);\n if (cached) {\n return cached;\n }\n\n // Base64 データを選択\n const base64 =\n weight === \"bold\" ? NOTO_SANS_JP_BOLD_BASE64 : NOTO_SANS_JP_REGULAR_BASE64;\n\n // ArrayBuffer に変換してパース\n const buffer = base64ToArrayBuffer(base64);\n const font = opentype.parse(buffer);\n\n // キャッシュに保存\n fontCache.set(cacheKey, font);\n\n return font;\n}\n\n/** バンドル済みフォント名の一覧 */\nconst BUNDLED_FONT_NAMES = new Set([\"Noto Sans JP\"]);\n\n/**\n * 指定されたフォントがバンドル済みかどうかを判定する\n */\nexport function isBundledFont(fontFamily: string): boolean {\n return BUNDLED_FONT_NAMES.has(fontFamily);\n}\n\n/**\n * 指定したテキストの幅を計測する\n * @param text 計測するテキスト\n * @param fontSizePx フォントサイズ(ピクセル)\n * @param weight フォントウェイト\n * @returns テキスト幅(ピクセル)\n */\nexport function measureTextWidth(\n text: string,\n fontSizePx: number,\n weight: \"normal\" | \"bold\",\n): number {\n const font = getFont(weight);\n return font.getAdvanceWidth(text, fontSizePx, { kerning: true });\n}\n\n/**\n * フォントの自然な行高さ比率を取得する\n *\n * PowerPoint の lineHeight はフォントサイズではなく、\n * フォントメトリクス(ascent + descent)に対する倍率として適用される。\n * この関数は fontSizePx に対する自然な行高さの比率を返す。\n *\n * - USE_TYPO_METRICS (fsSelection bit 7) が設定されている場合:\n * sTypoAscender, sTypoDescender, sTypoLineGap を使用\n * - 設定されていない場合:\n * usWinAscent, usWinDescent を使用\n *\n * @param weight フォントウェイト\n * @returns fontSizePx に対する行高さの比率(例: 1.448)\n */\nexport function measureFontLineHeightRatio(weight: \"normal\" | \"bold\"): number {\n const font = getFont(weight);\n const upm = font.unitsPerEm;\n const os2 = font.tables?.os2;\n\n if (!os2) {\n return 1.0;\n }\n\n const useTypoMetrics = Boolean(os2.fsSelection & (1 << 7));\n\n if (useTypoMetrics) {\n return (os2.sTypoAscender - os2.sTypoDescender + os2.sTypoLineGap) / upm;\n }\n\n return (os2.usWinAscent + os2.usWinDescent) / upm;\n}\n"],"mappings":";;;;AAcA,MAAM,WACH,eAAkE,WACnE;AAGF,MAAM,4BAAY,IAAI,IAAkB;;;;;AAMxC,SAAS,oBAAoB,QAA6B;CAExD,IAAI,OAAO,WAAW,aAAa;EACjC,MAAM,SAAS,OAAO,KAAK,QAAQ,QAAQ;EAC3C,OAAO,OAAO,OAAO,MACnB,OAAO,YACP,OAAO,aAAa,OAAO,UAC7B;CACF;CAEA,MAAM,eAAe,KAAK,MAAM;CAChC,MAAM,QAAQ,IAAI,WAAW,aAAa,MAAM;CAChD,KAAK,IAAI,IAAI,GAAG,IAAI,aAAa,QAAQ,KACvC,MAAM,KAAK,aAAa,WAAW,CAAC;CAEtC,OAAO,MAAM;AACf;;;;;;AAOA,SAAS,QAAQ,QAAiC;CAChD,MAAM,WAAW;CAGjB,MAAM,SAAS,UAAU,IAAI,QAAQ;CACrC,IAAI,QACF,OAAO;CAQT,MAAM,SAAS,oBAHb,WAAW,SAAS,2BAA2B,2BAGR;CACzC,MAAM,OAAO,SAAS,MAAM,MAAM;CAGlC,UAAU,IAAI,UAAU,IAAI;CAE5B,OAAO;AACT;;AAGA,MAAM,qBAAqB,IAAI,IAAI,CAAC,cAAc,CAAC;;;;AAKnD,SAAgB,cAAc,YAA6B;CACzD,OAAO,mBAAmB,IAAI,UAAU;AAC1C;;;;;;;;AASA,SAAgB,iBACd,MACA,YACA,QACQ;CAER,OADa,QAAQ,MACX,EAAE,gBAAgB,MAAM,YAAY,EAAE,SAAS,KAAK,CAAC;AACjE;;;;;;;;;;;;;;;;AAiBA,SAAgB,2BAA2B,QAAmC;CAC5E,MAAM,OAAO,QAAQ,MAAM;CAC3B,MAAM,MAAM,KAAK;CACjB,MAAM,MAAM,KAAK,QAAQ;CAEzB,IAAI,CAAC,KACH,OAAO;CAKT,IAFuB,QAAQ,IAAI,cAAe,GAEjC,GACf,QAAQ,IAAI,gBAAgB,IAAI,iBAAiB,IAAI,gBAAgB;CAGvE,QAAQ,IAAI,cAAc,IAAI,gBAAgB;AAChD"}
1
+ {"version":3,"file":"fontLoader.js","names":[],"sources":["../../src/calcYogaLayout/fontLoader.ts"],"sourcesContent":["/**\n * opentype.js を使用したフォント読み込みモジュール\n * Node.js とブラウザ両方で動作する\n */\n\nimport type { Font } from \"opentype.js\";\nimport * as opentypeModule from \"opentype.js\";\nimport { NOTO_SANS_JP_REGULAR_BASE64 } from \"./fonts/notoSansJPRegular.ts\";\nimport { NOTO_SANS_JP_BOLD_BASE64 } from \"./fonts/notoSansJPBold.ts\";\n\n// opentype.js 2.0 は ESM ビルドで named export のみを提供する一方、\n// CJS UMD ビルドでは module.exports = factory() の動的構造のため\n// Node ESM から取り込むと named exports が静的解析できない。\n// どちらの形でも動くよう default プロパティを優先して unwrap する。\nconst opentype =\n (opentypeModule as unknown as { default?: typeof opentypeModule }).default ??\n opentypeModule;\n\n// フォントキャッシュ\nconst fontCache = new Map<string, Font>();\n\n/**\n * Base64 文字列を ArrayBuffer に変換する\n * Node.js とブラウザ両方で動作する\n */\nfunction base64ToArrayBuffer(base64: string): ArrayBuffer {\n // Node.js 環境\n if (typeof Buffer !== \"undefined\") {\n const buffer = Buffer.from(base64, \"base64\");\n return buffer.buffer.slice(\n buffer.byteOffset,\n buffer.byteOffset + buffer.byteLength,\n );\n }\n // ブラウザ環境\n const binaryString = atob(base64);\n const bytes = new Uint8Array(binaryString.length);\n for (let i = 0; i < binaryString.length; i++) {\n bytes[i] = binaryString.charCodeAt(i);\n }\n return bytes.buffer;\n}\n\n/**\n * フォントを取得する(キャッシュ付き)\n * @param weight フォントウェイト (\"normal\" or \"bold\")\n * @returns opentype.js の Font オブジェクト\n */\nfunction getFont(weight: \"normal\" | \"bold\"): Font {\n const cacheKey = weight;\n\n // キャッシュがあればそれを返す\n const cached = fontCache.get(cacheKey);\n if (cached) {\n return cached;\n }\n\n // Base64 データを選択\n const base64 =\n weight === \"bold\" ? NOTO_SANS_JP_BOLD_BASE64 : NOTO_SANS_JP_REGULAR_BASE64;\n\n // ArrayBuffer に変換してパース\n const buffer = base64ToArrayBuffer(base64);\n const font = opentype.parse(buffer);\n\n // キャッシュに保存\n fontCache.set(cacheKey, font);\n\n return font;\n}\n\n/** バンドル済みフォント名の一覧 */\nconst BUNDLED_FONT_NAMES = new Set([\"Noto Sans JP\"]);\n\n/**\n * 指定されたフォントがバンドル済みかどうかを判定する\n */\nexport function isBundledFont(fontFamily: string): boolean {\n return BUNDLED_FONT_NAMES.has(fontFamily);\n}\n\n/**\n * 指定したテキストの幅を計測する\n * @param text 計測するテキスト\n * @param fontSizePx フォントサイズ(ピクセル)\n * @param weight フォントウェイト\n * @returns テキスト幅(ピクセル)\n */\nexport function measureTextWidth(\n text: string,\n fontSizePx: number,\n weight: \"normal\" | \"bold\",\n): number {\n const font = getFont(weight);\n return font.getAdvanceWidth(text, fontSizePx, { kerning: true });\n}\n\n/**\n * フォントの自然な行高さ比率を取得する\n *\n * PowerPoint の lineHeight はフォントサイズではなく、\n * フォントメトリクス(ascent + descent)に対する倍率として適用される。\n * この関数は fontSizePx に対する自然な行高さの比率を返す。\n *\n * - USE_TYPO_METRICS (fsSelection bit 7) が設定されている場合:\n * sTypoAscender, sTypoDescender, sTypoLineGap を使用\n * - 設定されていない場合:\n * usWinAscent, usWinDescent を使用\n *\n * @param weight フォントウェイト\n * @returns fontSizePx に対する行高さの比率(例: 1.448)\n */\nexport function measureFontLineHeightRatio(weight: \"normal\" | \"bold\"): number {\n const font = getFont(weight);\n const upm = font.unitsPerEm;\n const os2 = font.tables?.os2;\n\n if (!os2) {\n return 1.0;\n }\n\n const useTypoMetrics = Boolean(os2.fsSelection & (1 << 7));\n\n if (useTypoMetrics) {\n return (os2.sTypoAscender - os2.sTypoDescender + os2.sTypoLineGap) / upm;\n }\n\n return (os2.usWinAscent + os2.usWinDescent) / upm;\n}\n"],"mappings":";;;;AAcA,MAAM,WACH,eAAkE,WACnE;AAGF,MAAM,4BAAY,IAAI,IAAkB;;;;;AAMxC,SAAS,oBAAoB,QAA6B;CAExD,IAAI,OAAO,WAAW,aAAa;EACjC,MAAM,SAAS,OAAO,KAAK,QAAQ,QAAQ;EAC3C,OAAO,OAAO,OAAO,MACnB,OAAO,YACP,OAAO,aAAa,OAAO,UAC7B;CACF;CAEA,MAAM,eAAe,KAAK,MAAM;CAChC,MAAM,QAAQ,IAAI,WAAW,aAAa,MAAM;CAChD,KAAK,IAAI,IAAI,GAAG,IAAI,aAAa,QAAQ,KACvC,MAAM,KAAK,aAAa,WAAW,CAAC;CAEtC,OAAO,MAAM;AACf;;;;;;AAOA,SAAS,QAAQ,QAAiC;CAChD,MAAM,WAAW;CAGjB,MAAM,SAAS,UAAU,IAAI,QAAQ;CACrC,IAAI,QACF,OAAO;CAQT,MAAM,SAAS,oBAHb,WAAW,SAAS,2BAA2B,2BAGR;CACzC,MAAM,OAAO,SAAS,MAAM,MAAM;CAGlC,UAAU,IAAI,UAAU,IAAI;CAE5B,OAAO;AACT;;AAGA,MAAM,qBAAqB,IAAI,IAAI,CAAC,cAAc,CAAC;;;;AAKnD,SAAgB,cAAc,YAA6B;CACzD,OAAO,mBAAmB,IAAI,UAAU;AAC1C;;;;;;;;AASA,SAAgB,iBACd,MACA,YACA,QACQ;CAER,OADa,QAAQ,MACX,CAAC,CAAC,gBAAgB,MAAM,YAAY,EAAE,SAAS,KAAK,CAAC;AACjE;;;;;;;;;;;;;;;;AAiBA,SAAgB,2BAA2B,QAAmC;CAC5E,MAAM,OAAO,QAAQ,MAAM;CAC3B,MAAM,MAAM,KAAK;CACjB,MAAM,MAAM,KAAK,QAAQ;CAEzB,IAAI,CAAC,KACH,OAAO;CAKT,IAFuB,QAAQ,IAAI,cAAe,GAEjC,GACf,QAAQ,IAAI,gBAAgB,IAAI,iBAAiB,IAAI,gBAAgB;CAGvE,QAAQ,IAAI,cAAc,IAAI,gBAAgB;AAChD"}
@@ -1 +1 @@
1
- {"version":3,"file":"measureText.d.ts","names":[],"sources":["../../src/calcYogaLayout/measureText.ts"],"mappings":";KAYY,mBAAA"}
1
+ {"version":3,"file":"measureText.d.ts","names":[],"sources":["../../src/calcYogaLayout/measureText.ts"],"mappings":";KAaY,mBAAA"}
@@ -33,6 +33,13 @@ function estimateTextWidth(text, fontSizePx) {
33
33
  return width;
34
34
  }
35
35
  /**
36
+ * 計測関数に letterSpacing(文字数 × 字間 px)の加算を合成する
37
+ */
38
+ function withLetterSpacing(measureWidth, letterSpacingPx) {
39
+ if (!letterSpacingPx) return measureWidth;
40
+ return (text) => measureWidth(text) + Array.from(text).length * letterSpacingPx;
41
+ }
42
+ /**
36
43
  * テキストを折り返して行ごとの幅を計算する
37
44
  */
38
45
  function wrapText(text, maxWidthPx, measureWidth) {
@@ -100,14 +107,14 @@ function measureText(text, maxWidthPx, opts, mode = "auto") {
100
107
  */
101
108
  function measureTextWithOpentype(text, maxWidthPx, opts) {
102
109
  const fontWeight = normalizeFontWeight(opts.fontWeight);
103
- return calculateResult(wrapText(text, maxWidthPx, (t) => measureTextWidth(t, opts.fontSizePx, fontWeight)), opts);
110
+ return calculateResult(wrapText(text, maxWidthPx, withLetterSpacing((t) => measureTextWidth(t, opts.fontSizePx, fontWeight), opts.letterSpacingPx)), opts);
104
111
  }
105
112
  /**
106
113
  * フォールバック計算を使ったテキスト計測
107
114
  */
108
115
  function measureTextFallback(text, maxWidthPx, opts) {
109
116
  const { fontSizePx } = opts;
110
- return calculateResult(wrapText(text, maxWidthPx, (t) => estimateTextWidth(t, fontSizePx)), opts);
117
+ return calculateResult(wrapText(text, maxWidthPx, withLetterSpacing((t) => estimateTextWidth(t, fontSizePx), opts.letterSpacingPx)), opts);
111
118
  }
112
119
  function splitForWrap(text) {
113
120
  if (/[\p{Script=Hiragana}\p{Script=Katakana}\p{Script=Han}]/u.test(text)) return Array.from(text);
@@ -1 +1 @@
1
- {"version":3,"file":"measureText.js","names":["measureTextWidthOpentype"],"sources":["../../src/calcYogaLayout/measureText.ts"],"sourcesContent":["import {\n measureTextWidth as measureTextWidthOpentype,\n isBundledFont,\n} from \"./fontLoader.ts\";\n\ntype MeasureOptions = {\n fontFamily: string;\n fontSizePx: number;\n fontWeight?: \"normal\" | \"bold\" | number;\n lineHeight?: number;\n};\n\nexport type TextMeasurementMode = \"opentype\" | \"fallback\" | \"auto\";\n\n/**\n * 文字がCJK(日本語・中国語・韓国語)文字かどうかを判定する\n */\nfunction isCJKChar(char: string): boolean {\n const code = char.codePointAt(0);\n if (code === undefined) return false;\n\n // CJK統合漢字\n if (code >= 0x4e00 && code <= 0x9fff) return true;\n // CJK統合漢字拡張A\n if (code >= 0x3400 && code <= 0x4dbf) return true;\n // CJK統合漢字拡張B-F\n if (code >= 0x20000 && code <= 0x2ebef) return true;\n // ひらがな\n if (code >= 0x3040 && code <= 0x309f) return true;\n // カタカナ\n if (code >= 0x30a0 && code <= 0x30ff) return true;\n // 全角英数字・記号\n if (code >= 0xff00 && code <= 0xffef) return true;\n // CJK記号\n if (code >= 0x3000 && code <= 0x303f) return true;\n\n return false;\n}\n\n/**\n * フォールバック計算で文字の幅を推定する\n * - CJK文字: 1em(= fontSizePx)\n * - 英数字・半角記号: 0.5em\n */\nfunction estimateCharWidth(char: string, fontSizePx: number): number {\n if (isCJKChar(char)) {\n return fontSizePx; // 1em\n }\n return fontSizePx * 0.5; // 0.5em\n}\n\n/**\n * フォールバック計算でテキスト幅を推定する\n */\nfunction estimateTextWidth(text: string, fontSizePx: number): number {\n let width = 0;\n for (const char of text) {\n width += estimateCharWidth(char, fontSizePx);\n }\n return width;\n}\n\n/**\n * テキスト幅計測関数の型\n */\ntype MeasureTextWidthFn = (text: string) => number;\n\n/**\n * テキストを折り返して行ごとの幅を計算する\n */\nfunction wrapText(\n text: string,\n maxWidthPx: number,\n measureWidth: MeasureTextWidthFn,\n): { widthPx: number }[] {\n const paragraphs = text.split(\"\\n\");\n const lines: { widthPx: number }[] = [];\n\n for (const paragraph of paragraphs) {\n if (paragraph === \"\") {\n lines.push({ widthPx: 0 });\n continue;\n }\n\n const words = splitForWrap(paragraph);\n let current = \"\";\n let currentWidth = 0;\n\n for (const word of words) {\n const candidate = current ? current + word : word;\n const w = measureWidth(candidate);\n\n if (w <= maxWidthPx || !current) {\n current = candidate;\n currentWidth = w;\n } else {\n lines.push({ widthPx: currentWidth });\n current = word;\n currentWidth = measureWidth(word);\n }\n }\n\n if (current) {\n lines.push({ widthPx: currentWidth });\n }\n }\n\n return lines;\n}\n\n/**\n * 行情報から最終的なサイズを計算する\n */\nfunction calculateResult(\n lines: { widthPx: number }[],\n opts: MeasureOptions,\n): { widthPx: number; heightPx: number } {\n const lineHeightRatio = opts.lineHeight ?? 1.3;\n const lineHeightPx = opts.fontSizePx * lineHeightRatio;\n const widthPx = lines.length ? Math.max(...lines.map((l) => l.widthPx)) : 0;\n const heightPx = lines.length * lineHeightPx;\n // 端数切り上げ+余裕分 10px を足す\n return { widthPx: widthPx + 10, heightPx };\n}\n\n/**\n * fontWeight を \"normal\" | \"bold\" に正規化する\n */\nfunction normalizeFontWeight(\n weight: \"normal\" | \"bold\" | number | undefined,\n): \"normal\" | \"bold\" {\n if (weight === \"bold\" || weight === 700) {\n return \"bold\";\n }\n return \"normal\";\n}\n\n/**\n * テキストを折り返し付きでレイアウトし、そのサイズを測定する\n */\nexport function measureText(\n text: string,\n maxWidthPx: number,\n opts: MeasureOptions,\n mode: TextMeasurementMode = \"auto\",\n): {\n widthPx: number;\n heightPx: number;\n} {\n // 計測方法を決定\n // \"opentype\" / \"fallback\" が明示指定された場合はそれを優先\n // \"auto\" の場合はバンドル外フォントならフォールバック計測を使用\n const shouldUseFallback = (() => {\n switch (mode) {\n case \"opentype\":\n return false;\n case \"fallback\":\n return true;\n case \"auto\":\n return !isBundledFont(opts.fontFamily);\n }\n })();\n\n if (shouldUseFallback) {\n return measureTextFallback(text, maxWidthPx, opts);\n }\n\n return measureTextWithOpentype(text, maxWidthPx, opts);\n}\n\n/**\n * opentype.js を使ったテキスト計測\n */\nfunction measureTextWithOpentype(\n text: string,\n maxWidthPx: number,\n opts: MeasureOptions,\n): { widthPx: number; heightPx: number } {\n const fontWeight = normalizeFontWeight(opts.fontWeight);\n const lines = wrapText(text, maxWidthPx, (t) =>\n measureTextWidthOpentype(t, opts.fontSizePx, fontWeight),\n );\n return calculateResult(lines, opts);\n}\n\n/**\n * フォールバック計算を使ったテキスト計測\n */\nfunction measureTextFallback(\n text: string,\n maxWidthPx: number,\n opts: MeasureOptions,\n): { widthPx: number; heightPx: number } {\n const { fontSizePx } = opts;\n const lines = wrapText(text, maxWidthPx, (t) =>\n estimateTextWidth(t, fontSizePx),\n );\n return calculateResult(lines, opts);\n}\n\n// ラップ用の分割ロジック\n// - 英文: 空白で分割しつつ、空白も行末に残す\n// - 日本語: とりあえず 1 文字ずつ(必要なら賢くする)\nfunction splitForWrap(text: string): string[] {\n // 超雑実装:全角ひらがな・カタカナ・漢字が多そうなら 1 文字ずつ、それ以外は空白区切り\n const hasCJK = /[\\p{Script=Hiragana}\\p{Script=Katakana}\\p{Script=Han}]/u.test(\n text,\n );\n\n if (hasCJK) {\n return Array.from(text); // 1 glyph ≒ 1 文字として扱う\n }\n\n // 英文用:単語 + 後続スペースをトークンにする\n const tokens: string[] = [];\n const re = /(\\S+\\s*|\\s+)/g;\n let m: RegExpExecArray | null;\n while ((m = re.exec(text))) {\n tokens.push(m[0]);\n }\n return tokens;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"measureText.js","names":["measureTextWidthOpentype"],"sources":["../../src/calcYogaLayout/measureText.ts"],"sourcesContent":["import {\n measureTextWidth as measureTextWidthOpentype,\n isBundledFont,\n} from \"./fontLoader.ts\";\n\ntype MeasureOptions = {\n fontFamily: string;\n fontSizePx: number;\n fontWeight?: \"normal\" | \"bold\" | number;\n lineHeight?: number;\n letterSpacingPx?: number;\n};\n\nexport type TextMeasurementMode = \"opentype\" | \"fallback\" | \"auto\";\n\n/**\n * 文字がCJK(日本語・中国語・韓国語)文字かどうかを判定する\n */\nfunction isCJKChar(char: string): boolean {\n const code = char.codePointAt(0);\n if (code === undefined) return false;\n\n // CJK統合漢字\n if (code >= 0x4e00 && code <= 0x9fff) return true;\n // CJK統合漢字拡張A\n if (code >= 0x3400 && code <= 0x4dbf) return true;\n // CJK統合漢字拡張B-F\n if (code >= 0x20000 && code <= 0x2ebef) return true;\n // ひらがな\n if (code >= 0x3040 && code <= 0x309f) return true;\n // カタカナ\n if (code >= 0x30a0 && code <= 0x30ff) return true;\n // 全角英数字・記号\n if (code >= 0xff00 && code <= 0xffef) return true;\n // CJK記号\n if (code >= 0x3000 && code <= 0x303f) return true;\n\n return false;\n}\n\n/**\n * フォールバック計算で文字の幅を推定する\n * - CJK文字: 1em(= fontSizePx)\n * - 英数字・半角記号: 0.5em\n */\nfunction estimateCharWidth(char: string, fontSizePx: number): number {\n if (isCJKChar(char)) {\n return fontSizePx; // 1em\n }\n return fontSizePx * 0.5; // 0.5em\n}\n\n/**\n * フォールバック計算でテキスト幅を推定する\n */\nfunction estimateTextWidth(text: string, fontSizePx: number): number {\n let width = 0;\n for (const char of text) {\n width += estimateCharWidth(char, fontSizePx);\n }\n return width;\n}\n\n/**\n * テキスト幅計測関数の型\n */\ntype MeasureTextWidthFn = (text: string) => number;\n\n/**\n * 計測関数に letterSpacing(文字数 × 字間 px)の加算を合成する\n */\nfunction withLetterSpacing(\n measureWidth: MeasureTextWidthFn,\n letterSpacingPx: number | undefined,\n): MeasureTextWidthFn {\n if (!letterSpacingPx) return measureWidth;\n return (text) =>\n measureWidth(text) + Array.from(text).length * letterSpacingPx;\n}\n\n/**\n * テキストを折り返して行ごとの幅を計算する\n */\nfunction wrapText(\n text: string,\n maxWidthPx: number,\n measureWidth: MeasureTextWidthFn,\n): { widthPx: number }[] {\n const paragraphs = text.split(\"\\n\");\n const lines: { widthPx: number }[] = [];\n\n for (const paragraph of paragraphs) {\n if (paragraph === \"\") {\n lines.push({ widthPx: 0 });\n continue;\n }\n\n const words = splitForWrap(paragraph);\n let current = \"\";\n let currentWidth = 0;\n\n for (const word of words) {\n const candidate = current ? current + word : word;\n const w = measureWidth(candidate);\n\n if (w <= maxWidthPx || !current) {\n current = candidate;\n currentWidth = w;\n } else {\n lines.push({ widthPx: currentWidth });\n current = word;\n currentWidth = measureWidth(word);\n }\n }\n\n if (current) {\n lines.push({ widthPx: currentWidth });\n }\n }\n\n return lines;\n}\n\n/**\n * 行情報から最終的なサイズを計算する\n */\nfunction calculateResult(\n lines: { widthPx: number }[],\n opts: MeasureOptions,\n): { widthPx: number; heightPx: number } {\n const lineHeightRatio = opts.lineHeight ?? 1.3;\n const lineHeightPx = opts.fontSizePx * lineHeightRatio;\n const widthPx = lines.length ? Math.max(...lines.map((l) => l.widthPx)) : 0;\n const heightPx = lines.length * lineHeightPx;\n // 端数切り上げ+余裕分 10px を足す\n return { widthPx: widthPx + 10, heightPx };\n}\n\n/**\n * fontWeight を \"normal\" | \"bold\" に正規化する\n */\nfunction normalizeFontWeight(\n weight: \"normal\" | \"bold\" | number | undefined,\n): \"normal\" | \"bold\" {\n if (weight === \"bold\" || weight === 700) {\n return \"bold\";\n }\n return \"normal\";\n}\n\n/**\n * テキストを折り返し付きでレイアウトし、そのサイズを測定する\n */\nexport function measureText(\n text: string,\n maxWidthPx: number,\n opts: MeasureOptions,\n mode: TextMeasurementMode = \"auto\",\n): {\n widthPx: number;\n heightPx: number;\n} {\n // 計測方法を決定\n // \"opentype\" / \"fallback\" が明示指定された場合はそれを優先\n // \"auto\" の場合はバンドル外フォントならフォールバック計測を使用\n const shouldUseFallback = (() => {\n switch (mode) {\n case \"opentype\":\n return false;\n case \"fallback\":\n return true;\n case \"auto\":\n return !isBundledFont(opts.fontFamily);\n }\n })();\n\n if (shouldUseFallback) {\n return measureTextFallback(text, maxWidthPx, opts);\n }\n\n return measureTextWithOpentype(text, maxWidthPx, opts);\n}\n\n/**\n * opentype.js を使ったテキスト計測\n */\nfunction measureTextWithOpentype(\n text: string,\n maxWidthPx: number,\n opts: MeasureOptions,\n): { widthPx: number; heightPx: number } {\n const fontWeight = normalizeFontWeight(opts.fontWeight);\n const lines = wrapText(\n text,\n maxWidthPx,\n withLetterSpacing(\n (t) => measureTextWidthOpentype(t, opts.fontSizePx, fontWeight),\n opts.letterSpacingPx,\n ),\n );\n return calculateResult(lines, opts);\n}\n\n/**\n * フォールバック計算を使ったテキスト計測\n */\nfunction measureTextFallback(\n text: string,\n maxWidthPx: number,\n opts: MeasureOptions,\n): { widthPx: number; heightPx: number } {\n const { fontSizePx } = opts;\n const lines = wrapText(\n text,\n maxWidthPx,\n withLetterSpacing(\n (t) => estimateTextWidth(t, fontSizePx),\n opts.letterSpacingPx,\n ),\n );\n return calculateResult(lines, opts);\n}\n\n// ラップ用の分割ロジック\n// - 英文: 空白で分割しつつ、空白も行末に残す\n// - 日本語: とりあえず 1 文字ずつ(必要なら賢くする)\nfunction splitForWrap(text: string): string[] {\n // 超雑実装:全角ひらがな・カタカナ・漢字が多そうなら 1 文字ずつ、それ以外は空白区切り\n const hasCJK = /[\\p{Script=Hiragana}\\p{Script=Katakana}\\p{Script=Han}]/u.test(\n text,\n );\n\n if (hasCJK) {\n return Array.from(text); // 1 glyph ≒ 1 文字として扱う\n }\n\n // 英文用:単語 + 後続スペースをトークンにする\n const tokens: string[] = [];\n const re = /(\\S+\\s*|\\s+)/g;\n let m: RegExpExecArray | null;\n while ((m = re.exec(text))) {\n tokens.push(m[0]);\n }\n return tokens;\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"diagnostics.js","names":[],"sources":["../src/diagnostics.ts"],"sourcesContent":["export type DiagnosticCode =\n | \"IMAGE_MEASURE_FAILED\"\n | \"IMAGE_NOT_PREFETCHED\"\n | \"AUTOFIT_OVERFLOW\"\n | \"SCALE_BELOW_THRESHOLD\"\n | \"MASTER_PPTX_PARSE_FAILED\"\n | \"ARROW_REF_NOT_FOUND\"\n | \"DUPLICATE_NODE_ID\";\n\nexport interface Diagnostic {\n code: DiagnosticCode;\n message: string;\n}\n\nexport class DiagnosticCollector {\n readonly items: Diagnostic[] = [];\n\n add(code: DiagnosticCode, message: string): void {\n this.items.push({ code, message });\n }\n}\n\nexport class DiagnosticsError extends Error {\n constructor(public readonly diagnostics: Diagnostic[]) {\n const summary = diagnostics\n .map((d) => `[${d.code}] ${d.message}`)\n .join(\"\\n\");\n super(`Build completed with diagnostics:\\n${summary}`);\n this.name = \"DiagnosticsError\";\n }\n}\n"],"mappings":";AAcA,IAAa,sBAAb,MAAiC;CAC/B,QAA+B,CAAC;CAEhC,IAAI,MAAsB,SAAuB;EAC/C,KAAK,MAAM,KAAK;GAAE;GAAM;EAAQ,CAAC;CACnC;AACF;AAEA,IAAa,mBAAb,cAAsC,MAAM;CACd;CAA5B,YAAY,aAA2C;EACrD,MAAM,UAAU,YACb,KAAK,MAAM,IAAI,EAAE,KAAK,IAAI,EAAE,SAAS,EACrC,KAAK,IAAI;EACZ,MAAM,sCAAsC,SAAS;EAJ3B,KAAA,cAAA;EAK1B,KAAK,OAAO;CACd;AACF"}
1
+ {"version":3,"file":"diagnostics.js","names":[],"sources":["../src/diagnostics.ts"],"sourcesContent":["export type DiagnosticCode =\n | \"IMAGE_MEASURE_FAILED\"\n | \"IMAGE_NOT_PREFETCHED\"\n | \"AUTOFIT_OVERFLOW\"\n | \"SCALE_BELOW_THRESHOLD\"\n | \"MASTER_PPTX_PARSE_FAILED\"\n | \"ARROW_REF_NOT_FOUND\"\n | \"DUPLICATE_NODE_ID\";\n\nexport interface Diagnostic {\n code: DiagnosticCode;\n message: string;\n}\n\nexport class DiagnosticCollector {\n readonly items: Diagnostic[] = [];\n\n add(code: DiagnosticCode, message: string): void {\n this.items.push({ code, message });\n }\n}\n\nexport class DiagnosticsError extends Error {\n constructor(public readonly diagnostics: Diagnostic[]) {\n const summary = diagnostics\n .map((d) => `[${d.code}] ${d.message}`)\n .join(\"\\n\");\n super(`Build completed with diagnostics:\\n${summary}`);\n this.name = \"DiagnosticsError\";\n }\n}\n"],"mappings":";AAcA,IAAa,sBAAb,MAAiC;CAC/B,QAA+B,CAAC;CAEhC,IAAI,MAAsB,SAAuB;EAC/C,KAAK,MAAM,KAAK;GAAE;GAAM;EAAQ,CAAC;CACnC;AACF;AAEA,IAAa,mBAAb,cAAsC,MAAM;CACd;CAA5B,YAAY,aAA2C;EACrD,MAAM,UAAU,YACb,KAAK,MAAM,IAAI,EAAE,KAAK,IAAI,EAAE,SAAS,CAAC,CACtC,KAAK,IAAI;EACZ,MAAM,sCAAsC,SAAS;EAJ3B,KAAA,cAAA;EAK1B,KAAK,OAAO;CACd;AACF"}
@@ -1 +1 @@
1
- {"version":3,"file":"renderIcon.js","names":[],"sources":["../../src/icons/renderIcon.ts"],"sourcesContent":["import { existsSync } from \"node:fs\";\nimport { readFile } from \"node:fs/promises\";\nimport { createRequire } from \"node:module\";\nimport { dirname, join } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { ICON_DATA } from \"./iconData.ts\";\n\n// @resvg/resvg-wasm を遅延ロードする。\n// バンドラ(webpack / Turbopack)のスタティック解析が require / require.resolve を\n// 追跡してエラーにするのを避けるため、Function コンストラクタで Node.js の require を\n// 取得し、モジュール名は文字列結合で構築する。\n// Turbopack が動的 require / import を Webpack 同等に解析できるようになれば\n// 隠蔽自体が不要になり、apps/website 側の outputFileTracingIncludes 設定もまとめて\n// 撤去できる。\n// 上流 issue: https://github.com/vercel/next.js/issues/85238\ntype ResvgWasm = typeof import(\"@resvg/resvg-wasm\");\nconst RESVG_PKG = [\"@resvg\", \"resvg-wasm\"].join(\"/\");\nlet resvgModule: ResvgWasm | undefined;\nlet wasmInitPromise: Promise<void> | undefined;\n\n// Function コンストラクタを使って require を取得することでバンドラの静的解析から\n// 完全に隠蔽する。実行時は createRequire で生成した require が利用される。\nfunction getNodeRequire(): NodeJS.Require {\n // eslint-disable-next-line @typescript-eslint/no-implied-eval\n const factory = new Function(\n \"url\",\n \"createRequire\",\n \"return createRequire(url)\",\n ) as (\n url: string,\n createRequire: typeof import(\"node:module\").createRequire,\n ) => NodeJS.Require;\n return factory(import.meta.url, createRequire);\n}\n\n/**\n * WASM バイナリのパスを解決する。\n * バンドル環境(esbuild)では同ディレクトリの index_bg.wasm を参照し、\n * 非バンドル環境では createRequire で node_modules から解決する。\n */\nfunction resolveWasmPath(): string {\n const dir = dirname(fileURLToPath(import.meta.url));\n const localPath = join(dir, \"index_bg.wasm\");\n if (existsSync(localPath)) return localPath;\n return getNodeRequire().resolve(`${RESVG_PKG}/index_bg.wasm`);\n}\n\n/**\n * WASM モジュールを初期化し、Resvg クラスを返す。\n * 並行呼び出しでも安全(Promise をキャッシュ)。\n */\nfunction ensureWasmInitialized(): Promise<void> {\n if (!wasmInitPromise) {\n wasmInitPromise = (async () => {\n const mod = getNodeRequire()(RESVG_PKG) as ResvgWasm;\n const wasmPath = resolveWasmPath();\n const wasmBuffer = await readFile(wasmPath);\n await mod.initWasm(wasmBuffer);\n resvgModule = mod;\n })();\n }\n return wasmInitPromise;\n}\n\nfunction getResvg() {\n if (!resvgModule) throw new Error(\"WASM not initialized\");\n return resvgModule.Resvg;\n}\n\nfunction buildIconSvg(name: string, size: number, color: string): string {\n const pathData = ICON_DATA[name];\n if (!pathData) {\n throw new Error(`Unknown icon name: \"${name}\"`);\n }\n return `<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"${size}\" height=\"${size}\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"${color}\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">${pathData}</svg>`;\n}\n\nexport async function rasterizeIcon(\n name: string,\n size: number,\n color: string,\n cache: Map<string, string>,\n): Promise<string> {\n const key = `${name}|${size}|${color}`;\n const cached = cache.get(key);\n if (cached) return cached;\n\n await ensureWasmInitialized();\n const Resvg = getResvg();\n const svg = buildIconSvg(name, size, color);\n const resvg = new Resvg(svg, { fitTo: { mode: \"width\", value: size } });\n const pngData = resvg.render();\n const pngBuffer = pngData.asPng();\n const result = `image/png;base64,${Buffer.from(pngBuffer).toString(\"base64\")}`;\n cache.set(key, result);\n return result;\n}\n\n/**\n * インライン SVG 文字列を指定サイズでラスタライズし、base64 PNG を返す。\n * color が指定された場合、SVG ルートに stroke / fill 属性を設定する。\n */\nexport async function rasterizeSvgContent(\n svgContent: string,\n width: number,\n color: string | undefined,\n cache: Map<string, string>,\n height?: number,\n): Promise<string> {\n const h = height ?? width;\n const key = `svg:${svgContent}|${width}|${h}|${color ?? \"\"}`;\n const cached = cache.get(key);\n if (cached) return cached;\n\n // SVG に xmlns / width / height を設定し、color があれば stroke / fill を注入\n let svg = svgContent;\n\n // xmlns が無ければ追加\n if (!svg.includes(\"xmlns\")) {\n svg = svg.replace(\"<svg\", '<svg xmlns=\"http://www.w3.org/2000/svg\"');\n }\n\n // width / height を上書き\n svg = svg.replace(/<svg([^>]*)>/, (match, attrs: string) => {\n let newAttrs = attrs\n .replace(/\\bwidth\\s*=\\s*\"[^\"]*\"/g, \"\")\n .replace(/\\bheight\\s*=\\s*\"[^\"]*\"/g, \"\");\n newAttrs += ` width=\"${width}\" height=\"${h}\"`;\n\n // color 指定時は stroke / fill を設定(プリセットアイコンとの一貫性)\n if (color) {\n if (!attrs.includes(\"stroke=\")) {\n newAttrs += ` stroke=\"${color}\"`;\n }\n if (!attrs.includes(\"fill=\")) {\n newAttrs += ` fill=\"none\"`;\n }\n }\n\n return `<svg${newAttrs}>`;\n });\n\n await ensureWasmInitialized();\n const Resvg = getResvg();\n const resvg = new Resvg(svg, { fitTo: { mode: \"width\", value: width } });\n const pngData = resvg.render();\n const pngBuffer = pngData.asPng();\n const result = `image/png;base64,${Buffer.from(pngBuffer).toString(\"base64\")}`;\n cache.set(key, result);\n return result;\n}\n"],"mappings":";;;;;;;AAgBA,MAAM,YAAY,CAAC,UAAU,YAAY,EAAE,KAAK,GAAG;AACnD,IAAI;AACJ,IAAI;AAIJ,SAAS,iBAAiC;CAUxC,OAAO,IARa,SAClB,OACA,iBACA,2BAKW,EAAE,OAAO,KAAK,KAAK,aAAa;AAC/C;;;;;;AAOA,SAAS,kBAA0B;CAEjC,MAAM,YAAY,KADN,QAAQ,cAAc,OAAO,KAAK,GAAG,CACxB,GAAG,eAAe;CAC3C,IAAI,WAAW,SAAS,GAAG,OAAO;CAClC,OAAO,eAAe,EAAE,QAAQ,GAAG,UAAU,eAAe;AAC9D;;;;;AAMA,SAAS,wBAAuC;CAC9C,IAAI,CAAC,iBACH,mBAAmB,YAAY;EAC7B,MAAM,MAAM,eAAe,EAAE,SAAS;EAEtC,MAAM,aAAa,MAAM,SADR,gBACwB,CAAC;EAC1C,MAAM,IAAI,SAAS,UAAU;EAC7B,cAAc;CAChB,GAAG;CAEL,OAAO;AACT;AAEA,SAAS,WAAW;CAClB,IAAI,CAAC,aAAa,MAAM,IAAI,MAAM,sBAAsB;CACxD,OAAO,YAAY;AACrB;AAEA,SAAS,aAAa,MAAc,MAAc,OAAuB;CACvE,MAAM,WAAW,UAAU;CAC3B,IAAI,CAAC,UACH,MAAM,IAAI,MAAM,uBAAuB,KAAK,EAAE;CAEhD,OAAO,kDAAkD,KAAK,YAAY,KAAK,4CAA4C,MAAM,oEAAoE,SAAS;AAChN;AAEA,eAAsB,cACpB,MACA,MACA,OACA,OACiB;CACjB,MAAM,MAAM,GAAG,KAAK,GAAG,KAAK,GAAG;CAC/B,MAAM,SAAS,MAAM,IAAI,GAAG;CAC5B,IAAI,QAAQ,OAAO;CAEnB,MAAM,sBAAsB;CAK5B,MAAM,YADU,KAHF,SAEQ,GADV,aAAa,MAAM,MAAM,KACX,GAAG,EAAE,OAAO;EAAE,MAAM;EAAS,OAAO;CAAK,EAAE,CACjD,EAAE,OACE,EAAE,MAAM;CAChC,MAAM,SAAS,oBAAoB,OAAO,KAAK,SAAS,EAAE,SAAS,QAAQ;CAC3E,MAAM,IAAI,KAAK,MAAM;CACrB,OAAO;AACT;;;;;AAMA,eAAsB,oBACpB,YACA,OACA,OACA,OACA,QACiB;CACjB,MAAM,IAAI,UAAU;CACpB,MAAM,MAAM,OAAO,WAAW,GAAG,MAAM,GAAG,EAAE,GAAG,SAAS;CACxD,MAAM,SAAS,MAAM,IAAI,GAAG;CAC5B,IAAI,QAAQ,OAAO;CAGnB,IAAI,MAAM;CAGV,IAAI,CAAC,IAAI,SAAS,OAAO,GACvB,MAAM,IAAI,QAAQ,QAAQ,2CAAyC;CAIrE,MAAM,IAAI,QAAQ,iBAAiB,OAAO,UAAkB;EAC1D,IAAI,WAAW,MACZ,QAAQ,0BAA0B,EAAE,EACpC,QAAQ,2BAA2B,EAAE;EACxC,YAAY,WAAW,MAAM,YAAY,EAAE;EAG3C,IAAI,OAAO;GACT,IAAI,CAAC,MAAM,SAAS,SAAS,GAC3B,YAAY,YAAY,MAAM;GAEhC,IAAI,CAAC,MAAM,SAAS,OAAO,GACzB,YAAY;EAEhB;EAEA,OAAO,OAAO,SAAS;CACzB,CAAC;CAED,MAAM,sBAAsB;CAI5B,MAAM,YADU,KAFF,SACQ,GAAE,KAAK,EAAE,OAAO;EAAE,MAAM;EAAS,OAAO;CAAM,EAAE,CAClD,EAAE,OACE,EAAE,MAAM;CAChC,MAAM,SAAS,oBAAoB,OAAO,KAAK,SAAS,EAAE,SAAS,QAAQ;CAC3E,MAAM,IAAI,KAAK,MAAM;CACrB,OAAO;AACT"}
1
+ {"version":3,"file":"renderIcon.js","names":[],"sources":["../../src/icons/renderIcon.ts"],"sourcesContent":["import { existsSync } from \"node:fs\";\nimport { readFile } from \"node:fs/promises\";\nimport { createRequire } from \"node:module\";\nimport { dirname, join } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { ICON_DATA } from \"./iconData.ts\";\n\n// @resvg/resvg-wasm を遅延ロードする。\n// バンドラ(webpack / Turbopack)のスタティック解析が require / require.resolve を\n// 追跡してエラーにするのを避けるため、Function コンストラクタで Node.js の require を\n// 取得し、モジュール名は文字列結合で構築する。\n// Turbopack が動的 require / import を Webpack 同等に解析できるようになれば\n// 隠蔽自体が不要になり、apps/website 側の outputFileTracingIncludes 設定もまとめて\n// 撤去できる。\n// 上流 issue: https://github.com/vercel/next.js/issues/85238\ntype ResvgWasm = typeof import(\"@resvg/resvg-wasm\");\nconst RESVG_PKG = [\"@resvg\", \"resvg-wasm\"].join(\"/\");\nlet resvgModule: ResvgWasm | undefined;\nlet wasmInitPromise: Promise<void> | undefined;\n\n// Function コンストラクタを使って require を取得することでバンドラの静的解析から\n// 完全に隠蔽する。実行時は createRequire で生成した require が利用される。\nfunction getNodeRequire(): NodeJS.Require {\n // eslint-disable-next-line @typescript-eslint/no-implied-eval\n const factory = new Function(\n \"url\",\n \"createRequire\",\n \"return createRequire(url)\",\n ) as (\n url: string,\n createRequire: typeof import(\"node:module\").createRequire,\n ) => NodeJS.Require;\n return factory(import.meta.url, createRequire);\n}\n\n/**\n * WASM バイナリのパスを解決する。\n * バンドル環境(esbuild)では同ディレクトリの index_bg.wasm を参照し、\n * 非バンドル環境では createRequire で node_modules から解決する。\n */\nfunction resolveWasmPath(): string {\n const dir = dirname(fileURLToPath(import.meta.url));\n const localPath = join(dir, \"index_bg.wasm\");\n if (existsSync(localPath)) return localPath;\n return getNodeRequire().resolve(`${RESVG_PKG}/index_bg.wasm`);\n}\n\n/**\n * WASM モジュールを初期化し、Resvg クラスを返す。\n * 並行呼び出しでも安全(Promise をキャッシュ)。\n */\nfunction ensureWasmInitialized(): Promise<void> {\n if (!wasmInitPromise) {\n wasmInitPromise = (async () => {\n const mod = getNodeRequire()(RESVG_PKG) as ResvgWasm;\n const wasmPath = resolveWasmPath();\n const wasmBuffer = await readFile(wasmPath);\n await mod.initWasm(wasmBuffer);\n resvgModule = mod;\n })();\n }\n return wasmInitPromise;\n}\n\nfunction getResvg() {\n if (!resvgModule) throw new Error(\"WASM not initialized\");\n return resvgModule.Resvg;\n}\n\nfunction buildIconSvg(name: string, size: number, color: string): string {\n const pathData = ICON_DATA[name];\n if (!pathData) {\n throw new Error(`Unknown icon name: \"${name}\"`);\n }\n return `<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"${size}\" height=\"${size}\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"${color}\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">${pathData}</svg>`;\n}\n\nexport async function rasterizeIcon(\n name: string,\n size: number,\n color: string,\n cache: Map<string, string>,\n): Promise<string> {\n const key = `${name}|${size}|${color}`;\n const cached = cache.get(key);\n if (cached) return cached;\n\n await ensureWasmInitialized();\n const Resvg = getResvg();\n const svg = buildIconSvg(name, size, color);\n const resvg = new Resvg(svg, { fitTo: { mode: \"width\", value: size } });\n const pngData = resvg.render();\n const pngBuffer = pngData.asPng();\n const result = `image/png;base64,${Buffer.from(pngBuffer).toString(\"base64\")}`;\n cache.set(key, result);\n return result;\n}\n\n/**\n * インライン SVG 文字列を指定サイズでラスタライズし、base64 PNG を返す。\n * color が指定された場合、SVG ルートに stroke / fill 属性を設定する。\n */\nexport async function rasterizeSvgContent(\n svgContent: string,\n width: number,\n color: string | undefined,\n cache: Map<string, string>,\n height?: number,\n): Promise<string> {\n const h = height ?? width;\n const key = `svg:${svgContent}|${width}|${h}|${color ?? \"\"}`;\n const cached = cache.get(key);\n if (cached) return cached;\n\n // SVG に xmlns / width / height を設定し、color があれば stroke / fill を注入\n let svg = svgContent;\n\n // xmlns が無ければ追加\n if (!svg.includes(\"xmlns\")) {\n svg = svg.replace(\"<svg\", '<svg xmlns=\"http://www.w3.org/2000/svg\"');\n }\n\n // width / height を上書き\n svg = svg.replace(/<svg([^>]*)>/, (match, attrs: string) => {\n let newAttrs = attrs\n .replace(/\\bwidth\\s*=\\s*\"[^\"]*\"/g, \"\")\n .replace(/\\bheight\\s*=\\s*\"[^\"]*\"/g, \"\");\n newAttrs += ` width=\"${width}\" height=\"${h}\"`;\n\n // color 指定時は stroke / fill を設定(プリセットアイコンとの一貫性)\n if (color) {\n if (!attrs.includes(\"stroke=\")) {\n newAttrs += ` stroke=\"${color}\"`;\n }\n if (!attrs.includes(\"fill=\")) {\n newAttrs += ` fill=\"none\"`;\n }\n }\n\n return `<svg${newAttrs}>`;\n });\n\n await ensureWasmInitialized();\n const Resvg = getResvg();\n const resvg = new Resvg(svg, { fitTo: { mode: \"width\", value: width } });\n const pngData = resvg.render();\n const pngBuffer = pngData.asPng();\n const result = `image/png;base64,${Buffer.from(pngBuffer).toString(\"base64\")}`;\n cache.set(key, result);\n return result;\n}\n"],"mappings":";;;;;;;AAgBA,MAAM,YAAY,CAAC,UAAU,YAAY,CAAC,CAAC,KAAK,GAAG;AACnD,IAAI;AACJ,IAAI;AAIJ,SAAS,iBAAiC;CAUxC,OAAO,IARa,SAClB,OACA,iBACA,2BAKW,CAAC,CAAC,OAAO,KAAK,KAAK,aAAa;AAC/C;;;;;;AAOA,SAAS,kBAA0B;CAEjC,MAAM,YAAY,KADN,QAAQ,cAAc,OAAO,KAAK,GAAG,CACxB,GAAG,eAAe;CAC3C,IAAI,WAAW,SAAS,GAAG,OAAO;CAClC,OAAO,eAAe,CAAC,CAAC,QAAQ,GAAG,UAAU,eAAe;AAC9D;;;;;AAMA,SAAS,wBAAuC;CAC9C,IAAI,CAAC,iBACH,mBAAmB,YAAY;EAC7B,MAAM,MAAM,eAAe,CAAC,CAAC,SAAS;EAEtC,MAAM,aAAa,MAAM,SADR,gBACwB,CAAC;EAC1C,MAAM,IAAI,SAAS,UAAU;EAC7B,cAAc;CAChB,EAAA,CAAG;CAEL,OAAO;AACT;AAEA,SAAS,WAAW;CAClB,IAAI,CAAC,aAAa,MAAM,IAAI,MAAM,sBAAsB;CACxD,OAAO,YAAY;AACrB;AAEA,SAAS,aAAa,MAAc,MAAc,OAAuB;CACvE,MAAM,WAAW,UAAU;CAC3B,IAAI,CAAC,UACH,MAAM,IAAI,MAAM,uBAAuB,KAAK,EAAE;CAEhD,OAAO,kDAAkD,KAAK,YAAY,KAAK,4CAA4C,MAAM,oEAAoE,SAAS;AAChN;AAEA,eAAsB,cACpB,MACA,MACA,OACA,OACiB;CACjB,MAAM,MAAM,GAAG,KAAK,GAAG,KAAK,GAAG;CAC/B,MAAM,SAAS,MAAM,IAAI,GAAG;CAC5B,IAAI,QAAQ,OAAO;CAEnB,MAAM,sBAAsB;CAK5B,MAAM,YADU,KAHF,SAEQ,GADV,aAAa,MAAM,MAAM,KACX,GAAG,EAAE,OAAO;EAAE,MAAM;EAAS,OAAO;CAAK,EAAE,CACjD,CAAC,CAAC,OACE,CAAC,CAAC,MAAM;CAChC,MAAM,SAAS,oBAAoB,OAAO,KAAK,SAAS,CAAC,CAAC,SAAS,QAAQ;CAC3E,MAAM,IAAI,KAAK,MAAM;CACrB,OAAO;AACT;;;;;AAMA,eAAsB,oBACpB,YACA,OACA,OACA,OACA,QACiB;CACjB,MAAM,IAAI,UAAU;CACpB,MAAM,MAAM,OAAO,WAAW,GAAG,MAAM,GAAG,EAAE,GAAG,SAAS;CACxD,MAAM,SAAS,MAAM,IAAI,GAAG;CAC5B,IAAI,QAAQ,OAAO;CAGnB,IAAI,MAAM;CAGV,IAAI,CAAC,IAAI,SAAS,OAAO,GACvB,MAAM,IAAI,QAAQ,QAAQ,2CAAyC;CAIrE,MAAM,IAAI,QAAQ,iBAAiB,OAAO,UAAkB;EAC1D,IAAI,WAAW,MACZ,QAAQ,0BAA0B,EAAE,CAAC,CACrC,QAAQ,2BAA2B,EAAE;EACxC,YAAY,WAAW,MAAM,YAAY,EAAE;EAG3C,IAAI,OAAO;GACT,IAAI,CAAC,MAAM,SAAS,SAAS,GAC3B,YAAY,YAAY,MAAM;GAEhC,IAAI,CAAC,MAAM,SAAS,OAAO,GACzB,YAAY;EAEhB;EAEA,OAAO,OAAO,SAAS;CACzB,CAAC;CAED,MAAM,sBAAsB;CAI5B,MAAM,YADU,KAFF,SACQ,GAAE,KAAK,EAAE,OAAO;EAAE,MAAM;EAAS,OAAO;CAAM,EAAE,CAClD,CAAC,CAAC,OACE,CAAC,CAAC,MAAM;CAChC,MAAM,SAAS,oBAAoB,OAAO,KAAK,SAAS,CAAC,CAAC,SAAS,QAAQ;CAC3E,MAAM,IAAI,KAAK,MAAM;CACrB,OAAO;AACT"}
@@ -1 +1 @@
1
- {"version":3,"file":"parseMasterPptx.js","names":[],"sources":["../src/parseMasterPptx.ts"],"sourcesContent":["import { XMLParser } from \"fast-xml-parser\";\nimport type { SlideMasterBackground } from \"./types.ts\";\n\n// JSZip は CJS パッケージのため動的 import で読み込む\nasync function loadJSZip(): Promise<typeof import(\"jszip\")> {\n const mod = await import(\"jszip\");\n /* eslint-disable @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-return */\n return (mod as any).default ?? mod;\n /* eslint-enable @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-return */\n}\n\nconst xmlParser = new XMLParser({\n ignoreAttributes: false,\n attributeNamePrefix: \"@_\",\n});\n\n/**\n * MIME タイプを拡張子から判定する\n */\nfunction mimeTypeFromExt(filePath: string): string {\n const ext = filePath.split(\".\").pop()?.toLowerCase();\n switch (ext) {\n case \"png\":\n return \"image/png\";\n case \"jpg\":\n case \"jpeg\":\n return \"image/jpeg\";\n case \"gif\":\n return \"image/gif\";\n case \"bmp\":\n return \"image/bmp\";\n case \"svg\":\n return \"image/svg+xml\";\n case \"tiff\":\n case \"tif\":\n return \"image/tiff\";\n case \"webp\":\n return \"image/webp\";\n default:\n return \"image/png\";\n }\n}\n\n/**\n * rels XML から rId に対応するファイルパスを取得する\n */\nfunction resolveRelId(relsXml: string, rId: string): string | undefined {\n /* eslint-disable @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access */\n const parsed = xmlParser.parse(relsXml);\n const relationships = parsed?.Relationships?.Relationship;\n if (!relationships) return undefined;\n\n const rels = Array.isArray(relationships) ? relationships : [relationships];\n for (const rel of rels) {\n if (rel[\"@_Id\"] === rId) {\n return rel[\"@_Target\"] as string;\n }\n }\n /* eslint-enable @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access */\n return undefined;\n}\n\n/* eslint-disable @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access */\n\n/**\n * bgPr 要素から背景情報を抽出する\n */\nasync function extractBackgroundFromBgPr(\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n bgPr: any,\n zip: import(\"jszip\"),\n relsPath: string,\n basePath: string,\n): Promise<SlideMasterBackground | undefined> {\n if (!bgPr) return undefined;\n\n // 単色塗りつぶし\n const solidFill = bgPr[\"a:solidFill\"];\n if (solidFill) {\n const srgbClr = solidFill[\"a:srgbClr\"];\n if (srgbClr) {\n const color = (srgbClr[\"@_val\"] as string) ?? undefined;\n if (color) return { color };\n }\n }\n\n // 画像背景\n const blipFill = bgPr[\"a:blipFill\"];\n if (blipFill) {\n const blip = blipFill[\"a:blip\"];\n const rId = blip?.[\"@_r:embed\"] as string | undefined;\n if (!rId) return undefined;\n\n const relsFile = zip.file(relsPath);\n if (!relsFile) return undefined;\n const relsXml = await relsFile.async(\"text\");\n const target = resolveRelId(relsXml, rId);\n if (!target) return undefined;\n\n // target は相対パスなので basePath からの相対パスとして解決\n const imagePath = new URL(\n target,\n `file:///${basePath}dummy`,\n ).pathname.slice(1);\n\n const imageFile = zip.file(imagePath);\n if (!imageFile) return undefined;\n\n const imageData = await imageFile.async(\"base64\");\n const mimeType = mimeTypeFromExt(imagePath);\n return { data: `data:${mimeType};base64,${imageData}` };\n }\n\n return undefined;\n}\n\n/* eslint-enable @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access */\n\n/**\n * マスター PPTX のバッファからスライドマスターの背景情報を抽出する。\n *\n * 探索順序:\n * 1. `ppt/slideMasters/slideMaster1.xml` の `p:bg > p:bgPr`\n * 2. 各スライドレイアウト (`ppt/slideLayouts/slideLayoutN.xml`) の `p:bg > p:bgPr`\n *\n * サポートする背景:\n * - 単色塗りつぶし (`a:solidFill` / `a:srgbClr`)\n * - 画像背景 (`a:blipFill` / `a:blip`)\n *\n * @returns 背景情報。背景が設定されていない場合は `undefined`。\n */\nexport async function parseMasterPptx(\n pptxBuffer: ArrayBuffer | Uint8Array,\n): Promise<SlideMasterBackground | undefined> {\n const JSZip = await loadJSZip();\n const zip = await JSZip.loadAsync(pptxBuffer);\n\n /* eslint-disable @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access */\n\n // 1. スライドマスター本体を探索\n const masterFile = zip.file(\"ppt/slideMasters/slideMaster1.xml\");\n if (masterFile) {\n const masterXml = await masterFile.async(\"text\");\n const parsed = xmlParser.parse(masterXml);\n const bgPr = parsed?.[\"p:sldMaster\"]?.[\"p:cSld\"]?.[\"p:bg\"]?.[\"p:bgPr\"];\n const result = await extractBackgroundFromBgPr(\n bgPr,\n zip,\n \"ppt/slideMasters/_rels/slideMaster1.xml.rels\",\n \"ppt/slideMasters/\",\n );\n if (result) return result;\n }\n\n // 2. スライドレイアウトを探索\n const layoutFiles = Object.keys(zip.files).filter(\n (f) => f.startsWith(\"ppt/slideLayouts/slideLayout\") && f.endsWith(\".xml\"),\n );\n // 番号順にソート(数値ソートで slideLayout2 < slideLayout10 の順序を保証)\n layoutFiles.sort((a, b) => {\n const numA = parseInt(a.match(/slideLayout(\\d+)\\.xml$/)?.[1] ?? \"0\", 10);\n const numB = parseInt(b.match(/slideLayout(\\d+)\\.xml$/)?.[1] ?? \"0\", 10);\n return numA - numB;\n });\n\n for (const layoutPath of layoutFiles) {\n const layoutFile = zip.file(layoutPath);\n if (!layoutFile) continue;\n\n const layoutXml = await layoutFile.async(\"text\");\n const parsed = xmlParser.parse(layoutXml);\n const bgPr = parsed?.[\"p:sldLayout\"]?.[\"p:cSld\"]?.[\"p:bg\"]?.[\"p:bgPr\"];\n const fileName = layoutPath.split(\"/\").pop()!;\n const relsPath = `ppt/slideLayouts/_rels/${fileName}.rels`;\n const result = await extractBackgroundFromBgPr(\n bgPr,\n zip,\n relsPath,\n \"ppt/slideLayouts/\",\n );\n if (result) return result;\n }\n\n /* eslint-enable @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access */\n\n return undefined;\n}\n"],"mappings":";;AAIA,eAAe,YAA6C;CAC1D,MAAM,MAAM,MAAM,OAAO;CAEzB,OAAQ,IAAY,WAAW;AAEjC;AAEA,MAAM,YAAY,IAAI,UAAU;CAC9B,kBAAkB;CAClB,qBAAqB;AACvB,CAAC;;;;AAKD,SAAS,gBAAgB,UAA0B;CAEjD,QADY,SAAS,MAAM,GAAG,EAAE,IAAI,GAAG,YAAY,GACnD;EACE,KAAK,OACH,OAAO;EACT,KAAK;EACL,KAAK,QACH,OAAO;EACT,KAAK,OACH,OAAO;EACT,KAAK,OACH,OAAO;EACT,KAAK,OACH,OAAO;EACT,KAAK;EACL,KAAK,OACH,OAAO;EACT,KAAK,QACH,OAAO;EACT,SACE,OAAO;CACX;AACF;;;;AAKA,SAAS,aAAa,SAAiB,KAAiC;CAGtE,MAAM,gBADS,UAAU,MAAM,OACJ,GAAG,eAAe;CAC7C,IAAI,CAAC,eAAe,OAAO,KAAA;CAE3B,MAAM,OAAO,MAAM,QAAQ,aAAa,IAAI,gBAAgB,CAAC,aAAa;CAC1E,KAAK,MAAM,OAAO,MAChB,IAAI,IAAI,YAAY,KAClB,OAAO,IAAI;AAKjB;;;;AAOA,eAAe,0BAEb,MACA,KACA,UACA,UAC4C;CAC5C,IAAI,CAAC,MAAM,OAAO,KAAA;CAGlB,MAAM,YAAY,KAAK;CACvB,IAAI,WAAW;EACb,MAAM,UAAU,UAAU;EAC1B,IAAI,SAAS;GACX,MAAM,QAAS,QAAQ,YAAuB,KAAA;GAC9C,IAAI,OAAO,OAAO,EAAE,MAAM;EAC5B;CACF;CAGA,MAAM,WAAW,KAAK;CACtB,IAAI,UAAU;EAEZ,MAAM,MADO,SAAS,YACH;EACnB,IAAI,CAAC,KAAK,OAAO,KAAA;EAEjB,MAAM,WAAW,IAAI,KAAK,QAAQ;EAClC,IAAI,CAAC,UAAU,OAAO,KAAA;EAEtB,MAAM,SAAS,aAAa,MADN,SAAS,MAAM,MAAM,GACN,GAAG;EACxC,IAAI,CAAC,QAAQ,OAAO,KAAA;EAGpB,MAAM,YAAY,IAAI,IACpB,QACA,WAAW,SAAS,MACtB,EAAE,SAAS,MAAM,CAAC;EAElB,MAAM,YAAY,IAAI,KAAK,SAAS;EACpC,IAAI,CAAC,WAAW,OAAO,KAAA;EAEvB,MAAM,YAAY,MAAM,UAAU,MAAM,QAAQ;EAEhD,OAAO,EAAE,MAAM,QADE,gBAAgB,SACH,EAAE,UAAU,YAAY;CACxD;AAGF;;;;;;;;;;;;;;AAiBA,eAAsB,gBACpB,YAC4C;CAE5C,MAAM,MAAM,OAAM,MADE,UAAU,GACN,UAAU,UAAU;CAK5C,MAAM,aAAa,IAAI,KAAK,mCAAmC;CAC/D,IAAI,YAAY;EACd,MAAM,YAAY,MAAM,WAAW,MAAM,MAAM;EAE/C,MAAM,OADS,UAAU,MAAM,SACb,IAAI,iBAAiB,YAAY,UAAU;EAC7D,MAAM,SAAS,MAAM,0BACnB,MACA,KACA,gDACA,mBACF;EACA,IAAI,QAAQ,OAAO;CACrB;CAGA,MAAM,cAAc,OAAO,KAAK,IAAI,KAAK,EAAE,QACxC,MAAM,EAAE,WAAW,8BAA8B,KAAK,EAAE,SAAS,MAAM,CAC1E;CAEA,YAAY,MAAM,GAAG,MAAM;EAGzB,OAFa,SAAS,EAAE,MAAM,wBAAwB,IAAI,MAAM,KAAK,EAE3D,IADG,SAAS,EAAE,MAAM,wBAAwB,IAAI,MAAM,KAAK,EACpD;CACnB,CAAC;CAED,KAAK,MAAM,cAAc,aAAa;EACpC,MAAM,aAAa,IAAI,KAAK,UAAU;EACtC,IAAI,CAAC,YAAY;EAEjB,MAAM,YAAY,MAAM,WAAW,MAAM,MAAM;EAE/C,MAAM,OADS,UAAU,MAAM,SACb,IAAI,iBAAiB,YAAY,UAAU;EAG7D,MAAM,SAAS,MAAM,0BACnB,MACA,KACA,0BALe,WAAW,MAAM,GAAG,EAAE,IACW,EAAE,QAKlD,mBACF;EACA,IAAI,QAAQ,OAAO;CACrB;AAKF"}
1
+ {"version":3,"file":"parseMasterPptx.js","names":[],"sources":["../src/parseMasterPptx.ts"],"sourcesContent":["import { XMLParser } from \"fast-xml-parser\";\nimport type { SlideMasterBackground } from \"./types.ts\";\n\n// JSZip は CJS パッケージのため動的 import で読み込む\nasync function loadJSZip(): Promise<typeof import(\"jszip\")> {\n const mod = await import(\"jszip\");\n /* eslint-disable @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-return */\n return (mod as any).default ?? mod;\n /* eslint-enable @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-return */\n}\n\nconst xmlParser = new XMLParser({\n ignoreAttributes: false,\n attributeNamePrefix: \"@_\",\n});\n\n/**\n * MIME タイプを拡張子から判定する\n */\nfunction mimeTypeFromExt(filePath: string): string {\n const ext = filePath.split(\".\").pop()?.toLowerCase();\n switch (ext) {\n case \"png\":\n return \"image/png\";\n case \"jpg\":\n case \"jpeg\":\n return \"image/jpeg\";\n case \"gif\":\n return \"image/gif\";\n case \"bmp\":\n return \"image/bmp\";\n case \"svg\":\n return \"image/svg+xml\";\n case \"tiff\":\n case \"tif\":\n return \"image/tiff\";\n case \"webp\":\n return \"image/webp\";\n default:\n return \"image/png\";\n }\n}\n\n/**\n * rels XML から rId に対応するファイルパスを取得する\n */\nfunction resolveRelId(relsXml: string, rId: string): string | undefined {\n /* eslint-disable @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access */\n const parsed = xmlParser.parse(relsXml);\n const relationships = parsed?.Relationships?.Relationship;\n if (!relationships) return undefined;\n\n const rels = Array.isArray(relationships) ? relationships : [relationships];\n for (const rel of rels) {\n if (rel[\"@_Id\"] === rId) {\n return rel[\"@_Target\"] as string;\n }\n }\n /* eslint-enable @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access */\n return undefined;\n}\n\n/* eslint-disable @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access */\n\n/**\n * bgPr 要素から背景情報を抽出する\n */\nasync function extractBackgroundFromBgPr(\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n bgPr: any,\n zip: import(\"jszip\"),\n relsPath: string,\n basePath: string,\n): Promise<SlideMasterBackground | undefined> {\n if (!bgPr) return undefined;\n\n // 単色塗りつぶし\n const solidFill = bgPr[\"a:solidFill\"];\n if (solidFill) {\n const srgbClr = solidFill[\"a:srgbClr\"];\n if (srgbClr) {\n const color = (srgbClr[\"@_val\"] as string) ?? undefined;\n if (color) return { color };\n }\n }\n\n // 画像背景\n const blipFill = bgPr[\"a:blipFill\"];\n if (blipFill) {\n const blip = blipFill[\"a:blip\"];\n const rId = blip?.[\"@_r:embed\"] as string | undefined;\n if (!rId) return undefined;\n\n const relsFile = zip.file(relsPath);\n if (!relsFile) return undefined;\n const relsXml = await relsFile.async(\"text\");\n const target = resolveRelId(relsXml, rId);\n if (!target) return undefined;\n\n // target は相対パスなので basePath からの相対パスとして解決\n const imagePath = new URL(\n target,\n `file:///${basePath}dummy`,\n ).pathname.slice(1);\n\n const imageFile = zip.file(imagePath);\n if (!imageFile) return undefined;\n\n const imageData = await imageFile.async(\"base64\");\n const mimeType = mimeTypeFromExt(imagePath);\n return { data: `data:${mimeType};base64,${imageData}` };\n }\n\n return undefined;\n}\n\n/* eslint-enable @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access */\n\n/**\n * マスター PPTX のバッファからスライドマスターの背景情報を抽出する。\n *\n * 探索順序:\n * 1. `ppt/slideMasters/slideMaster1.xml` の `p:bg > p:bgPr`\n * 2. 各スライドレイアウト (`ppt/slideLayouts/slideLayoutN.xml`) の `p:bg > p:bgPr`\n *\n * サポートする背景:\n * - 単色塗りつぶし (`a:solidFill` / `a:srgbClr`)\n * - 画像背景 (`a:blipFill` / `a:blip`)\n *\n * @returns 背景情報。背景が設定されていない場合は `undefined`。\n */\nexport async function parseMasterPptx(\n pptxBuffer: ArrayBuffer | Uint8Array,\n): Promise<SlideMasterBackground | undefined> {\n const JSZip = await loadJSZip();\n const zip = await JSZip.loadAsync(pptxBuffer);\n\n /* eslint-disable @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access */\n\n // 1. スライドマスター本体を探索\n const masterFile = zip.file(\"ppt/slideMasters/slideMaster1.xml\");\n if (masterFile) {\n const masterXml = await masterFile.async(\"text\");\n const parsed = xmlParser.parse(masterXml);\n const bgPr = parsed?.[\"p:sldMaster\"]?.[\"p:cSld\"]?.[\"p:bg\"]?.[\"p:bgPr\"];\n const result = await extractBackgroundFromBgPr(\n bgPr,\n zip,\n \"ppt/slideMasters/_rels/slideMaster1.xml.rels\",\n \"ppt/slideMasters/\",\n );\n if (result) return result;\n }\n\n // 2. スライドレイアウトを探索\n const layoutFiles = Object.keys(zip.files).filter(\n (f) => f.startsWith(\"ppt/slideLayouts/slideLayout\") && f.endsWith(\".xml\"),\n );\n // 番号順にソート(数値ソートで slideLayout2 < slideLayout10 の順序を保証)\n layoutFiles.sort((a, b) => {\n const numA = parseInt(a.match(/slideLayout(\\d+)\\.xml$/)?.[1] ?? \"0\", 10);\n const numB = parseInt(b.match(/slideLayout(\\d+)\\.xml$/)?.[1] ?? \"0\", 10);\n return numA - numB;\n });\n\n for (const layoutPath of layoutFiles) {\n const layoutFile = zip.file(layoutPath);\n if (!layoutFile) continue;\n\n const layoutXml = await layoutFile.async(\"text\");\n const parsed = xmlParser.parse(layoutXml);\n const bgPr = parsed?.[\"p:sldLayout\"]?.[\"p:cSld\"]?.[\"p:bg\"]?.[\"p:bgPr\"];\n const fileName = layoutPath.split(\"/\").pop()!;\n const relsPath = `ppt/slideLayouts/_rels/${fileName}.rels`;\n const result = await extractBackgroundFromBgPr(\n bgPr,\n zip,\n relsPath,\n \"ppt/slideLayouts/\",\n );\n if (result) return result;\n }\n\n /* eslint-enable @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access */\n\n return undefined;\n}\n"],"mappings":"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"}
@@ -247,6 +247,7 @@ const BASE_RULES = {
247
247
  id: "string",
248
248
  w: LENGTH_RULE,
249
249
  h: LENGTH_RULE,
250
+ grow: "number",
250
251
  minW: "number",
251
252
  maxW: "number",
252
253
  minH: "number",
@@ -254,6 +255,7 @@ const BASE_RULES = {
254
255
  padding: PADDING_RULE,
255
256
  margin: PADDING_RULE,
256
257
  backgroundColor: "string",
258
+ backgroundGradient: "string",
257
259
  backgroundImage: BACKGROUND_IMAGE_RULE,
258
260
  border: BORDER_STYLE_RULE,
259
261
  borderRadius: "number",
@@ -283,7 +285,8 @@ const NODE_COERCION_MAP = {
283
285
  text: {
284
286
  ...BASE_RULES,
285
287
  text: "string",
286
- ...TEXT_STYLE_RULES
288
+ ...TEXT_STYLE_RULES,
289
+ letterSpacing: "number"
287
290
  },
288
291
  ul: {
289
292
  ...BASE_RULES,
@@ -511,7 +514,8 @@ const CHILD_ELEMENT_COERCION_MAP = {
511
514
  I: {},
512
515
  Span: {
513
516
  color: "string",
514
- fontFamily: "string"
517
+ fontFamily: "string",
518
+ letterSpacing: "number"
515
519
  }
516
520
  };
517
521
  //#endregion