docgen-utils 1.0.12 → 1.0.13
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/dist/bundle.js +3189 -1238
- package/dist/bundle.min.js +101 -99
- package/dist/cli.js +2653 -1117
- package/dist/packages/cli/commands/export-docs.d.ts.map +1 -1
- package/dist/packages/cli/commands/export-docs.js +131 -2
- package/dist/packages/cli/commands/export-docs.js.map +1 -1
- package/dist/packages/cli/commands/export-slides.d.ts.map +1 -1
- package/dist/packages/cli/commands/export-slides.js +25 -1
- package/dist/packages/cli/commands/export-slides.js.map +1 -1
- package/dist/packages/docs/common.d.ts +10 -0
- package/dist/packages/docs/common.d.ts.map +1 -1
- package/dist/packages/docs/common.js.map +1 -1
- package/dist/packages/docs/convert.d.ts.map +1 -1
- package/dist/packages/docs/convert.js +246 -218
- package/dist/packages/docs/convert.js.map +1 -1
- package/dist/packages/docs/create-document.d.ts.map +1 -1
- package/dist/packages/docs/create-document.js +43 -3
- package/dist/packages/docs/create-document.js.map +1 -1
- package/dist/packages/docs/export.d.ts +9 -8
- package/dist/packages/docs/export.d.ts.map +1 -1
- package/dist/packages/docs/export.js +23 -36
- package/dist/packages/docs/export.js.map +1 -1
- package/dist/packages/docs/parse-colors.d.ts +37 -0
- package/dist/packages/docs/parse-colors.d.ts.map +1 -0
- package/dist/packages/docs/parse-colors.js +507 -0
- package/dist/packages/docs/parse-colors.js.map +1 -0
- package/dist/packages/docs/parse-css.d.ts +98 -0
- package/dist/packages/docs/parse-css.d.ts.map +1 -0
- package/dist/packages/docs/parse-css.js +1592 -0
- package/dist/packages/docs/parse-css.js.map +1 -0
- package/dist/packages/docs/parse-helpers.d.ts +45 -0
- package/dist/packages/docs/parse-helpers.d.ts.map +1 -0
- package/dist/packages/docs/parse-helpers.js +214 -0
- package/dist/packages/docs/parse-helpers.js.map +1 -0
- package/dist/packages/docs/parse-inline.d.ts +41 -0
- package/dist/packages/docs/parse-inline.d.ts.map +1 -0
- package/dist/packages/docs/parse-inline.js +473 -0
- package/dist/packages/docs/parse-inline.js.map +1 -0
- package/dist/packages/docs/parse-layout.d.ts +57 -0
- package/dist/packages/docs/parse-layout.d.ts.map +1 -0
- package/dist/packages/docs/parse-layout.js +295 -0
- package/dist/packages/docs/parse-layout.js.map +1 -0
- package/dist/packages/docs/parse-special.d.ts +51 -0
- package/dist/packages/docs/parse-special.d.ts.map +1 -0
- package/dist/packages/docs/parse-special.js +251 -0
- package/dist/packages/docs/parse-special.js.map +1 -0
- package/dist/packages/docs/parse-units.d.ts +68 -0
- package/dist/packages/docs/parse-units.d.ts.map +1 -0
- package/dist/packages/docs/parse-units.js +275 -0
- package/dist/packages/docs/parse-units.js.map +1 -0
- package/dist/packages/docs/parse.d.ts.map +1 -1
- package/dist/packages/docs/parse.js +957 -2800
- package/dist/packages/docs/parse.js.map +1 -1
- package/dist/packages/slides/common.d.ts +7 -0
- package/dist/packages/slides/common.d.ts.map +1 -1
- package/dist/packages/slides/convert.d.ts.map +1 -1
- package/dist/packages/slides/convert.js +92 -7
- package/dist/packages/slides/convert.js.map +1 -1
- package/dist/packages/slides/parse.d.ts.map +1 -1
- package/dist/packages/slides/parse.js +723 -40
- package/dist/packages/slides/parse.js.map +1 -1
- package/dist/packages/slides/transform.d.ts.map +1 -1
- package/dist/packages/slides/transform.js +12 -7
- package/dist/packages/slides/transform.js.map +1 -1
- package/package.json +1 -1
|
@@ -87,6 +87,273 @@ function extractDashType(borderStyle) {
|
|
|
87
87
|
}
|
|
88
88
|
}
|
|
89
89
|
// ---------------------------------------------------------------------------
|
|
90
|
+
// CSS transform rotation extraction
|
|
91
|
+
// ---------------------------------------------------------------------------
|
|
92
|
+
/**
|
|
93
|
+
* Extract the rotation angle (in degrees) from a computed CSS `transform`.
|
|
94
|
+
*
|
|
95
|
+
* Browsers resolve any CSS transform to a `matrix(a, b, c, d, e, f)` value.
|
|
96
|
+
* The rotation component is `atan2(b, a)`. Returns `null` for no rotation
|
|
97
|
+
* or for rotations smaller than 0.5° (effectively none).
|
|
98
|
+
*/
|
|
99
|
+
function extractRotationAngle(computed) {
|
|
100
|
+
const transform = computed.transform;
|
|
101
|
+
if (!transform || transform === 'none')
|
|
102
|
+
return null;
|
|
103
|
+
const matrixMatch = transform.match(/matrix\(([-\d.e]+),\s*([-\d.e]+)/);
|
|
104
|
+
if (matrixMatch) {
|
|
105
|
+
const a = parseFloat(matrixMatch[1]);
|
|
106
|
+
const b = parseFloat(matrixMatch[2]);
|
|
107
|
+
const angle = Math.atan2(b, a) * (180 / Math.PI);
|
|
108
|
+
if (Math.abs(angle) > 0.5)
|
|
109
|
+
return Math.round(angle * 10) / 10;
|
|
110
|
+
}
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
// ---------------------------------------------------------------------------
|
|
114
|
+
// Effective opacity (accumulated from ancestor chain)
|
|
115
|
+
// ---------------------------------------------------------------------------
|
|
116
|
+
/**
|
|
117
|
+
* Compute the effective (accumulated) opacity of an element by walking up the
|
|
118
|
+
* ancestor chain. CSS `opacity` is NOT inherited — each element has its own
|
|
119
|
+
* computed value. However, visually the browser composites each stacking
|
|
120
|
+
* context, so the real visual opacity is the product of all ancestor opacities.
|
|
121
|
+
*
|
|
122
|
+
* Stops at `<body>` since that is the slide boundary.
|
|
123
|
+
*/
|
|
124
|
+
function getEffectiveOpacity(el, win) {
|
|
125
|
+
let opacity = 1;
|
|
126
|
+
let node = el;
|
|
127
|
+
while (node && node.tagName !== 'HTML') {
|
|
128
|
+
const style = win.getComputedStyle(node);
|
|
129
|
+
const nodeOpacity = parseFloat(style.opacity);
|
|
130
|
+
if (!isNaN(nodeOpacity))
|
|
131
|
+
opacity *= nodeOpacity;
|
|
132
|
+
node = node.parentElement;
|
|
133
|
+
}
|
|
134
|
+
return opacity;
|
|
135
|
+
}
|
|
136
|
+
// ---------------------------------------------------------------------------
|
|
137
|
+
// Split multiple CSS background gradients
|
|
138
|
+
// ---------------------------------------------------------------------------
|
|
139
|
+
/**
|
|
140
|
+
* Split a CSS `background-image` value that may contain multiple
|
|
141
|
+
* comma-separated gradients into individual gradient strings.
|
|
142
|
+
*
|
|
143
|
+
* Correctly handles nesting (e.g. `rgba()` inside `linear-gradient()`).
|
|
144
|
+
*
|
|
145
|
+
* Example:
|
|
146
|
+
* "radial-gradient(...), radial-gradient(...)" → ["radial-gradient(...)", "radial-gradient(...)"]
|
|
147
|
+
*/
|
|
148
|
+
function splitCssBackgroundGradients(bgImage) {
|
|
149
|
+
if (!bgImage || bgImage === 'none')
|
|
150
|
+
return [];
|
|
151
|
+
const parts = [];
|
|
152
|
+
let depth = 0;
|
|
153
|
+
let current = '';
|
|
154
|
+
for (const char of bgImage) {
|
|
155
|
+
if (char === '(')
|
|
156
|
+
depth++;
|
|
157
|
+
else if (char === ')')
|
|
158
|
+
depth--;
|
|
159
|
+
if (char === ',' && depth === 0) {
|
|
160
|
+
const trimmed = current.trim();
|
|
161
|
+
if (trimmed)
|
|
162
|
+
parts.push(trimmed);
|
|
163
|
+
current = '';
|
|
164
|
+
}
|
|
165
|
+
else {
|
|
166
|
+
current += char;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
const trimmed = current.trim();
|
|
170
|
+
if (trimmed)
|
|
171
|
+
parts.push(trimmed);
|
|
172
|
+
return parts.filter((p) => p.includes('linear-gradient') || p.includes('radial-gradient'));
|
|
173
|
+
}
|
|
174
|
+
// ---------------------------------------------------------------------------
|
|
175
|
+
// Rasterize complex repeating CSS gradient patterns to canvas image
|
|
176
|
+
// ---------------------------------------------------------------------------
|
|
177
|
+
/**
|
|
178
|
+
* Draw a single CSS linear-gradient on a canvas context.
|
|
179
|
+
*
|
|
180
|
+
* Parses the gradient string for direction (deg or `to` keyword) and color
|
|
181
|
+
* stops, then creates a native CanvasGradient and fills the context.
|
|
182
|
+
*
|
|
183
|
+
* The CSS color strings coming from `getComputedStyle` are already resolved
|
|
184
|
+
* (no CSS variables) and are valid canvas color values.
|
|
185
|
+
*/
|
|
186
|
+
function drawCssLinearGradientOnCanvas(ctx, gradStr, w, h) {
|
|
187
|
+
// Extract content inside linear-gradient(...)
|
|
188
|
+
const idx = gradStr.indexOf('linear-gradient(');
|
|
189
|
+
if (idx === -1)
|
|
190
|
+
return;
|
|
191
|
+
let depth = 0;
|
|
192
|
+
const start = idx + 'linear-gradient('.length;
|
|
193
|
+
let end = start;
|
|
194
|
+
for (let i = start; i < gradStr.length; i++) {
|
|
195
|
+
if (gradStr[i] === '(')
|
|
196
|
+
depth++;
|
|
197
|
+
else if (gradStr[i] === ')') {
|
|
198
|
+
if (depth === 0) {
|
|
199
|
+
end = i;
|
|
200
|
+
break;
|
|
201
|
+
}
|
|
202
|
+
depth--;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
const content = gradStr.substring(start, end);
|
|
206
|
+
// Split by commas respecting parentheses
|
|
207
|
+
const parts = [];
|
|
208
|
+
let current = '';
|
|
209
|
+
let d = 0;
|
|
210
|
+
for (const char of content) {
|
|
211
|
+
if (char === '(')
|
|
212
|
+
d++;
|
|
213
|
+
else if (char === ')')
|
|
214
|
+
d--;
|
|
215
|
+
if (char === ',' && d === 0) {
|
|
216
|
+
parts.push(current.trim());
|
|
217
|
+
current = '';
|
|
218
|
+
}
|
|
219
|
+
else {
|
|
220
|
+
current += char;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
if (current.trim())
|
|
224
|
+
parts.push(current.trim());
|
|
225
|
+
// Parse angle
|
|
226
|
+
let cssAngle = 180; // default: "to bottom"
|
|
227
|
+
let colorStops = parts;
|
|
228
|
+
const angleMatch = parts[0]?.match(/^(-?[\d.]+)deg$/);
|
|
229
|
+
if (angleMatch) {
|
|
230
|
+
cssAngle = parseFloat(angleMatch[1]);
|
|
231
|
+
colorStops = parts.slice(1);
|
|
232
|
+
}
|
|
233
|
+
else if (parts[0]?.startsWith('to ')) {
|
|
234
|
+
const dir = parts[0].replace('to ', '').trim();
|
|
235
|
+
const dirMap = {
|
|
236
|
+
'top': 0, 'right': 90, 'bottom': 180, 'left': 270,
|
|
237
|
+
'top right': 45, 'right top': 45, 'bottom right': 135,
|
|
238
|
+
'right bottom': 135, 'bottom left': 225, 'left bottom': 225,
|
|
239
|
+
'top left': 315, 'left top': 315,
|
|
240
|
+
};
|
|
241
|
+
cssAngle = dirMap[dir] ?? 180;
|
|
242
|
+
colorStops = parts.slice(1);
|
|
243
|
+
}
|
|
244
|
+
// Compute gradient line endpoints per CSS spec:
|
|
245
|
+
// direction = (sin(θ), -cos(θ)) in canvas coords (y-down)
|
|
246
|
+
// half-length = (|w·sin(θ)| + |h·cos(θ)|) / 2
|
|
247
|
+
const rad = cssAngle * Math.PI / 180;
|
|
248
|
+
const dx = Math.sin(rad);
|
|
249
|
+
const dy = -Math.cos(rad);
|
|
250
|
+
const halfLen = (Math.abs(w * Math.sin(rad)) + Math.abs(h * Math.cos(rad))) / 2;
|
|
251
|
+
const cx = w / 2;
|
|
252
|
+
const cy = h / 2;
|
|
253
|
+
const grad = ctx.createLinearGradient(cx - halfLen * dx, cy - halfLen * dy, cx + halfLen * dx, cy + halfLen * dy);
|
|
254
|
+
// Add color stops — CSS color strings from computedStyle work natively in canvas
|
|
255
|
+
for (let i = 0; i < colorStops.length; i++) {
|
|
256
|
+
const stop = colorStops[i].trim();
|
|
257
|
+
const posMatch = stop.match(/([\d.]+)%\s*$/);
|
|
258
|
+
const position = posMatch
|
|
259
|
+
? parseFloat(posMatch[1]) / 100
|
|
260
|
+
: i / Math.max(colorStops.length - 1, 1);
|
|
261
|
+
const colorPart = posMatch
|
|
262
|
+
? stop.replace(/([\d.]+)%\s*$/, '').trim()
|
|
263
|
+
: stop.trim();
|
|
264
|
+
try {
|
|
265
|
+
grad.addColorStop(Math.max(0, Math.min(1, position)), colorPart);
|
|
266
|
+
}
|
|
267
|
+
catch {
|
|
268
|
+
// Invalid color string — skip this stop
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
ctx.fillStyle = grad;
|
|
272
|
+
ctx.fillRect(0, 0, w, h);
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* Render a complex repeating CSS gradient pattern as a rasterized PNG image.
|
|
276
|
+
*
|
|
277
|
+
* Some CSS backgrounds use multiple linear-gradient layers combined with
|
|
278
|
+
* background-size / background-position to create tiled patterns (hex grids,
|
|
279
|
+
* noise textures, etc.) that PowerPoint gradients cannot represent. This
|
|
280
|
+
* function rasterises them faithfully using Canvas 2D:
|
|
281
|
+
*
|
|
282
|
+
* 1. Parse background-size to get the tile dimensions
|
|
283
|
+
* 2. Parse background-position offsets per gradient layer
|
|
284
|
+
* 3. For each gradient layer, draw it on a tile-sized canvas
|
|
285
|
+
* 4. Use createPattern() to tile each layer across the full element
|
|
286
|
+
* 5. Return the composited result as a PNG data URI
|
|
287
|
+
*
|
|
288
|
+
* @returns PNG data URI or null on failure
|
|
289
|
+
*/
|
|
290
|
+
function renderRepeatingGradientPatternAsImage(bgImage, bgSize, bgPosition, width, height, doc) {
|
|
291
|
+
try {
|
|
292
|
+
const gradParts = splitCssBackgroundGradients(bgImage);
|
|
293
|
+
if (gradParts.length === 0)
|
|
294
|
+
return null;
|
|
295
|
+
// Parse background-size → tile dimensions.
|
|
296
|
+
// `computed.backgroundSize` may be comma-separated (one per layer),
|
|
297
|
+
// e.g. "80px 140px, 80px 140px, ...". We use the first entry.
|
|
298
|
+
let tileW = width;
|
|
299
|
+
let tileH = height;
|
|
300
|
+
if (bgSize && bgSize !== 'auto') {
|
|
301
|
+
const firstLayerSize = bgSize.split(',')[0].trim();
|
|
302
|
+
const sizeParts = firstLayerSize.split(/\s+/);
|
|
303
|
+
const pw = parseFloat(sizeParts[0]);
|
|
304
|
+
if (!isNaN(pw) && pw > 0)
|
|
305
|
+
tileW = pw;
|
|
306
|
+
if (sizeParts.length >= 2) {
|
|
307
|
+
const ph = parseFloat(sizeParts[1]);
|
|
308
|
+
if (!isNaN(ph) && ph > 0)
|
|
309
|
+
tileH = ph;
|
|
310
|
+
}
|
|
311
|
+
else {
|
|
312
|
+
tileH = tileW; // single value → square tiles
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
// Parse background-position (comma-separated, one per layer)
|
|
316
|
+
const positionEntries = bgPosition.split(',').map((p) => p.trim());
|
|
317
|
+
const canvas = doc.createElement('canvas');
|
|
318
|
+
canvas.width = Math.round(width);
|
|
319
|
+
canvas.height = Math.round(height);
|
|
320
|
+
const ctx = canvas.getContext('2d');
|
|
321
|
+
if (!ctx)
|
|
322
|
+
return null;
|
|
323
|
+
for (let i = 0; i < gradParts.length; i++) {
|
|
324
|
+
const gradStr = gradParts[i].trim();
|
|
325
|
+
if (!gradStr.includes('linear-gradient'))
|
|
326
|
+
continue; // only handle linear for now
|
|
327
|
+
// Per-layer position offset (falls back to first entry, then 0 0)
|
|
328
|
+
const pos = positionEntries[i] || positionEntries[0] || '0px 0px';
|
|
329
|
+
const posParts = pos.trim().split(/\s+/);
|
|
330
|
+
const offX = parseFloat(posParts[0]) || 0;
|
|
331
|
+
const offY = parseFloat(posParts[1]) || 0;
|
|
332
|
+
// Draw gradient onto a tile-sized canvas
|
|
333
|
+
const tileCanvas = doc.createElement('canvas');
|
|
334
|
+
tileCanvas.width = Math.round(tileW);
|
|
335
|
+
tileCanvas.height = Math.round(tileH);
|
|
336
|
+
const tileCtx = tileCanvas.getContext('2d');
|
|
337
|
+
if (!tileCtx)
|
|
338
|
+
continue;
|
|
339
|
+
drawCssLinearGradientOnCanvas(tileCtx, gradStr, tileW, tileH);
|
|
340
|
+
// Tile the gradient across the full element with the offset
|
|
341
|
+
const pattern = ctx.createPattern(tileCanvas, 'repeat');
|
|
342
|
+
if (!pattern)
|
|
343
|
+
continue;
|
|
344
|
+
ctx.save();
|
|
345
|
+
ctx.translate(offX, offY);
|
|
346
|
+
ctx.fillStyle = pattern;
|
|
347
|
+
ctx.fillRect(-offX, -offY, canvas.width, canvas.height);
|
|
348
|
+
ctx.restore();
|
|
349
|
+
}
|
|
350
|
+
return canvas.toDataURL('image/png');
|
|
351
|
+
}
|
|
352
|
+
catch {
|
|
353
|
+
return null;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
// ---------------------------------------------------------------------------
|
|
90
357
|
// parseCssGradient (exported, used by other modules)
|
|
91
358
|
// ---------------------------------------------------------------------------
|
|
92
359
|
/**
|
|
@@ -1173,6 +1440,164 @@ function extractPseudoElements(el, win) {
|
|
|
1173
1440
|
pLeft = 0;
|
|
1174
1441
|
if (isNaN(pTop))
|
|
1175
1442
|
pTop = 0;
|
|
1443
|
+
// ---- Flex / Grid alignment correction for flow-positioned pseudos ----
|
|
1444
|
+
//
|
|
1445
|
+
// Problem: getComputedStyle on pseudo-elements returns 'auto' for left/top
|
|
1446
|
+
// when their position is static. The browser's flex/grid engine computes
|
|
1447
|
+
// offsets from alignment properties (align-items, justify-content, gap)
|
|
1448
|
+
// but does NOT expose them through the CSSOM for pseudo-elements.
|
|
1449
|
+
//
|
|
1450
|
+
// Strategy:
|
|
1451
|
+
// Cross-axis → derive from align-items (or align-self override)
|
|
1452
|
+
// Main-axis → anchor off the nearest real child's rendered
|
|
1453
|
+
// getBoundingClientRect; fall back to justify-content
|
|
1454
|
+
// when no real children exist.
|
|
1455
|
+
//
|
|
1456
|
+
const pseudoPos = pComputed.position;
|
|
1457
|
+
if (!pseudoPos || pseudoPos === 'static') {
|
|
1458
|
+
const parentComputed = win.getComputedStyle(el);
|
|
1459
|
+
const parentDisplay = parentComputed.display;
|
|
1460
|
+
const isFlex = parentDisplay === 'flex' || parentDisplay === 'inline-flex';
|
|
1461
|
+
const isGrid = parentDisplay === 'grid' || parentDisplay === 'inline-grid';
|
|
1462
|
+
if (isFlex) {
|
|
1463
|
+
const flexDir = parentComputed.flexDirection || 'row';
|
|
1464
|
+
const isRow = flexDir === 'row' || flexDir === 'row-reverse';
|
|
1465
|
+
const isReverse = flexDir.endsWith('-reverse');
|
|
1466
|
+
// -- Effective cross-axis alignment --
|
|
1467
|
+
// align-self on the pseudo overrides the container's align-items
|
|
1468
|
+
const alignSelf = pComputed.alignSelf;
|
|
1469
|
+
const effectiveAlign = (alignSelf && alignSelf !== 'auto')
|
|
1470
|
+
? alignSelf
|
|
1471
|
+
: (parentComputed.alignItems || 'stretch');
|
|
1472
|
+
// -- Pseudo-element margins --
|
|
1473
|
+
const mTop = parseFloat(pComputed.marginTop) || 0;
|
|
1474
|
+
const mRight = parseFloat(pComputed.marginRight) || 0;
|
|
1475
|
+
const mBottom = parseFloat(pComputed.marginBottom) || 0;
|
|
1476
|
+
const mLeft = parseFloat(pComputed.marginLeft) || 0;
|
|
1477
|
+
// -- Cross-axis offset --
|
|
1478
|
+
// The cross-axis is vertical for row, horizontal for column.
|
|
1479
|
+
const crossSize = isRow ? parentRect.height : parentRect.width;
|
|
1480
|
+
const pseudoCrossOuter = isRow
|
|
1481
|
+
? (pHeight + mTop + mBottom)
|
|
1482
|
+
: (pWidth + mLeft + mRight);
|
|
1483
|
+
const crossMarginBefore = isRow ? mTop : mLeft;
|
|
1484
|
+
let crossOffset;
|
|
1485
|
+
switch (effectiveAlign) {
|
|
1486
|
+
case 'center':
|
|
1487
|
+
crossOffset = (crossSize - pseudoCrossOuter) / 2 + crossMarginBefore;
|
|
1488
|
+
break;
|
|
1489
|
+
case 'flex-end':
|
|
1490
|
+
case 'end':
|
|
1491
|
+
crossOffset = crossSize - pseudoCrossOuter + crossMarginBefore;
|
|
1492
|
+
break;
|
|
1493
|
+
case 'stretch':
|
|
1494
|
+
case 'baseline':
|
|
1495
|
+
default: // flex-start, start, normal
|
|
1496
|
+
crossOffset = crossMarginBefore;
|
|
1497
|
+
break;
|
|
1498
|
+
}
|
|
1499
|
+
// -- Main-axis offset --
|
|
1500
|
+
// Parse the flex gap along the main axis
|
|
1501
|
+
const gapProp = isRow ? parentComputed.columnGap : parentComputed.rowGap;
|
|
1502
|
+
const mainGap = parseFloat(gapProp) || parseFloat(parentComputed.gap) || 0;
|
|
1503
|
+
// Collect visible, in-flow child elements to use as position anchors.
|
|
1504
|
+
// Pseudo-elements are first (::before) or last (::after) in flex order,
|
|
1505
|
+
// so we anchor off the adjacent real child to compute the main-axis offset.
|
|
1506
|
+
const realChildren = Array.from(el.children).filter(c => {
|
|
1507
|
+
const s = win.getComputedStyle(c);
|
|
1508
|
+
return s.display !== 'none' && s.position !== 'absolute' && s.position !== 'fixed';
|
|
1509
|
+
});
|
|
1510
|
+
let mainOffset;
|
|
1511
|
+
if (realChildren.length > 0) {
|
|
1512
|
+
// Determine which child to anchor against and in which direction.
|
|
1513
|
+
// ::before + normal → sits before first child (screen-left / screen-top)
|
|
1514
|
+
// ::before + reverse → sits after first child (screen-right / screen-bottom)
|
|
1515
|
+
// ::after + normal → sits after last child
|
|
1516
|
+
// ::after + reverse → sits before last child
|
|
1517
|
+
const anchor = pseudo === '::before'
|
|
1518
|
+
? realChildren[0]
|
|
1519
|
+
: realChildren[realChildren.length - 1];
|
|
1520
|
+
const anchorRect = anchor.getBoundingClientRect();
|
|
1521
|
+
const pseudoSitsAfterAnchor = (pseudo === '::before' && isReverse) ||
|
|
1522
|
+
(pseudo === '::after' && !isReverse);
|
|
1523
|
+
if (pseudoSitsAfterAnchor) {
|
|
1524
|
+
// Pseudo is toward main-end (screen-right in row, screen-bottom in column)
|
|
1525
|
+
const anchorEnd = isRow
|
|
1526
|
+
? (anchorRect.right - parentRect.left)
|
|
1527
|
+
: (anchorRect.bottom - parentRect.top);
|
|
1528
|
+
const marginBefore = isRow ? mLeft : mTop;
|
|
1529
|
+
mainOffset = anchorEnd + mainGap + marginBefore;
|
|
1530
|
+
}
|
|
1531
|
+
else {
|
|
1532
|
+
// Pseudo is toward main-start (screen-left in row, screen-top in column)
|
|
1533
|
+
const anchorStart = isRow
|
|
1534
|
+
? (anchorRect.left - parentRect.left)
|
|
1535
|
+
: (anchorRect.top - parentRect.top);
|
|
1536
|
+
const pseudoMainSize = isRow ? pWidth : pHeight;
|
|
1537
|
+
const marginAfter = isRow ? mRight : mBottom;
|
|
1538
|
+
const marginBefore = isRow ? mLeft : mTop;
|
|
1539
|
+
mainOffset = anchorStart - mainGap - marginAfter - pseudoMainSize;
|
|
1540
|
+
// Clamp: don't go before the start of the parent content area
|
|
1541
|
+
if (mainOffset < marginBefore)
|
|
1542
|
+
mainOffset = marginBefore;
|
|
1543
|
+
}
|
|
1544
|
+
}
|
|
1545
|
+
else {
|
|
1546
|
+
// No visible real children — position based on justify-content.
|
|
1547
|
+
// This covers text-only flex containers where the pseudo is the
|
|
1548
|
+
// only true flex box item (text nodes form anonymous items but
|
|
1549
|
+
// have no getBoundingClientRect we can query).
|
|
1550
|
+
const justify = parentComputed.justifyContent || 'normal';
|
|
1551
|
+
const mainSize = isRow ? parentRect.width : parentRect.height;
|
|
1552
|
+
const pseudoMainOuter = isRow
|
|
1553
|
+
? (pWidth + mLeft + mRight)
|
|
1554
|
+
: (pHeight + mTop + mBottom);
|
|
1555
|
+
const marginBefore = isRow ? mLeft : mTop;
|
|
1556
|
+
if (justify === 'center') {
|
|
1557
|
+
mainOffset = (mainSize - pseudoMainOuter) / 2 + marginBefore;
|
|
1558
|
+
}
|
|
1559
|
+
else if (justify === 'flex-end' || justify === 'end') {
|
|
1560
|
+
mainOffset = mainSize - pseudoMainOuter + marginBefore;
|
|
1561
|
+
}
|
|
1562
|
+
else {
|
|
1563
|
+
// flex-start, start, normal, space-between/around/evenly
|
|
1564
|
+
// (with a single item these behave like flex-start)
|
|
1565
|
+
mainOffset = marginBefore;
|
|
1566
|
+
}
|
|
1567
|
+
}
|
|
1568
|
+
// Assign to the correct axes
|
|
1569
|
+
pLeft = isRow ? mainOffset : crossOffset;
|
|
1570
|
+
pTop = isRow ? crossOffset : mainOffset;
|
|
1571
|
+
}
|
|
1572
|
+
else if (isGrid) {
|
|
1573
|
+
// Basic grid cross-axis alignment.
|
|
1574
|
+
// Grid pseudo-elements participate as grid items; their alignment
|
|
1575
|
+
// is governed by align-items / justify-items (or their -self variants).
|
|
1576
|
+
const alignSelf = pComputed.alignSelf;
|
|
1577
|
+
const alignItems = (alignSelf && alignSelf !== 'auto')
|
|
1578
|
+
? alignSelf
|
|
1579
|
+
: (parentComputed.alignItems || 'stretch');
|
|
1580
|
+
const justifySelf = pComputed.justifySelf;
|
|
1581
|
+
const justifyItems = (justifySelf && justifySelf !== 'auto')
|
|
1582
|
+
? justifySelf
|
|
1583
|
+
: (parentComputed.justifyItems || 'stretch');
|
|
1584
|
+
if (alignItems === 'center') {
|
|
1585
|
+
pTop = (parentRect.height - pHeight) / 2;
|
|
1586
|
+
}
|
|
1587
|
+
else if (alignItems === 'end') {
|
|
1588
|
+
pTop = parentRect.height - pHeight;
|
|
1589
|
+
}
|
|
1590
|
+
if (justifyItems === 'center') {
|
|
1591
|
+
pLeft = (parentRect.width - pWidth) / 2;
|
|
1592
|
+
}
|
|
1593
|
+
else if (justifyItems === 'end') {
|
|
1594
|
+
pLeft = parentRect.width - pWidth;
|
|
1595
|
+
}
|
|
1596
|
+
}
|
|
1597
|
+
// For non-flex/grid containers (block/inline), pLeft=0, pTop=0 is a
|
|
1598
|
+
// reasonable approximation: ::before generates content at the start of
|
|
1599
|
+
// the element's content area.
|
|
1600
|
+
}
|
|
1176
1601
|
// Convert to absolute page coordinates
|
|
1177
1602
|
const absLeft = parentRect.left + pLeft;
|
|
1178
1603
|
const absTop = parentRect.top + pTop;
|
|
@@ -1184,14 +1609,24 @@ function extractPseudoElements(el, win) {
|
|
|
1184
1609
|
const hasBgImage = bgImage && bgImage !== 'none' && bgImage.includes('url(');
|
|
1185
1610
|
if (!hasBg && !hasGradient && !hasBgImage)
|
|
1186
1611
|
continue;
|
|
1187
|
-
// Parse gradient
|
|
1612
|
+
// Parse gradient(s) — a single background-image property may contain multiple gradients
|
|
1188
1613
|
let gradient = null;
|
|
1614
|
+
let extraPseudoGradients = [];
|
|
1189
1615
|
if (hasGradient) {
|
|
1190
|
-
|
|
1616
|
+
const gradParts = splitCssBackgroundGradients(bgImage);
|
|
1617
|
+
if (gradParts.length > 0)
|
|
1618
|
+
gradient = parseCssGradient(gradParts[0]);
|
|
1619
|
+
for (let gi = 1; gi < Math.min(gradParts.length, 4); gi++) {
|
|
1620
|
+
const g = parseCssGradient(gradParts[gi]);
|
|
1621
|
+
if (g)
|
|
1622
|
+
extraPseudoGradients.push(g);
|
|
1623
|
+
}
|
|
1191
1624
|
}
|
|
1192
|
-
// Parse opacity
|
|
1193
|
-
const
|
|
1194
|
-
const
|
|
1625
|
+
// Parse effective opacity (including ancestor chain)
|
|
1626
|
+
const parentEffectiveOpacity = getEffectiveOpacity(el, win);
|
|
1627
|
+
const pseudoOwnOpacity = parseFloat(pComputed.opacity);
|
|
1628
|
+
const elementOpacity = parentEffectiveOpacity * (isNaN(pseudoOwnOpacity) ? 1 : pseudoOwnOpacity);
|
|
1629
|
+
const hasOpacity = elementOpacity < 1;
|
|
1195
1630
|
// Parse border-radius and detect ellipse shape
|
|
1196
1631
|
let rectRadius = 0;
|
|
1197
1632
|
const borderRadius = pComputed.borderRadius;
|
|
@@ -1213,6 +1648,42 @@ function extractPseudoElements(el, win) {
|
|
|
1213
1648
|
}
|
|
1214
1649
|
// Parse box-shadow
|
|
1215
1650
|
const shadow = parseBoxShadow(pComputed.boxShadow);
|
|
1651
|
+
// Inherit parent border-radius when pseudo-element is flush at the edge of a
|
|
1652
|
+
// parent with overflow:hidden. In CSS, the parent clips the pseudo-element;
|
|
1653
|
+
// in PPTX there's no clipping group, so we must round the pseudo-element's
|
|
1654
|
+
// corners to match.
|
|
1655
|
+
let effectiveRectRadius = isEllipse ? 0 : rectRadius;
|
|
1656
|
+
if (rectRadius === 0 && !isEllipse) {
|
|
1657
|
+
const parentComputed = win.getComputedStyle(el);
|
|
1658
|
+
const parentOverflow = parentComputed.overflow;
|
|
1659
|
+
if (parentOverflow === 'hidden' || parentOverflow === 'clip') {
|
|
1660
|
+
const parentRadius = parseFloat(parentComputed.borderRadius);
|
|
1661
|
+
if (parentRadius > 0) {
|
|
1662
|
+
// Account for parent border widths — absolutely positioned pseudo-elements
|
|
1663
|
+
// with left:0/right:0 resolve inside the padding box, not the border box.
|
|
1664
|
+
const parentBorderLeft = parseFloat(parentComputed.borderLeftWidth) || 0;
|
|
1665
|
+
const parentBorderRight = parseFloat(parentComputed.borderRightWidth) || 0;
|
|
1666
|
+
const parentBorderTop = parseFloat(parentComputed.borderTopWidth) || 0;
|
|
1667
|
+
const parentBorderBottom = parseFloat(parentComputed.borderBottomWidth) || 0;
|
|
1668
|
+
const parentInnerWidth = parentRect.width - parentBorderLeft - parentBorderRight;
|
|
1669
|
+
const parentInnerHeight = parentRect.height - parentBorderTop - parentBorderBottom;
|
|
1670
|
+
// Check which edges are flush (within 2px tolerance to handle subpixel rounding)
|
|
1671
|
+
const flushTop = Math.abs(pTop) < 2;
|
|
1672
|
+
const flushLeft = Math.abs(pLeft) < 2;
|
|
1673
|
+
const flushRight = Math.abs(pLeft + pWidth - parentRect.width) < 2 ||
|
|
1674
|
+
Math.abs(pLeft + pWidth - parentInnerWidth) < 2;
|
|
1675
|
+
const flushBottom = Math.abs(pTop + pHeight - parentRect.height) < 2 ||
|
|
1676
|
+
Math.abs(pTop + pHeight - parentInnerHeight) < 2;
|
|
1677
|
+
// Only apply if flush at a corner (top+spanning width, or side+spanning height)
|
|
1678
|
+
const spansFullWidth = flushLeft && flushRight;
|
|
1679
|
+
const spansFullHeight = flushTop && flushBottom;
|
|
1680
|
+
if ((flushTop && spansFullWidth) || (flushBottom && spansFullWidth) ||
|
|
1681
|
+
(flushLeft && spansFullHeight) || (flushRight && spansFullHeight)) {
|
|
1682
|
+
effectiveRectRadius = pxToInch(parentRadius);
|
|
1683
|
+
}
|
|
1684
|
+
}
|
|
1685
|
+
}
|
|
1686
|
+
}
|
|
1216
1687
|
// Create shape element
|
|
1217
1688
|
const shapeElement = {
|
|
1218
1689
|
type: 'shape',
|
|
@@ -1230,15 +1701,40 @@ function extractPseudoElements(el, win) {
|
|
|
1230
1701
|
gradient: gradient,
|
|
1231
1702
|
transparency: hasBg ? extractAlpha(pComputed.backgroundColor) : null,
|
|
1232
1703
|
line: null,
|
|
1233
|
-
rectRadius:
|
|
1704
|
+
rectRadius: effectiveRectRadius,
|
|
1234
1705
|
shadow: shadow,
|
|
1235
1706
|
opacity: hasOpacity ? elementOpacity : null,
|
|
1236
1707
|
isEllipse: isEllipse,
|
|
1237
1708
|
softEdge: null,
|
|
1709
|
+
rotate: null,
|
|
1238
1710
|
customGeometry: null,
|
|
1239
1711
|
},
|
|
1240
1712
|
};
|
|
1241
1713
|
results.push(shapeElement);
|
|
1714
|
+
// Add additional shape elements for extra gradient layers
|
|
1715
|
+
for (const extraGrad of extraPseudoGradients) {
|
|
1716
|
+
const extraShape = {
|
|
1717
|
+
type: 'shape',
|
|
1718
|
+
text: '',
|
|
1719
|
+
textRuns: null,
|
|
1720
|
+
style: null,
|
|
1721
|
+
position: { ...shapeElement.position },
|
|
1722
|
+
shape: {
|
|
1723
|
+
fill: null,
|
|
1724
|
+
gradient: extraGrad,
|
|
1725
|
+
transparency: null,
|
|
1726
|
+
line: null,
|
|
1727
|
+
rectRadius: 0,
|
|
1728
|
+
shadow: null,
|
|
1729
|
+
opacity: shapeElement.shape.opacity,
|
|
1730
|
+
isEllipse: false,
|
|
1731
|
+
softEdge: null,
|
|
1732
|
+
rotate: null,
|
|
1733
|
+
customGeometry: null,
|
|
1734
|
+
},
|
|
1735
|
+
};
|
|
1736
|
+
results.push(extraShape);
|
|
1737
|
+
}
|
|
1242
1738
|
}
|
|
1243
1739
|
return results;
|
|
1244
1740
|
}
|
|
@@ -1408,8 +1904,10 @@ export function parseSlideHtml(doc) {
|
|
|
1408
1904
|
return;
|
|
1409
1905
|
}
|
|
1410
1906
|
}
|
|
1411
|
-
// Handle SPAN elements with backgrounds/borders as shapes
|
|
1412
|
-
|
|
1907
|
+
// Handle SPAN and A elements with backgrounds/borders as shapes
|
|
1908
|
+
// <a> tags styled as buttons (e.g. CTA buttons with gradient backgrounds,
|
|
1909
|
+
// border-radius, box-shadow) need the same shape treatment as SPANs.
|
|
1910
|
+
if (el.tagName === 'SPAN' || el.tagName === 'A') {
|
|
1413
1911
|
const computed = win.getComputedStyle(el);
|
|
1414
1912
|
const hasBg = computed.backgroundColor && computed.backgroundColor !== 'rgba(0, 0, 0, 0)';
|
|
1415
1913
|
const hasBorder = (computed.borderWidth && parseFloat(computed.borderWidth) > 0) ||
|
|
@@ -1466,9 +1964,9 @@ export function parseSlideHtml(doc) {
|
|
|
1466
1964
|
borderTop === borderRight &&
|
|
1467
1965
|
borderRight === borderBottom &&
|
|
1468
1966
|
borderBottom === borderLeft;
|
|
1469
|
-
// Extract opacity
|
|
1470
|
-
const spanOpacity =
|
|
1471
|
-
const hasSpanOpacity =
|
|
1967
|
+
// Extract effective opacity (including ancestor chain)
|
|
1968
|
+
const spanOpacity = getEffectiveOpacity(el, win);
|
|
1969
|
+
const hasSpanOpacity = spanOpacity < 1;
|
|
1472
1970
|
// Extract box-shadow
|
|
1473
1971
|
const spanShadow = parseBoxShadow(computed.boxShadow);
|
|
1474
1972
|
// For small elements (badges, labels), disable text wrapping to prevent overflow
|
|
@@ -1513,6 +2011,7 @@ export function parseSlideHtml(doc) {
|
|
|
1513
2011
|
opacity: hasSpanOpacity ? spanOpacity : null,
|
|
1514
2012
|
isEllipse: spanIsEllipse,
|
|
1515
2013
|
softEdge: null,
|
|
2014
|
+
rotate: null,
|
|
1516
2015
|
customGeometry: null,
|
|
1517
2016
|
},
|
|
1518
2017
|
};
|
|
@@ -1676,9 +2175,9 @@ export function parseSlideHtml(doc) {
|
|
|
1676
2175
|
imgComputed.webkitMaskImage ||
|
|
1677
2176
|
imgComputed.getPropertyValue('mask-image') ||
|
|
1678
2177
|
imgComputed.getPropertyValue('-webkit-mask-image');
|
|
1679
|
-
// Extract CSS opacity
|
|
1680
|
-
const imgOpacity =
|
|
1681
|
-
const hasOpacity =
|
|
2178
|
+
// Extract effective CSS opacity (including ancestor chain) so we can bake it into the mask if present
|
|
2179
|
+
const imgOpacity = getEffectiveOpacity(el, win);
|
|
2180
|
+
const hasOpacity = imgOpacity < 1 && imgOpacity >= 0;
|
|
1682
2181
|
// Pre-bake CSS mask-image gradient into the image using canvas.
|
|
1683
2182
|
// This creates a transparency gradient effect (PNG with alpha) that PPTX displays natively.
|
|
1684
2183
|
// When a mask is applied, we also bake in the CSS opacity to ensure correct alpha blending.
|
|
@@ -1754,11 +2253,13 @@ export function parseSlideHtml(doc) {
|
|
|
1754
2253
|
if (el.tagName.toLowerCase() === 'svg') {
|
|
1755
2254
|
const rect = htmlEl.getBoundingClientRect();
|
|
1756
2255
|
if (rect.width > 0 && rect.height > 0) {
|
|
1757
|
-
// Skip SVGs with very low opacity - these are usually decorative overlays
|
|
1758
|
-
// (e.g., grain textures, noise patterns) that won't render well in PPTX
|
|
2256
|
+
// Skip SVGs with very low own opacity - these are usually decorative overlays
|
|
2257
|
+
// (e.g., grain textures, noise patterns) that won't render well in PPTX.
|
|
2258
|
+
// Only check the element's own opacity for the skip decision, not ancestors.
|
|
2259
|
+
// Ancestor opacity is applied as transparency on the ImageElement below.
|
|
1759
2260
|
const computedStyle = win.getComputedStyle(htmlEl);
|
|
1760
|
-
const
|
|
1761
|
-
if (
|
|
2261
|
+
const ownOpacity = parseFloat(computedStyle.opacity || '1');
|
|
2262
|
+
if (ownOpacity < 0.1) {
|
|
1762
2263
|
processed.add(el);
|
|
1763
2264
|
el.querySelectorAll('*').forEach((child) => processed.add(child));
|
|
1764
2265
|
return;
|
|
@@ -1907,6 +2408,10 @@ export function parseSlideHtml(doc) {
|
|
|
1907
2408
|
}
|
|
1908
2409
|
}
|
|
1909
2410
|
}
|
|
2411
|
+
// Apply effective opacity (including ancestor chain) as image transparency.
|
|
2412
|
+
// PPTX doesn't have opacity containers, so the visual opacity must be
|
|
2413
|
+
// applied directly to each element.
|
|
2414
|
+
const svgEffectiveOpacity = getEffectiveOpacity(el, win);
|
|
1910
2415
|
const imageElement = {
|
|
1911
2416
|
type: 'image',
|
|
1912
2417
|
src: dataUri,
|
|
@@ -1917,6 +2422,7 @@ export function parseSlideHtml(doc) {
|
|
|
1917
2422
|
h: pxToInch(rect.height),
|
|
1918
2423
|
},
|
|
1919
2424
|
sizing: null,
|
|
2425
|
+
transparency: svgEffectiveOpacity < 1 ? Math.round((1 - svgEffectiveOpacity) * 100) : undefined,
|
|
1920
2426
|
};
|
|
1921
2427
|
elements.push(imageElement);
|
|
1922
2428
|
processed.add(el);
|
|
@@ -2058,6 +2564,7 @@ export function parseSlideHtml(doc) {
|
|
|
2058
2564
|
}
|
|
2059
2565
|
// === CONIC GRADIENT: render as image ===
|
|
2060
2566
|
// PPTX has no native conic-gradient support. Render as canvas image.
|
|
2567
|
+
let hasConicGradientImage = false;
|
|
2061
2568
|
const divBgImageForConic = computed.backgroundImage;
|
|
2062
2569
|
if (divBgImageForConic && divBgImageForConic.includes('conic-gradient')) {
|
|
2063
2570
|
const rect = htmlEl.getBoundingClientRect();
|
|
@@ -2082,6 +2589,7 @@ export function parseSlideHtml(doc) {
|
|
|
2082
2589
|
rectRadius: 0,
|
|
2083
2590
|
};
|
|
2084
2591
|
elements.push(imgElement);
|
|
2592
|
+
hasConicGradientImage = true;
|
|
2085
2593
|
// Don't mark as fully processed — children (text, pseudo-elements) still need extraction
|
|
2086
2594
|
// Just skip the shape creation for this element's background
|
|
2087
2595
|
}
|
|
@@ -2097,10 +2605,54 @@ export function parseSlideHtml(doc) {
|
|
|
2097
2605
|
let bgImageSize = null;
|
|
2098
2606
|
let bgImagePosition = null;
|
|
2099
2607
|
let bgGradient = null;
|
|
2608
|
+
let extraDivGradients = [];
|
|
2100
2609
|
if (elBgImage && elBgImage !== 'none') {
|
|
2101
2610
|
if (elBgImage.includes('linear-gradient') ||
|
|
2102
2611
|
elBgImage.includes('radial-gradient')) {
|
|
2103
|
-
|
|
2612
|
+
const gradParts = splitCssBackgroundGradients(elBgImage);
|
|
2613
|
+
// Detect complex repeating-pattern backgrounds (4+ gradient layers,
|
|
2614
|
+
// often combined with a small background-size for tiling). These CSS
|
|
2615
|
+
// patterns (hex grids, noise textures, etc.) cannot be faithfully
|
|
2616
|
+
// reproduced as PPTX gradients, so we rasterize them to a canvas image.
|
|
2617
|
+
const bgSize = computed.backgroundSize;
|
|
2618
|
+
const isRepeatingPattern = gradParts.length >= 4 &&
|
|
2619
|
+
bgSize &&
|
|
2620
|
+
bgSize !== 'auto' &&
|
|
2621
|
+
!bgSize.includes('100%');
|
|
2622
|
+
if (isRepeatingPattern) {
|
|
2623
|
+
// Complex repeating CSS pattern — rasterize to canvas image.
|
|
2624
|
+
// PPTX gradients cannot represent tiled multi-layer patterns like
|
|
2625
|
+
// hex grids, but the Canvas 2D API can draw them faithfully.
|
|
2626
|
+
const rect = htmlEl.getBoundingClientRect();
|
|
2627
|
+
const patternDataUri = renderRepeatingGradientPatternAsImage(elBgImage, bgSize, computed.backgroundPosition || '0px 0px', rect.width, rect.height, win.document);
|
|
2628
|
+
if (patternDataUri) {
|
|
2629
|
+
const effOp = getEffectiveOpacity(htmlEl, win);
|
|
2630
|
+
const imgElement = {
|
|
2631
|
+
type: 'image',
|
|
2632
|
+
src: patternDataUri,
|
|
2633
|
+
position: {
|
|
2634
|
+
x: pxToInch(rect.left),
|
|
2635
|
+
y: pxToInch(rect.top),
|
|
2636
|
+
w: pxToInch(rect.width),
|
|
2637
|
+
h: pxToInch(rect.height),
|
|
2638
|
+
},
|
|
2639
|
+
sizing: null,
|
|
2640
|
+
rectRadius: 0,
|
|
2641
|
+
transparency: effOp < 1 ? Math.round((1 - effOp) * 100) : undefined,
|
|
2642
|
+
};
|
|
2643
|
+
elements.push(imgElement);
|
|
2644
|
+
}
|
|
2645
|
+
bgGradient = null;
|
|
2646
|
+
}
|
|
2647
|
+
else if (gradParts.length > 0) {
|
|
2648
|
+
bgGradient = parseCssGradient(gradParts[0]);
|
|
2649
|
+
// Collect additional gradient layers as separate shapes (up to 3 extra)
|
|
2650
|
+
for (let gi = 1; gi < Math.min(gradParts.length, 4); gi++) {
|
|
2651
|
+
const g = parseCssGradient(gradParts[gi]);
|
|
2652
|
+
if (g)
|
|
2653
|
+
extraDivGradients.push(g);
|
|
2654
|
+
}
|
|
2655
|
+
}
|
|
2104
2656
|
}
|
|
2105
2657
|
else {
|
|
2106
2658
|
const urlMatch = elBgImage.match(/url\(["']?([^"')]+)["']?\)/);
|
|
@@ -2183,7 +2735,7 @@ export function parseSlideHtml(doc) {
|
|
|
2183
2735
|
});
|
|
2184
2736
|
}
|
|
2185
2737
|
}
|
|
2186
|
-
if (hasBg || hasBorder || bgImageUrl || bgGradient) {
|
|
2738
|
+
if (hasBg || hasBorder || bgImageUrl || bgGradient || hasConicGradientImage) {
|
|
2187
2739
|
const rect = htmlEl.getBoundingClientRect();
|
|
2188
2740
|
if (rect.width > 0 && rect.height > 0) {
|
|
2189
2741
|
const shadow = parseBoxShadow(computed.boxShadow);
|
|
@@ -2557,9 +3109,9 @@ export function parseSlideHtml(doc) {
|
|
|
2557
3109
|
}
|
|
2558
3110
|
}
|
|
2559
3111
|
if (hasBg || hasUniformBorder || bgGradient) {
|
|
2560
|
-
// Detect
|
|
2561
|
-
const elementOpacity =
|
|
2562
|
-
const hasOpacity =
|
|
3112
|
+
// Detect effective opacity (including ancestor chain)
|
|
3113
|
+
const elementOpacity = getEffectiveOpacity(htmlEl, win);
|
|
3114
|
+
const hasOpacity = elementOpacity < 1;
|
|
2563
3115
|
// Detect CSS filter: blur() for soft-edge effect
|
|
2564
3116
|
let softEdgePt = null;
|
|
2565
3117
|
const filterStr = computed.filter;
|
|
@@ -2593,16 +3145,39 @@ export function parseSlideHtml(doc) {
|
|
|
2593
3145
|
const paddingBottom = parseFloat(computed.paddingBottom) || 0;
|
|
2594
3146
|
const paddingLeft = parseFloat(computed.paddingLeft) || 0;
|
|
2595
3147
|
const hasPadding = paddingTop > 2 || paddingRight > 2 || paddingBottom > 2 || paddingLeft > 2;
|
|
3148
|
+
// --- CSS transform rotation handling ---
|
|
3149
|
+
// If the element has a CSS rotation, use the pre-rotation dimensions
|
|
3150
|
+
// and store the rotation angle for the PPTX shape. This prevents
|
|
3151
|
+
// thin rotated elements (e.g., decorative 1px gradient lines) from
|
|
3152
|
+
// being blown up to their bounding-box size.
|
|
3153
|
+
const rotationAngle = extractRotationAngle(computed);
|
|
3154
|
+
let shapeX = rect.left;
|
|
3155
|
+
let shapeY = rect.top;
|
|
3156
|
+
let shapeW = rect.width;
|
|
3157
|
+
let shapeH = rect.height;
|
|
3158
|
+
if (rotationAngle !== null) {
|
|
3159
|
+
const origW = parseFloat(computed.width);
|
|
3160
|
+
const origH = parseFloat(computed.height);
|
|
3161
|
+
if (!isNaN(origW) && origW > 0 && !isNaN(origH) && origH > 0) {
|
|
3162
|
+
// Use bounding-rect center as shape center
|
|
3163
|
+
const cx = (rect.left + rect.right) / 2;
|
|
3164
|
+
const cy = (rect.top + rect.bottom) / 2;
|
|
3165
|
+
shapeW = origW;
|
|
3166
|
+
shapeH = origH;
|
|
3167
|
+
shapeX = cx - origW / 2;
|
|
3168
|
+
shapeY = cy - origH / 2;
|
|
3169
|
+
}
|
|
3170
|
+
}
|
|
2596
3171
|
const shapeElement = {
|
|
2597
3172
|
type: 'shape',
|
|
2598
3173
|
text: shapeText,
|
|
2599
3174
|
textRuns: shapeTextRuns,
|
|
2600
3175
|
style: shapeStyle,
|
|
2601
3176
|
position: {
|
|
2602
|
-
x: pxToInch(
|
|
2603
|
-
y: pxToInch(
|
|
2604
|
-
w: pxToInch(
|
|
2605
|
-
h: pxToInch(
|
|
3177
|
+
x: pxToInch(shapeX),
|
|
3178
|
+
y: pxToInch(shapeY),
|
|
3179
|
+
w: pxToInch(shapeW),
|
|
3180
|
+
h: pxToInch(shapeH),
|
|
2606
3181
|
},
|
|
2607
3182
|
shape: {
|
|
2608
3183
|
fill: hasBg ? rgbToHex(computed.backgroundColor) : null,
|
|
@@ -2637,13 +3212,14 @@ export function parseSlideHtml(doc) {
|
|
|
2637
3212
|
opacity: hasOpacity ? elementOpacity : null,
|
|
2638
3213
|
isEllipse: isEllipse,
|
|
2639
3214
|
softEdge: softEdgePt,
|
|
3215
|
+
rotate: rotationAngle,
|
|
2640
3216
|
customGeometry: clipPathPolygon ? (() => {
|
|
2641
3217
|
// Convert percentage-based polygon points to EMU coordinates
|
|
2642
3218
|
// relative to the shape's bounding box.
|
|
2643
3219
|
// PptxGenJS custGeom uses the cx/cy (extent) as the path coordinate space.
|
|
2644
3220
|
const EMU = 914400; // EMUs per inch
|
|
2645
|
-
const pathW = Math.round(
|
|
2646
|
-
const pathH = Math.round(
|
|
3221
|
+
const pathW = Math.round(shapeW / PX_PER_IN * EMU);
|
|
3222
|
+
const pathH = Math.round(shapeH / PX_PER_IN * EMU);
|
|
2647
3223
|
const points = [];
|
|
2648
3224
|
clipPathPolygon.forEach((pt, i) => {
|
|
2649
3225
|
points.push({
|
|
@@ -2688,6 +3264,32 @@ export function parseSlideHtml(doc) {
|
|
|
2688
3264
|
];
|
|
2689
3265
|
}
|
|
2690
3266
|
elements.push(shapeElement);
|
|
3267
|
+
// Add additional shape elements for extra gradient layers (multiple CSS background gradients)
|
|
3268
|
+
if (extraDivGradients.length > 0) {
|
|
3269
|
+
for (const extraGrad of extraDivGradients) {
|
|
3270
|
+
const extraShape = {
|
|
3271
|
+
type: 'shape',
|
|
3272
|
+
text: '',
|
|
3273
|
+
textRuns: null,
|
|
3274
|
+
style: null,
|
|
3275
|
+
position: { ...shapeElement.position },
|
|
3276
|
+
shape: {
|
|
3277
|
+
fill: null,
|
|
3278
|
+
gradient: extraGrad,
|
|
3279
|
+
transparency: null,
|
|
3280
|
+
line: null,
|
|
3281
|
+
rectRadius: 0,
|
|
3282
|
+
shadow: null,
|
|
3283
|
+
opacity: shapeElement.shape.opacity,
|
|
3284
|
+
isEllipse: false,
|
|
3285
|
+
softEdge: null,
|
|
3286
|
+
rotate: null,
|
|
3287
|
+
customGeometry: null,
|
|
3288
|
+
},
|
|
3289
|
+
};
|
|
3290
|
+
elements.push(extraShape);
|
|
3291
|
+
}
|
|
3292
|
+
}
|
|
2691
3293
|
}
|
|
2692
3294
|
else if (shapeStyle && shapeStyle.fontFill) {
|
|
2693
3295
|
const fontFillTextElement = {
|
|
@@ -2703,6 +3305,37 @@ export function parseSlideHtml(doc) {
|
|
|
2703
3305
|
};
|
|
2704
3306
|
elements.push(fontFillTextElement);
|
|
2705
3307
|
}
|
|
3308
|
+
else if (hasConicGradientImage && shapeStyle && (shapeText || (shapeTextRuns && shapeTextRuns.length > 0))) {
|
|
3309
|
+
// Conic gradient div has text content but no bg/border — create a transparent
|
|
3310
|
+
// text-only shape to preserve flex alignment (center/middle) on top of the
|
|
3311
|
+
// conic gradient image.
|
|
3312
|
+
const conicTextElement = {
|
|
3313
|
+
type: 'shape',
|
|
3314
|
+
text: shapeText,
|
|
3315
|
+
textRuns: shapeTextRuns,
|
|
3316
|
+
style: shapeStyle,
|
|
3317
|
+
position: {
|
|
3318
|
+
x: pxToInch(rect.left),
|
|
3319
|
+
y: pxToInch(rect.top),
|
|
3320
|
+
w: pxToInch(rect.width),
|
|
3321
|
+
h: pxToInch(rect.height),
|
|
3322
|
+
},
|
|
3323
|
+
shape: {
|
|
3324
|
+
fill: null,
|
|
3325
|
+
gradient: null,
|
|
3326
|
+
transparency: null,
|
|
3327
|
+
line: null,
|
|
3328
|
+
rectRadius: 0,
|
|
3329
|
+
shadow: null,
|
|
3330
|
+
opacity: null,
|
|
3331
|
+
isEllipse: false,
|
|
3332
|
+
softEdge: null,
|
|
3333
|
+
rotate: null,
|
|
3334
|
+
customGeometry: null,
|
|
3335
|
+
},
|
|
3336
|
+
};
|
|
3337
|
+
elements.push(conicTextElement);
|
|
3338
|
+
}
|
|
2706
3339
|
else if (isSingleTextChild) {
|
|
2707
3340
|
processed.delete(textChildren[0]);
|
|
2708
3341
|
}
|
|
@@ -3010,6 +3643,10 @@ export function parseSlideHtml(doc) {
|
|
|
3010
3643
|
// find the actual text position using a Range on the text nodes.
|
|
3011
3644
|
// This ensures text is positioned at its visual location, not the container's.
|
|
3012
3645
|
const isFlexContainer = computed.display === 'flex' || computed.display === 'inline-flex';
|
|
3646
|
+
// Track the content child element if a specific child was selected for text
|
|
3647
|
+
// extraction (used later to scope parseInlineFormatting to the content child
|
|
3648
|
+
// instead of the full parent, avoiding duplicate text from shape-children).
|
|
3649
|
+
let flexContentChild = null;
|
|
3013
3650
|
if (isFlexContainer && el.children.length > 0) {
|
|
3014
3651
|
// Collect direct text nodes (not in child elements)
|
|
3015
3652
|
const textNodes = [];
|
|
@@ -3033,16 +3670,50 @@ export function parseSlideHtml(doc) {
|
|
|
3033
3670
|
}
|
|
3034
3671
|
}
|
|
3035
3672
|
else {
|
|
3036
|
-
// No direct text nodes — check for text inside child SPAN
|
|
3037
|
-
// This handles flex LI items like:
|
|
3673
|
+
// No direct text nodes — check for text inside child SPAN/elements.
|
|
3674
|
+
// This handles flex LI items like:
|
|
3675
|
+
// <li><span class="icon">1</span><span class="text">Content...</span></li>
|
|
3676
|
+
//
|
|
3677
|
+
// Skip children that have a background or border — those will be
|
|
3678
|
+
// extracted as independent shapes (e.g. numbered circle badges).
|
|
3679
|
+
// Use the first text-bearing "content" child for the rect and text
|
|
3680
|
+
// so the text element is sized to the content area, not the badge.
|
|
3038
3681
|
for (const child of Array.from(el.children)) {
|
|
3039
|
-
if (child.
|
|
3040
|
-
|
|
3041
|
-
|
|
3042
|
-
|
|
3043
|
-
|
|
3044
|
-
|
|
3045
|
-
|
|
3682
|
+
if (!child.textContent?.trim())
|
|
3683
|
+
continue;
|
|
3684
|
+
const childEl = child;
|
|
3685
|
+
const childComp = win.getComputedStyle(childEl);
|
|
3686
|
+
const childHasBg = childComp.backgroundColor && childComp.backgroundColor !== 'rgba(0, 0, 0, 0)';
|
|
3687
|
+
const childHasBorder = (parseFloat(childComp.borderTopWidth) || 0) > 0 ||
|
|
3688
|
+
(parseFloat(childComp.borderRightWidth) || 0) > 0 ||
|
|
3689
|
+
(parseFloat(childComp.borderBottomWidth) || 0) > 0 ||
|
|
3690
|
+
(parseFloat(childComp.borderLeftWidth) || 0) > 0;
|
|
3691
|
+
const childHasGradient = childComp.backgroundImage && childComp.backgroundImage !== 'none' &&
|
|
3692
|
+
(childComp.backgroundImage.includes('linear-gradient') || childComp.backgroundImage.includes('radial-gradient'));
|
|
3693
|
+
// Skip shape-like children (bg, border, gradient) — they become
|
|
3694
|
+
// independent shapes and their text is already included there.
|
|
3695
|
+
if (childHasBg || childHasBorder || childHasGradient)
|
|
3696
|
+
continue;
|
|
3697
|
+
const childRect = childEl.getBoundingClientRect();
|
|
3698
|
+
if (childRect.width > 0 && childRect.height > 0) {
|
|
3699
|
+
flexContentChild = child;
|
|
3700
|
+
rect = childRect;
|
|
3701
|
+
text = getTransformedText(childEl, childComp);
|
|
3702
|
+
break;
|
|
3703
|
+
}
|
|
3704
|
+
}
|
|
3705
|
+
// If no content child found (all children are shapes), fall back
|
|
3706
|
+
// to the first child with text for backwards compatibility.
|
|
3707
|
+
if (!flexContentChild) {
|
|
3708
|
+
for (const child of Array.from(el.children)) {
|
|
3709
|
+
if (child.textContent?.trim()) {
|
|
3710
|
+
const childRect = child.getBoundingClientRect();
|
|
3711
|
+
if (childRect.width > 0 && childRect.height > 0) {
|
|
3712
|
+
rect = childRect;
|
|
3713
|
+
const childComputed = win.getComputedStyle(child);
|
|
3714
|
+
text = getTransformedText(child, childComputed);
|
|
3715
|
+
break;
|
|
3716
|
+
}
|
|
3046
3717
|
}
|
|
3047
3718
|
}
|
|
3048
3719
|
}
|
|
@@ -3135,10 +3806,14 @@ export function parseSlideHtml(doc) {
|
|
|
3135
3806
|
}
|
|
3136
3807
|
}
|
|
3137
3808
|
}
|
|
3138
|
-
|
|
3809
|
+
// When a specific content child was selected from a flex container,
|
|
3810
|
+
// scope formatting checks & parseInlineFormatting to that child so
|
|
3811
|
+
// text from shape-children (e.g. numbered circles) isn't duplicated.
|
|
3812
|
+
const formattingRoot = flexContentChild ?? el;
|
|
3813
|
+
const hasFormatting = formattingRoot.querySelector('b, i, u, strong, em, span, br, code, a, mark, sub, sup, small, s, del, ins, abbr, time, cite, q, dfn, kbd, samp, var');
|
|
3139
3814
|
if (hasFormatting) {
|
|
3140
3815
|
const transformStr = computed.textTransform;
|
|
3141
|
-
const runs = parseInlineFormatting(
|
|
3816
|
+
const runs = parseInlineFormatting(formattingRoot, {}, [], (str) => applyTextTransform(str, transformStr), win);
|
|
3142
3817
|
const textElement = {
|
|
3143
3818
|
type: el.tagName.toLowerCase(),
|
|
3144
3819
|
text: runs,
|
|
@@ -3165,6 +3840,14 @@ export function parseSlideHtml(doc) {
|
|
|
3165
3840
|
elements.push(textElement);
|
|
3166
3841
|
}
|
|
3167
3842
|
processed.add(el);
|
|
3843
|
+
// When a flex content child was selected for text extraction (e.g., a DIV
|
|
3844
|
+
// inside a flex LI), mark it and all its descendants as processed so the
|
|
3845
|
+
// DOM walk doesn't create duplicate text elements from child <p> tags
|
|
3846
|
+
// (injected by transform.ts text wrapping) or other text-bearing children.
|
|
3847
|
+
if (flexContentChild) {
|
|
3848
|
+
processed.add(flexContentChild);
|
|
3849
|
+
flexContentChild.querySelectorAll('*').forEach(desc => processed.add(desc));
|
|
3850
|
+
}
|
|
3168
3851
|
});
|
|
3169
3852
|
// Second pass: extract pseudo-elements for all processed elements
|
|
3170
3853
|
// This must be done after the main pass so we know which elements have been processed
|