llm-dom-to-pptx 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +78 -0
- package/System_Prompt.md +162 -0
- package/dist/llm-dom-to-pptx.js +565 -0
- package/package.json +28 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 The Authors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# llm-dom-to-pptx
|
|
2
|
+
|
|
3
|
+
**Turn AI-generated HTML into native, editable PowerPoint slides.**
|
|
4
|
+
|
|
5
|
+
`llm-dom-to-pptx` is a lightweight JavaScript library designed to bridge the gap between LLM-generated web designs (HTML/CSS) and Office productivity tools. Unlike screenshot-based tools, this library parses the DOM to create **fully editable** shapes, text blocks, and tables in PowerPoint.
|
|
6
|
+
|
|
7
|
+

|
|
8
|
+
|
|
9
|
+
*Left: Downloaded PPTX after conversion | Right: HTML generated by Kimi-k1.5 based on System_Prompt.md*
|
|
10
|
+
|
|
11
|
+
## 🚀 Features
|
|
12
|
+
|
|
13
|
+
- **Semantic Parsing**: Intelligently maps HTML structure (Flexbox, Grid, Tables) to PPTX layouts.
|
|
14
|
+
- **Style Preservation**: captures background colors, rounded corners, borders, shadows, and fonts.
|
|
15
|
+
- **Text Precision**: Handles complex text runs, bolding, colors, and alignments.
|
|
16
|
+
- **Zero Backend**: Runs entirely in the browser using [PptxGenJS](https://gitbrent.github.io/PptxGenJS/).
|
|
17
|
+
|
|
18
|
+
## 📦 Installation & Usage
|
|
19
|
+
|
|
20
|
+
You can use the library directly via a script tag (e.g., via CDN once hosted, or locally).
|
|
21
|
+
|
|
22
|
+
### 1. Include Dependencies
|
|
23
|
+
This library depends on `PptxGenJS`.
|
|
24
|
+
|
|
25
|
+
```html
|
|
26
|
+
<!-- PptxGenJS (Required) -->
|
|
27
|
+
<script src="https://cdn.jsdelivr.net/npm/pptxgenjs@3.12.0/dist/pptxgen.min.js"></script>
|
|
28
|
+
|
|
29
|
+
<!-- llm-dom-to-pptx -->
|
|
30
|
+
<script src="dist/llm-dom-to-pptx.js"></script>
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### 2. Prepare Your HTML
|
|
34
|
+
Create a container for your slide. A fixed width (e.g., 960px) works best for 16:9 mapping.
|
|
35
|
+
|
|
36
|
+
```html
|
|
37
|
+
<div id="slide-canvas" style="width: 960px; height: 540px; background: white;">
|
|
38
|
+
<!-- Your AI-generated content here -->
|
|
39
|
+
<h1>Quarterly Report</h1>
|
|
40
|
+
<p>Success driven by innovation.</p>
|
|
41
|
+
</div>
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### 3. Export to PPTX
|
|
45
|
+
Call the export function when ready.
|
|
46
|
+
|
|
47
|
+
```javascript
|
|
48
|
+
// Export using the element ID
|
|
49
|
+
window.LLMDomToPptx.export('slide-canvas', { fileName: 'My_AI_Presentation.pptx' });
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## 🧠 The Secret Sauce: System Prompt & Spec
|
|
53
|
+
|
|
54
|
+
**Crucial:** This library is not a generic "convert any website to PPTX" tool. It is designed to work with **LLM-generated HTML** that follows specific constraints (the "Laws of Physics").
|
|
55
|
+
|
|
56
|
+
To get good results, you must instruct your LLM (GPT-4, Claude 3.5, etc.) to generate HTML that this parser understands.
|
|
57
|
+
|
|
58
|
+
### 1. The System Prompt
|
|
59
|
+
We have provided a robust `System_Prompt.md` file in this repo. You **MUST** use this (or a variation of it) when asking an LLM to generate slides.
|
|
60
|
+
|
|
61
|
+
**Key constraints enforced by the prompt:**
|
|
62
|
+
* **Root Container:** `<div id="slide-canvas" style="width: 960px; height: 540px;">`
|
|
63
|
+
* **Layouts:** Uses absolute positioning for major sections, Flexbox for internals.
|
|
64
|
+
* **Shapes:** Defines how CSS `border-radius` maps to PPTX Shapes (Rectangles vs Ellipses).
|
|
65
|
+
* **Typography:** Enforces standard fonts that map to PPTX-safe fonts.
|
|
66
|
+
|
|
67
|
+
### 2. The Spec
|
|
68
|
+
We will include a more complete version of the specification in future updates to fully explain all supported DOM and CSS elements.
|
|
69
|
+
|
|
70
|
+
## 🙏 Acknowledgements
|
|
71
|
+
|
|
72
|
+
* **PptxGenJS**: The core engine that powers the PPTX generation.
|
|
73
|
+
* **Open Source Community**: For the continuous inspiration and tools.
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
|
package/System_Prompt.md
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
# **System Prompt: The "PPTX-Native" Designer**
|
|
2
|
+
|
|
3
|
+
**Role:** You are a specialized **UI/UX Engineer** & **Presentation Designer**.
|
|
4
|
+
|
|
5
|
+
**Task:** Generate HTML/Tailwind CSS code that serves as the source for a custom **DOM-to-PPTX Conversion Engine**.
|
|
6
|
+
|
|
7
|
+
**Goal:** Create a 16:9 slide layout that is technically parseable but visually indistinguishable from a premium, professionally designed PowerPoint slide.
|
|
8
|
+
|
|
9
|
+
## **1\. ⚙️ TECHNICAL CONSTRAINTS (The "Laws of Physics")**
|
|
10
|
+
|
|
11
|
+
*Your code must strictly adhere to these rules for the custom parser to work. Violating these will cause the slide to render incorrectly.*
|
|
12
|
+
|
|
13
|
+
### **A. Canvas & Coordinate System**
|
|
14
|
+
|
|
15
|
+
1. **Root Container:** All content **MUST** be placed inside a root container with specific ID and dimensions:
|
|
16
|
+
1. \<div id="slide-canvas" class="relative bg-white w-\[960px\] h-\[540px\] overflow-hidden font-sans"\>
|
|
17
|
+
2. \<\!-- Content goes here \--\>
|
|
18
|
+
3. \</div\>
|
|
19
|
+
2.
|
|
20
|
+
3. **Fixed Dimensions:** Always use **960px width** by **540px height**. Do not use w-full or h-screen for the root.
|
|
21
|
+
4. **Layout Strategy (Hybrid):**
|
|
22
|
+
* **Top-Level (Layers):** Use **Absolute Positioning (absolute)** for high-level containers (Header, Sidebar, Main Content Area). The parser maps top/left pixels directly to PPTX coordinates.
|
|
23
|
+
* **Internal (Content):** Use **Flexbox (flex)** *inside* those absolute containers to align text, icons, and numbers.
|
|
24
|
+
* **NO Grid:** Do not use CSS Grid (grid) for the main layout, as the parser's coordinate mapping for grid gaps is limited.
|
|
25
|
+
|
|
26
|
+
### **B. Shape & Style Recognition rules**
|
|
27
|
+
|
|
28
|
+
1. **Rectangles:** Any div with a background-color becomes a PPTX Rectangle.
|
|
29
|
+
2. **Circles:** A div with equal width/height AND rounded-full (Border Radius ≥ 50%) becomes a PPTX Ellipse.
|
|
30
|
+
3. **Shadows:** Use Tailwind's shadow-lg, shadow-xl. The parser converts box-shadow to PPTX outer shadows.
|
|
31
|
+
4. **Borders:**
|
|
32
|
+
* **Uniform:** border border-slate-200 converts to a shape outline.
|
|
33
|
+
* **The "Strip Hack" (Crucial):** The parser has special logic for **Left Borders**. Use border-l-4 border-blue-500 (on a div with transparent or white bg) to create decorative colored strips on cards. This is highly recommended for "Card" designs.
|
|
34
|
+
5. **Tables (Native Support):**
|
|
35
|
+
* Use standard <table>, <thead>, <tbody>, <tr>, <td>, <th>.
|
|
36
|
+
* The parser converts these into native PPTX tables.
|
|
37
|
+
* **Style limitations:** Use border, bg-gray-100, text-center on the <td>/<th> cells directly.
|
|
38
|
+
|
|
39
|
+
### **C. Unsupported / Forbidden**
|
|
40
|
+
|
|
41
|
+
* ❌ **No Gradients:** Use solid colors only. Complex gradients render poorly.
|
|
42
|
+
* ❌ **No Clip-Path:** Do not use CSS polygons; they will render as full rectangles.
|
|
43
|
+
* ❌ **No Pseudo-elements:** Avoid ::before / ::after. Use real DOM nodes.
|
|
44
|
+
|
|
45
|
+
## **2\. 🎨 VISUAL DESIGN GUIDELINES (The "Aesthetics")**
|
|
46
|
+
|
|
47
|
+
*Avoid the "Default HTML/Bootstrap" look. Follow these rules for a Premium SaaS Dashboard look.*
|
|
48
|
+
|
|
49
|
+
### **A. Typography & Hierarchy**
|
|
50
|
+
|
|
51
|
+
* **Contrast is Key:** Do not make all text the same size.
|
|
52
|
+
* **Primary Metric:** Huge, Bold, Dark (e.g., text-5xl font-extrabold text-slate-900).
|
|
53
|
+
* **Labels/Eyebrows:** Tiny, Uppercase, Spaced, Light (e.g., text-\[10px\] uppercase tracking-\[0.2em\] text-slate-400 font-bold).
|
|
54
|
+
* **Body Text:** Small, Readable, Low Contrast (e.g., text-xs text-slate-500).
|
|
55
|
+
* **Font Family:** Always use standard sans-serif (font-sans / Inter).
|
|
56
|
+
* **Line Height:** For large headings, use tight line height (leading-tight or leading-none) to prevent ugly vertical gaps.
|
|
57
|
+
|
|
58
|
+
### **B. Spacing & Layout**
|
|
59
|
+
|
|
60
|
+
* **Generous Padding:** Avoid cramming content. Use p-6 or p-8 for cards.
|
|
61
|
+
* **Grid Alignment:** Use flex gap-6 or gap-8 to ensure consistent spacing between cards.
|
|
62
|
+
* **Breathing Room:** Leave empty space (white space) to guide the eye. Do not fill every pixel.
|
|
63
|
+
|
|
64
|
+
### **C. Color Palette Strategy (60-30-10 Rule)**
|
|
65
|
+
|
|
66
|
+
* **60% Neutral:** bg-slate-50 or bg-white (Backgrounds). Use off-white for the canvas to add depth.
|
|
67
|
+
* **30% Secondary:** slate-200, slate-800 (Borders, Dividers).
|
|
68
|
+
* **10% Accent:** indigo-600, emerald-500, rose-500 (Key metrics, Buttons).
|
|
69
|
+
* **No Pure Black:** Never use \#000000. Use text-slate-900 or text-gray-800.
|
|
70
|
+
|
|
71
|
+
### **D. Card Design (Physicality)**
|
|
72
|
+
|
|
73
|
+
* **Definition:** Cards should look like physical objects.
|
|
74
|
+
* **Style:** bg-white rounded-xl shadow-lg border border-slate-100.
|
|
75
|
+
* **Accents:** Add a splash of color using the "Strip Hack" (e.g., border-l-4 border-indigo-500).
|
|
76
|
+
|
|
77
|
+
### **C. Table Design (If using tables)**
|
|
78
|
+
|
|
79
|
+
* **Headers:** Use a light background (bg-slate-50) and bold text (font-bold) for <thead>.
|
|
80
|
+
* **Borders:** Use simple borders (border-b border-slate-200) for rows. Avoid heavy grid lines on every cell.
|
|
81
|
+
* **Spacing:** Use padding (p-3) in cells to keep data readable.
|
|
82
|
+
|
|
83
|
+
## **3\. 💡 FEW-SHOT EXAMPLES (Copy these styles)**
|
|
84
|
+
|
|
85
|
+
### **Style 1: "Soft Modern" (Cards, Shadows, Friendly)**
|
|
86
|
+
|
|
87
|
+
4. \<div id="slide-canvas" class="relative bg-slate-50 w-\[960px\] h-\[540px\] overflow-hidden text-slate-800 font-sans"\>
|
|
88
|
+
5. \<\!-- Header \--\>
|
|
89
|
+
6. \<div class="absolute top-0 left-0 w-full px-12 py-10 z-10"\>
|
|
90
|
+
7. \<span class="text-indigo-500 font-bold tracking-\[0.2em\] text-xs uppercase mb-2 block"\>Executive Summary\</span\>
|
|
91
|
+
8. \<h1 class="text-4xl font-extrabold text-slate-900"\>Q4 Performance Overview\</h1\>
|
|
92
|
+
9. \</div\>
|
|
93
|
+
10. \<\!-- Cards \--\>
|
|
94
|
+
11. \<div class="absolute top-40 left-0 w-full px-12 flex gap-8 z-20"\>
|
|
95
|
+
12. \<\!-- Card 1 \--\>
|
|
96
|
+
13. \<div class="flex-1 bg-white h-56 rounded-2xl shadow-xl border border-slate-200 border-l-8 border-l-indigo-500 p-8 flex flex-col justify-between"\>
|
|
97
|
+
14. \<span class="text-slate-400 font-bold text-xs uppercase tracking-wider"\>Total Revenue\</span\>
|
|
98
|
+
15. \<span class="text-5xl font-extrabold text-slate-900"\>$1.2M\</span\>
|
|
99
|
+
16. \<span class="bg-indigo-50 text-indigo-700 px-3 py-1 rounded-lg text-xs font-bold self-start"\>+12% YoY\</span\>
|
|
100
|
+
17. \</div\>
|
|
101
|
+
18. \<\!-- Card 2 \--\>
|
|
102
|
+
19. \<div class="flex-1 bg-white h-56 rounded-2xl shadow-xl border border-slate-200 border-l-8 border-l-emerald-500 p-8 flex flex-col justify-between"\>
|
|
103
|
+
20. \<span class="text-slate-400 font-bold text-xs uppercase tracking-wider"\>Active Users\</span\>
|
|
104
|
+
21. \<span class="text-5xl font-extrabold text-slate-900"\>850K\</span\>
|
|
105
|
+
22. \<span class="text-slate-400 text-xs"\>Monthly Active Users\</span\>
|
|
106
|
+
23. \</div\>
|
|
107
|
+
24. \</div\>
|
|
108
|
+
25. \</div\>
|
|
109
|
+
|
|
110
|
+
### **Style 2: "Dark Tech" (High Contrast, Neon, Futuristic)**
|
|
111
|
+
|
|
112
|
+
26. \<div id="slide-canvas" class="relative bg-slate-900 w-\[960px\] h-\[540px\] overflow-hidden text-white font-sans"\>
|
|
113
|
+
27. \<\!-- Background Accents \--\>
|
|
114
|
+
28. \<div class="absolute top-0 right-0 w-64 h-64 bg-blue-600 rounded-full opacity-20 blur-3xl"\>\</div\>
|
|
115
|
+
29.
|
|
116
|
+
30. \<\!-- Header \--\>
|
|
117
|
+
31. \<div class="absolute top-10 left-12 z-10"\>
|
|
118
|
+
32. \<h1 class="text-4xl font-bold"\>Server Metrics\</h1\>
|
|
119
|
+
33. \<p class="text-slate-400 text-sm mt-1"\>Real-time status report\</p\>
|
|
120
|
+
34. \</div\>
|
|
121
|
+
35.
|
|
122
|
+
36. \<\!-- Content \--\>
|
|
123
|
+
37. \<div class="absolute top-36 left-12 flex gap-6 z-20"\>
|
|
124
|
+
38. \<div class="w-64 bg-slate-800 rounded-lg p-6 border border-slate-700 relative overflow-hidden"\>
|
|
125
|
+
39. \<div class="absolute top-0 left-0 w-full h-1 bg-cyan-400"\>\</div\>
|
|
126
|
+
40. \<p class="text-slate-400 text-\[10px\] uppercase tracking-widest"\>Uptime\</p\>
|
|
127
|
+
41. \<p class="text-4xl font-mono font-bold text-white mt-2"\>99.9%\</p\>
|
|
128
|
+
42. \</div\>
|
|
129
|
+
43. \</div\>
|
|
130
|
+
44. \</div\>
|
|
131
|
+
|
|
132
|
+
### **Style 3: "Swiss Grid" (Minimalist, Clean, Typography-focused)**
|
|
133
|
+
|
|
134
|
+
45. \<div id="slide-canvas" class="relative bg-stone-50 w-\[960px\] h-\[540px\] overflow-hidden text-stone-900 font-sans"\>
|
|
135
|
+
46. \<\!-- Sidebar \--\>
|
|
136
|
+
47. \<div class="absolute top-0 left-0 w-\[280px\] h-full bg-stone-200 border-r border-stone-300 p-10 flex flex-col"\>
|
|
137
|
+
48. \<div class="mb-10"\>
|
|
138
|
+
49. \<div class="w-10 h-10 bg-black rounded-full mb-4"\>\</div\>
|
|
139
|
+
50. \<h2 class="text-xs font-bold tracking-widest uppercase mb-1 text-stone-500"\>Quarter 4\</h2\>
|
|
140
|
+
51. \<h1 class="text-3xl font-bold leading-tight"\>Sales\<br\>Briefing\</h1\>
|
|
141
|
+
52. \</div\>
|
|
142
|
+
53. \</div\>
|
|
143
|
+
54. \<\!-- Right Content \--\>
|
|
144
|
+
55. \<div class="absolute top-0 left-\[280px\] w-\[680px\] h-full p-10"\>
|
|
145
|
+
56. \<div class="border-b border-stone-300 pb-8"\>
|
|
146
|
+
57. \<span class="text-xs font-bold text-stone-500 uppercase block mb-2"\>Total Revenue\</span\>
|
|
147
|
+
58. \<div class="flex items-baseline gap-4"\>
|
|
148
|
+
59. \<span class="text-6xl font-black tracking-tighter"\>$1,250,000\</span\>
|
|
149
|
+
60. \<span class="text-emerald-600 font-bold text-lg"\>▲ 15%\</span\>
|
|
150
|
+
61. \</div\>
|
|
151
|
+
62. \</div\>
|
|
152
|
+
63. \</div\>
|
|
153
|
+
64. \</div\>
|
|
154
|
+
|
|
155
|
+
## **4\. 🚀 FINAL INSTRUCTION**
|
|
156
|
+
|
|
157
|
+
Generate the HTML code for the user's request based on the guidelines above.
|
|
158
|
+
|
|
159
|
+
1. **Output ONLY the HTML** starting with the \<div id="slide-canvas"\> tag.
|
|
160
|
+
2. Ensure all CSS uses valid **Tailwind CSS** utility classes.
|
|
161
|
+
3. **Check:** Did you use 960px width? Did you use absolute for layout? Did you use high contrast typography?
|
|
162
|
+
4. **Use Tables:** if the user asks for detailed data comparisons or lists with multiple columns.
|
|
@@ -0,0 +1,565 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LLM DOM to PPTX - v1.0.0
|
|
3
|
+
* Converts Semantic HTML/CSS (e.g. from LLMs) into editable PPTX.
|
|
4
|
+
*
|
|
5
|
+
* Dependencies:
|
|
6
|
+
* - PptxGenJS (https://gitbrent.github.io/PptxGenJS/)
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
(function (root) {
|
|
10
|
+
'use strict';
|
|
11
|
+
|
|
12
|
+
// --- 1. Font Mapping Utilities ---
|
|
13
|
+
|
|
14
|
+
const FONT_MAP = {
|
|
15
|
+
// Sans-Serif
|
|
16
|
+
"Inter": "Arial",
|
|
17
|
+
"Roboto": "Arial",
|
|
18
|
+
"Open Sans": "Calibri",
|
|
19
|
+
"Lato": "Calibri",
|
|
20
|
+
"Montserrat": "Arial",
|
|
21
|
+
"Source Sans Pro": "Arial",
|
|
22
|
+
"Noto Sans": "Arial",
|
|
23
|
+
"Helvetica": "Arial",
|
|
24
|
+
"San Francisco": "Arial",
|
|
25
|
+
"Segoe UI": "Segoe UI",
|
|
26
|
+
"System-UI": "Segoe UI",
|
|
27
|
+
|
|
28
|
+
// Serif
|
|
29
|
+
"Times New Roman": "Times New Roman",
|
|
30
|
+
"Georgia": "Georgia",
|
|
31
|
+
"Merriweather": "Times New Roman",
|
|
32
|
+
"Playfair Display": "Georgia",
|
|
33
|
+
|
|
34
|
+
// Monospace
|
|
35
|
+
"Courier New": "Courier New",
|
|
36
|
+
"Fira Code": "Courier New",
|
|
37
|
+
"Roboto Mono": "Courier New",
|
|
38
|
+
|
|
39
|
+
// Fallbacks
|
|
40
|
+
"sans-serif": "Arial",
|
|
41
|
+
"serif": "Times New Roman",
|
|
42
|
+
"monospace": "Courier New"
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Returns a PPTX-safe font name based on the input web font family.
|
|
47
|
+
*/
|
|
48
|
+
function getSafeFont(fontFamily) {
|
|
49
|
+
if (!fontFamily) return "Arial";
|
|
50
|
+
const clean = fontFamily.replace(/['"]/g, '');
|
|
51
|
+
const fonts = clean.split(',').map(f => f.trim());
|
|
52
|
+
for (let f of fonts) {
|
|
53
|
+
if (FONT_MAP[f]) return FONT_MAP[f];
|
|
54
|
+
const key = Object.keys(FONT_MAP).find(k => k.toLowerCase() === f.toLowerCase());
|
|
55
|
+
if (key) return FONT_MAP[key];
|
|
56
|
+
}
|
|
57
|
+
return "Arial";
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// --- 2. Color & Unit Utilities ---
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Parses a color string into Hex and Transparency (%).
|
|
64
|
+
* Handles #Hex, rgb(), and rgba().
|
|
65
|
+
* @returns {Object|null} { color: "RRGGBB", transparency: 0-100 } or null
|
|
66
|
+
*/
|
|
67
|
+
function parseColor(colorStr, opacity = 1) {
|
|
68
|
+
if (!colorStr || colorStr === 'rgba(0, 0, 0, 0)' || colorStr === 'transparent') return null;
|
|
69
|
+
|
|
70
|
+
let r, g, b, a = 1;
|
|
71
|
+
|
|
72
|
+
if (colorStr.startsWith('#')) {
|
|
73
|
+
const hex = colorStr.substring(1);
|
|
74
|
+
if (hex.length === 3) {
|
|
75
|
+
r = parseInt(hex[0] + hex[0], 16);
|
|
76
|
+
g = parseInt(hex[1] + hex[1], 16);
|
|
77
|
+
b = parseInt(hex[2] + hex[2], 16);
|
|
78
|
+
} else {
|
|
79
|
+
r = parseInt(hex.substring(0, 2), 16);
|
|
80
|
+
g = parseInt(hex.substring(2, 4), 16);
|
|
81
|
+
b = parseInt(hex.substring(4, 6), 16);
|
|
82
|
+
}
|
|
83
|
+
} else if (colorStr.startsWith('rgb')) {
|
|
84
|
+
const values = colorStr.match(/(\d+(\.\d+)?)/g);
|
|
85
|
+
if (!values || values.length < 3) return null;
|
|
86
|
+
r = parseFloat(values[0]);
|
|
87
|
+
g = parseFloat(values[1]);
|
|
88
|
+
b = parseFloat(values[2]);
|
|
89
|
+
if (values.length > 3) {
|
|
90
|
+
a = parseFloat(values[3]);
|
|
91
|
+
}
|
|
92
|
+
} else {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Combine CSS opacity with Color Alpha
|
|
97
|
+
const finalAlpha = a * opacity;
|
|
98
|
+
|
|
99
|
+
// Convert to Hex string "RRGGBB"
|
|
100
|
+
const toHex = (n) => {
|
|
101
|
+
const h = Math.round(n).toString(16);
|
|
102
|
+
return h.length === 1 ? '0' + h : h;
|
|
103
|
+
};
|
|
104
|
+
const hex = toHex(r) + toHex(g) + toHex(b);
|
|
105
|
+
|
|
106
|
+
// Calculate Transparency Percent (0 = Opaque, 100 = Transparent)
|
|
107
|
+
// PPTX gen uses percent transparency
|
|
108
|
+
const transparency = Math.round((1 - finalAlpha) * 100);
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
color: hex,
|
|
112
|
+
transparency: transparency
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Deprecated but kept for compatibility references if any
|
|
117
|
+
function rgbToHex(rgb) {
|
|
118
|
+
const p = parseColor(rgb);
|
|
119
|
+
return p ? '#' + p.color : null;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const PPI = 96;
|
|
123
|
+
const SLIDE_WIDTH_PX = 960; // Default reference width
|
|
124
|
+
const PPT_WIDTH_IN = 10;
|
|
125
|
+
|
|
126
|
+
// We calculate generic scale based on 960px.
|
|
127
|
+
// If the user's container is different, we might want to adjust,
|
|
128
|
+
// but usually 960px is a good "logical" width for a slide.
|
|
129
|
+
const SCALE = PPT_WIDTH_IN / SLIDE_WIDTH_PX;
|
|
130
|
+
|
|
131
|
+
function pxToInch(px) {
|
|
132
|
+
return parseFloat(px) * SCALE;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// --- 3. Main Export Function ---
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Exports a DOM element to PPTX.
|
|
139
|
+
* @param {string|HTMLElement} elementOrId - The DOM element or ID to export.
|
|
140
|
+
* @param {object} options - Options { fileName: "presentation.pptx" }
|
|
141
|
+
*/
|
|
142
|
+
async function exportToPPTX(elementOrId = 'slide-canvas', options = {}) {
|
|
143
|
+
const fileName = options.fileName || "presentation.pptx";
|
|
144
|
+
|
|
145
|
+
console.log(`Starting PPTX Export for ${elementOrId}...`);
|
|
146
|
+
|
|
147
|
+
if (typeof PptxGenJS === 'undefined') {
|
|
148
|
+
console.error("PptxGenJS is not loaded. Please include it via CDN.");
|
|
149
|
+
alert("Error: PptxGenJS library missing.");
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const pres = new PptxGenJS();
|
|
154
|
+
pres.layout = 'LAYOUT_16x9';
|
|
155
|
+
|
|
156
|
+
const slide = pres.addSlide();
|
|
157
|
+
|
|
158
|
+
// Resolve Container
|
|
159
|
+
let container;
|
|
160
|
+
if (typeof elementOrId === 'string') {
|
|
161
|
+
container = document.getElementById(elementOrId);
|
|
162
|
+
} else {
|
|
163
|
+
container = elementOrId;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (!container) {
|
|
167
|
+
console.error(`Slide container '${elementOrId}' not found!`);
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const containerRect = container.getBoundingClientRect();
|
|
172
|
+
|
|
173
|
+
// --- 0. Slide Background ---
|
|
174
|
+
const containerStyle = window.getComputedStyle(container);
|
|
175
|
+
const bgParsed = parseColor(containerStyle.backgroundColor);
|
|
176
|
+
if (bgParsed) {
|
|
177
|
+
slide.background = { color: bgParsed.color, transparency: bgParsed.transparency };
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Visited nodes set to prevent ghosting/duplication
|
|
181
|
+
const processedNodes = new Set();
|
|
182
|
+
|
|
183
|
+
// --- Helper: recurse gather text runs ---
|
|
184
|
+
function collectTextRuns(node, parentStyle) {
|
|
185
|
+
let runs = [];
|
|
186
|
+
if (!node) return runs;
|
|
187
|
+
|
|
188
|
+
// Skip non-visible if element
|
|
189
|
+
if (node.nodeType === Node.ELEMENT_NODE) {
|
|
190
|
+
const s = window.getComputedStyle(node);
|
|
191
|
+
if (s.display === 'none' || s.visibility === 'hidden' || parseFloat(s.opacity) === 0) return runs;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
node.childNodes.forEach(child => {
|
|
195
|
+
if (child.nodeType === Node.TEXT_NODE) {
|
|
196
|
+
const text = child.textContent;
|
|
197
|
+
if (!text) return;
|
|
198
|
+
|
|
199
|
+
// Inherit style from parent element if current is text node
|
|
200
|
+
const style = (node.nodeType === Node.ELEMENT_NODE) ? window.getComputedStyle(node) : parentStyle;
|
|
201
|
+
|
|
202
|
+
const colorParsed = parseColor(style.color, parseFloat(style.opacity) || 1);
|
|
203
|
+
const fontSize = parseFloat(style.fontSize);
|
|
204
|
+
const fontWeight = (style.fontWeight === '700' || style.fontWeight === 'bold' || parseInt(style.fontWeight) >= 600);
|
|
205
|
+
|
|
206
|
+
// Normalize Whitespace:
|
|
207
|
+
// 1. Newlines/Tabs -> Space
|
|
208
|
+
let runText = text.replace(/[\r\n\t]+/g, ' ');
|
|
209
|
+
|
|
210
|
+
if (style.textTransform === 'uppercase') runText = runText.toUpperCase();
|
|
211
|
+
|
|
212
|
+
if (!runText) return;
|
|
213
|
+
|
|
214
|
+
const runOpts = {
|
|
215
|
+
color: colorParsed ? colorParsed.color : '000000',
|
|
216
|
+
fontSize: fontSize * 0.75, // px to pt
|
|
217
|
+
bold: fontWeight,
|
|
218
|
+
fontFace: getSafeFont(style.fontFamily),
|
|
219
|
+
charSpacing: (style.letterSpacing && style.letterSpacing !== 'normal') ? parseFloat(style.letterSpacing) : 0,
|
|
220
|
+
breakLine: false
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
if (colorParsed && colorParsed.transparency > 0) {
|
|
224
|
+
runOpts.transparency = colorParsed.transparency;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// highlight (background color) support
|
|
228
|
+
const bgParsed = parseColor(style.backgroundColor, parseFloat(style.opacity) || 1);
|
|
229
|
+
if (bgParsed && bgParsed.transparency < 100) {
|
|
230
|
+
runOpts.highlight = bgParsed.color;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
runs.push({
|
|
234
|
+
text: runText,
|
|
235
|
+
options: runOpts
|
|
236
|
+
});
|
|
237
|
+
} else if (child.nodeType === Node.ELEMENT_NODE) {
|
|
238
|
+
if (child.tagName === 'BR') {
|
|
239
|
+
runs.push({ text: '', options: { breakLine: true } });
|
|
240
|
+
} else {
|
|
241
|
+
runs.push(...collectTextRuns(child, window.getComputedStyle(child)));
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
});
|
|
245
|
+
return runs;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// --- Helper: Identify logical Text Block ---
|
|
249
|
+
function isTextBlock(node) {
|
|
250
|
+
if (node.nodeType !== Node.ELEMENT_NODE) return false;
|
|
251
|
+
if (node.childNodes.length === 0) return false;
|
|
252
|
+
|
|
253
|
+
let hasText = false;
|
|
254
|
+
node.childNodes.forEach(c => {
|
|
255
|
+
if (c.nodeType === Node.TEXT_NODE && c.textContent.trim().length > 0) hasText = true;
|
|
256
|
+
});
|
|
257
|
+
return hasText;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function processNode(node) {
|
|
261
|
+
if (node.nodeType !== Node.ELEMENT_NODE) return;
|
|
262
|
+
if (processedNodes.has(node)) return;
|
|
263
|
+
processedNodes.add(node);
|
|
264
|
+
|
|
265
|
+
const style = window.getComputedStyle(node);
|
|
266
|
+
const rect = node.getBoundingClientRect();
|
|
267
|
+
const opacity = parseFloat(style.opacity) || 1;
|
|
268
|
+
|
|
269
|
+
// Skip invisible
|
|
270
|
+
if (style.display === 'none' || style.visibility === 'hidden' || opacity === 0) return;
|
|
271
|
+
|
|
272
|
+
// Skip zero size (unless overflow)
|
|
273
|
+
if (rect.width < 1 || rect.height < 1) return;
|
|
274
|
+
|
|
275
|
+
// Relative Coordinates
|
|
276
|
+
const x = pxToInch(rect.left - containerRect.left);
|
|
277
|
+
const y = pxToInch(rect.top - containerRect.top);
|
|
278
|
+
const w = pxToInch(rect.width);
|
|
279
|
+
const h = pxToInch(rect.height);
|
|
280
|
+
|
|
281
|
+
// --- TABLE HANDLING ---
|
|
282
|
+
if (node.tagName === 'TABLE') {
|
|
283
|
+
// Shadow Handler (Updated)
|
|
284
|
+
if (style.boxShadow && style.boxShadow !== 'none') {
|
|
285
|
+
let tableBg = style.backgroundColor;
|
|
286
|
+
let tableOp = parseFloat(style.opacity) || 1;
|
|
287
|
+
let shadowFill = parseColor(tableBg, tableOp);
|
|
288
|
+
|
|
289
|
+
if (!shadowFill || shadowFill.transparency === 100) {
|
|
290
|
+
shadowFill = { color: 'FFFFFF', transparency: 99 };
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Now we trust 'h' matches sum(rowHeights) because we enforce it below.
|
|
294
|
+
slide.addShape(pres.ShapeType.rect, {
|
|
295
|
+
x: x, y: y, w: w, h: h,
|
|
296
|
+
fill: { color: shadowFill.color, transparency: shadowFill.transparency },
|
|
297
|
+
shadow: { type: 'outer', angle: 45, blur: 10, offset: 4, opacity: 0.3 },
|
|
298
|
+
rectRadius: 0
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const tableRows = [];
|
|
303
|
+
let colWidths = [];
|
|
304
|
+
let rowHeights = []; // New strictly mapped row heights
|
|
305
|
+
|
|
306
|
+
if (node.rows.length > 0) {
|
|
307
|
+
colWidths = Array.from(node.rows[0].cells).map(c => pxToInch(c.getBoundingClientRect().width));
|
|
308
|
+
|
|
309
|
+
// Capture exact row heights
|
|
310
|
+
rowHeights = Array.from(node.rows).map(r => pxToInch(r.getBoundingClientRect().height));
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
Array.from(node.rows).forEach(row => {
|
|
314
|
+
const rowData = [];
|
|
315
|
+
Array.from(row.cells).forEach(cell => {
|
|
316
|
+
const cStyle = window.getComputedStyle(cell);
|
|
317
|
+
const cRuns = collectTextRuns(cell, cStyle);
|
|
318
|
+
|
|
319
|
+
// backgroundColor fallback: Cell -> Row -> Row Parent (tbody/thead) -> Table
|
|
320
|
+
let effectiveBg = cStyle.backgroundColor;
|
|
321
|
+
let effectiveOpacity = parseFloat(cStyle.opacity) || 1;
|
|
322
|
+
|
|
323
|
+
if ((!effectiveBg || effectiveBg === 'rgba(0, 0, 0, 0)' || effectiveBg === 'transparent') && row) {
|
|
324
|
+
const rStyle = window.getComputedStyle(row);
|
|
325
|
+
effectiveBg = rStyle.backgroundColor;
|
|
326
|
+
effectiveOpacity = parseFloat(rStyle.opacity) || 1;
|
|
327
|
+
|
|
328
|
+
if ((!effectiveBg || effectiveBg === 'rgba(0, 0, 0, 0)' || effectiveBg === 'transparent') && row.parentElement) {
|
|
329
|
+
const pStyle = window.getComputedStyle(row.parentElement);
|
|
330
|
+
effectiveBg = pStyle.backgroundColor;
|
|
331
|
+
effectiveOpacity = parseFloat(pStyle.opacity) || 1;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
const bgP = parseColor(effectiveBg, effectiveOpacity);
|
|
336
|
+
// Borders logic handles separately or assumes solid for now
|
|
337
|
+
const bCP = parseColor(cStyle.borderColor);
|
|
338
|
+
|
|
339
|
+
let vAlign = 'top';
|
|
340
|
+
if (cStyle.verticalAlign === 'middle') vAlign = 'middle';
|
|
341
|
+
if (cStyle.verticalAlign === 'bottom') vAlign = 'bottom';
|
|
342
|
+
|
|
343
|
+
const pt = (parseFloat(cStyle.paddingTop) || 0) * 0.75;
|
|
344
|
+
const pr = (parseFloat(cStyle.paddingRight) || 0) * 0.75;
|
|
345
|
+
const pb = (parseFloat(cStyle.paddingBottom) || 0) * 0.75;
|
|
346
|
+
const pl = (parseFloat(cStyle.paddingLeft) || 0) * 0.75;
|
|
347
|
+
const margin = [pt, pr, pb, pl];
|
|
348
|
+
|
|
349
|
+
const getBdr = (w, c, s) => {
|
|
350
|
+
if (!w || parseFloat(w) === 0 || s === 'none') return null;
|
|
351
|
+
const co = parseColor(c) || { color: '000000' };
|
|
352
|
+
return { pt: parseFloat(w) * 0.75, color: co.color };
|
|
353
|
+
};
|
|
354
|
+
|
|
355
|
+
const bTop = getBdr(cStyle.borderTopWidth, cStyle.borderTopColor, cStyle.borderTopStyle);
|
|
356
|
+
const bRight = getBdr(cStyle.borderRightWidth, cStyle.borderRightColor, cStyle.borderRightStyle);
|
|
357
|
+
const bBot = getBdr(cStyle.borderBottomWidth, cStyle.borderBottomColor, cStyle.borderBottomStyle);
|
|
358
|
+
const bLeft = getBdr(cStyle.borderLeftWidth, cStyle.borderLeftColor, cStyle.borderLeftStyle);
|
|
359
|
+
|
|
360
|
+
const cellOpts = {
|
|
361
|
+
valign: vAlign,
|
|
362
|
+
align: cStyle.textAlign === 'center' ? 'center' : (cStyle.textAlign === 'right' ? 'right' : 'left'),
|
|
363
|
+
margin: margin,
|
|
364
|
+
border: [bTop, bRight, bBot, bLeft]
|
|
365
|
+
};
|
|
366
|
+
|
|
367
|
+
if (bgP && bgP.transparency < 100) {
|
|
368
|
+
cellOpts.fill = { color: bgP.color };
|
|
369
|
+
if (bgP.transparency > 0) cellOpts.fill.transparency = bgP.transparency;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
rowData.push({
|
|
373
|
+
text: cRuns,
|
|
374
|
+
options: cellOpts
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
const markChildren = (n) => {
|
|
378
|
+
processedNodes.add(n);
|
|
379
|
+
Array.from(n.children).forEach(markChildren);
|
|
380
|
+
}
|
|
381
|
+
markChildren(cell);
|
|
382
|
+
processedNodes.add(cell);
|
|
383
|
+
});
|
|
384
|
+
tableRows.push(rowData);
|
|
385
|
+
processedNodes.add(row);
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
if (tableRows.length > 0) {
|
|
389
|
+
slide.addTable(tableRows, {
|
|
390
|
+
x: x, y: y, w: w, colW: colWidths, rowH: rowHeights, autoPage: true
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
processedNodes.add(node);
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// --- A. BACKGROUNDS & BORDERS ---
|
|
398
|
+
const bgParsed = parseColor(style.backgroundColor, opacity);
|
|
399
|
+
const borderW = parseFloat(style.borderWidth) || 0;
|
|
400
|
+
const borderParsed = parseColor(style.borderColor);
|
|
401
|
+
|
|
402
|
+
let hasFill = bgParsed && bgParsed.transparency < 100;
|
|
403
|
+
let hasBorder = borderW > 0 && borderParsed;
|
|
404
|
+
|
|
405
|
+
let shapeOpts = { x, y, w, h };
|
|
406
|
+
|
|
407
|
+
const borderRadius = parseFloat(style.borderRadius) || 0;
|
|
408
|
+
// Strict circle check
|
|
409
|
+
const isCircle = (Math.abs(rect.width - rect.height) < 2) && (borderRadius >= rect.width / 2 - 1);
|
|
410
|
+
|
|
411
|
+
// Shadow Support
|
|
412
|
+
if (style.boxShadow && style.boxShadow !== 'none') {
|
|
413
|
+
shapeOpts.shadow = { type: 'outer', angle: 45, blur: 6, offset: 2, opacity: 0.2 };
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// --- Radius Logic ---
|
|
417
|
+
let shapeType = pres.ShapeType.rect;
|
|
418
|
+
if (isCircle) {
|
|
419
|
+
shapeType = pres.ShapeType.ellipse;
|
|
420
|
+
} else if (borderRadius > 0) {
|
|
421
|
+
const minDim = Math.min(rect.width, rect.height);
|
|
422
|
+
let ratio = borderRadius / (minDim / 2);
|
|
423
|
+
shapeOpts.rectRadius = Math.min(ratio, 1.0);
|
|
424
|
+
shapeType = pres.ShapeType.roundRect || 'roundRect';
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// --- Border Logic ---
|
|
428
|
+
if (hasBorder && style.borderLeftWidth === style.borderRightWidth) {
|
|
429
|
+
shapeOpts.line = { color: borderParsed.color, width: borderW * 0.75 };
|
|
430
|
+
} else {
|
|
431
|
+
shapeOpts.line = null;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// --- B. LEFT ACCENT BORDER (Custom Strategy) ---
|
|
435
|
+
const lW = parseFloat(style.borderLeftWidth) || 0;
|
|
436
|
+
const leftBorderParsed = parseColor(style.borderLeftColor);
|
|
437
|
+
|
|
438
|
+
const hasLeftBorder = lW > 0 && leftBorderParsed && style.borderStyle !== 'none';
|
|
439
|
+
|
|
440
|
+
if (hasLeftBorder && !shapeOpts.line) {
|
|
441
|
+
if (hasFill) {
|
|
442
|
+
// Underlay Strategy
|
|
443
|
+
const underlayOpts = { ...shapeOpts };
|
|
444
|
+
underlayOpts.fill = { color: leftBorderParsed.color };
|
|
445
|
+
underlayOpts.line = null;
|
|
446
|
+
slide.addShape(shapeType, underlayOpts);
|
|
447
|
+
|
|
448
|
+
// Adjust Main Shape
|
|
449
|
+
const borderInch = pxToInch(lW);
|
|
450
|
+
shapeOpts.x += borderInch;
|
|
451
|
+
shapeOpts.w -= borderInch;
|
|
452
|
+
delete shapeOpts.shadow; // Remove duplicate shadow
|
|
453
|
+
} else {
|
|
454
|
+
// Side Strip Strategy
|
|
455
|
+
slide.addShape(pres.ShapeType.rect, {
|
|
456
|
+
x: x, y: y, w: pxToInch(lW), h: h,
|
|
457
|
+
fill: { color: leftBorderParsed.color },
|
|
458
|
+
rectRadius: isCircle ? 0 : (shapeOpts.rectRadius || 0)
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// Draw Main Shape
|
|
464
|
+
if (hasFill) {
|
|
465
|
+
shapeOpts.fill = { color: bgParsed.color };
|
|
466
|
+
if (bgParsed.transparency > 0) {
|
|
467
|
+
shapeOpts.fill.transparency = bgParsed.transparency;
|
|
468
|
+
}
|
|
469
|
+
slide.addShape(shapeType, shapeOpts);
|
|
470
|
+
} else if (hasBorder && shapeOpts.line) {
|
|
471
|
+
slide.addShape(shapeType, shapeOpts);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// --- Gradient Fallback ---
|
|
475
|
+
if (style.backgroundImage && style.backgroundImage.includes('gradient')) {
|
|
476
|
+
// If it's a bar/strip
|
|
477
|
+
if (rect.height < 15 && rect.width > 100) {
|
|
478
|
+
slide.addShape(pres.ShapeType.rect, {
|
|
479
|
+
x: x, y: y, w: w, h: h,
|
|
480
|
+
fill: { color: '4F46E5' } // Fallback
|
|
481
|
+
});
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// --- C. TEXT CONTENT ---
|
|
486
|
+
if (isTextBlock(node)) {
|
|
487
|
+
|
|
488
|
+
// Extra check: If this is a very deep node, does it have children that are also text blocks?
|
|
489
|
+
// Logic: isTextBlock returns true if it has text nodes.
|
|
490
|
+
|
|
491
|
+
const runs = collectTextRuns(node, style);
|
|
492
|
+
|
|
493
|
+
if (runs.length > 0) {
|
|
494
|
+
if (runs.length > 0) {
|
|
495
|
+
runs[0].text = runs[0].text.replace(/^\s+/, '');
|
|
496
|
+
runs[runs.length - 1].text = runs[runs.length - 1].text.replace(/\s+$/, '');
|
|
497
|
+
}
|
|
498
|
+
const validRuns = runs.filter(r => r.text !== '' || r.options.breakLine);
|
|
499
|
+
|
|
500
|
+
if (validRuns.length > 0) {
|
|
501
|
+
let align = 'left';
|
|
502
|
+
if (style.textAlign === 'center') align = 'center';
|
|
503
|
+
if (style.textAlign === 'right') align = 'right';
|
|
504
|
+
if (style.textAlign === 'justify') align = 'justify';
|
|
505
|
+
|
|
506
|
+
let valign = 'top';
|
|
507
|
+
const pt = parseFloat(style.paddingTop) || 0;
|
|
508
|
+
const pb = parseFloat(style.paddingBottom) || 0;
|
|
509
|
+
const boxH = rect.height;
|
|
510
|
+
const textH = parseFloat(style.fontSize) * 1.2;
|
|
511
|
+
|
|
512
|
+
if (style.display.includes('flex') && style.alignItems === 'center') valign = 'middle';
|
|
513
|
+
else if (Math.abs(pt - pb) < 5 && pt > 5) valign = 'middle';
|
|
514
|
+
else if (boxH < 40 && boxH > textH) valign = 'middle';
|
|
515
|
+
|
|
516
|
+
if (style.display.includes('flex')) {
|
|
517
|
+
if (style.justifyContent === 'center') align = 'center';
|
|
518
|
+
else if (style.justifyContent === 'flex-end' || style.justifyContent === 'right') align = 'right';
|
|
519
|
+
}
|
|
520
|
+
if (node.tagName === 'SPAN') {
|
|
521
|
+
align = 'center'; valign = 'middle';
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
const widthBuffer = pxToInch(4);
|
|
525
|
+
const inset = Math.max(0, pxToInch(Math.min(pt, parseFloat(style.paddingLeft) || 0)));
|
|
526
|
+
|
|
527
|
+
slide.addText(runs, {
|
|
528
|
+
x: x, y: y, w: w + widthBuffer, h: h,
|
|
529
|
+
align: align, valign: valign, margin: 0, inset: inset,
|
|
530
|
+
autoFit: false, wrap: true
|
|
531
|
+
});
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
// Mark children as processed
|
|
535
|
+
const markSeen = (n) => {
|
|
536
|
+
n.childNodes.forEach(c => {
|
|
537
|
+
if (c.nodeType === Node.ELEMENT_NODE) {
|
|
538
|
+
processedNodes.add(c);
|
|
539
|
+
markSeen(c);
|
|
540
|
+
}
|
|
541
|
+
});
|
|
542
|
+
};
|
|
543
|
+
markSeen(node);
|
|
544
|
+
}
|
|
545
|
+
} else {
|
|
546
|
+
Array.from(node.children).forEach(processNode);
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
// Start Processing
|
|
551
|
+
Array.from(container.children).forEach(processNode);
|
|
552
|
+
|
|
553
|
+
// Save
|
|
554
|
+
pres.writeFile({ fileName: fileName });
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
// --- Exports ---
|
|
558
|
+
root.LLMDomToPptx = {
|
|
559
|
+
export: exportToPPTX
|
|
560
|
+
};
|
|
561
|
+
|
|
562
|
+
// Keep old global for compatibility if needed
|
|
563
|
+
root.exportToPPTX = exportToPPTX;
|
|
564
|
+
|
|
565
|
+
})(window);
|
package/package.json
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "llm-dom-to-pptx",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Turn AI-generated HTML/CSS into native, editable PowerPoint slides.",
|
|
5
|
+
"main": "dist/llm-dom-to-pptx.js",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
8
|
+
},
|
|
9
|
+
"keywords": [
|
|
10
|
+
"pptx",
|
|
11
|
+
"powerpoint",
|
|
12
|
+
"html-to-pptx",
|
|
13
|
+
"llm",
|
|
14
|
+
"ai",
|
|
15
|
+
"dom",
|
|
16
|
+
"presentation"
|
|
17
|
+
],
|
|
18
|
+
"author": "User",
|
|
19
|
+
"license": "MIT",
|
|
20
|
+
"files": [
|
|
21
|
+
"dist",
|
|
22
|
+
"README.md",
|
|
23
|
+
"System_Prompt.md"
|
|
24
|
+
],
|
|
25
|
+
"peerDependencies": {
|
|
26
|
+
"pptxgenjs": "^3.12.0"
|
|
27
|
+
}
|
|
28
|
+
}
|