Compare commits

...
2 Commits
Author SHA1 Message Date
oldliandClaude Sonnet 5 0f8a797b7a PM: html2pptx v3 - add rich text preservation + PPTX adapt
- extractRuns(): recursive DOM walker to extract inline formatting
  (bold, italic, color, fontSize, fontFamily) from strong/em/span
- Element data now includes 'runs' array alongside flat text
- PPTX generation: rich text uses pptxgenjs run array format,
  plain text keeps original single-style logic
- Added bounds constraints to prevent textbox overflow
- Verified: 14 slides from dashi-deck, 10 from simple,
  54 rich text elements detected

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-22 20:20:22 +08:00
oldliandClaude Sonnet 5 ca1e4dfa72 PM: html2pptx v3 - fix slide detection to direct children only
- deck.querySelectorAll('.ppt-slide') -> deck.querySelectorAll(':scope > .ppt-slide')
  prevents nested .ppt-slide elements from being counted as separate slides
- Enhanced visibility filter: check opacity, display, visibility in addition to rect size
- Tested: dashi-deck.html now returns 14 slides (was 20), simple.html returns 10

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-22 20:17:39 +08:00
+104 -19
View File
@@ -158,7 +158,7 @@ async function main() {
// DashiAI format: .ppt-deck > .ppt-slide
var deck = document.querySelector('.ppt-deck');
if (deck) {
slides = deck.querySelectorAll('.ppt-slide');
slides = deck.querySelectorAll(':scope > .ppt-slide');
}
if (!slides || slides.length <= 1) {
slides = document.querySelectorAll(
@@ -170,9 +170,37 @@ async function main() {
}
slides = Array.from(slides).filter(function (s) {
var r = s.getBoundingClientRect();
return r.width > 0 && r.height > 0;
var style = window.getComputedStyle(s);
return r.width > 0 && r.height > 0 &&
style.opacity !== '0' &&
style.display !== 'none' &&
style.visibility !== 'hidden';
});
// Recursively extract inline text runs with computed styles
function extractRuns(node) {
var runs = [];
for (var ri = 0; ri < node.childNodes.length; ri++) {
var child = node.childNodes[ri];
if (child.nodeType === 3) {
var t = (child.textContent || '').trim();
if (t) runs.push({ text: t });
} else if (child.nodeType === 1) {
var cs = window.getComputedStyle(child);
var childRuns = extractRuns(child);
for (var cj = 0; cj < childRuns.length; cj++) {
childRuns[cj].bold = childRuns[cj].bold || parseInt(cs.fontWeight) >= 600;
childRuns[cj].italic = childRuns[cj].italic || cs.fontStyle === 'italic';
childRuns[cj].color = childRuns[cj].color || cs.color;
childRuns[cj].fontSize = childRuns[cj].fontSize || parseFloat(cs.fontSize);
childRuns[cj].fontFamily = childRuns[cj].fontFamily || cs.fontFamily;
}
runs = runs.concat(childRuns);
}
}
return runs.length > 0 ? runs : [{ text: node.textContent || '' }];
}
// Tags whose full text content should be a single text box
var TEXT_BLOCK_TAGS = new Set([
'p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
@@ -203,7 +231,9 @@ async function main() {
}
if (skip) continue;
var text = (el.textContent || '').trim();
var runs = extractRuns(el);
if (!runs || runs.length === 0) continue;
var text = runs.map(function (r) { return r.text || ''; }).join(' ').trim();
if (!text) continue;
var rect = el.getBoundingClientRect();
@@ -211,7 +241,13 @@ async function main() {
var style = window.getComputedStyle(el);
// Truncate overly long runs
runs.forEach(function (r) {
if (r.text && r.text.length > 2000) r.text = r.text.substring(0, 2000);
});
elements.push({
runs: runs,
text: text.substring(0, 2000),
x: rect.left - slideRect.left,
y: rect.top - slideRect.top,
@@ -281,25 +317,74 @@ async function main() {
var w = Math.max(el.width * scaleX, 0.3);
var h = Math.max(el.height * scaleY, 0.2);
// Font size: CSS px → pt (1pt = 1/72in, CSS 1px = 1/96in, so px × 72/96 = pt)
// Bounds constraints: prevent textbox from overflowing slide
var MAX_W = 10 - x;
var MAX_H = 5.625 - y;
w = Math.min(w, MAX_W);
h = Math.min(h, MAX_H);
// Base font size: CSS px → pt (1pt = 1/72in, CSS 1px = 1/96in)
var fontSize = el.fontSize * 72 / 96;
fontSize = Math.max(fontSize, 8);
slide.addText(t, {
x: x,
y: y,
w: w,
h: h,
fontSize: fontSize,
fontFace: (el.fontFamily || 'Arial').split(',')[0].trim().replace(/['"]/g, ''),
color: parseCSSColor(el.color, '#000000'),
bold: parseFontWeight(el.fontWeight),
italic: el.fontStyle === 'italic' || el.fontStyle === 'oblique',
align: normalizeAlign(el.textAlign),
valign: 'top',
wrap: true,
margin: 0,
});
// Check if rich text is needed (runs with differing styles)
var hasRuns = el.runs && el.runs.length > 0;
var isSimpleRun = hasRuns && el.runs.length === 1 &&
el.runs[0].bold === undefined &&
el.runs[0].italic === undefined &&
el.runs[0].color === undefined &&
el.runs[0].fontSize === undefined &&
el.runs[0].fontFamily === undefined;
if (hasRuns && !isSimpleRun && el.runs.length > 1) {
// Rich text: render each run with its own style
var pptxRuns = el.runs.map(function (run) {
if (!run.text || !run.text.trim()) return null;
var baseSize = el.fontSize * 72 / 96;
return {
text: run.text,
options: {
fontSize: run.fontSize ? Math.max(run.fontSize * 72 / 96, 8) : baseSize,
bold: run.bold === true || (run.bold === undefined && parseFontWeight(el.fontWeight)),
italic: run.italic === true || (run.italic === undefined && (el.fontStyle === 'italic' || el.fontStyle === 'oblique')),
color: run.color ? parseCSSColor(run.color, '#000000') : parseCSSColor(el.color, '#000000'),
fontFace: run.fontFamily
? run.fontFamily.split(',')[0].trim().replace(/['"]/g, '')
: (el.fontFamily || 'Arial').split(',')[0].trim().replace(/['"]/g, ''),
},
};
}).filter(Boolean);
if (pptxRuns.length > 0) {
slide.addText(pptxRuns, {
x: x,
y: y,
w: w,
h: h,
align: normalizeAlign(el.textAlign),
valign: 'top',
wrap: true,
margin: 0,
});
}
} else {
// Plain text: original single-style logic
slide.addText(t, {
x: x,
y: y,
w: w,
h: h,
fontSize: fontSize,
fontFace: (el.fontFamily || 'Arial').split(',')[0].trim().replace(/['"]/g, ''),
color: parseCSSColor(el.color, '#000000'),
bold: parseFontWeight(el.fontWeight),
italic: el.fontStyle === 'italic' || el.fontStyle === 'oblique',
align: normalizeAlign(el.textAlign),
valign: 'top',
wrap: true,
margin: 0,
});
}
} catch (err) {
// Skip individual element failure
}