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
JavaScript
/**
* 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 = {
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": '''
};
return text.replace(/[&<>"']/g, m => map[m]);
}
module.exports = {
markdownToElements,
createSlideHTML,
cleanInlineFormatting
};