UNPKG

marp2pptx

Version:

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

251 lines (217 loc) 6.16 kB
/** * Markdown to HTML Converter * Converts markdown content to structured HTML elements */ const { marked } = require('marked'); const { JSDOM } = require('jsdom'); /** * Parse markdown content and extract structured elements * @param {string} markdown - Markdown content * @returns {Array} Array of element objects */ function markdownToElements(markdown) { // Convert markdown to HTML using marked const html = marked.parse(markdown); // Parse HTML to extract structured elements const dom = new JSDOM(`<div>${html}</div>`); const container = dom.window.document.querySelector('div'); const elements = []; for (const child of container.children) { const tagName = child.tagName.toLowerCase(); const text = child.textContent.trim(); if (!text && tagName !== 'pre') 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': // Check if it contains code const code = child.querySelector('code'); if (code && child.children.length === 1) { elements.push({ type: 'inline-code', text: code.textContent }); } else { elements.push({ type: 'p', text }); } break; case 'ul': const ulItems = Array.from(child.querySelectorAll('li')).map(li => { // Handle inline formatting in list items return cleanInlineFormatting(li.innerHTML); }); elements.push({ type: 'ul', items: ulItems }); break; case 'ol': const olItems = Array.from(child.querySelectorAll('li')).map(li => { return cleanInlineFormatting(li.innerHTML); }); elements.push({ type: 'ol', items: olItems }); break; case 'pre': const codeBlock = child.querySelector('code'); const codeText = codeBlock ? codeBlock.textContent : child.textContent; elements.push({ type: 'code', text: codeText }); break; case 'blockquote': elements.push({ type: 'blockquote', text }); break; } } return elements; } /** * Clean inline HTML formatting and convert to plain text with markers * @param {string} html - HTML string * @returns {string} Cleaned text */ function cleanInlineFormatting(html) { // Simple cleanup - remove HTML tags but preserve text // For PowerPoint, we'll use plain text and apply formatting via pptxgenjs options const dom = new JSDOM(`<div>${html}</div>`); return dom.window.document.querySelector('div').textContent.trim(); } /** * Create slide HTML for intermediate processing * @param {Object} slide - Slide object with metadata and content * @param {Object} options - Style options * @returns {string} HTML string */ function createSlideHTML(slide, options = {}) { const { primaryColor = '#E67E22', accentColor = '#16A085', bgColor = '#FFFFFF', textColor = '#2C3E50' } = options; const bodyClass = slide.cssClass === 'lead' ? 'col center' : 'col'; const elements = markdownToElements(slide.content); // Convert elements back to HTML for PPTX processing let contentHtml = ''; for (const elem of elements) { switch (elem.type) { case 'h1': contentHtml += `<h1>${escapeHtml(elem.text)}</h1>\n`; break; case 'h2': contentHtml += `<h2>${escapeHtml(elem.text)}</h2>\n`; break; case 'h3': contentHtml += `<h3>${escapeHtml(elem.text)}</h3>\n`; break; case 'p': contentHtml += `<p>${escapeHtml(elem.text)}</p>\n`; break; case 'ul': contentHtml += '<ul>\n'; elem.items.forEach(item => { contentHtml += ` <li>${escapeHtml(item)}</li>\n`; }); contentHtml += '</ul>\n'; break; case 'ol': contentHtml += '<ol>\n'; elem.items.forEach(item => { contentHtml += ` <li>${escapeHtml(item)}</li>\n`; }); contentHtml += '</ol>\n'; break; case 'code': contentHtml += `<pre><code>${escapeHtml(elem.text)}</code></pre>\n`; break; case 'blockquote': contentHtml += `<blockquote><p>${escapeHtml(elem.text)}</p></blockquote>\n`; break; } } const css = getCSSTemplate(primaryColor, accentColor, bgColor, textColor); return `<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <style>${css}</style> </head> <body class="${bodyClass}" style="width: 960px; height: 540px; padding: 2rem 3rem 3.5rem 3rem;"> <div class="fill-height col ${slide.cssClass === 'lead' ? 'center' : ''}"> ${contentHtml} </div> </body> </html>`; } /** * Get CSS template for slides */ function getCSSTemplate(primaryColor, accentColor, bgColor, textColor) { return ` :root { --color-primary: ${primaryColor}; --color-accent: ${accentColor}; --color-surface: ${bgColor}; --color-surface-foreground: ${textColor}; --color-muted-foreground: #7F8C8D; } body { font-family: Arial, sans-serif; line-height: 1.6; } h1, h2, h3 { color: var(--color-primary); font-weight: 700; margin: 0 0 1rem 0; } p { margin: 0 0 0.75rem 0; font-size: 1.125rem; } ul, ol { margin: 0 0 1rem 0; padding-left: 2rem; } li { margin-bottom: 0.5rem; font-size: 1.125rem; } pre { background-color: #f5f5f5; padding: 1rem; border-radius: 0.5rem; font-family: 'Courier New', monospace; font-size: 0.9rem; } blockquote { border-left: 4px solid var(--color-accent); padding-left: 1rem; margin: 1rem 0; color: var(--color-muted-foreground); } .center { text-align: center; display: flex; align-items: center; justify-content: center; flex-direction: column; } `; } /** * Escape HTML special characters */ function escapeHtml(text) { const map = { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#039;' }; return text.replace(/[&<>"']/g, m => map[m]); } module.exports = { markdownToElements, createSlideHTML, cleanInlineFormatting };