llm-dom-to-pptx 1.0.0 → 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 +4 -3
- package/System_Prompt.md +168 -162
- package/dist/llm-dom-to-pptx.js +309 -68
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -4,9 +4,8 @@
|
|
|
4
4
|
|
|
5
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
6
|
|
|
7
|
-
|
|
7
|
+
https://github.com/user-attachments/assets/527efb28-b0ae-450a-9710-60cac2924acc
|
|
8
8
|
|
|
9
|
-
*Left: Downloaded PPTX after conversion | Right: HTML generated by Kimi-k1.5 based on System_Prompt.md*
|
|
10
9
|
|
|
11
10
|
## 🚀 Features
|
|
12
11
|
|
|
@@ -24,10 +23,12 @@ This library depends on `PptxGenJS`.
|
|
|
24
23
|
|
|
25
24
|
```html
|
|
26
25
|
<!-- PptxGenJS (Required) -->
|
|
26
|
+
<script src="https://cdn.tailwindcss.com"></script>
|
|
27
|
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js"></script>
|
|
27
28
|
<script src="https://cdn.jsdelivr.net/npm/pptxgenjs@3.12.0/dist/pptxgen.min.js"></script>
|
|
28
29
|
|
|
29
30
|
<!-- llm-dom-to-pptx -->
|
|
30
|
-
<script src="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>
|
|
31
32
|
```
|
|
32
33
|
|
|
33
34
|
### 2. Prepare Your HTML
|
package/System_Prompt.md
CHANGED
|
@@ -1,162 +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.
|
|
17
|
-
2.
|
|
18
|
-
3.
|
|
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
|
|
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
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
12
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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
|
+
```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.
|
package/dist/llm-dom-to-pptx.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* LLM DOM to PPTX - v1.0.
|
|
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:
|
|
@@ -153,7 +153,10 @@
|
|
|
153
153
|
const pres = new PptxGenJS();
|
|
154
154
|
pres.layout = 'LAYOUT_16x9';
|
|
155
155
|
|
|
156
|
-
const
|
|
156
|
+
const PPT_HEIGHT_IN = 5.625; // 16:9 aspect ratio of 10 inch width
|
|
157
|
+
|
|
158
|
+
let slide = pres.addSlide();
|
|
159
|
+
let currentSlideYOffset = 0; // Tracks Y offset for multi-page support
|
|
157
160
|
|
|
158
161
|
// Resolve Container
|
|
159
162
|
let container;
|
|
@@ -204,8 +207,7 @@
|
|
|
204
207
|
const fontWeight = (style.fontWeight === '700' || style.fontWeight === 'bold' || parseInt(style.fontWeight) >= 600);
|
|
205
208
|
|
|
206
209
|
// Normalize Whitespace:
|
|
207
|
-
|
|
208
|
-
let runText = text.replace(/[\r\n\t]+/g, ' ');
|
|
210
|
+
let runText = text.replace(/\s+/g, ' ');
|
|
209
211
|
|
|
210
212
|
if (style.textTransform === 'uppercase') runText = runText.toUpperCase();
|
|
211
213
|
|
|
@@ -216,7 +218,6 @@
|
|
|
216
218
|
fontSize: fontSize * 0.75, // px to pt
|
|
217
219
|
bold: fontWeight,
|
|
218
220
|
fontFace: getSafeFont(style.fontFamily),
|
|
219
|
-
charSpacing: (style.letterSpacing && style.letterSpacing !== 'normal') ? parseFloat(style.letterSpacing) : 0,
|
|
220
221
|
breakLine: false
|
|
221
222
|
};
|
|
222
223
|
|
|
@@ -224,10 +225,32 @@
|
|
|
224
225
|
runOpts.transparency = colorParsed.transparency;
|
|
225
226
|
}
|
|
226
227
|
|
|
227
|
-
// highlight
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
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' };
|
|
231
254
|
}
|
|
232
255
|
|
|
233
256
|
runs.push({
|
|
@@ -274,13 +297,18 @@
|
|
|
274
297
|
|
|
275
298
|
// Relative Coordinates
|
|
276
299
|
const x = pxToInch(rect.left - containerRect.left);
|
|
277
|
-
const y = pxToInch(rect.top - containerRect.top);
|
|
278
|
-
|
|
279
|
-
|
|
300
|
+
const y = pxToInch(rect.top - containerRect.top) - currentSlideYOffset;
|
|
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;
|
|
280
308
|
|
|
281
309
|
// --- TABLE HANDLING ---
|
|
282
310
|
if (node.tagName === 'TABLE') {
|
|
283
|
-
// Shadow Handler
|
|
311
|
+
// Shadow Handler
|
|
284
312
|
if (style.boxShadow && style.boxShadow !== 'none') {
|
|
285
313
|
let tableBg = style.backgroundColor;
|
|
286
314
|
let tableOp = parseFloat(style.opacity) || 1;
|
|
@@ -290,7 +318,6 @@
|
|
|
290
318
|
shadowFill = { color: 'FFFFFF', transparency: 99 };
|
|
291
319
|
}
|
|
292
320
|
|
|
293
|
-
// Now we trust 'h' matches sum(rowHeights) because we enforce it below.
|
|
294
321
|
slide.addShape(pres.ShapeType.rect, {
|
|
295
322
|
x: x, y: y, w: w, h: h,
|
|
296
323
|
fill: { color: shadowFill.color, transparency: shadowFill.transparency },
|
|
@@ -301,12 +328,10 @@
|
|
|
301
328
|
|
|
302
329
|
const tableRows = [];
|
|
303
330
|
let colWidths = [];
|
|
304
|
-
let rowHeights = [];
|
|
331
|
+
let rowHeights = [];
|
|
305
332
|
|
|
306
333
|
if (node.rows.length > 0) {
|
|
307
334
|
colWidths = Array.from(node.rows[0].cells).map(c => pxToInch(c.getBoundingClientRect().width));
|
|
308
|
-
|
|
309
|
-
// Capture exact row heights
|
|
310
335
|
rowHeights = Array.from(node.rows).map(r => pxToInch(r.getBoundingClientRect().height));
|
|
311
336
|
}
|
|
312
337
|
|
|
@@ -316,7 +341,6 @@
|
|
|
316
341
|
const cStyle = window.getComputedStyle(cell);
|
|
317
342
|
const cRuns = collectTextRuns(cell, cStyle);
|
|
318
343
|
|
|
319
|
-
// backgroundColor fallback: Cell -> Row -> Row Parent (tbody/thead) -> Table
|
|
320
344
|
let effectiveBg = cStyle.backgroundColor;
|
|
321
345
|
let effectiveOpacity = parseFloat(cStyle.opacity) || 1;
|
|
322
346
|
|
|
@@ -333,28 +357,41 @@
|
|
|
333
357
|
}
|
|
334
358
|
|
|
335
359
|
const bgP = parseColor(effectiveBg, effectiveOpacity);
|
|
336
|
-
// Borders logic handles separately or assumes solid for now
|
|
337
|
-
const bCP = parseColor(cStyle.borderColor);
|
|
338
360
|
|
|
339
361
|
let vAlign = 'top';
|
|
340
362
|
if (cStyle.verticalAlign === 'middle') vAlign = 'middle';
|
|
341
363
|
if (cStyle.verticalAlign === 'bottom') vAlign = 'bottom';
|
|
342
364
|
|
|
343
|
-
const pt = (parseFloat(cStyle.paddingTop) || 0)
|
|
344
|
-
const pr = (parseFloat(cStyle.paddingRight) || 0)
|
|
345
|
-
const pb = (parseFloat(cStyle.paddingBottom) || 0)
|
|
346
|
-
const pl = (parseFloat(cStyle.paddingLeft) || 0)
|
|
365
|
+
const pt = pxToInch(parseFloat(cStyle.paddingTop) || 0);
|
|
366
|
+
const pr = pxToInch(parseFloat(cStyle.paddingRight) || 0);
|
|
367
|
+
const pb = pxToInch(parseFloat(cStyle.paddingBottom) || 0);
|
|
368
|
+
const pl = pxToInch(parseFloat(cStyle.paddingLeft) || 0);
|
|
347
369
|
const margin = [pt, pr, pb, pl];
|
|
348
370
|
|
|
349
|
-
const getBdr = (w, c, s) => {
|
|
350
|
-
if (
|
|
351
|
-
|
|
352
|
-
|
|
371
|
+
const getBdr = (w, c, s, fallbackW, fallbackC, fallbackS) => {
|
|
372
|
+
if (w && parseFloat(w) > 0 && s !== 'none') {
|
|
373
|
+
const co = parseColor(c) || { color: '000000' };
|
|
374
|
+
return { pt: parseFloat(w) * 0.75, color: co.color };
|
|
375
|
+
}
|
|
376
|
+
if (fallbackW && parseFloat(fallbackW) > 0 && fallbackS !== 'none') {
|
|
377
|
+
const co = parseColor(fallbackC) || { color: '000000' };
|
|
378
|
+
return { pt: parseFloat(fallbackW) * 0.75, color: co.color };
|
|
379
|
+
}
|
|
380
|
+
return null;
|
|
353
381
|
};
|
|
354
382
|
|
|
355
|
-
const
|
|
383
|
+
const rStyle = row ? window.getComputedStyle(row) : null;
|
|
384
|
+
let rbTopW = 0, rbTopC = null, rbTopS = 'none';
|
|
385
|
+
let rbBotW = 0, rbBotC = null, rbBotS = 'none';
|
|
386
|
+
|
|
387
|
+
if (rStyle) {
|
|
388
|
+
rbTopW = rStyle.borderTopWidth; rbTopC = rStyle.borderTopColor; rbTopS = rStyle.borderTopStyle;
|
|
389
|
+
rbBotW = rStyle.borderBottomWidth; rbBotC = rStyle.borderBottomColor; rbBotS = rStyle.borderBottomStyle;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
const bTop = getBdr(cStyle.borderTopWidth, cStyle.borderTopColor, cStyle.borderTopStyle, rbTopW, rbTopC, rbTopS);
|
|
356
393
|
const bRight = getBdr(cStyle.borderRightWidth, cStyle.borderRightColor, cStyle.borderRightStyle);
|
|
357
|
-
const bBot = getBdr(cStyle.borderBottomWidth, cStyle.borderBottomColor, cStyle.borderBottomStyle);
|
|
394
|
+
const bBot = getBdr(cStyle.borderBottomWidth, cStyle.borderBottomColor, cStyle.borderBottomStyle, rbBotW, rbBotC, rbBotS);
|
|
358
395
|
const bLeft = getBdr(cStyle.borderLeftWidth, cStyle.borderLeftColor, cStyle.borderLeftStyle);
|
|
359
396
|
|
|
360
397
|
const cellOpts = {
|
|
@@ -386,9 +423,25 @@
|
|
|
386
423
|
});
|
|
387
424
|
|
|
388
425
|
if (tableRows.length > 0) {
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
426
|
+
const availableH = PPT_HEIGHT_IN - y - 0.5;
|
|
427
|
+
if (h <= availableH) {
|
|
428
|
+
slide.addTable(tableRows, {
|
|
429
|
+
x: x, y: y, w: w, colW: colWidths, rowH: rowHeights, autoPage: false
|
|
430
|
+
});
|
|
431
|
+
} else {
|
|
432
|
+
console.warn('Table does not fit on current slide, splitting manually (disabled autoPage)...');
|
|
433
|
+
if (y > 1.0) {
|
|
434
|
+
slide = pres.addSlide();
|
|
435
|
+
currentSlideYOffset += PPT_HEIGHT_IN;
|
|
436
|
+
slide.addTable(tableRows, {
|
|
437
|
+
x: x, y: y, w: w, colW: colWidths, rowH: rowHeights, autoPage: false
|
|
438
|
+
});
|
|
439
|
+
} else {
|
|
440
|
+
slide.addTable(tableRows, {
|
|
441
|
+
x: x, y: y, w: w, colW: colWidths, rowH: rowHeights, autoPage: false
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
}
|
|
392
445
|
}
|
|
393
446
|
processedNodes.add(node);
|
|
394
447
|
return;
|
|
@@ -404,7 +457,14 @@
|
|
|
404
457
|
|
|
405
458
|
let shapeOpts = { x, y, w, h };
|
|
406
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
|
+
|
|
407
466
|
const borderRadius = parseFloat(style.borderRadius) || 0;
|
|
467
|
+
|
|
408
468
|
// Strict circle check
|
|
409
469
|
const isCircle = (Math.abs(rect.width - rect.height) < 2) && (borderRadius >= rect.width / 2 - 1);
|
|
410
470
|
|
|
@@ -413,20 +473,62 @@
|
|
|
413
473
|
shapeOpts.shadow = { type: 'outer', angle: 45, blur: 6, offset: 2, opacity: 0.2 };
|
|
414
474
|
}
|
|
415
475
|
|
|
416
|
-
// --- Radius Logic ---
|
|
417
476
|
let shapeType = pres.ShapeType.rect;
|
|
477
|
+
let rotation = 0;
|
|
478
|
+
|
|
418
479
|
if (isCircle) {
|
|
419
480
|
shapeType = pres.ShapeType.ellipse;
|
|
420
|
-
} else
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
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;
|
|
425
515
|
}
|
|
426
516
|
|
|
427
517
|
// --- Border Logic ---
|
|
428
|
-
|
|
429
|
-
|
|
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;
|
|
430
532
|
} else {
|
|
431
533
|
shapeOpts.line = null;
|
|
432
534
|
}
|
|
@@ -434,24 +536,24 @@
|
|
|
434
536
|
// --- B. LEFT ACCENT BORDER (Custom Strategy) ---
|
|
435
537
|
const lW = parseFloat(style.borderLeftWidth) || 0;
|
|
436
538
|
const leftBorderParsed = parseColor(style.borderLeftColor);
|
|
437
|
-
|
|
438
539
|
const hasLeftBorder = lW > 0 && leftBorderParsed && style.borderStyle !== 'none';
|
|
439
540
|
|
|
440
|
-
if (hasLeftBorder && !
|
|
541
|
+
if (hasLeftBorder && !isUniformBorder) {
|
|
441
542
|
if (hasFill) {
|
|
442
|
-
// Underlay Strategy
|
|
443
543
|
const underlayOpts = { ...shapeOpts };
|
|
444
544
|
underlayOpts.fill = { color: leftBorderParsed.color };
|
|
445
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.
|
|
446
550
|
slide.addShape(shapeType, underlayOpts);
|
|
447
551
|
|
|
448
|
-
// Adjust Main Shape
|
|
449
552
|
const borderInch = pxToInch(lW);
|
|
450
553
|
shapeOpts.x += borderInch;
|
|
451
554
|
shapeOpts.w -= borderInch;
|
|
452
|
-
delete shapeOpts.shadow;
|
|
555
|
+
delete shapeOpts.shadow;
|
|
453
556
|
} else {
|
|
454
|
-
// Side Strip Strategy
|
|
455
557
|
slide.addShape(pres.ShapeType.rect, {
|
|
456
558
|
x: x, y: y, w: pxToInch(lW), h: h,
|
|
457
559
|
fill: { color: leftBorderParsed.color },
|
|
@@ -460,6 +562,110 @@
|
|
|
460
562
|
}
|
|
461
563
|
}
|
|
462
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
|
+
|
|
463
669
|
// Draw Main Shape
|
|
464
670
|
if (hasFill) {
|
|
465
671
|
shapeOpts.fill = { color: bgParsed.color };
|
|
@@ -473,23 +679,17 @@
|
|
|
473
679
|
|
|
474
680
|
// --- Gradient Fallback ---
|
|
475
681
|
if (style.backgroundImage && style.backgroundImage.includes('gradient')) {
|
|
476
|
-
// If it's a bar/strip
|
|
477
682
|
if (rect.height < 15 && rect.width > 100) {
|
|
478
683
|
slide.addShape(pres.ShapeType.rect, {
|
|
479
684
|
x: x, y: y, w: w, h: h,
|
|
480
|
-
fill: { color: '4F46E5' }
|
|
685
|
+
fill: { color: '4F46E5' }
|
|
481
686
|
});
|
|
482
687
|
}
|
|
483
688
|
}
|
|
484
689
|
|
|
485
690
|
// --- C. TEXT CONTENT ---
|
|
486
691
|
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
692
|
const runs = collectTextRuns(node, style);
|
|
492
|
-
|
|
493
693
|
if (runs.length > 0) {
|
|
494
694
|
if (runs.length > 0) {
|
|
495
695
|
runs[0].text = runs[0].text.replace(/^\s+/, '');
|
|
@@ -504,34 +704,48 @@
|
|
|
504
704
|
if (style.textAlign === 'justify') align = 'justify';
|
|
505
705
|
|
|
506
706
|
let valign = 'top';
|
|
507
|
-
const
|
|
508
|
-
const
|
|
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
|
+
|
|
509
712
|
const boxH = rect.height;
|
|
510
713
|
const textH = parseFloat(style.fontSize) * 1.2;
|
|
511
714
|
|
|
512
715
|
if (style.display.includes('flex') && style.alignItems === 'center') valign = 'middle';
|
|
513
|
-
else if (Math.abs(
|
|
716
|
+
else if (Math.abs(ptPx - pbPx) < 5 && ptPx > 5) valign = 'middle';
|
|
514
717
|
else if (boxH < 40 && boxH > textH) valign = 'middle';
|
|
515
718
|
|
|
516
719
|
if (style.display.includes('flex')) {
|
|
517
720
|
if (style.justifyContent === 'center') align = 'center';
|
|
518
721
|
else if (style.justifyContent === 'flex-end' || style.justifyContent === 'right') align = 'right';
|
|
519
722
|
}
|
|
520
|
-
if (node.tagName === 'SPAN') {
|
|
521
|
-
align = 'center'; valign = 'middle';
|
|
522
|
-
}
|
|
523
723
|
|
|
524
|
-
|
|
525
|
-
const
|
|
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;
|
|
526
738
|
|
|
739
|
+
const widthBuffer = pxToInch(12);
|
|
740
|
+
|
|
741
|
+
// We use inset:0 because we already applied padding via x/y/w/h
|
|
527
742
|
slide.addText(runs, {
|
|
528
|
-
x:
|
|
529
|
-
align: align, valign: valign, margin: 0, inset:
|
|
743
|
+
x: tx, y: ty, w: tw + widthBuffer, h: th,
|
|
744
|
+
align: align, valign: valign, margin: 0, inset: 0,
|
|
530
745
|
autoFit: false, wrap: true
|
|
531
746
|
});
|
|
532
747
|
}
|
|
533
748
|
|
|
534
|
-
// Mark children as processed
|
|
535
749
|
const markSeen = (n) => {
|
|
536
750
|
n.childNodes.forEach(c => {
|
|
537
751
|
if (c.nodeType === Node.ELEMENT_NODE) {
|
|
@@ -543,12 +757,39 @@
|
|
|
543
757
|
markSeen(node);
|
|
544
758
|
}
|
|
545
759
|
} else {
|
|
546
|
-
|
|
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));
|
|
547
779
|
}
|
|
548
780
|
}
|
|
549
781
|
|
|
550
|
-
// Start Processing
|
|
551
|
-
Array.from(container.children).
|
|
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));
|
|
552
793
|
|
|
553
794
|
// Save
|
|
554
795
|
pres.writeFile({ fileName: fileName });
|