const fs = require('fs'); const path = require('path'); const Handlebars = require('handlebars'); function mediaKind(mediaPath) { const ext = path.extname(mediaPath || '').toLowerCase(); if (['.png', '.jpg', '.jpeg', '.gif', '.webp', '.bmp', '.svg'].includes(ext)) { return 'image'; } if (['.mp4', '.webm', '.ogg'].includes(ext)) { return 'video'; } if (ext === '.pdf') { return 'pdf'; } return 'file'; } function escapeHtml(value) { return String(value ?? '') .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } function sanitizeFontFamily(value) { return String(value || '').replace(/[^a-zA-Z0-9 ,"-]/g, '').trim(); } function sanitizeFontSize(value) { return Math.max(8, Number(value || 0) || 24); } function sanitizeTextColor(value, fallback) { const raw = String(value || '').trim(); if (/^#[0-9a-fA-F]{6}$/.test(raw) || /^#[0-9a-fA-F]{3}$/.test(raw)) { return raw; } return fallback || '#000000'; } const ALLOWED_RICH_TEXT_TAGS = ['b', 'strong', 'i', 'em', 'u', 'br', 'p', 'div', 'ul', 'ol', 'li']; function safeJsonForScript(value) { return JSON.stringify(value === undefined ? null : value).replace(//gi, ''); output = output.replace(//gi, ''); return output.replace(/<[^>]+>/g, (tag) => { const match = tag.match(/^<\s*(\/?)\s*([a-z0-9]+)(?:\s[^>]*)?>$/i); if (!match) { return ''; } const closing = Boolean(match[1]); const name = String(match[2] || '').toLowerCase(); if (!ALLOWED_RICH_TEXT_TAGS.includes(name)) { return ''; } if (name === 'br') { return '
'; } return closing ? `` : `<${name}>`; }); } function renderEditorJsBlock(block) { if (!block || !block.type || !block.data) { return ''; } if (block.type === 'header') { const level = Math.max(1, Math.min(6, Number(block.data.level || 2))); return '' + sanitizeRichText(block.data.text || '') + ''; } else if (block.type === 'list') { const tag = block.data.style === 'ordered' ? 'ol' : 'ul'; const items = Array.isArray(block.data.items) ? block.data.items : []; return '<' + tag + ' style="list-style-type:' + (tag === 'ol' ? 'decimal' : 'disc') + ';padding-left:1.4em;">' + items.map((item) => renderEditorJsListItem(item, tag)).join('') + ''; } else if (block.type === 'delimiter') { return '
'; } else if (block.type === 'code') { return '
' + escapeHtml(block.data.code || '') + '
'; } else if (block.type === 'table') { return renderEditorJsTable(block.data); } else if (block.type === 'paragraph') { return '

' + sanitizeRichText(block.data.text || '') + '

'; } return ''; } function renderEditorJsListItem(item, tag) { if (item && typeof item === 'object') { const content = item.content !== undefined ? item.content : (item.text !== undefined ? item.text : item.value !== undefined ? item.value : ''); const children = Array.isArray(item.items) ? item.items : Array.isArray(item.subItems) ? item.subItems : Array.isArray(item.children) ? item.children : []; const nested = children.length ? '<' + tag + ' style="list-style-type:' + (tag === 'ol' ? 'decimal' : 'disc') + ';padding-left:1.4em;">' + children.map((child) => renderEditorJsListItem(child, tag)).join('') + '' : ''; return '
  • ' + sanitizeRichText(content || '') + nested + '
  • '; } return '
  • ' + sanitizeRichText(item || '') + '
  • '; } function renderEditorJsTable(data) { const rows = Array.isArray(data.content) ? data.content : Array.isArray(data.rows) ? data.rows : []; if (!rows.length) { return ''; } const hasHeadings = Boolean(data.withHeadings); const tableRows = rows.map(function (row, rowIndex) { const cells = Array.isArray(row) ? row : []; const cellTag = hasHeadings && rowIndex === 0 ? 'th' : 'td'; const cellAttrs = cellTag === 'th' ? ' scope="col"' : ''; return '' + cells.map(function (cell) { return '<' + cellTag + cellAttrs + '>' + sanitizeRichText(cell || '') + ''; }).join('') + ''; }).join(''); return '' + tableRows + '
    '; } function renderEditorJsContent(value) { if (value && typeof value === 'object') { if (Array.isArray(value.blocks)) { return value.blocks.map(renderEditorJsBlock).join(''); } if (value.value !== undefined) { return renderEditorJsContent(value.value); } } const raw = String(value || ''); try { const parsed = JSON.parse(raw); if (parsed && Array.isArray(parsed.blocks)) { return parsed.blocks.map(renderEditorJsBlock).join(''); } } catch (_error) { // fall through to legacy HTML rendering } return sanitizeRichText(raw); } function renderHtmlRegionContent(value) { const html = String(value || '').trim(); if (!html) { return '
    HTML
    '; } return ''; } function fitCanvasSize(canvasWidth, canvasHeight, maxWidth, maxHeight) { const width = Math.max(1, Number(canvasWidth || 0) || 1920); const height = Math.max(1, Number(canvasHeight || 0) || 1080); const viewportWidth = Math.max(1, Number(maxWidth || 0) || width); const viewportHeight = Math.max(1, Number(maxHeight || 0) || height); const scale = Math.min(viewportWidth / width, viewportHeight / height); return { width: Math.round(width * scale), height: Math.round(height * scale) }; } const playerPageTemplatePath = path.join(__dirname, 'player-page.template.html'); const playerPageScriptPath = path.join(__dirname, 'player-page.script.html'); let playerPageTemplateCache = null; let playerPageScriptCache = null; function loadTemplate(filePath, cache) { const stat = fs.statSync(filePath); if (cache.value && cache.mtimeMs === stat.mtimeMs) { return cache.value; } const compiled = Handlebars.compile(fs.readFileSync(filePath, 'utf8')); cache.value = compiled; cache.mtimeMs = stat.mtimeMs; return compiled; } function getPlayerPageTemplate() { return loadTemplate(playerPageTemplatePath, playerPageTemplateCache || (playerPageTemplateCache = {})); } function getPlayerPageScript() { return loadTemplate(playerPageScriptPath, playerPageScriptCache || (playerPageScriptCache = {})); } function renderPlayerPage(slug, initialData) { const template = getPlayerPageTemplate(); const script = getPlayerPageScript()({ SLUG_JSON: new Handlebars.SafeString(JSON.stringify(slug)), INITIAL_DATA_JSON: new Handlebars.SafeString(safeJsonForScript(initialData || null)) }); return template({ TITLE: 'Screen ' + slug, SCRIPT_BLOCK: new Handlebars.SafeString(script) }); } module.exports = { mediaKind, renderPlayerPage };