llm-dom-to-pptx 1.0.1 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -28,7 +28,7 @@ This library depends on `PptxGenJS`.
28
28
  <script src="https://cdn.jsdelivr.net/npm/pptxgenjs@3.12.0/dist/pptxgen.min.js"></script>
29
29
 
30
30
  <!-- llm-dom-to-pptx -->
31
- <script src="https://cdn.jsdelivr.net/npm/llm-dom-to-pptx@1.0.0/dist/llm-dom-to-pptx.js"></script>
31
+ <script src="https://cdn.jsdelivr.net/npm/llm-dom-to-pptx@1.0.2/dist/llm-dom-to-pptx.js"></script>
32
32
  ```
33
33
 
34
34
  ### 2. Prepare Your HTML
package/System_Prompt.md CHANGED
@@ -1,168 +1,168 @@
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
- ```html
88
- <div id="slide-canvas" class="relative bg-slate-50 w-[960px] h-[540px] overflow-hidden text-slate-800 font-sans">
89
- <!-- Header -->
90
- <div class="absolute top-0 left-0 w-full px-12 py-10 z-10">
91
- <span class="text-indigo-500 font-bold tracking-[0.2em] text-xs uppercase mb-2 block">Executive Summary</span>
92
- <h1 class="text-4xl font-extrabold text-slate-900">Q4 Performance Overview</h1>
93
- </div>
94
- <!-- Cards -->
95
- <div class="absolute top-40 left-0 w-full px-12 flex gap-8 z-20">
96
- <!-- Card 1 -->
97
- <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">
98
- <span class="text-slate-400 font-bold text-xs uppercase tracking-wider">Total Revenue</span>
99
- <span class="text-5xl font-extrabold text-slate-900">$1.2M</span>
100
- <span class="bg-indigo-50 text-indigo-700 px-3 py-1 rounded-lg text-xs font-bold self-start">+12% YoY</span>
101
- </div>
102
- <!-- Card 2 -->
103
- <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">
104
- <span class="text-slate-400 font-bold text-xs uppercase tracking-wider">Active Users</span>
105
- <span class="text-5xl font-extrabold text-slate-900">850K</span>
106
- <span class="text-slate-400 text-xs">Monthly Active Users</span>
107
- </div>
108
- </div>
109
- </div>
110
- ```
111
-
112
- ### **Style 2: "Dark Tech" (High Contrast, Neon, Futuristic)**
113
-
114
- ```html
115
- <div id="slide-canvas" class="relative bg-slate-900 w-[960px] h-[540px] overflow-hidden text-white font-sans">
116
- <!-- Background Accents -->
117
- <div class="absolute top-0 right-0 w-64 h-64 bg-blue-600 rounded-full opacity-20 blur-3xl"></div>
118
-
119
- <!-- Header -->
120
- <div class="absolute top-10 left-12 z-10">
121
- <h1 class="text-4xl font-bold">Server Metrics</h1>
122
- <p class="text-slate-400 text-sm mt-1">Real-time status report</p>
123
- </div>
124
-
125
- <!-- Content -->
126
- <div class="absolute top-36 left-12 flex gap-6 z-20">
127
- <div class="w-64 bg-slate-800 rounded-lg p-6 border border-slate-700 relative overflow-hidden">
128
- <div class="absolute top-0 left-0 w-full h-1 bg-cyan-400"></div>
129
- <p class="text-slate-400 text-[10px] uppercase tracking-widest">Uptime</p>
130
- <p class="text-4xl font-mono font-bold text-white mt-2">99.9%</p>
131
- </div>
132
- </div>
133
- </div>
134
- ```
135
-
136
- ### **Style 3: "Swiss Grid" (Minimalist, Clean, Typography-focused)**
137
-
138
- ```html
139
- <div id="slide-canvas" class="relative bg-stone-50 w-[960px] h-[540px] overflow-hidden text-stone-900 font-sans">
140
- <!-- Sidebar -->
141
- <div class="absolute top-0 left-0 w-[280px] h-full bg-stone-200 border-r border-stone-300 p-10 flex flex-col">
142
- <div class="mb-10">
143
- <div class="w-10 h-10 bg-black rounded-full mb-4"></div>
144
- <h2 class="text-xs font-bold tracking-widest uppercase mb-1 text-stone-500">Quarter 4</h2>
145
- <h1 class="text-3xl font-bold leading-tight">Sales<br>Briefing</h1>
146
- </div>
147
- </div>
148
- <!-- Right Content -->
149
- <div class="absolute top-0 left-[280px] w-[680px] h-full p-10">
150
- <div class="border-b border-stone-300 pb-8">
151
- <span class="text-xs font-bold text-stone-500 uppercase block mb-2">Total Revenue</span>
152
- <div class="flex items-baseline gap-4">
153
- <span class="text-6xl font-black tracking-tighter">$1,250,000</span>
154
- <span class="text-emerald-600 font-bold text-lg">▲ 15%</span>
155
- </div>
156
- </div>
157
- </div>
158
- </div>
159
- ```
160
-
161
- ## **4\. 🚀 FINAL INSTRUCTION**
162
-
163
- Generate the HTML code for the user's request based on the guidelines above.
164
-
165
- 1. **Output ONLY the HTML** starting with the \<div id="slide-canvas"\> tag.
166
- 2. Ensure all CSS uses valid **Tailwind CSS** utility classes.
167
- 3. **Check:** Did you use 960px width? Did you use absolute for layout? Did you use high contrast typography?
168
- 4. **Use Tables:** if the user asks for detailed data comparisons or lists with multiple columns.
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
+ ```html
88
+ <div id="slide-canvas" class="relative bg-slate-50 w-[960px] h-[540px] overflow-hidden text-slate-800 font-sans">
89
+ <!-- Header -->
90
+ <div class="absolute top-0 left-0 w-full px-12 py-10 z-10">
91
+ <span class="text-indigo-500 font-bold tracking-[0.2em] text-xs uppercase mb-2 block">Executive Summary</span>
92
+ <h1 class="text-4xl font-extrabold text-slate-900">Q4 Performance Overview</h1>
93
+ </div>
94
+ <!-- Cards -->
95
+ <div class="absolute top-40 left-0 w-full px-12 flex gap-8 z-20">
96
+ <!-- Card 1 -->
97
+ <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">
98
+ <span class="text-slate-400 font-bold text-xs uppercase tracking-wider">Total Revenue</span>
99
+ <span class="text-5xl font-extrabold text-slate-900">$1.2M</span>
100
+ <span class="bg-indigo-50 text-indigo-700 px-3 py-1 rounded-lg text-xs font-bold self-start">+12% YoY</span>
101
+ </div>
102
+ <!-- Card 2 -->
103
+ <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">
104
+ <span class="text-slate-400 font-bold text-xs uppercase tracking-wider">Active Users</span>
105
+ <span class="text-5xl font-extrabold text-slate-900">850K</span>
106
+ <span class="text-slate-400 text-xs">Monthly Active Users</span>
107
+ </div>
108
+ </div>
109
+ </div>
110
+ ```
111
+
112
+ ### **Style 2: "Dark Tech" (High Contrast, Neon, Futuristic)**
113
+
114
+ ```html
115
+ <div id="slide-canvas" class="relative bg-slate-900 w-[960px] h-[540px] overflow-hidden text-white font-sans">
116
+ <!-- Background Accents -->
117
+ <div class="absolute top-0 right-0 w-64 h-64 bg-blue-600 rounded-full opacity-20 blur-3xl"></div>
118
+
119
+ <!-- Header -->
120
+ <div class="absolute top-10 left-12 z-10">
121
+ <h1 class="text-4xl font-bold">Server Metrics</h1>
122
+ <p class="text-slate-400 text-sm mt-1">Real-time status report</p>
123
+ </div>
124
+
125
+ <!-- Content -->
126
+ <div class="absolute top-36 left-12 flex gap-6 z-20">
127
+ <div class="w-64 bg-slate-800 rounded-lg p-6 border border-slate-700 relative overflow-hidden">
128
+ <div class="absolute top-0 left-0 w-full h-1 bg-cyan-400"></div>
129
+ <p class="text-slate-400 text-[10px] uppercase tracking-widest">Uptime</p>
130
+ <p class="text-4xl font-mono font-bold text-white mt-2">99.9%</p>
131
+ </div>
132
+ </div>
133
+ </div>
134
+ ```
135
+
136
+ ### **Style 3: "Swiss Grid" (Minimalist, Clean, Typography-focused)**
137
+
138
+ ```html
139
+ <div id="slide-canvas" class="relative bg-stone-50 w-[960px] h-[540px] overflow-hidden text-stone-900 font-sans">
140
+ <!-- Sidebar -->
141
+ <div class="absolute top-0 left-0 w-[280px] h-full bg-stone-200 border-r border-stone-300 p-10 flex flex-col">
142
+ <div class="mb-10">
143
+ <div class="w-10 h-10 bg-black rounded-full mb-4"></div>
144
+ <h2 class="text-xs font-bold tracking-widest uppercase mb-1 text-stone-500">Quarter 4</h2>
145
+ <h1 class="text-3xl font-bold leading-tight">Sales<br>Briefing</h1>
146
+ </div>
147
+ </div>
148
+ <!-- Right Content -->
149
+ <div class="absolute top-0 left-[280px] w-[680px] h-full p-10">
150
+ <div class="border-b border-stone-300 pb-8">
151
+ <span class="text-xs font-bold text-stone-500 uppercase block mb-2">Total Revenue</span>
152
+ <div class="flex items-baseline gap-4">
153
+ <span class="text-6xl font-black tracking-tighter">$1,250,000</span>
154
+ <span class="text-emerald-600 font-bold text-lg">▲ 15%</span>
155
+ </div>
156
+ </div>
157
+ </div>
158
+ </div>
159
+ ```
160
+
161
+ ## **4\. 🚀 FINAL INSTRUCTION**
162
+
163
+ Generate the HTML code for the user's request based on the guidelines above.
164
+
165
+ 1. **Output ONLY the HTML** starting with the \<div id="slide-canvas"\> tag.
166
+ 2. Ensure all CSS uses valid **Tailwind CSS** utility classes.
167
+ 3. **Check:** Did you use 960px width? Did you use absolute for layout? Did you use high contrast typography?
168
+ 4. **Use Tables:** if the user asks for detailed data comparisons or lists with multiple columns.
@@ -1,5 +1,5 @@
1
1
  /**
2
- * LLM DOM to PPTX - v1.0.1
2
+ * LLM DOM to PPTX - v1.0.2
3
3
  * Converts Semantic HTML/CSS (e.g. from LLMs) into editable PPTX.
4
4
  *
5
5
  * Dependencies:
@@ -207,8 +207,6 @@
207
207
  const fontWeight = (style.fontWeight === '700' || style.fontWeight === 'bold' || parseInt(style.fontWeight) >= 600);
208
208
 
209
209
  // Normalize Whitespace:
210
- // Collapse all whitespace (newlines, tabs, concurrent spaces) to single space
211
- // This matches browser rendering behavior for standard text.
212
210
  let runText = text.replace(/\s+/g, ' ');
213
211
 
214
212
  if (style.textTransform === 'uppercase') runText = runText.toUpperCase();
@@ -220,7 +218,6 @@
220
218
  fontSize: fontSize * 0.75, // px to pt
221
219
  bold: fontWeight,
222
220
  fontFace: getSafeFont(style.fontFamily),
223
- // charSpacing: Removed due to rendering issues (huge gaps) in PptxGenJS
224
221
  breakLine: false
225
222
  };
226
223
 
@@ -228,10 +225,32 @@
228
225
  runOpts.transparency = colorParsed.transparency;
229
226
  }
230
227
 
231
- // highlight (background color) support
232
- const bgParsed = parseColor(style.backgroundColor, parseFloat(style.opacity) || 1);
233
- if (bgParsed && bgParsed.transparency < 100) {
234
- runOpts.highlight = bgParsed.color;
228
+ // NOTE: We intentionally do NOT apply 'highlight' here because:
229
+ // 1. If this text is in a parent that has a background, that background
230
+ // is already drawn as a shape by processNode.
231
+ // 2. Adding highlight would create duplicate/overlapping backgrounds.
232
+ // 3. PPTX highlight is meant for inline text highlighting, not block backgrounds.
233
+
234
+ // Border-bottom as text underline support (only for inline/paragraph elements)
235
+ // Common pattern: <span class="border-b-2 border-b-indigo-500">text</span>
236
+ // Exclude headings (h1-h6) as they use border-b as section separators
237
+ const parentTag = node.tagName ? node.tagName.toUpperCase() : '';
238
+ const isInlineOrParagraph = ['SPAN', 'P', 'A', 'LABEL', 'STRONG', 'EM', 'B', 'I'].includes(parentTag);
239
+
240
+ const borderBottomWidth = parseFloat(style.borderBottomWidth) || 0;
241
+ const borderBottomStyle = style.borderBottomStyle;
242
+ if (isInlineOrParagraph && borderBottomWidth > 0 && borderBottomStyle !== 'none') {
243
+ runOpts.underline = { style: 'sng' }; // Single underline
244
+ // Try to get underline color from border color
245
+ const borderColorParsed = parseColor(style.borderBottomColor);
246
+ if (borderColorParsed) {
247
+ runOpts.underline.color = borderColorParsed.color;
248
+ }
249
+ }
250
+
251
+ // CSS text-decoration: underline support
252
+ if (style.textDecoration && style.textDecoration.includes('underline')) {
253
+ runOpts.underline = { style: 'sng' };
235
254
  }
236
255
 
237
256
  runs.push({
@@ -279,12 +298,17 @@
279
298
  // Relative Coordinates
280
299
  const x = pxToInch(rect.left - containerRect.left);
281
300
  const y = pxToInch(rect.top - containerRect.top) - currentSlideYOffset;
282
- const w = pxToInch(rect.width);
283
- const h = pxToInch(rect.height);
301
+ let w = pxToInch(rect.width);
302
+ let h = pxToInch(rect.height);
303
+
304
+ // Enforce minimum dimensions for very thin elements (like h-px lines)
305
+ const MIN_DIM = 0.02; // ~2px in PowerPoint
306
+ if (h < MIN_DIM && h > 0) h = MIN_DIM;
307
+ if (w < MIN_DIM && w > 0) w = MIN_DIM;
284
308
 
285
309
  // --- TABLE HANDLING ---
286
310
  if (node.tagName === 'TABLE') {
287
- // Shadow Handler (Updated)
311
+ // Shadow Handler
288
312
  if (style.boxShadow && style.boxShadow !== 'none') {
289
313
  let tableBg = style.backgroundColor;
290
314
  let tableOp = parseFloat(style.opacity) || 1;
@@ -294,7 +318,6 @@
294
318
  shadowFill = { color: 'FFFFFF', transparency: 99 };
295
319
  }
296
320
 
297
- // Now we trust 'h' matches sum(rowHeights) because we enforce it below.
298
321
  slide.addShape(pres.ShapeType.rect, {
299
322
  x: x, y: y, w: w, h: h,
300
323
  fill: { color: shadowFill.color, transparency: shadowFill.transparency },
@@ -303,15 +326,12 @@
303
326
  });
304
327
  }
305
328
 
306
-
307
329
  const tableRows = [];
308
330
  let colWidths = [];
309
- let rowHeights = []; // New strictly mapped row heights
331
+ let rowHeights = [];
310
332
 
311
333
  if (node.rows.length > 0) {
312
334
  colWidths = Array.from(node.rows[0].cells).map(c => pxToInch(c.getBoundingClientRect().width));
313
-
314
- // Capture exact row heights
315
335
  rowHeights = Array.from(node.rows).map(r => pxToInch(r.getBoundingClientRect().height));
316
336
  }
317
337
 
@@ -321,7 +341,6 @@
321
341
  const cStyle = window.getComputedStyle(cell);
322
342
  const cRuns = collectTextRuns(cell, cStyle);
323
343
 
324
- // backgroundColor fallback: Cell -> Row -> Row Parent (tbody/thead) -> Table
325
344
  let effectiveBg = cStyle.backgroundColor;
326
345
  let effectiveOpacity = parseFloat(cStyle.opacity) || 1;
327
346
 
@@ -338,20 +357,11 @@
338
357
  }
339
358
 
340
359
  const bgP = parseColor(effectiveBg, effectiveOpacity);
341
- // Borders logic handles separately or assumes solid for now
342
- const bCP = parseColor(cStyle.borderColor);
343
360
 
344
361
  let vAlign = 'top';
345
362
  if (cStyle.verticalAlign === 'middle') vAlign = 'middle';
346
363
  if (cStyle.verticalAlign === 'bottom') vAlign = 'bottom';
347
364
 
348
-
349
-
350
- // Padding / Margin Handling
351
- // PptxGenJS 'margin' is in Inches (like x,y,w,h), NOT Points.
352
- // Previous bug: * 0.75 converted px -> pt, but was interpreted as Inches (Massive margins).
353
- // Fix: Use pxToInch().
354
-
355
365
  const pt = pxToInch(parseFloat(cStyle.paddingTop) || 0);
356
366
  const pr = pxToInch(parseFloat(cStyle.paddingRight) || 0);
357
367
  const pb = pxToInch(parseFloat(cStyle.paddingBottom) || 0);
@@ -363,7 +373,6 @@
363
373
  const co = parseColor(c) || { color: '000000' };
364
374
  return { pt: parseFloat(w) * 0.75, color: co.color };
365
375
  }
366
- // Fallback to row border
367
376
  if (fallbackW && parseFloat(fallbackW) > 0 && fallbackS !== 'none') {
368
377
  const co = parseColor(fallbackC) || { color: '000000' };
369
378
  return { pt: parseFloat(fallbackW) * 0.75, color: co.color };
@@ -371,7 +380,6 @@
371
380
  return null;
372
381
  };
373
382
 
374
- // Fallback styles from Row
375
383
  const rStyle = row ? window.getComputedStyle(row) : null;
376
384
  let rbTopW = 0, rbTopC = null, rbTopS = 'none';
377
385
  let rbBotW = 0, rbBotC = null, rbBotS = 'none';
@@ -381,9 +389,6 @@
381
389
  rbBotW = rStyle.borderBottomWidth; rbBotC = rStyle.borderBottomColor; rbBotS = rStyle.borderBottomStyle;
382
390
  }
383
391
 
384
- // NOTE: Row side borders usually don't apply to every cell, but for simple 'border-b' on div/tr, we want it.
385
- // We will allow Top/Bottom fallback. Side borders on TR are rare/tricky (start/end).
386
-
387
392
  const bTop = getBdr(cStyle.borderTopWidth, cStyle.borderTopColor, cStyle.borderTopStyle, rbTopW, rbTopC, rbTopS);
388
393
  const bRight = getBdr(cStyle.borderRightWidth, cStyle.borderRightColor, cStyle.borderRightStyle);
389
394
  const bBot = getBdr(cStyle.borderBottomWidth, cStyle.borderBottomColor, cStyle.borderBottomStyle, rbBotW, rbBotC, rbBotS);
@@ -418,53 +423,16 @@
418
423
  });
419
424
 
420
425
  if (tableRows.length > 0) {
421
-
422
- // Manual Pagination Logic to fix 'Array expected' error in PptxGenJS when autoPage:true + rowH
423
- const availableH = PPT_HEIGHT_IN - y - 0.5; // 0.5 inch safety margin
424
-
426
+ const availableH = PPT_HEIGHT_IN - y - 0.5;
425
427
  if (h <= availableH) {
426
- // Case A: Fits on current slide
427
428
  slide.addTable(tableRows, {
428
429
  x: x, y: y, w: w, colW: colWidths, rowH: rowHeights, autoPage: false
429
430
  });
430
431
  } else {
431
- // Case B: Needs split
432
- console.warn('Table does not fit on current slide, splitting manually...');
433
-
434
- // NOTE: This is a simplified split logic.
435
- // Ideally we should calculate exactly how many rows fit.
436
- // Depending on user needs, we might implement complex splitting here.
437
- // For now, if it exceeds, we just push the whole table to the NEXT slide if it fits there.
438
- // If it's bigger than a whole slide, we rely on autoPage=false which might clip,
439
- // or we stick to the user's current request of fixing the CRASH.
440
-
441
- // Strategy:
442
- // 1. If y is significantly down (>1 inch), try creating a new slide and put table there.
443
- // 2. If it is already at top or still too big, we just add it and warn (handling >1 page tables requires loop).
444
-
432
+ console.warn('Table does not fit on current slide, splitting manually (disabled autoPage)...');
445
433
  if (y > 1.0) {
446
- // Move to next slide
447
434
  slide = pres.addSlide();
448
- currentSlideYOffset += PPT_HEIGHT_IN; // Estimate offset
449
-
450
- // Re-calculate y for new slide (should be roughly top margin)
451
- // But since we are inside a loop with absolute-ish coordinates...
452
- // If we move to a new slide, we reset y to top margin?
453
- // Let's place it at top margin (0.5 in)
454
- const newY = 0.5;
455
-
456
- // We need to adjust 'currentSlideYOffset' so that subsequent elements (also processed by absolute rect)
457
- // land correctly relative to this new slide.
458
- // old absolute Y of table was e.g. 5.0. New relative Y is 0.5.
459
- // So new offset = 5.0 - 0.5 = 4.5.
460
- // But this might break other elements aligned with the table.
461
- // For this patch, we mainly ensure it doesn't crash.
462
-
463
- // Let's try to just add it to current slide with autoPage:false first,
464
- // but PptxGenJS might clip it.
465
- // The user error was SPECIFICALLY about the crash.
466
-
467
- // FIX: Just disable autoPage.
435
+ currentSlideYOffset += PPT_HEIGHT_IN;
468
436
  slide.addTable(tableRows, {
469
437
  x: x, y: y, w: w, colW: colWidths, rowH: rowHeights, autoPage: false
470
438
  });
@@ -489,7 +457,14 @@
489
457
 
490
458
  let shapeOpts = { x, y, w, h };
491
459
 
460
+ // -- NEW: Precise Border Radius Logic --
461
+ const rtl = parseFloat(style.borderTopLeftRadius) || 0;
462
+ const rtr = parseFloat(style.borderTopRightRadius) || 0;
463
+ const rbr = parseFloat(style.borderBottomRightRadius) || 0;
464
+ const rbl = parseFloat(style.borderBottomLeftRadius) || 0;
465
+
492
466
  const borderRadius = parseFloat(style.borderRadius) || 0;
467
+
493
468
  // Strict circle check
494
469
  const isCircle = (Math.abs(rect.width - rect.height) < 2) && (borderRadius >= rect.width / 2 - 1);
495
470
 
@@ -498,20 +473,62 @@
498
473
  shapeOpts.shadow = { type: 'outer', angle: 45, blur: 6, offset: 2, opacity: 0.2 };
499
474
  }
500
475
 
501
- // --- Radius Logic ---
502
476
  let shapeType = pres.ShapeType.rect;
477
+ let rotation = 0;
478
+
503
479
  if (isCircle) {
504
480
  shapeType = pres.ShapeType.ellipse;
505
- } else if (borderRadius > 0) {
506
- const minDim = Math.min(rect.width, rect.height);
507
- let ratio = borderRadius / (minDim / 2);
508
- shapeOpts.rectRadius = Math.min(ratio, 1.0);
509
- shapeType = pres.ShapeType.roundRect || 'roundRect';
481
+ } else {
482
+ // Determine Shape based on corners
483
+ // Case 1: All Uniform
484
+ if (rtl === rtr && rtr === rbr && rbr === rbl && rtl > 0) {
485
+ const minDim = Math.min(rect.width, rect.height);
486
+ let ratio = rtl / (minDim / 2);
487
+ shapeOpts.rectRadius = Math.min(ratio, 1.0);
488
+ shapeType = pres.ShapeType.roundRect;
489
+ }
490
+ // Case 2: Vertical Rounding (Left or Right) -> PptxGenJS doesn't inherently support 'Right Rounded' well without rotation?
491
+ // Actually PptxGenJS has 'round2SameRect' which is Top Corners Rounded by default.
492
+
493
+ // Case 3: Top Rounded (Common in cards)
494
+ else if (rtl > 0 && rtr > 0 && rbr === 0 && rbl === 0) {
495
+ shapeType = pres.ShapeType.round2SameRect;
496
+ // Rotate? Default is Top.
497
+ rotation = 0;
498
+ }
499
+ // Case 4: Bottom Rounded
500
+ else if (rtl === 0 && rtr === 0 && rbr > 0 && rbl > 0) {
501
+ shapeType = pres.ShapeType.round2SameRect;
502
+ rotation = 180;
503
+ }
504
+ // Case 5: Single corners or diagonal? Fallback to rect (Square) to avoid "All Rounded" bug.
505
+ else if (borderRadius > 0) {
506
+ // It has some radius, but not uniform and not simple top/bottom pair.
507
+ // Fallback: If we use 'roundRect' it rounds all.
508
+ // Better to use 'rect' (Sharp) than incorrect 'roundRect' for things like "only top-left".
509
+ shapeType = pres.ShapeType.rect;
510
+ }
511
+ }
512
+
513
+ if (rotation !== 0) {
514
+ shapeOpts.rotate = rotation;
510
515
  }
511
516
 
512
517
  // --- Border Logic ---
513
- if (hasBorder && style.borderLeftWidth === style.borderRightWidth) {
514
- shapeOpts.line = { color: borderParsed.color, width: borderW * 0.75 };
518
+ // Check all 4 sides for true uniformity
519
+ const bTop = parseFloat(style.borderTopWidth) || 0;
520
+ const bRight = parseFloat(style.borderRightWidth) || 0;
521
+ const bBot = parseFloat(style.borderBottomWidth) || 0;
522
+ const bLeft = parseFloat(style.borderLeftWidth) || 0;
523
+
524
+ const isUniformBorder = (bTop === bRight && bRight === bBot && bBot === bLeft && bTop > 0);
525
+
526
+ if (isUniformBorder && borderParsed) {
527
+ const lineOpts = { color: borderParsed.color, width: bTop * 0.75 };
528
+ if (borderParsed.transparency > 0) {
529
+ lineOpts.transparency = borderParsed.transparency;
530
+ }
531
+ shapeOpts.line = lineOpts;
515
532
  } else {
516
533
  shapeOpts.line = null;
517
534
  }
@@ -519,24 +536,24 @@
519
536
  // --- B. LEFT ACCENT BORDER (Custom Strategy) ---
520
537
  const lW = parseFloat(style.borderLeftWidth) || 0;
521
538
  const leftBorderParsed = parseColor(style.borderLeftColor);
522
-
523
539
  const hasLeftBorder = lW > 0 && leftBorderParsed && style.borderStyle !== 'none';
524
540
 
525
- if (hasLeftBorder && !shapeOpts.line) {
541
+ if (hasLeftBorder && !isUniformBorder) {
526
542
  if (hasFill) {
527
- // Underlay Strategy
528
543
  const underlayOpts = { ...shapeOpts };
529
544
  underlayOpts.fill = { color: leftBorderParsed.color };
530
545
  underlayOpts.line = null;
546
+
547
+ // If rotated, the underlay needs careful handling.
548
+ // Simpler: Just draw a side strip if rotation is involved, or complex underlay.
549
+ // For now, keep original logic but verify rotation impact.
531
550
  slide.addShape(shapeType, underlayOpts);
532
551
 
533
- // Adjust Main Shape
534
552
  const borderInch = pxToInch(lW);
535
553
  shapeOpts.x += borderInch;
536
554
  shapeOpts.w -= borderInch;
537
- delete shapeOpts.shadow; // Remove duplicate shadow
555
+ delete shapeOpts.shadow;
538
556
  } else {
539
- // Side Strip Strategy
540
557
  slide.addShape(pres.ShapeType.rect, {
541
558
  x: x, y: y, w: pxToInch(lW), h: h,
542
559
  fill: { color: leftBorderParsed.color },
@@ -545,6 +562,110 @@
545
562
  }
546
563
  }
547
564
 
565
+ // --- B2. RIGHT ACCENT BORDER (Custom Strategy) ---
566
+ const rW = parseFloat(style.borderRightWidth) || 0;
567
+ const rightBorderParsed = parseColor(style.borderRightColor);
568
+ const hasRightBorder = rW > 0 && rightBorderParsed && style.borderRightStyle !== 'none';
569
+
570
+ if (hasRightBorder && !isUniformBorder) {
571
+ if (hasFill) {
572
+ const underlayOpts = { ...shapeOpts };
573
+ underlayOpts.fill = { color: rightBorderParsed.color };
574
+ if (rightBorderParsed.transparency > 0) {
575
+ underlayOpts.fill.transparency = rightBorderParsed.transparency;
576
+ }
577
+ underlayOpts.line = null;
578
+ slide.addShape(shapeType, underlayOpts);
579
+
580
+ // Shrink main shape from right to reveal right border
581
+ const borderInch = pxToInch(rW);
582
+ shapeOpts.w -= borderInch;
583
+ delete shapeOpts.shadow;
584
+ } else {
585
+ // No fill: Draw simple strip at right edge
586
+ const stripOpts = {
587
+ x: x + w - pxToInch(rW), y: y, w: pxToInch(rW), h: h,
588
+ fill: { color: rightBorderParsed.color }
589
+ };
590
+ if (rightBorderParsed.transparency > 0) {
591
+ stripOpts.fill.transparency = rightBorderParsed.transparency;
592
+ }
593
+ slide.addShape(pres.ShapeType.rect, stripOpts);
594
+ }
595
+ }
596
+
597
+ // --- C. TOP ACCENT BORDER (Underlay Strategy - BEFORE main shape) ---
598
+ const tW = parseFloat(style.borderTopWidth) || 0;
599
+ const topBorderParsed = parseColor(style.borderTopColor);
600
+ const hasTopBorder = tW > 0 && topBorderParsed && style.borderTopStyle !== 'none';
601
+
602
+ if (hasTopBorder && !isUniformBorder) {
603
+ if (hasFill) {
604
+ // Draw full shape in border color as underlay
605
+ const underlayOpts = { ...shapeOpts };
606
+ underlayOpts.fill = { color: topBorderParsed.color };
607
+ if (topBorderParsed.transparency > 0) {
608
+ underlayOpts.fill.transparency = topBorderParsed.transparency;
609
+ }
610
+ underlayOpts.line = null;
611
+ slide.addShape(shapeType, underlayOpts);
612
+
613
+ // Offset main shape to reveal top border
614
+ const borderInch = pxToInch(tW);
615
+ shapeOpts.y += borderInch;
616
+ shapeOpts.h -= borderInch;
617
+ delete shapeOpts.shadow;
618
+ } else {
619
+ // No fill: Draw simple strip
620
+ const stripOpts = {
621
+ x: x, y: y, w: w, h: pxToInch(tW),
622
+ fill: { color: topBorderParsed.color }
623
+ };
624
+ if (topBorderParsed.transparency > 0) {
625
+ stripOpts.fill.transparency = topBorderParsed.transparency;
626
+ }
627
+ slide.addShape(pres.ShapeType.rect, stripOpts);
628
+ }
629
+ }
630
+
631
+ // --- D. BOTTOM ACCENT BORDER (Underlay Strategy - BEFORE main shape) ---
632
+ const bW = parseFloat(style.borderBottomWidth) || 0;
633
+ const bottomBorderParsed = parseColor(style.borderBottomColor);
634
+ const hasBottomBorder = bW > 0 && bottomBorderParsed && style.borderBottomStyle !== 'none';
635
+
636
+ if (hasBottomBorder && !isUniformBorder) {
637
+ if (hasFill && !hasTopBorder) {
638
+ // Only do underlay if we didn't already do it for top border
639
+ const underlayOpts = { ...shapeOpts };
640
+ underlayOpts.fill = { color: bottomBorderParsed.color };
641
+ if (bottomBorderParsed.transparency > 0) {
642
+ underlayOpts.fill.transparency = bottomBorderParsed.transparency;
643
+ }
644
+ underlayOpts.line = null;
645
+ slide.addShape(shapeType, underlayOpts);
646
+
647
+ // Shrink main shape from bottom to reveal bottom border
648
+ const borderInch = pxToInch(bW);
649
+ shapeOpts.h -= borderInch;
650
+ delete shapeOpts.shadow;
651
+ } else if (hasFill && hasTopBorder) {
652
+ // Both top and bottom: already have underlay, just shrink from bottom too
653
+ const borderInch = pxToInch(bW);
654
+ shapeOpts.h -= borderInch;
655
+ } else {
656
+ // No fill: Draw simple strip at bottom
657
+ const bH = pxToInch(bW);
658
+ const stripOpts = {
659
+ x: x, y: y + h - bH, w: w, h: bH,
660
+ fill: { color: bottomBorderParsed.color }
661
+ };
662
+ if (bottomBorderParsed.transparency > 0) {
663
+ stripOpts.fill.transparency = bottomBorderParsed.transparency;
664
+ }
665
+ slide.addShape(pres.ShapeType.rect, stripOpts);
666
+ }
667
+ }
668
+
548
669
  // Draw Main Shape
549
670
  if (hasFill) {
550
671
  shapeOpts.fill = { color: bgParsed.color };
@@ -558,23 +679,17 @@
558
679
 
559
680
  // --- Gradient Fallback ---
560
681
  if (style.backgroundImage && style.backgroundImage.includes('gradient')) {
561
- // If it's a bar/strip
562
682
  if (rect.height < 15 && rect.width > 100) {
563
683
  slide.addShape(pres.ShapeType.rect, {
564
684
  x: x, y: y, w: w, h: h,
565
- fill: { color: '4F46E5' } // Fallback
685
+ fill: { color: '4F46E5' }
566
686
  });
567
687
  }
568
688
  }
569
689
 
570
690
  // --- C. TEXT CONTENT ---
571
691
  if (isTextBlock(node)) {
572
-
573
- // Extra check: If this is a very deep node, does it have children that are also text blocks?
574
- // Logic: isTextBlock returns true if it has text nodes.
575
-
576
692
  const runs = collectTextRuns(node, style);
577
-
578
693
  if (runs.length > 0) {
579
694
  if (runs.length > 0) {
580
695
  runs[0].text = runs[0].text.replace(/^\s+/, '');
@@ -589,33 +704,48 @@
589
704
  if (style.textAlign === 'justify') align = 'justify';
590
705
 
591
706
  let valign = 'top';
592
- const pt = parseFloat(style.paddingTop) || 0;
593
- const pb = parseFloat(style.paddingBottom) || 0;
707
+ const ptPx = parseFloat(style.paddingTop) || 0;
708
+ const pbPx = parseFloat(style.paddingBottom) || 0;
709
+ const plPx = parseFloat(style.paddingLeft) || 0;
710
+ const prPx = parseFloat(style.paddingRight) || 0;
711
+
594
712
  const boxH = rect.height;
595
713
  const textH = parseFloat(style.fontSize) * 1.2;
596
714
 
597
715
  if (style.display.includes('flex') && style.alignItems === 'center') valign = 'middle';
598
- else if (Math.abs(pt - pb) < 5 && pt > 5) valign = 'middle';
716
+ else if (Math.abs(ptPx - pbPx) < 5 && ptPx > 5) valign = 'middle';
599
717
  else if (boxH < 40 && boxH > textH) valign = 'middle';
600
718
 
601
719
  if (style.display.includes('flex')) {
602
720
  if (style.justifyContent === 'center') align = 'center';
603
721
  else if (style.justifyContent === 'flex-end' || style.justifyContent === 'right') align = 'right';
604
722
  }
605
- // Removed forced center for SPAN. It should respect parent/computed alignment.
606
- // if (node.tagName === 'SPAN') { align = 'center'; valign = 'middle'; }
607
723
 
608
- const widthBuffer = pxToInch(12); // Increased buffer (was 4) to prevent CJK wrapping issues
609
- const inset = Math.max(0, pxToInch(Math.min(pt, parseFloat(style.paddingLeft) || 0)));
724
+ // Convert to Inches for Geometry
725
+ const pt = pxToInch(ptPx);
726
+ const pr = pxToInch(prPx);
727
+ const pb = pxToInch(pbPx);
728
+ const pl = pxToInch(plPx);
729
+
730
+ // Geometry Shift Strategy for Padding
731
+ let tx = x + pl;
732
+ let ty = y + pt;
733
+ let tw = w - pl - pr;
734
+ let th = h - pt - pb;
735
+
736
+ if (tw < 0) tw = 0;
737
+ if (th < 0) th = 0;
610
738
 
739
+ const widthBuffer = pxToInch(12);
740
+
741
+ // We use inset:0 because we already applied padding via x/y/w/h
611
742
  slide.addText(runs, {
612
- x: x, y: y, w: w + widthBuffer, h: h,
613
- align: align, valign: valign, margin: 0, inset: inset,
743
+ x: tx, y: ty, w: tw + widthBuffer, h: th,
744
+ align: align, valign: valign, margin: 0, inset: 0,
614
745
  autoFit: false, wrap: true
615
746
  });
616
747
  }
617
748
 
618
- // Mark children as processed
619
749
  const markSeen = (n) => {
620
750
  n.childNodes.forEach(c => {
621
751
  if (c.nodeType === Node.ELEMENT_NODE) {
@@ -627,12 +757,39 @@
627
757
  markSeen(node);
628
758
  }
629
759
  } else {
630
- Array.from(node.children).forEach(processNode);
760
+ // -- NEW: Z-INDEX SORTING & RECURSION --
761
+
762
+ // Get all element children
763
+ const children = Array.from(node.children);
764
+
765
+ // Map to object with z-index
766
+ const sortedChildren = children.map(c => {
767
+ const zStr = window.getComputedStyle(c).zIndex;
768
+ return {
769
+ node: c,
770
+ zIndex: (zStr === 'auto') ? 0 : parseInt(zStr)
771
+ };
772
+ });
773
+
774
+ // Sort: ascending z-index
775
+ sortedChildren.sort((a, b) => a.zIndex - b.zIndex);
776
+
777
+ // Recurse
778
+ sortedChildren.forEach(item => processNode(item.node));
631
779
  }
632
780
  }
633
781
 
634
- // Start Processing
635
- Array.from(container.children).forEach(processNode);
782
+ // Start Processing (Sorted Top-Level Children)
783
+ const rootChildren = Array.from(container.children).map(c => {
784
+ const zStr = window.getComputedStyle(c).zIndex;
785
+ return {
786
+ node: c,
787
+ zIndex: (zStr === 'auto') ? 0 : parseInt(zStr)
788
+ };
789
+ });
790
+ rootChildren.sort((a, b) => a.zIndex - b.zIndex);
791
+
792
+ rootChildren.forEach(item => processNode(item.node));
636
793
 
637
794
  // Save
638
795
  pres.writeFile({ fileName: fileName });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "llm-dom-to-pptx",
3
- "version": "1.0.1",
3
+ "version": "1.0.2",
4
4
  "description": "Turn AI-generated HTML/CSS into native, editable PowerPoint slides.",
5
5
  "main": "dist/llm-dom-to-pptx.js",
6
6
  "scripts": {