dom-to-pptx 1.0.8 → 1.1.0
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 +17 -0
- package/README.md +56 -62
- package/dist/dom-to-pptx.bundle.js +45947 -724
- package/dist/dom-to-pptx.cjs +606 -7890
- package/dist/dom-to-pptx.cjs.map +1 -0
- package/dist/dom-to-pptx.min.js +55536 -406
- package/dist/dom-to-pptx.mjs +585 -7875
- package/dist/dom-to-pptx.mjs.map +1 -0
- package/package.json +83 -73
- package/rollup.config.js +63 -24
- package/src/font-embedder.js +159 -0
- package/src/font-utils.js +35 -0
- package/src/index.js +173 -23
- package/src/utils.js +226 -40
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,103 @@ export function generateBlurredSVG(w, h, color, radius, blurPx) {
|
|
|
485
572
|
data: 'data:image/svg+xml;base64,' + btoa(svg),
|
|
486
573
|
padding: padding,
|
|
487
574
|
};
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
// src/utils.js
|
|
578
|
+
|
|
579
|
+
// ... (keep all existing exports) ...
|
|
580
|
+
|
|
581
|
+
/**
|
|
582
|
+
* Traverses the target DOM and collects all unique font-family names used.
|
|
583
|
+
*/
|
|
584
|
+
export function getUsedFontFamilies(root) {
|
|
585
|
+
const families = new Set();
|
|
586
|
+
|
|
587
|
+
function scan(node) {
|
|
588
|
+
if (node.nodeType === 1) { // Element
|
|
589
|
+
const style = window.getComputedStyle(node);
|
|
590
|
+
const fontList = style.fontFamily.split(',');
|
|
591
|
+
// The first font in the stack is the primary one
|
|
592
|
+
const primary = fontList[0].trim().replace(/['"]/g, '');
|
|
593
|
+
if (primary) families.add(primary);
|
|
594
|
+
}
|
|
595
|
+
for (const child of node.childNodes) {
|
|
596
|
+
scan(child);
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
// Handle array of roots or single root
|
|
601
|
+
const elements = Array.isArray(root) ? root : [root];
|
|
602
|
+
elements.forEach(el => {
|
|
603
|
+
const node = typeof el === 'string' ? document.querySelector(el) : el;
|
|
604
|
+
if (node) scan(node);
|
|
605
|
+
});
|
|
606
|
+
|
|
607
|
+
return families;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
/**
|
|
611
|
+
* Scans document.styleSheets to find @font-face URLs for the requested families.
|
|
612
|
+
* Returns an array of { name, url } objects.
|
|
613
|
+
*/
|
|
614
|
+
export async function getAutoDetectedFonts(usedFamilies) {
|
|
615
|
+
const foundFonts = [];
|
|
616
|
+
const processedUrls = new Set();
|
|
617
|
+
|
|
618
|
+
// Helper to extract clean URL from CSS src string
|
|
619
|
+
const extractUrl = (srcStr) => {
|
|
620
|
+
// Look for url("...") or url('...') or url(...)
|
|
621
|
+
// Prioritize woff, ttf, otf. Avoid woff2 if possible as handling is harder,
|
|
622
|
+
// but if it's the only one, take it (convert logic handles it best effort).
|
|
623
|
+
const matches = srcStr.match(/url\((['"]?)(.*?)\1\)/g);
|
|
624
|
+
if (!matches) return null;
|
|
625
|
+
|
|
626
|
+
// Filter for preferred formats
|
|
627
|
+
let chosenUrl = null;
|
|
628
|
+
for (const match of matches) {
|
|
629
|
+
const urlRaw = match.replace(/url\((['"]?)(.*?)\1\)/, '$2');
|
|
630
|
+
// Skip data URIs for now (unless you want to support base64 embedding)
|
|
631
|
+
if (urlRaw.startsWith('data:')) continue;
|
|
632
|
+
|
|
633
|
+
if (urlRaw.includes('.ttf') || urlRaw.includes('.otf') || urlRaw.includes('.woff')) {
|
|
634
|
+
chosenUrl = urlRaw;
|
|
635
|
+
break; // Found a good one
|
|
636
|
+
}
|
|
637
|
+
// Fallback
|
|
638
|
+
if (!chosenUrl) chosenUrl = urlRaw;
|
|
639
|
+
}
|
|
640
|
+
return chosenUrl;
|
|
641
|
+
};
|
|
642
|
+
|
|
643
|
+
for (const sheet of Array.from(document.styleSheets)) {
|
|
644
|
+
try {
|
|
645
|
+
// Accessing cssRules on cross-origin sheets (like Google Fonts) might fail
|
|
646
|
+
// if CORS headers aren't set. We wrap in try/catch.
|
|
647
|
+
const rules = sheet.cssRules || sheet.rules;
|
|
648
|
+
if (!rules) continue;
|
|
649
|
+
|
|
650
|
+
for (const rule of Array.from(rules)) {
|
|
651
|
+
if (rule.constructor.name === 'CSSFontFaceRule' || rule.type === 5) {
|
|
652
|
+
const familyName = rule.style.getPropertyValue('font-family').replace(/['"]/g, '').trim();
|
|
653
|
+
|
|
654
|
+
if (usedFamilies.has(familyName)) {
|
|
655
|
+
const src = rule.style.getPropertyValue('src');
|
|
656
|
+
const url = extractUrl(src);
|
|
657
|
+
|
|
658
|
+
if (url && !processedUrls.has(url)) {
|
|
659
|
+
processedUrls.add(url);
|
|
660
|
+
foundFonts.push({ name: familyName, url: url });
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
} catch (e) {
|
|
666
|
+
// SecurityError is common for external stylesheets (CORS).
|
|
667
|
+
// We cannot scan those automatically via CSSOM.
|
|
668
|
+
console.warn("error:", e);
|
|
669
|
+
console.warn('Cannot scan stylesheet for fonts (CORS restriction):', sheet.href);
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
return foundFonts;
|
|
488
674
|
}
|