llm-dom-to-pptx 1.0.1 → 1.0.3
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 +1 -1
- package/System_Prompt.md +168 -168
- package/dist/llm-dom-to-pptx.js +308 -107
- package/package.json +1 -1
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.
|
|
31
|
+
<script src="https://cdn.jsdelivr.net/npm/llm-dom-to-pptx@1.0.3/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
|
|
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.
|
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.3
|
|
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,18 +218,60 @@
|
|
|
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
|
|
|
224
|
+
// Letter Spacing (Tracking) Support
|
|
225
|
+
// Converts CSS letter-spacing (px/em) to PPTX charSpacing (points)
|
|
226
|
+
const letterSpacingStr = style.letterSpacing;
|
|
227
|
+
if (letterSpacingStr && letterSpacingStr !== 'normal') {
|
|
228
|
+
let spacingPx = 0;
|
|
229
|
+
if (letterSpacingStr.includes('em')) {
|
|
230
|
+
// e.g. "0.2em" -> 0.2 * fontSize
|
|
231
|
+
spacingPx = parseFloat(letterSpacingStr) * (parseFloat(style.fontSize) || 16);
|
|
232
|
+
} else if (letterSpacingStr.includes('px')) {
|
|
233
|
+
spacingPx = parseFloat(letterSpacingStr);
|
|
234
|
+
} else if (!isNaN(parseFloat(letterSpacingStr))) {
|
|
235
|
+
// Fallback if just number (unlikely in CSS computed style but safe)
|
|
236
|
+
spacingPx = parseFloat(letterSpacingStr);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Convert px to pt (1px = 0.75pt)
|
|
240
|
+
if (spacingPx !== 0) {
|
|
241
|
+
runOpts.charSpacing = spacingPx * 0.75;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
227
245
|
if (colorParsed && colorParsed.transparency > 0) {
|
|
228
246
|
runOpts.transparency = colorParsed.transparency;
|
|
229
247
|
}
|
|
230
248
|
|
|
231
|
-
// highlight
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
249
|
+
// NOTE: We intentionally do NOT apply 'highlight' here because:
|
|
250
|
+
// 1. If this text is in a parent that has a background, that background
|
|
251
|
+
// is already drawn as a shape by processNode.
|
|
252
|
+
// 2. Adding highlight would create duplicate/overlapping backgrounds.
|
|
253
|
+
// 3. PPTX highlight is meant for inline text highlighting, not block backgrounds.
|
|
254
|
+
|
|
255
|
+
// Border-bottom as text underline support (only for inline/paragraph elements)
|
|
256
|
+
// Common pattern: <span class="border-b-2 border-b-indigo-500">text</span>
|
|
257
|
+
// Exclude headings (h1-h6) as they use border-b as section separators
|
|
258
|
+
const parentTag = node.tagName ? node.tagName.toUpperCase() : '';
|
|
259
|
+
const isInlineOrParagraph = ['SPAN', 'P', 'A', 'LABEL', 'STRONG', 'EM', 'B', 'I'].includes(parentTag);
|
|
260
|
+
|
|
261
|
+
const borderBottomWidth = parseFloat(style.borderBottomWidth) || 0;
|
|
262
|
+
const borderBottomStyle = style.borderBottomStyle;
|
|
263
|
+
if (isInlineOrParagraph && borderBottomWidth > 0 && borderBottomStyle !== 'none') {
|
|
264
|
+
runOpts.underline = { style: 'sng' }; // Single underline
|
|
265
|
+
// Try to get underline color from border color
|
|
266
|
+
const borderColorParsed = parseColor(style.borderBottomColor);
|
|
267
|
+
if (borderColorParsed) {
|
|
268
|
+
runOpts.underline.color = borderColorParsed.color;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// CSS text-decoration: underline support
|
|
273
|
+
if (style.textDecoration && style.textDecoration.includes('underline')) {
|
|
274
|
+
runOpts.underline = { style: 'sng' };
|
|
235
275
|
}
|
|
236
276
|
|
|
237
277
|
runs.push({
|
|
@@ -279,12 +319,17 @@
|
|
|
279
319
|
// Relative Coordinates
|
|
280
320
|
const x = pxToInch(rect.left - containerRect.left);
|
|
281
321
|
const y = pxToInch(rect.top - containerRect.top) - currentSlideYOffset;
|
|
282
|
-
|
|
283
|
-
|
|
322
|
+
let w = pxToInch(rect.width);
|
|
323
|
+
let h = pxToInch(rect.height);
|
|
324
|
+
|
|
325
|
+
// Enforce minimum dimensions for very thin elements (like h-px lines)
|
|
326
|
+
const MIN_DIM = 0.02; // ~2px in PowerPoint
|
|
327
|
+
if (h < MIN_DIM && h > 0) h = MIN_DIM;
|
|
328
|
+
if (w < MIN_DIM && w > 0) w = MIN_DIM;
|
|
284
329
|
|
|
285
330
|
// --- TABLE HANDLING ---
|
|
286
331
|
if (node.tagName === 'TABLE') {
|
|
287
|
-
// Shadow Handler
|
|
332
|
+
// Shadow Handler
|
|
288
333
|
if (style.boxShadow && style.boxShadow !== 'none') {
|
|
289
334
|
let tableBg = style.backgroundColor;
|
|
290
335
|
let tableOp = parseFloat(style.opacity) || 1;
|
|
@@ -294,7 +339,6 @@
|
|
|
294
339
|
shadowFill = { color: 'FFFFFF', transparency: 99 };
|
|
295
340
|
}
|
|
296
341
|
|
|
297
|
-
// Now we trust 'h' matches sum(rowHeights) because we enforce it below.
|
|
298
342
|
slide.addShape(pres.ShapeType.rect, {
|
|
299
343
|
x: x, y: y, w: w, h: h,
|
|
300
344
|
fill: { color: shadowFill.color, transparency: shadowFill.transparency },
|
|
@@ -303,15 +347,12 @@
|
|
|
303
347
|
});
|
|
304
348
|
}
|
|
305
349
|
|
|
306
|
-
|
|
307
350
|
const tableRows = [];
|
|
308
351
|
let colWidths = [];
|
|
309
|
-
let rowHeights = [];
|
|
352
|
+
let rowHeights = [];
|
|
310
353
|
|
|
311
354
|
if (node.rows.length > 0) {
|
|
312
355
|
colWidths = Array.from(node.rows[0].cells).map(c => pxToInch(c.getBoundingClientRect().width));
|
|
313
|
-
|
|
314
|
-
// Capture exact row heights
|
|
315
356
|
rowHeights = Array.from(node.rows).map(r => pxToInch(r.getBoundingClientRect().height));
|
|
316
357
|
}
|
|
317
358
|
|
|
@@ -321,7 +362,6 @@
|
|
|
321
362
|
const cStyle = window.getComputedStyle(cell);
|
|
322
363
|
const cRuns = collectTextRuns(cell, cStyle);
|
|
323
364
|
|
|
324
|
-
// backgroundColor fallback: Cell -> Row -> Row Parent (tbody/thead) -> Table
|
|
325
365
|
let effectiveBg = cStyle.backgroundColor;
|
|
326
366
|
let effectiveOpacity = parseFloat(cStyle.opacity) || 1;
|
|
327
367
|
|
|
@@ -338,20 +378,11 @@
|
|
|
338
378
|
}
|
|
339
379
|
|
|
340
380
|
const bgP = parseColor(effectiveBg, effectiveOpacity);
|
|
341
|
-
// Borders logic handles separately or assumes solid for now
|
|
342
|
-
const bCP = parseColor(cStyle.borderColor);
|
|
343
381
|
|
|
344
382
|
let vAlign = 'top';
|
|
345
383
|
if (cStyle.verticalAlign === 'middle') vAlign = 'middle';
|
|
346
384
|
if (cStyle.verticalAlign === 'bottom') vAlign = 'bottom';
|
|
347
385
|
|
|
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
386
|
const pt = pxToInch(parseFloat(cStyle.paddingTop) || 0);
|
|
356
387
|
const pr = pxToInch(parseFloat(cStyle.paddingRight) || 0);
|
|
357
388
|
const pb = pxToInch(parseFloat(cStyle.paddingBottom) || 0);
|
|
@@ -363,7 +394,6 @@
|
|
|
363
394
|
const co = parseColor(c) || { color: '000000' };
|
|
364
395
|
return { pt: parseFloat(w) * 0.75, color: co.color };
|
|
365
396
|
}
|
|
366
|
-
// Fallback to row border
|
|
367
397
|
if (fallbackW && parseFloat(fallbackW) > 0 && fallbackS !== 'none') {
|
|
368
398
|
const co = parseColor(fallbackC) || { color: '000000' };
|
|
369
399
|
return { pt: parseFloat(fallbackW) * 0.75, color: co.color };
|
|
@@ -371,7 +401,6 @@
|
|
|
371
401
|
return null;
|
|
372
402
|
};
|
|
373
403
|
|
|
374
|
-
// Fallback styles from Row
|
|
375
404
|
const rStyle = row ? window.getComputedStyle(row) : null;
|
|
376
405
|
let rbTopW = 0, rbTopC = null, rbTopS = 'none';
|
|
377
406
|
let rbBotW = 0, rbBotC = null, rbBotS = 'none';
|
|
@@ -381,9 +410,6 @@
|
|
|
381
410
|
rbBotW = rStyle.borderBottomWidth; rbBotC = rStyle.borderBottomColor; rbBotS = rStyle.borderBottomStyle;
|
|
382
411
|
}
|
|
383
412
|
|
|
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
413
|
const bTop = getBdr(cStyle.borderTopWidth, cStyle.borderTopColor, cStyle.borderTopStyle, rbTopW, rbTopC, rbTopS);
|
|
388
414
|
const bRight = getBdr(cStyle.borderRightWidth, cStyle.borderRightColor, cStyle.borderRightStyle);
|
|
389
415
|
const bBot = getBdr(cStyle.borderBottomWidth, cStyle.borderBottomColor, cStyle.borderBottomStyle, rbBotW, rbBotC, rbBotS);
|
|
@@ -418,53 +444,16 @@
|
|
|
418
444
|
});
|
|
419
445
|
|
|
420
446
|
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
|
-
|
|
447
|
+
const availableH = PPT_HEIGHT_IN - y - 0.5;
|
|
425
448
|
if (h <= availableH) {
|
|
426
|
-
// Case A: Fits on current slide
|
|
427
449
|
slide.addTable(tableRows, {
|
|
428
450
|
x: x, y: y, w: w, colW: colWidths, rowH: rowHeights, autoPage: false
|
|
429
451
|
});
|
|
430
452
|
} else {
|
|
431
|
-
|
|
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
|
-
|
|
453
|
+
console.warn('Table does not fit on current slide, splitting manually (disabled autoPage)...');
|
|
445
454
|
if (y > 1.0) {
|
|
446
|
-
// Move to next slide
|
|
447
455
|
slide = pres.addSlide();
|
|
448
|
-
currentSlideYOffset += PPT_HEIGHT_IN;
|
|
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.
|
|
456
|
+
currentSlideYOffset += PPT_HEIGHT_IN;
|
|
468
457
|
slide.addTable(tableRows, {
|
|
469
458
|
x: x, y: y, w: w, colW: colWidths, rowH: rowHeights, autoPage: false
|
|
470
459
|
});
|
|
@@ -489,7 +478,14 @@
|
|
|
489
478
|
|
|
490
479
|
let shapeOpts = { x, y, w, h };
|
|
491
480
|
|
|
481
|
+
// -- NEW: Precise Border Radius Logic --
|
|
482
|
+
const rtl = parseFloat(style.borderTopLeftRadius) || 0;
|
|
483
|
+
const rtr = parseFloat(style.borderTopRightRadius) || 0;
|
|
484
|
+
const rbr = parseFloat(style.borderBottomRightRadius) || 0;
|
|
485
|
+
const rbl = parseFloat(style.borderBottomLeftRadius) || 0;
|
|
486
|
+
|
|
492
487
|
const borderRadius = parseFloat(style.borderRadius) || 0;
|
|
488
|
+
|
|
493
489
|
// Strict circle check
|
|
494
490
|
const isCircle = (Math.abs(rect.width - rect.height) < 2) && (borderRadius >= rect.width / 2 - 1);
|
|
495
491
|
|
|
@@ -498,20 +494,62 @@
|
|
|
498
494
|
shapeOpts.shadow = { type: 'outer', angle: 45, blur: 6, offset: 2, opacity: 0.2 };
|
|
499
495
|
}
|
|
500
496
|
|
|
501
|
-
// --- Radius Logic ---
|
|
502
497
|
let shapeType = pres.ShapeType.rect;
|
|
498
|
+
let rotation = 0;
|
|
499
|
+
|
|
503
500
|
if (isCircle) {
|
|
504
501
|
shapeType = pres.ShapeType.ellipse;
|
|
505
|
-
} else
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
502
|
+
} else {
|
|
503
|
+
// Determine Shape based on corners
|
|
504
|
+
// Case 1: All Uniform
|
|
505
|
+
if (rtl === rtr && rtr === rbr && rbr === rbl && rtl > 0) {
|
|
506
|
+
const minDim = Math.min(rect.width, rect.height);
|
|
507
|
+
let ratio = rtl / (minDim / 2);
|
|
508
|
+
shapeOpts.rectRadius = Math.min(ratio, 1.0);
|
|
509
|
+
shapeType = pres.ShapeType.roundRect;
|
|
510
|
+
}
|
|
511
|
+
// Case 2: Vertical Rounding (Left or Right) -> PptxGenJS doesn't inherently support 'Right Rounded' well without rotation?
|
|
512
|
+
// Actually PptxGenJS has 'round2SameRect' which is Top Corners Rounded by default.
|
|
513
|
+
|
|
514
|
+
// Case 3: Top Rounded (Common in cards)
|
|
515
|
+
else if (rtl > 0 && rtr > 0 && rbr === 0 && rbl === 0) {
|
|
516
|
+
shapeType = pres.ShapeType.round2SameRect;
|
|
517
|
+
// Rotate? Default is Top.
|
|
518
|
+
rotation = 0;
|
|
519
|
+
}
|
|
520
|
+
// Case 4: Bottom Rounded
|
|
521
|
+
else if (rtl === 0 && rtr === 0 && rbr > 0 && rbl > 0) {
|
|
522
|
+
shapeType = pres.ShapeType.round2SameRect;
|
|
523
|
+
rotation = 180;
|
|
524
|
+
}
|
|
525
|
+
// Case 5: Single corners or diagonal? Fallback to rect (Square) to avoid "All Rounded" bug.
|
|
526
|
+
else if (borderRadius > 0) {
|
|
527
|
+
// It has some radius, but not uniform and not simple top/bottom pair.
|
|
528
|
+
// Fallback: If we use 'roundRect' it rounds all.
|
|
529
|
+
// Better to use 'rect' (Sharp) than incorrect 'roundRect' for things like "only top-left".
|
|
530
|
+
shapeType = pres.ShapeType.rect;
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
if (rotation !== 0) {
|
|
535
|
+
shapeOpts.rotate = rotation;
|
|
510
536
|
}
|
|
511
537
|
|
|
512
538
|
// --- Border Logic ---
|
|
513
|
-
|
|
514
|
-
|
|
539
|
+
// Check all 4 sides for true uniformity
|
|
540
|
+
const bTop = parseFloat(style.borderTopWidth) || 0;
|
|
541
|
+
const bRight = parseFloat(style.borderRightWidth) || 0;
|
|
542
|
+
const bBot = parseFloat(style.borderBottomWidth) || 0;
|
|
543
|
+
const bLeft = parseFloat(style.borderLeftWidth) || 0;
|
|
544
|
+
|
|
545
|
+
const isUniformBorder = (bTop === bRight && bRight === bBot && bBot === bLeft && bTop > 0);
|
|
546
|
+
|
|
547
|
+
if (isUniformBorder && borderParsed) {
|
|
548
|
+
const lineOpts = { color: borderParsed.color, width: bTop * 0.75 };
|
|
549
|
+
if (borderParsed.transparency > 0) {
|
|
550
|
+
lineOpts.transparency = borderParsed.transparency;
|
|
551
|
+
}
|
|
552
|
+
shapeOpts.line = lineOpts;
|
|
515
553
|
} else {
|
|
516
554
|
shapeOpts.line = null;
|
|
517
555
|
}
|
|
@@ -519,24 +557,24 @@
|
|
|
519
557
|
// --- B. LEFT ACCENT BORDER (Custom Strategy) ---
|
|
520
558
|
const lW = parseFloat(style.borderLeftWidth) || 0;
|
|
521
559
|
const leftBorderParsed = parseColor(style.borderLeftColor);
|
|
522
|
-
|
|
523
560
|
const hasLeftBorder = lW > 0 && leftBorderParsed && style.borderStyle !== 'none';
|
|
524
561
|
|
|
525
|
-
if (hasLeftBorder && !
|
|
562
|
+
if (hasLeftBorder && !isUniformBorder) {
|
|
526
563
|
if (hasFill) {
|
|
527
|
-
// Underlay Strategy
|
|
528
564
|
const underlayOpts = { ...shapeOpts };
|
|
529
565
|
underlayOpts.fill = { color: leftBorderParsed.color };
|
|
530
566
|
underlayOpts.line = null;
|
|
567
|
+
|
|
568
|
+
// If rotated, the underlay needs careful handling.
|
|
569
|
+
// Simpler: Just draw a side strip if rotation is involved, or complex underlay.
|
|
570
|
+
// For now, keep original logic but verify rotation impact.
|
|
531
571
|
slide.addShape(shapeType, underlayOpts);
|
|
532
572
|
|
|
533
|
-
// Adjust Main Shape
|
|
534
573
|
const borderInch = pxToInch(lW);
|
|
535
574
|
shapeOpts.x += borderInch;
|
|
536
575
|
shapeOpts.w -= borderInch;
|
|
537
|
-
delete shapeOpts.shadow;
|
|
576
|
+
delete shapeOpts.shadow;
|
|
538
577
|
} else {
|
|
539
|
-
// Side Strip Strategy
|
|
540
578
|
slide.addShape(pres.ShapeType.rect, {
|
|
541
579
|
x: x, y: y, w: pxToInch(lW), h: h,
|
|
542
580
|
fill: { color: leftBorderParsed.color },
|
|
@@ -545,6 +583,110 @@
|
|
|
545
583
|
}
|
|
546
584
|
}
|
|
547
585
|
|
|
586
|
+
// --- B2. RIGHT ACCENT BORDER (Custom Strategy) ---
|
|
587
|
+
const rW = parseFloat(style.borderRightWidth) || 0;
|
|
588
|
+
const rightBorderParsed = parseColor(style.borderRightColor);
|
|
589
|
+
const hasRightBorder = rW > 0 && rightBorderParsed && style.borderRightStyle !== 'none';
|
|
590
|
+
|
|
591
|
+
if (hasRightBorder && !isUniformBorder) {
|
|
592
|
+
if (hasFill) {
|
|
593
|
+
const underlayOpts = { ...shapeOpts };
|
|
594
|
+
underlayOpts.fill = { color: rightBorderParsed.color };
|
|
595
|
+
if (rightBorderParsed.transparency > 0) {
|
|
596
|
+
underlayOpts.fill.transparency = rightBorderParsed.transparency;
|
|
597
|
+
}
|
|
598
|
+
underlayOpts.line = null;
|
|
599
|
+
slide.addShape(shapeType, underlayOpts);
|
|
600
|
+
|
|
601
|
+
// Shrink main shape from right to reveal right border
|
|
602
|
+
const borderInch = pxToInch(rW);
|
|
603
|
+
shapeOpts.w -= borderInch;
|
|
604
|
+
delete shapeOpts.shadow;
|
|
605
|
+
} else {
|
|
606
|
+
// No fill: Draw simple strip at right edge
|
|
607
|
+
const stripOpts = {
|
|
608
|
+
x: x + w - pxToInch(rW), y: y, w: pxToInch(rW), h: h,
|
|
609
|
+
fill: { color: rightBorderParsed.color }
|
|
610
|
+
};
|
|
611
|
+
if (rightBorderParsed.transparency > 0) {
|
|
612
|
+
stripOpts.fill.transparency = rightBorderParsed.transparency;
|
|
613
|
+
}
|
|
614
|
+
slide.addShape(pres.ShapeType.rect, stripOpts);
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
// --- C. TOP ACCENT BORDER (Underlay Strategy - BEFORE main shape) ---
|
|
619
|
+
const tW = parseFloat(style.borderTopWidth) || 0;
|
|
620
|
+
const topBorderParsed = parseColor(style.borderTopColor);
|
|
621
|
+
const hasTopBorder = tW > 0 && topBorderParsed && style.borderTopStyle !== 'none';
|
|
622
|
+
|
|
623
|
+
if (hasTopBorder && !isUniformBorder) {
|
|
624
|
+
if (hasFill) {
|
|
625
|
+
// Draw full shape in border color as underlay
|
|
626
|
+
const underlayOpts = { ...shapeOpts };
|
|
627
|
+
underlayOpts.fill = { color: topBorderParsed.color };
|
|
628
|
+
if (topBorderParsed.transparency > 0) {
|
|
629
|
+
underlayOpts.fill.transparency = topBorderParsed.transparency;
|
|
630
|
+
}
|
|
631
|
+
underlayOpts.line = null;
|
|
632
|
+
slide.addShape(shapeType, underlayOpts);
|
|
633
|
+
|
|
634
|
+
// Offset main shape to reveal top border
|
|
635
|
+
const borderInch = pxToInch(tW);
|
|
636
|
+
shapeOpts.y += borderInch;
|
|
637
|
+
shapeOpts.h -= borderInch;
|
|
638
|
+
delete shapeOpts.shadow;
|
|
639
|
+
} else {
|
|
640
|
+
// No fill: Draw simple strip
|
|
641
|
+
const stripOpts = {
|
|
642
|
+
x: x, y: y, w: w, h: pxToInch(tW),
|
|
643
|
+
fill: { color: topBorderParsed.color }
|
|
644
|
+
};
|
|
645
|
+
if (topBorderParsed.transparency > 0) {
|
|
646
|
+
stripOpts.fill.transparency = topBorderParsed.transparency;
|
|
647
|
+
}
|
|
648
|
+
slide.addShape(pres.ShapeType.rect, stripOpts);
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
// --- D. BOTTOM ACCENT BORDER (Underlay Strategy - BEFORE main shape) ---
|
|
653
|
+
const bW = parseFloat(style.borderBottomWidth) || 0;
|
|
654
|
+
const bottomBorderParsed = parseColor(style.borderBottomColor);
|
|
655
|
+
const hasBottomBorder = bW > 0 && bottomBorderParsed && style.borderBottomStyle !== 'none';
|
|
656
|
+
|
|
657
|
+
if (hasBottomBorder && !isUniformBorder) {
|
|
658
|
+
if (hasFill && !hasTopBorder) {
|
|
659
|
+
// Only do underlay if we didn't already do it for top border
|
|
660
|
+
const underlayOpts = { ...shapeOpts };
|
|
661
|
+
underlayOpts.fill = { color: bottomBorderParsed.color };
|
|
662
|
+
if (bottomBorderParsed.transparency > 0) {
|
|
663
|
+
underlayOpts.fill.transparency = bottomBorderParsed.transparency;
|
|
664
|
+
}
|
|
665
|
+
underlayOpts.line = null;
|
|
666
|
+
slide.addShape(shapeType, underlayOpts);
|
|
667
|
+
|
|
668
|
+
// Shrink main shape from bottom to reveal bottom border
|
|
669
|
+
const borderInch = pxToInch(bW);
|
|
670
|
+
shapeOpts.h -= borderInch;
|
|
671
|
+
delete shapeOpts.shadow;
|
|
672
|
+
} else if (hasFill && hasTopBorder) {
|
|
673
|
+
// Both top and bottom: already have underlay, just shrink from bottom too
|
|
674
|
+
const borderInch = pxToInch(bW);
|
|
675
|
+
shapeOpts.h -= borderInch;
|
|
676
|
+
} else {
|
|
677
|
+
// No fill: Draw simple strip at bottom
|
|
678
|
+
const bH = pxToInch(bW);
|
|
679
|
+
const stripOpts = {
|
|
680
|
+
x: x, y: y + h - bH, w: w, h: bH,
|
|
681
|
+
fill: { color: bottomBorderParsed.color }
|
|
682
|
+
};
|
|
683
|
+
if (bottomBorderParsed.transparency > 0) {
|
|
684
|
+
stripOpts.fill.transparency = bottomBorderParsed.transparency;
|
|
685
|
+
}
|
|
686
|
+
slide.addShape(pres.ShapeType.rect, stripOpts);
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
|
|
548
690
|
// Draw Main Shape
|
|
549
691
|
if (hasFill) {
|
|
550
692
|
shapeOpts.fill = { color: bgParsed.color };
|
|
@@ -558,23 +700,17 @@
|
|
|
558
700
|
|
|
559
701
|
// --- Gradient Fallback ---
|
|
560
702
|
if (style.backgroundImage && style.backgroundImage.includes('gradient')) {
|
|
561
|
-
// If it's a bar/strip
|
|
562
703
|
if (rect.height < 15 && rect.width > 100) {
|
|
563
704
|
slide.addShape(pres.ShapeType.rect, {
|
|
564
705
|
x: x, y: y, w: w, h: h,
|
|
565
|
-
fill: { color: '4F46E5' }
|
|
706
|
+
fill: { color: '4F46E5' }
|
|
566
707
|
});
|
|
567
708
|
}
|
|
568
709
|
}
|
|
569
710
|
|
|
570
711
|
// --- C. TEXT CONTENT ---
|
|
571
712
|
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
713
|
const runs = collectTextRuns(node, style);
|
|
577
|
-
|
|
578
714
|
if (runs.length > 0) {
|
|
579
715
|
if (runs.length > 0) {
|
|
580
716
|
runs[0].text = runs[0].text.replace(/^\s+/, '');
|
|
@@ -589,33 +725,71 @@
|
|
|
589
725
|
if (style.textAlign === 'justify') align = 'justify';
|
|
590
726
|
|
|
591
727
|
let valign = 'top';
|
|
592
|
-
const
|
|
593
|
-
const
|
|
728
|
+
const ptPx = parseFloat(style.paddingTop) || 0;
|
|
729
|
+
const pbPx = parseFloat(style.paddingBottom) || 0;
|
|
730
|
+
const plPx = parseFloat(style.paddingLeft) || 0;
|
|
731
|
+
const prPx = parseFloat(style.paddingRight) || 0;
|
|
732
|
+
|
|
594
733
|
const boxH = rect.height;
|
|
595
734
|
const textH = parseFloat(style.fontSize) * 1.2;
|
|
596
735
|
|
|
597
736
|
if (style.display.includes('flex') && style.alignItems === 'center') valign = 'middle';
|
|
598
|
-
else if (Math.abs(
|
|
737
|
+
else if (Math.abs(ptPx - pbPx) < 5 && ptPx > 5) valign = 'middle';
|
|
599
738
|
else if (boxH < 40 && boxH > textH) valign = 'middle';
|
|
600
739
|
|
|
601
740
|
if (style.display.includes('flex')) {
|
|
602
741
|
if (style.justifyContent === 'center') align = 'center';
|
|
603
742
|
else if (style.justifyContent === 'flex-end' || style.justifyContent === 'right') align = 'right';
|
|
604
743
|
}
|
|
605
|
-
// Removed forced center for SPAN. It should respect parent/computed alignment.
|
|
606
|
-
// if (node.tagName === 'SPAN') { align = 'center'; valign = 'middle'; }
|
|
607
744
|
|
|
608
|
-
|
|
609
|
-
const
|
|
745
|
+
// Convert to Inches for Geometry
|
|
746
|
+
const pt = pxToInch(ptPx);
|
|
747
|
+
const pr = pxToInch(prPx);
|
|
748
|
+
const pb = pxToInch(pbPx);
|
|
749
|
+
const pl = pxToInch(plPx);
|
|
750
|
+
|
|
751
|
+
// Geometry Shift Strategy for Padding
|
|
752
|
+
let tx = x + pl;
|
|
753
|
+
let ty = y + pt;
|
|
754
|
+
let tw = w - pl - pr;
|
|
755
|
+
let th = h - pt - pb;
|
|
756
|
+
|
|
757
|
+
if (tw < 0) tw = 0;
|
|
758
|
+
if (th < 0) th = 0;
|
|
759
|
+
|
|
760
|
+
// Line Height (Leading) Support
|
|
761
|
+
// Convert CSS line-height to PPTX lineSpacing (Points)
|
|
762
|
+
let lineSpacingPoints = null;
|
|
763
|
+
const lhStr = style.lineHeight;
|
|
764
|
+
if (lhStr && lhStr !== 'normal') {
|
|
765
|
+
const lhPx = parseFloat(lhStr);
|
|
766
|
+
if (!isNaN(lhPx)) {
|
|
767
|
+
lineSpacingPoints = lhPx * 0.75; // px to pt
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
// Add small buffer to text box width to prevent premature wrapping
|
|
772
|
+
// due to minor font rendering differences
|
|
773
|
+
const widthBuffer = pxToInch(2);
|
|
774
|
+
let finalTx = tx;
|
|
775
|
+
|
|
776
|
+
// Adjust x position for center alignment to keep it visually centered
|
|
777
|
+
if (align === 'center') finalTx -= widthBuffer / 2;
|
|
610
778
|
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
779
|
+
// We use inset:0 because we already applied padding via x/y/w/h
|
|
780
|
+
const textOpts = {
|
|
781
|
+
x: finalTx, y: ty, w: tw + widthBuffer, h: th,
|
|
782
|
+
align: align, valign: valign, margin: 0, inset: 0,
|
|
614
783
|
autoFit: false, wrap: true
|
|
615
|
-
}
|
|
784
|
+
};
|
|
785
|
+
|
|
786
|
+
if (lineSpacingPoints) {
|
|
787
|
+
textOpts.lineSpacing = lineSpacingPoints;
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
slide.addText(runs, textOpts);
|
|
616
791
|
}
|
|
617
792
|
|
|
618
|
-
// Mark children as processed
|
|
619
793
|
const markSeen = (n) => {
|
|
620
794
|
n.childNodes.forEach(c => {
|
|
621
795
|
if (c.nodeType === Node.ELEMENT_NODE) {
|
|
@@ -627,12 +801,39 @@
|
|
|
627
801
|
markSeen(node);
|
|
628
802
|
}
|
|
629
803
|
} else {
|
|
630
|
-
|
|
804
|
+
// -- NEW: Z-INDEX SORTING & RECURSION --
|
|
805
|
+
|
|
806
|
+
// Get all element children
|
|
807
|
+
const children = Array.from(node.children);
|
|
808
|
+
|
|
809
|
+
// Map to object with z-index
|
|
810
|
+
const sortedChildren = children.map(c => {
|
|
811
|
+
const zStr = window.getComputedStyle(c).zIndex;
|
|
812
|
+
return {
|
|
813
|
+
node: c,
|
|
814
|
+
zIndex: (zStr === 'auto') ? 0 : parseInt(zStr)
|
|
815
|
+
};
|
|
816
|
+
});
|
|
817
|
+
|
|
818
|
+
// Sort: ascending z-index
|
|
819
|
+
sortedChildren.sort((a, b) => a.zIndex - b.zIndex);
|
|
820
|
+
|
|
821
|
+
// Recurse
|
|
822
|
+
sortedChildren.forEach(item => processNode(item.node));
|
|
631
823
|
}
|
|
632
824
|
}
|
|
633
825
|
|
|
634
|
-
// Start Processing
|
|
635
|
-
Array.from(container.children).
|
|
826
|
+
// Start Processing (Sorted Top-Level Children)
|
|
827
|
+
const rootChildren = Array.from(container.children).map(c => {
|
|
828
|
+
const zStr = window.getComputedStyle(c).zIndex;
|
|
829
|
+
return {
|
|
830
|
+
node: c,
|
|
831
|
+
zIndex: (zStr === 'auto') ? 0 : parseInt(zStr)
|
|
832
|
+
};
|
|
833
|
+
});
|
|
834
|
+
rootChildren.sort((a, b) => a.zIndex - b.zIndex);
|
|
835
|
+
|
|
836
|
+
rootChildren.forEach(item => processNode(item.node));
|
|
636
837
|
|
|
637
838
|
// Save
|
|
638
839
|
pres.writeFile({ fileName: fileName });
|