APP-Backend / src /utils /presentation-renderer.js
Luca448's picture
Update independent app to newest state with LFS and binary removals
ff36e71
Raw History Blame Contribute Delete
7.8 kB
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('<presentation>')) {
const match = cleanXml.match(/<presentation>[\s\S]*<\/presentation>/i);
if (match) cleanXml = match[0];
else cleanXml = `<presentation>${cleanXml}</presentation>`;
}
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', `<h1>${slideData.title}</h1>`);
if (slideData.subtitle) container.insertAdjacentHTML('beforeend', `<h2>${slideData.subtitle}</h2>`);
}
else if (slideData.layout === 'quote') {
if (slideData.quote) container.insertAdjacentHTML('beforeend', `<blockquote>${slideData.quote}</blockquote>`);
}
else if (slideData.layout === 'split') {
const textSide = document.createElement('div');
textSide.className = 'slide-text-side';
if (slideData.title) textSide.insertAdjacentHTML('beforeend', `<h2 class="slide-title">${slideData.title}</h2>`);
if (slideData.bullets) {
const ul = document.createElement('ul');
ul.className = 'slide-bullets';
ul.innerHTML = slideData.bullets.map(b => `<li>${marked.parseInline(b)}</li>`).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', `<div class="mermaid">${slideData.mermaid}</div>`);
}
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', `<h2 class="slide-title">${slideData.title}</h2>`);
if (slideData.bullets) {
const ul = document.createElement('ul');
ul.className = 'slide-bullets';
ul.innerHTML = slideData.bullets.map(b => `<li>${marked.parseInline(b)}</li>`).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', `<div class="pres-image-wrapper"><img src="${imgUrl}" alt="${slideData.imageQuery}" /></div>`);
}
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', `<h1>${slideData.title}</h1>`);
if (slideData.subtitle) overlay.insertAdjacentHTML('beforeend', `<h2>${slideData.subtitle}</h2>`);
if (slideData.quote) overlay.insertAdjacentHTML('beforeend', `<blockquote>${slideData.quote}</blockquote>`);
container.appendChild(overlay);
}
else {
// Default / Bullets
if (slideData.title) container.insertAdjacentHTML('beforeend', `<h2 class="slide-title" style="text-align: center; margin-bottom: 4rem;">${slideData.title}</h2>`);
if (slideData.bullets) {
const ul = document.createElement('ul');
ul.className = 'slide-bullets';
ul.innerHTML = slideData.bullets.map(b => `<li>${marked.parseInline(b)}</li>`).join('');
container.appendChild(ul);
} else if (slideData.htmlContent) {
container.insertAdjacentHTML('beforeend', slideData.htmlContent);
}
}
return container;
}
}