dom-to-pptx 1.0.8 → 1.0.9
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/CHANGELOG.md +7 -0
- package/dist/dom-to-pptx.bundle.js +224 -59
- package/dist/dom-to-pptx.cjs +224 -59
- package/dist/dom-to-pptx.min.js +224 -59
- package/dist/dom-to-pptx.mjs +224 -59
- package/package.json +1 -1
- package/src/index.js +97 -19
- package/src/utils.js +128 -41
package/src/utils.js
CHANGED
|
@@ -233,6 +233,7 @@ export function getTextStyle(style, scale) {
|
|
|
233
233
|
|
|
234
234
|
/**
|
|
235
235
|
* Determines if a given DOM node is primarily a text container.
|
|
236
|
+
* Updated to correctly reject Icon elements so they are rendered as images.
|
|
236
237
|
*/
|
|
237
238
|
export function isTextContainer(node) {
|
|
238
239
|
const hasText = node.textContent.trim().length > 0;
|
|
@@ -241,28 +242,46 @@ export function isTextContainer(node) {
|
|
|
241
242
|
const children = Array.from(node.children);
|
|
242
243
|
if (children.length === 0) return true;
|
|
243
244
|
|
|
244
|
-
// Check if children are purely inline text formatting or visual shapes
|
|
245
245
|
const isSafeInline = (el) => {
|
|
246
|
-
// 1. Reject Web Components /
|
|
246
|
+
// 1. Reject Web Components / Custom Elements
|
|
247
247
|
if (el.tagName.includes('-')) return false;
|
|
248
|
+
// 2. Reject Explicit Images/SVGs
|
|
248
249
|
if (el.tagName === 'IMG' || el.tagName === 'SVG') return false;
|
|
249
250
|
|
|
251
|
+
// 3. Reject Class-based Icons (FontAwesome, Material, Bootstrap, etc.)
|
|
252
|
+
// If an <i> or <span> has icon classes, it is a visual object, not text.
|
|
253
|
+
if (el.tagName === 'I' || el.tagName === 'SPAN') {
|
|
254
|
+
const cls = el.getAttribute('class') || '';
|
|
255
|
+
if (
|
|
256
|
+
cls.includes('fa-') ||
|
|
257
|
+
cls.includes('fas') ||
|
|
258
|
+
cls.includes('far') ||
|
|
259
|
+
cls.includes('fab') ||
|
|
260
|
+
cls.includes('material-icons') ||
|
|
261
|
+
cls.includes('bi-') ||
|
|
262
|
+
cls.includes('icon')
|
|
263
|
+
) {
|
|
264
|
+
return false;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
250
268
|
const style = window.getComputedStyle(el);
|
|
251
269
|
const display = style.display;
|
|
252
270
|
|
|
253
|
-
//
|
|
254
|
-
const isInlineTag = ['SPAN', 'B', 'STRONG', 'EM', 'I', 'A', 'SMALL', 'MARK'].includes(
|
|
271
|
+
// 4. Standard Inline Tag Check
|
|
272
|
+
const isInlineTag = ['SPAN', 'B', 'STRONG', 'EM', 'I', 'A', 'SMALL', 'MARK'].includes(
|
|
273
|
+
el.tagName
|
|
274
|
+
);
|
|
255
275
|
const isInlineDisplay = display.includes('inline');
|
|
256
276
|
|
|
257
277
|
if (!isInlineTag && !isInlineDisplay) return false;
|
|
258
278
|
|
|
259
|
-
//
|
|
260
|
-
//
|
|
261
|
-
// If a child element has these, the parent is NOT a simple text container;
|
|
262
|
-
// it is a layout container composed of styled blocks.
|
|
279
|
+
// 5. Structural Styling Check
|
|
280
|
+
// If a child has a background or border, it's a layout block, not a simple text span.
|
|
263
281
|
const bgColor = parseColor(style.backgroundColor);
|
|
264
282
|
const hasVisibleBg = bgColor.hex && bgColor.opacity > 0;
|
|
265
|
-
const hasBorder =
|
|
283
|
+
const hasBorder =
|
|
284
|
+
parseFloat(style.borderWidth) > 0 && parseColor(style.borderColor).opacity > 0;
|
|
266
285
|
|
|
267
286
|
if (hasVisibleBg || hasBorder) {
|
|
268
287
|
return false;
|
|
@@ -383,57 +402,119 @@ export function getVisibleShadow(shadowStr, scale) {
|
|
|
383
402
|
return null;
|
|
384
403
|
}
|
|
385
404
|
|
|
405
|
+
/**
|
|
406
|
+
* Generates an SVG image for gradients, supporting degrees and keywords.
|
|
407
|
+
*/
|
|
386
408
|
export function generateGradientSVG(w, h, bgString, radius, border) {
|
|
387
409
|
try {
|
|
388
410
|
const match = bgString.match(/linear-gradient\((.*)\)/);
|
|
389
411
|
if (!match) return null;
|
|
390
412
|
const content = match[1];
|
|
413
|
+
|
|
414
|
+
// Split by comma, ignoring commas inside parentheses (e.g. rgba())
|
|
391
415
|
const parts = content.split(/,(?![^()]*\))/).map((p) => p.trim());
|
|
416
|
+
if (parts.length < 2) return null;
|
|
392
417
|
|
|
393
418
|
let x1 = '0%',
|
|
394
419
|
y1 = '0%',
|
|
395
420
|
x2 = '0%',
|
|
396
421
|
y2 = '100%';
|
|
397
|
-
let
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
422
|
+
let stopsStartIndex = 0;
|
|
423
|
+
const firstPart = parts[0].toLowerCase();
|
|
424
|
+
|
|
425
|
+
// 1. Check for Keywords (to right, etc.)
|
|
426
|
+
if (firstPart.startsWith('to ')) {
|
|
427
|
+
stopsStartIndex = 1;
|
|
428
|
+
const direction = firstPart.replace('to ', '').trim();
|
|
429
|
+
switch (direction) {
|
|
430
|
+
case 'top':
|
|
431
|
+
y1 = '100%';
|
|
432
|
+
y2 = '0%';
|
|
433
|
+
break;
|
|
434
|
+
case 'bottom':
|
|
435
|
+
y1 = '0%';
|
|
436
|
+
y2 = '100%';
|
|
437
|
+
break;
|
|
438
|
+
case 'left':
|
|
439
|
+
x1 = '100%';
|
|
440
|
+
x2 = '0%';
|
|
441
|
+
break;
|
|
442
|
+
case 'right':
|
|
443
|
+
x2 = '100%';
|
|
444
|
+
break;
|
|
445
|
+
case 'top right':
|
|
446
|
+
x1 = '0%';
|
|
447
|
+
y1 = '100%';
|
|
448
|
+
x2 = '100%';
|
|
449
|
+
y2 = '0%';
|
|
450
|
+
break;
|
|
451
|
+
case 'top left':
|
|
452
|
+
x1 = '100%';
|
|
453
|
+
y1 = '100%';
|
|
454
|
+
x2 = '0%';
|
|
455
|
+
y2 = '0%';
|
|
456
|
+
break;
|
|
457
|
+
case 'bottom right':
|
|
458
|
+
x2 = '100%';
|
|
459
|
+
y2 = '100%';
|
|
460
|
+
break;
|
|
461
|
+
case 'bottom left':
|
|
462
|
+
x1 = '100%';
|
|
463
|
+
y2 = '100%';
|
|
464
|
+
break;
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
// 2. Check for Degrees (45deg, 90deg, etc.)
|
|
468
|
+
else if (firstPart.match(/^-?[\d.]+(deg|rad|turn|grad)$/)) {
|
|
469
|
+
stopsStartIndex = 1;
|
|
470
|
+
const val = parseFloat(firstPart);
|
|
471
|
+
// CSS 0deg is Top (North), 90deg is Right (East), 180deg is Bottom (South)
|
|
472
|
+
// We convert this to SVG coordinates on a unit square (0-100%).
|
|
473
|
+
// Formula: Map angle to perimeter coordinates.
|
|
474
|
+
if (!isNaN(val)) {
|
|
475
|
+
const deg = firstPart.includes('rad') ? val * (180 / Math.PI) : val;
|
|
476
|
+
const cssRad = ((deg - 90) * Math.PI) / 180; // Correct CSS angle offset
|
|
477
|
+
|
|
478
|
+
// Calculate standard vector for rectangle center (50, 50)
|
|
479
|
+
const scale = 50; // Distance from center to edge (approx)
|
|
480
|
+
const cos = Math.cos(cssRad); // Y component (reversed in SVG)
|
|
481
|
+
const sin = Math.sin(cssRad); // X component
|
|
482
|
+
|
|
483
|
+
// Invert Y for SVG coordinate system
|
|
484
|
+
x1 = (50 - sin * scale).toFixed(1) + '%';
|
|
485
|
+
y1 = (50 + cos * scale).toFixed(1) + '%';
|
|
486
|
+
x2 = (50 + sin * scale).toFixed(1) + '%';
|
|
487
|
+
y2 = (50 - cos * scale).toFixed(1) + '%';
|
|
488
|
+
}
|
|
416
489
|
}
|
|
417
490
|
|
|
491
|
+
// 3. Process Color Stops
|
|
418
492
|
let stopsXML = '';
|
|
419
|
-
const stopParts = parts.slice(
|
|
493
|
+
const stopParts = parts.slice(stopsStartIndex);
|
|
494
|
+
|
|
420
495
|
stopParts.forEach((part, idx) => {
|
|
496
|
+
// Parse "Color Position" (e.g., "red 50%")
|
|
497
|
+
// Regex looks for optional space + number + unit at the end of the string
|
|
421
498
|
let color = part;
|
|
422
499
|
let offset = Math.round((idx / (stopParts.length - 1)) * 100) + '%';
|
|
423
|
-
|
|
500
|
+
|
|
501
|
+
const posMatch = part.match(/^(.*?)\s+(-?[\d.]+(?:%|px)?)$/);
|
|
424
502
|
if (posMatch) {
|
|
425
503
|
color = posMatch[1];
|
|
426
504
|
offset = posMatch[2];
|
|
427
505
|
}
|
|
506
|
+
|
|
507
|
+
// Handle RGBA/RGB for SVG compatibility
|
|
428
508
|
let opacity = 1;
|
|
429
509
|
if (color.includes('rgba')) {
|
|
430
|
-
const
|
|
431
|
-
if (
|
|
432
|
-
opacity =
|
|
433
|
-
color = `rgb(${
|
|
510
|
+
const rgbaMatch = color.match(/[\d.]+/g);
|
|
511
|
+
if (rgbaMatch && rgbaMatch.length >= 4) {
|
|
512
|
+
opacity = rgbaMatch[3];
|
|
513
|
+
color = `rgb(${rgbaMatch[0]},${rgbaMatch[1]},${rgbaMatch[2]})`;
|
|
434
514
|
}
|
|
435
515
|
}
|
|
436
|
-
|
|
516
|
+
|
|
517
|
+
stopsXML += `<stop offset="${offset}" stop-color="${color.trim()}" stop-opacity="${opacity}"/>`;
|
|
437
518
|
});
|
|
438
519
|
|
|
439
520
|
let strokeAttr = '';
|
|
@@ -442,12 +523,18 @@ export function generateGradientSVG(w, h, bgString, radius, border) {
|
|
|
442
523
|
}
|
|
443
524
|
|
|
444
525
|
const svg = `
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
526
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="${w}" height="${h}" viewBox="0 0 ${w} ${h}">
|
|
527
|
+
<defs>
|
|
528
|
+
<linearGradient id="grad" x1="${x1}" y1="${y1}" x2="${x2}" y2="${y2}">
|
|
529
|
+
${stopsXML}
|
|
530
|
+
</linearGradient>
|
|
531
|
+
</defs>
|
|
532
|
+
<rect x="0" y="0" width="${w}" height="${h}" rx="${radius}" ry="${radius}" fill="url(#grad)" ${strokeAttr} />
|
|
533
|
+
</svg>`;
|
|
534
|
+
|
|
449
535
|
return 'data:image/svg+xml;base64,' + btoa(svg);
|
|
450
|
-
} catch {
|
|
536
|
+
} catch (e) {
|
|
537
|
+
console.warn('Gradient generation failed:', e);
|
|
451
538
|
return null;
|
|
452
539
|
}
|
|
453
540
|
}
|
|
@@ -485,4 +572,4 @@ export function generateBlurredSVG(w, h, color, radius, blurPx) {
|
|
|
485
572
|
data: 'data:image/svg+xml;base64,' + btoa(svg),
|
|
486
573
|
padding: padding,
|
|
487
574
|
};
|
|
488
|
-
}
|
|
575
|
+
}
|