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 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
+ ![Demo Comparison](demo/demo.png)
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
+
@@ -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
+ }