import { getIconSvg } from './icons.js'; import { marked } from 'marked'; export class PresentationRenderer { /** * Parses the XML output from the AI into a structured JS object. * @param {string} xmlString * @returns {Object} { theme: Object, slides: Array } */ static parseXML(xmlString) { const parser = new DOMParser(); // Sometimes AI wraps it in markdown code blocks let cleanXml = xmlString.replace(/```xml/g, '').replace(/```/g, '').trim(); // Ensure it has a root if missing if (!cleanXml.startsWith('')) { const match = cleanXml.match(/[\s\S]*<\/presentation>/i); if (match) cleanXml = match[0]; else cleanXml = `${cleanXml}`; } const doc = parser.parseFromString(cleanXml, "text/html"); // Parse Theme let theme = null; const themeNode = doc.querySelector('theme'); if (themeNode) { theme = { background: themeNode.querySelector('background')?.textContent || null, accent: themeNode.querySelector('accent')?.textContent || null, font: themeNode.querySelector('font')?.textContent || null }; // Inject Google Font if needed if (theme.font && theme.font.toLowerCase() !== 'inter') { const fontName = theme.font.trim(); const fontId = 'font-' + fontName.replace(/\s+/g, '-').toLowerCase(); if (!document.getElementById(fontId)) { const link = document.createElement('link'); link.id = fontId; link.rel = 'stylesheet'; link.href = `https://fonts.googleapis.com/css2?family=${fontName.replace(/\s+/g, '+')}:ital,wght@0,300;0,400;0,600;0,700;0,800;1,400&display=swap`; document.head.appendChild(link); } } } // Parse Slides const slideNodes = doc.querySelectorAll('slide'); const slides = Array.from(slideNodes).map(node => { const layout = node.getAttribute('layout') || 'title'; const slide = { layout }; // Common elements const title = node.querySelector('title'); if (title) slide.title = title.textContent; const subtitle = node.querySelector('subtitle'); if (subtitle) slide.subtitle = subtitle.textContent; const icon = node.querySelector('icon'); if (icon) slide.icon = icon.textContent; const quote = node.querySelector('quote'); if (quote) slide.quote = quote.textContent; // For split/bullets layout const contentNode = node.querySelector('content'); if (contentNode) { // Collect list items or raw HTML const listItems = contentNode.querySelectorAll('item'); if (listItems.length > 0) { slide.bullets = Array.from(listItems).map(li => li.innerHTML); } else { slide.htmlContent = contentNode.innerHTML; } } // Visual elements (Mermaid or Images) const visualNode = node.querySelector('visual'); if (visualNode) { const mermaid = visualNode.querySelector('mermaid'); if (mermaid) slide.mermaid = mermaid.textContent; const image = visualNode.querySelector('image'); if (image) slide.imageQuery = image.textContent.trim(); } return slide; }); return { theme, slides }; } /** * Renders a slide object into a DOM element. * @param {Object} slideData * @returns {HTMLElement} */ static renderSlide(slideData) { const container = document.createElement('div'); container.className = `slide-content-wrapper slide-layout-${slideData.layout}`; if (slideData.layout === 'title') { if (slideData.icon) { const iconSvg = getIconSvg(slideData.icon) || ''; if (iconSvg) container.insertAdjacentHTML('beforeend', iconSvg); } if (slideData.title) container.insertAdjacentHTML('beforeend', `

${slideData.title}

`); if (slideData.subtitle) container.insertAdjacentHTML('beforeend', `

${slideData.subtitle}

`); } else if (slideData.layout === 'quote') { if (slideData.quote) container.insertAdjacentHTML('beforeend', `
${slideData.quote}
`); } else if (slideData.layout === 'split') { const textSide = document.createElement('div'); textSide.className = 'slide-text-side'; if (slideData.title) textSide.insertAdjacentHTML('beforeend', `

${slideData.title}

`); if (slideData.bullets) { const ul = document.createElement('ul'); ul.className = 'slide-bullets'; ul.innerHTML = slideData.bullets.map(b => `
  • ${marked.parseInline(b)}
  • `).join(''); textSide.appendChild(ul); } else if (slideData.htmlContent) { textSide.insertAdjacentHTML('beforeend', slideData.htmlContent); } const visualSide = document.createElement('div'); visualSide.className = 'slide-visual-side'; if (slideData.mermaid) { visualSide.insertAdjacentHTML('beforeend', `
    ${slideData.mermaid}
    `); } container.appendChild(textSide); container.appendChild(visualSide); } else if (slideData.layout === 'image-split') { const textSide = document.createElement('div'); textSide.className = 'slide-text-side'; if (slideData.title) textSide.insertAdjacentHTML('beforeend', `

    ${slideData.title}

    `); if (slideData.bullets) { const ul = document.createElement('ul'); ul.className = 'slide-bullets'; ul.innerHTML = slideData.bullets.map(b => `
  • ${marked.parseInline(b)}
  • `).join(''); textSide.appendChild(ul); } else if (slideData.htmlContent) { textSide.insertAdjacentHTML('beforeend', slideData.htmlContent); } const visualSide = document.createElement('div'); visualSide.className = 'slide-visual-side'; if (slideData.imageQuery) { const imgUrl = `https://tse1.mm.bing.net/th?q=${encodeURIComponent(slideData.imageQuery)}&w=1000&h=1000&c=7&rs=1`; visualSide.insertAdjacentHTML('beforeend', `
    ${slideData.imageQuery}
    `); } container.appendChild(textSide); container.appendChild(visualSide); } else if (slideData.layout === 'cover') { if (slideData.imageQuery) { const imgUrl = `https://tse1.mm.bing.net/th?q=${encodeURIComponent(slideData.imageQuery)}&w=1920&h=1080&c=7&rs=1`; container.style.backgroundImage = `url('${imgUrl}')`; } const overlay = document.createElement('div'); overlay.className = 'slide-cover-overlay'; if (slideData.title) overlay.insertAdjacentHTML('beforeend', `

    ${slideData.title}

    `); if (slideData.subtitle) overlay.insertAdjacentHTML('beforeend', `

    ${slideData.subtitle}

    `); if (slideData.quote) overlay.insertAdjacentHTML('beforeend', `
    ${slideData.quote}
    `); container.appendChild(overlay); } else { // Default / Bullets if (slideData.title) container.insertAdjacentHTML('beforeend', `

    ${slideData.title}

    `); if (slideData.bullets) { const ul = document.createElement('ul'); ul.className = 'slide-bullets'; ul.innerHTML = slideData.bullets.map(b => `
  • ${marked.parseInline(b)}
  • `).join(''); container.appendChild(ul); } else if (slideData.htmlContent) { container.insertAdjacentHTML('beforeend', slideData.htmlContent); } } return container; } }