diff --git a/bin/html2pptx.js b/bin/html2pptx.js index 7a38b3a..6660185 100644 --- a/bin/html2pptx.js +++ b/bin/html2pptx.js @@ -177,6 +177,30 @@ async function main() { 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', @@ -207,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(); @@ -215,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, @@ -285,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 }