Spaces:
Running
Running
Download src/utils/presentation-renderer.js from Luca448/APP-Backend: direct link, hf CLI and curl.
- Browser
- Download file 7.8 kB
-
https://huggingface.co/spaces/Luca448/APP-Backend/resolve/main/src/utils/presentation-renderer.js
- Command line
-
hf download hf://spaces/Luca448/APP-Backend/src/utils/presentation-renderer.js
-
curl -L -o presentation-renderer.js https://huggingface.co/spaces/Luca448/APP-Backend/resolve/main/src/utils/presentation-renderer.js
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; | |
| } | |
| } | |