UNPKG

marp2pptx

Version:

Convert Marp markdown presentations to theme-compatible, editable PowerPoint files. Single command, zero Python dependencies, fully editable output.

348 lines (306 loc) 8.2 kB
/** * PowerPoint Generator * Creates theme-compatible, editable PPTX files with placeholders */ const pptxgen = require('pptxgenjs'); const { JSDOM } = require('jsdom'); // Color scheme const COLORS = { primary: 'E67E22', // Orange for headings accent: '16A085', // Teal for accents text: '2C3E50', // Dark gray for body text muted: '7F8C8D', // Muted gray codeBg: 'F5F5F5' // Code background }; /** * Parse HTML slide to extract elements * @param {string} html - HTML content * @returns {Object} Object with elements array and isLead boolean */ function parseHTMLSlide(html) { const dom = new JSDOM(html); const doc = dom.window.document; const body = doc.querySelector('body'); // Determine if it's a lead/centered slide const isLead = body.className.includes('center'); // Extract all content elements const elements = []; const contentDiv = doc.querySelector('.fill-height') || body; for (const child of contentDiv.children) { const tagName = child.tagName.toLowerCase(); const text = child.textContent.trim(); if (!text) continue; switch (tagName) { case 'h1': elements.push({ type: 'h1', text }); break; case 'h2': elements.push({ type: 'h2', text }); break; case 'h3': elements.push({ type: 'h3', text }); break; case 'p': elements.push({ type: 'p', text }); break; case 'ul': const ulItems = Array.from(child.querySelectorAll('li')).map(li => li.textContent.trim()); elements.push({ type: 'ul', items: ulItems }); break; case 'ol': const olItems = Array.from(child.querySelectorAll('li')).map(li => li.textContent.trim()); elements.push({ type: 'ol', items: olItems }); break; case 'pre': const code = child.querySelector('code'); elements.push({ type: 'code', text: code ? code.textContent : child.textContent }); break; case 'blockquote': elements.push({ type: 'blockquote', text }); break; } } return { elements, isLead }; } /** * Detect slide type */ function detectSlideType(elements, isLead) { const hasH1 = elements.some(e => e.type === 'h1'); const hasH2 = elements.some(e => e.type === 'h2'); if (isLead && hasH1) { return 'title'; } if (hasH1 || hasH2) { return 'content'; } return 'content'; } /** * Create title slide */ function createTitleSlide(slide, elements) { const h1 = elements.find(e => e.type === 'h1'); const h2 = elements.find(e => e.type === 'h2'); const otherContent = elements.filter(e => e.type !== 'h1' && e.type !== 'h2'); // Title placeholder if (h1) { slide.addText(h1.text, { placeholder: 'title', fontSize: 44, bold: true, color: COLORS.primary, align: 'center' }); } // Subtitle placeholder let subtitleText = ''; if (h2) { subtitleText = h2.text; } if (otherContent.length > 0) { const additionalText = otherContent .map(e => e.type === 'p' ? e.text : '') .filter(t => t) .join('\n'); subtitleText += (subtitleText ? '\n' : '') + additionalText; } if (subtitleText) { slide.addText(subtitleText, { placeholder: 'body', fontSize: 20, color: COLORS.text, align: 'center' }); } } /** * Create content slide */ function createContentSlide(slide, elements) { // Extract title (first h1, h2, or h3) const titleElem = elements.find(e => ['h1', 'h2', 'h3'].includes(e.type)); const contentElements = elements.filter(e => e !== titleElem); // Add title to placeholder if (titleElem) { const fontSize = titleElem.type === 'h1' ? 40 : (titleElem.type === 'h2' ? 36 : 32); slide.addText(titleElem.text, { placeholder: 'title', fontSize: fontSize, bold: true, color: COLORS.primary }); } // Build content for body placeholder if (contentElements.length > 0) { const bodyContent = buildBodyContent(contentElements); if (bodyContent.length > 0) { slide.addText(bodyContent, { placeholder: 'body', fontSize: 18, color: COLORS.text, valign: 'top' }); } } } /** * Build body content array */ function buildBodyContent(elements) { const content = []; for (const elem of elements) { switch (elem.type) { case 'p': content.push({ text: elem.text, options: { breakLine: true } }); break; case 'ul': elem.items.forEach((item, idx) => { content.push({ text: item, options: { bullet: { type: 'bullet' }, indentLevel: 0, breakLine: idx < elem.items.length - 1 } }); }); break; case 'ol': elem.items.forEach((item, idx) => { content.push({ text: item, options: { bullet: { type: 'number' }, indentLevel: 0, breakLine: idx < elem.items.length - 1 } }); }); break; case 'code': content.push({ text: elem.text, options: { fontFace: 'Courier New', fontSize: 14, color: COLORS.text, breakLine: true } }); break; case 'blockquote': content.push({ text: elem.text, options: { italic: true, color: COLORS.muted, breakLine: true } }); break; case 'h3': content.push({ text: elem.text, options: { fontSize: 24, bold: true, color: COLORS.text, breakLine: true } }); break; } } return content; } /** * Generate PPTX from HTML slides * @param {Array} htmlSlides - Array of HTML strings * @param {string} outputPath - Output file path * @returns {Promise} Promise that resolves when PPTX is created */ async function generatePPTX(htmlSlides, outputPath) { const pptx = new pptxgen(); pptx.layout = 'LAYOUT_16x9'; pptx.author = 'Marp to PPTX Converter'; // Define custom master slide layouts with placeholders pptx.defineSlideMaster({ title: 'CUSTOM_LAYOUT', background: { color: 'FFFFFF' }, objects: [ // Title placeholder { placeholder: { options: { name: 'title', type: 'title', x: 0.5, y: 0.5, w: 9.0, h: 1.0 } } }, // Body/Content placeholder { placeholder: { options: { name: 'body', type: 'body', x: 0.5, y: 1.7, w: 9.0, h: 4.0 } } } ] }); let successCount = 0; let errorCount = 0; const errors = []; for (let i = 0; i < htmlSlides.length; i++) { const html = htmlSlides[i]; try { const { elements, isLead } = parseHTMLSlide(html); const slideType = detectSlideType(elements, isLead); const slide = pptx.addSlide({ masterName: 'CUSTOM_LAYOUT' }); if (slideType === 'title') { createTitleSlide(slide, elements); } else { createContentSlide(slide, elements); } successCount++; } catch (error) { errors.push({ slide: i + 1, error: error.message }); errorCount++; // Add error slide try { const slide = pptx.addSlide(); slide.addText(`Error loading slide ${i + 1}`, { x: 1, y: 2, w: 8, h: 1, fontSize: 18, color: 'FF0000' }); slide.addText(error.message, { x: 1, y: 3, w: 8, h: 2, fontSize: 12, color: '666666' }); } catch (e) { // Failed to add error slide, just record the error } } } await pptx.writeFile({ fileName: outputPath }); return { successCount, errorCount, errors }; } module.exports = { generatePPTX, parseHTMLSlide, detectSlideType };