// General sanitization and sizing helpers. // Strip unsupported characters from a font family string. function sanitizeFontFamily(value) { return String(value || '').replace(/[^a-zA-Z0-9 ,\"-]/g, '').trim(); } // Clamp font size to the supported range. function sanitizeFontSize(value) { return Math.max(8, Number(value || 0) || 24); } // Validate a text color and fall back when needed. function sanitizeTextColor(value, fallback) { var raw = String(value || '').trim(); if (/^#[0-9a-fA-F]{6}$/.test(raw) || /^#[0-9a-fA-F]{3}$/.test(raw)) { return raw; } return fallback || '#000000'; } // Read the template's canvas dimensions with safe defaults. function getTemplateCanvasSize(template) { return { width: Math.max(1, Number(template.canvas_size_width || 1920)), height: Math.max(1, Number(template.canvas_size_height || 1080)) }; } // Read the server-supplied playlist revision, or fall back to the ETag. function getPlaylistRevision(data) { if (data && data.revision) { return String(data.revision); } if (data && data.playlist && data.playlist.revision) { return String(data.playlist.revision); } if (currentPlaylistEtag) { return String(currentPlaylistEtag).replace(/^"|"$/g, ''); } return String(Date.now()); } // Scale a canvas to fit within the viewport. function fitCanvasSize(canvasWidth, canvasHeight, maxWidth, maxHeight) { var width = Math.max(1, Number(canvasWidth || 0) || 1920); var height = Math.max(1, Number(canvasHeight || 0) || 1080); var viewportWidth = Math.max(1, Number(maxWidth || 0) || width); var viewportHeight = Math.max(1, Number(maxHeight || 0) || height); var scale = Math.min(viewportWidth / width, viewportHeight / height); return { width: Math.round(width * scale), height: Math.round(height * scale) }; } function normalizePlayerAnimationStep(value, fallbackPreset) { if (value && typeof value === 'object' && !Array.isArray(value)) { return { preset: String(value.preset || fallbackPreset || 'none').trim(), duration_ms: Number.isFinite(Number(value.duration_ms)) && Number(value.duration_ms) > 0 ? Math.round(Number(value.duration_ms)) : null, delay_ms: Number.isFinite(Number(value.delay_ms)) && Number(value.delay_ms) >= 0 ? Math.round(Number(value.delay_ms)) : null, iterations: Number.isFinite(Number(value.iterations)) && Number(value.iterations) > 0 ? Math.round(Number(value.iterations)) : null }; } return { preset: String(typeof value === 'string' ? value : fallbackPreset || 'none').trim(), duration_ms: null, delay_ms: null, iterations: null }; } function normalizePlayerAnimationConfig(value) { var raw = value; if (typeof raw === 'string') { var text = String(raw || '').trim(); if (!text) { raw = null; } else { try { raw = JSON.parse(text); } catch (_error) { raw = null; } } } if (!raw || typeof raw !== 'object' || Array.isArray(raw)) { raw = {}; } return { intro: normalizePlayerAnimationStep(raw.intro, 'none'), outro: normalizePlayerAnimationStep(raw.outro !== undefined ? raw.outro : raw.out, 'none'), loop: normalizePlayerAnimationStep(raw.loop, 'none') }; } function hasPlayerAnimation(config) { return Boolean(config && ['intro', 'outro', 'loop'].some(function (stepName) { return String((config[stepName] && config[stepName].preset) || '').trim() && String((config[stepName] && config[stepName].preset) || '').trim() !== 'none'; })); } function decorateRegionMarkup(markup, region) { if (!markup || !region || !hasPlayerAnimation(region.animationConfig)) { return markup; } var animationJson = escapeHtml(JSON.stringify(region.animationConfig)); return markup.replace(/^(\s*)<([a-z0-9-]+)(\s[^>]*)?>/i, function (match, leadingWhitespace, tagName, attributes) { return leadingWhitespace + '<' + tagName + (attributes || '') + ' data-animation-json="' + animationJson + '">'; }); } function clearRegionAnimationClasses(root) { if (!root) { return; } var elements = []; if (typeof root.matches === 'function' && root.matches('[data-animation-json]')) { elements.push(root); } Array.prototype.forEach.call(root.querySelectorAll('[data-animation-json]'), function (element) { elements.push(element); }); elements.forEach(function (element) { if (!element) { return; } element.classList.remove('animate__animated', 'animate__infinite'); element.classList.remove('animate__repeat-1', 'animate__repeat-2', 'animate__repeat-3'); Array.prototype.slice.call(element.classList || []).forEach(function (className) { if (String(className || '').indexOf('animate__') === 0) { element.classList.remove(className); } }); element.style.removeProperty('--animate-duration'); element.style.removeProperty('--animate-delay'); element.style.removeProperty('--animate-repeat'); }); } function isAttentionSeekerAnimation(preset) { return ['bounce', 'flash', 'pulse', 'rubberBand', 'shakeX', 'shakeY', 'headShake', 'swing', 'tada', 'wobble', 'jello', 'heartBeat'].indexOf(String(preset || '').trim()) !== -1; } function applyAnimationStep(element, step, phase) { if (!element || !step) { return; } var preset = String(step.preset || 'none').trim(); if (!preset || preset === 'none') { return; } element.classList.add('animate__animated', 'animate__' + preset); element.style.setProperty('--animate-duration', String(Math.max(1, Number(step.duration_ms || 0) || 1000)) + 'ms'); if (Number(step.delay_ms || 0) > 0) { element.style.setProperty('--animate-delay', String(Math.max(0, Number(step.delay_ms || 0))) + 'ms'); } else { element.style.removeProperty('--animate-delay'); } if (phase === 'loop') { var repeatCount = Number(step.iterations); if (!Number.isFinite(repeatCount) || repeatCount < 1) { repeatCount = 1; } if (repeatCount > 1) { element.classList.add('animate__repeat-1'); } element.style.setProperty('--animate-repeat', String(repeatCount)); return; } if (isAttentionSeekerAnimation(preset) && Number(step.iterations || 0) > 1) { element.style.setProperty('--animate-repeat', String(Math.max(1, Number(step.iterations || 1)))); } } function getAnimationStepTimingMs(step) { if (!step) { return 0; } var preset = String(step.preset || 'none').trim(); if (!preset || preset === 'none') { return 0; } var durationMs = Number(step.duration_ms); if (!Number.isFinite(durationMs) || durationMs <= 0) { durationMs = 1000; } var delayMs = Number(step.delay_ms); if (!Number.isFinite(delayMs) || delayMs < 0) { delayMs = 0; } var iterations = Number(step.iterations); if (!Number.isFinite(iterations) || iterations < 1) { iterations = 1; } return delayMs + (durationMs * iterations); } function getRegionAnimationPhaseTimingMs(root, phase) { var timings = getRegionAnimationPhaseTimings(root, phase); return timings.reduce(function (maxTimingMs, entry) { return Math.max(maxTimingMs, Number(entry && entry.timingMs || 0)); }, 0); } function getRegionAnimationPhaseTimings(root, phase) { if (!root || isThumbnailPreview()) { return []; } var normalizedPhase = String(phase || 'intro').trim().toLowerCase(); if (normalizedPhase !== 'intro' && normalizedPhase !== 'outro') { normalizedPhase = 'intro'; } var timings = []; Array.prototype.forEach.call(root.querySelectorAll('[data-animation-json]'), function (element) { if (!element) { return; } var config; try { config = normalizePlayerAnimationConfig(element.getAttribute('data-animation-json') || '{}'); } catch (_error) { config = null; } if (!config) { return; } var timingMs = getAnimationStepTimingMs(normalizedPhase === 'outro' ? config.outro : config.intro); if (timingMs > 0) { timings.push({ element: element, timingMs: timingMs }); } }); return timings; } function playRegionAnimation(element, phase) { if (!element || isThumbnailPreview()) { return; } var normalizedPhase = String(phase || 'intro').trim().toLowerCase(); if (normalizedPhase !== 'intro' && normalizedPhase !== 'outro') { normalizedPhase = 'intro'; } var config; try { config = normalizePlayerAnimationConfig(element.getAttribute('data-animation-json') || '{}'); } catch (_error) { config = null; } if (!config) { return; } element.dataset.animationPhase = normalizedPhase; clearRegionAnimationClasses(element); if (normalizedPhase === 'outro') { applyAnimationStep(element, config.outro, 'outro'); return; } if (config.intro && String(config.intro.preset || '').trim() && String(config.intro.preset || '').trim() !== 'none') { applyAnimationStep(element, config.intro, 'intro'); if (config.loop && String(config.loop.preset || '').trim() && String(config.loop.preset || '').trim() !== 'none') { element.addEventListener('animationend', function handleAnimationEnd(event) { if (event.target !== element) { return; } if (String(element.dataset.animationPhase || '').trim() !== 'intro') { return; } element.removeEventListener('animationend', handleAnimationEnd); clearRegionAnimationClasses(element); applyAnimationStep(element, config.loop, 'loop'); }); } return; } applyAnimationStep(element, config.loop, 'loop'); } function playRegionAnimations(root, phase) { if (!root || isThumbnailPreview()) { return; } var normalizedPhase = String(phase || 'intro').trim().toLowerCase(); if (normalizedPhase !== 'intro' && normalizedPhase !== 'outro') { normalizedPhase = 'intro'; } var elements = Array.prototype.slice.call(root.querySelectorAll('[data-animation-json]')); elements.forEach(function (element) { playRegionAnimation(element, normalizedPhase); }); } function setPlayerCanvasDimensions(canvasWidth, canvasHeight) { if (!document || !document.documentElement) { return; } var width = Math.max(1, Math.round(Number(canvasWidth) || 0) || 0); var height = Math.max(1, Math.round(Number(canvasHeight) || 0) || 0); document.documentElement.style.setProperty('--player-canvas-width', width + 'px'); document.documentElement.style.setProperty('--player-canvas-height', height + 'px'); } const ALLOWED_RICH_TEXT_TAGS = ['a', 'b', 'blockquote', 'br', 'code', 'col', 'colgroup', 'div', 'em', 'figure', 'figcaption', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'hr', 'i', 'img', 'li', 'ol', 'p', 'pre', 'span', 'strong', 'sub', 'sup', 'table', 'tbody', 'td', 'th', 'thead', 'tr', 'u', 'ul']; function sanitizeRichTextAttributes(tagName, attrText) { const allowedAttributes = { a: ['href', 'title', 'target', 'rel', 'class', 'style'], blockquote: ['class', 'style'], div: ['class', 'style'], figure: ['class', 'style'], figcaption: ['class', 'style'], h1: ['class', 'style'], h2: ['class', 'style'], h3: ['class', 'style'], h4: ['class', 'style'], h5: ['class', 'style'], h6: ['class', 'style'], img: ['src', 'alt', 'title', 'width', 'height', 'class', 'style', 'loading', 'decoding'], col: ['class', 'style', 'span', 'width'], colgroup: ['class', 'style', 'span'], li: ['class', 'style'], ol: ['class', 'style', 'start'], p: ['class', 'style'], pre: ['class', 'style'], span: ['class', 'style'], table: ['class', 'style'], tbody: ['class', 'style'], td: ['class', 'style', 'colspan', 'rowspan'], th: ['class', 'style', 'colspan', 'rowspan', 'scope'], thead: ['class', 'style'], tr: ['class', 'style'], ul: ['class', 'style'] }; const allowed = allowedAttributes[tagName] || []; if (!allowed.length) { return ''; } if (tagName === 'img') { const srcMatch = String(attrText || '').match(/\bsrc\s*=\s*("([^"]*)"|'([^']*)'|([^\s"'>/=`]+))/i); const srcValue = srcMatch ? String(srcMatch[2] !== undefined ? srcMatch[2] : srcMatch[3] !== undefined ? srcMatch[3] : srcMatch[4] !== undefined ? srcMatch[4] : '').trim() : ''; if (!srcValue || !/^(?:https?:\/\/|\/media\/|\/assets\/|\/[^/]|data:image\/)/i.test(srcValue)) { return ''; } } const attrs = []; String(attrText || '').replace(/([a-zA-Z0-9:-]+)(?:\s*=\s*("([^"]*)"|'([^']*)'|([^\s"'>/=`]+)))?/g, (_full, key, _valuePart, doubleQuoted, singleQuoted, bareValue) => { const lowerKey = String(key || '').toLowerCase(); if (!allowed.includes(lowerKey)) { return ''; } const value = doubleQuoted !== undefined ? doubleQuoted : singleQuoted !== undefined ? singleQuoted : bareValue !== undefined ? bareValue : ''; if (lowerKey === 'href' && /^(?:\s*javascript:|\s*data:)/i.test(String(value || ''))) { return ''; } if (lowerKey === 'style' && /(?:expression\s*\(|javascript:|url\s*\()/i.test(String(value || ''))) { return ''; } if (lowerKey === 'target') { const targetValue = String(value || '').trim(); if (targetValue === '_blank') { attrs.push(' target="_blank"'); if (!attrs.includes(' rel="noreferrer noopener"')) { attrs.push(' rel="noreferrer noopener"'); } return ''; } } attrs.push(' ' + lowerKey + '="' + escapeHtml(value) + '"'); return ''; }); return attrs.join(''); } // Remove unsafe markup while preserving richer rich-text formatting. function sanitizeRichText(html) { var output = String(html || ''); output = output.replace(//gi, ''); output = output.replace(//gi, ''); return output.replace(/<[^>]+>/g, function (tag) { var match = tag.match(/^<\s*(\/?)\s*([a-z0-9]+)([\s\S]*?)(\/?)>$/i); if (!match) { return ''; } var closing = Boolean(match[1]); var name = String(match[2] || '').toLowerCase(); var attrText = String(match[3] || ''); if (ALLOWED_RICH_TEXT_TAGS.indexOf(name) === -1) { return ''; } if (closing) { return ''; } return '<' + name + sanitizeRichTextAttributes(name, attrText) + '>'; }); } // Render a single Editor.js block to HTML. function renderEditorJsBlock(block) { if (!block || !block.type || !block.data) { return ''; } if (block.type === 'header') { var level = Math.max(1, Math.min(6, Number(block.data.level || 2))); return '' + sanitizeRichText(block.data.text || '') + ''; } if (block.type === 'list') { var tag = block.data.style === 'ordered' ? 'ol' : 'ul'; var 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(function (item) { return renderEditorJsListItem(item, tag); }).join('') + ''; } if (block.type === 'delimiter') { return '
'; } if (block.type === 'code') { return '
' + escapeHtml(block.data.code || '') + '
'; } if (block.type === 'table') { return renderEditorJsTable(block.data); } if (block.type === 'paragraph') { return '

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

'; } return ''; } // Render a list item and any nested sub-items. function renderEditorJsListItem(item, tag) { if (item && typeof item === 'object') { var content = item.content !== undefined ? item.content : (item.text !== undefined ? item.text : item.value !== undefined ? item.value : ''); var children = Array.isArray(item.items) ? item.items : Array.isArray(item.subItems) ? item.subItems : Array.isArray(item.children) ? item.children : []; var nested = children.length ? '<' + tag + ' style="list-style-type:' + (tag === 'ol' ? 'decimal' : 'disc') + ';padding-left:1.4em;">' + children.map(function (child) { return renderEditorJsListItem(child, tag); }).join('') + '' : ''; return '
  • ' + sanitizeRichText(content || '') + nested + '
  • '; } return '
  • ' + sanitizeRichText(item || '') + '
  • '; } // Render an Editor.js table block. function renderEditorJsTable(data) { var rows = Array.isArray(data.content) ? data.content : Array.isArray(data.rows) ? data.rows : []; if (!rows.length) { return ''; } var hasHeadings = Boolean(data.withHeadings); var tableRows = rows.map(function (row, rowIndex) { var cells = Array.isArray(row) ? row : []; var cellTag = hasHeadings && rowIndex === 0 ? 'th' : 'td'; var cellAttrs = cellTag === 'th' ? ' scope="col"' : ''; return '' + cells.map(function (cell) { return '<' + cellTag + cellAttrs + '>' + sanitizeRichText(cell || '') + ''; }).join('') + ''; }).join(''); return '' + tableRows + '
    '; } function wrapRichTextParagraph(html) { var raw = String(html || '').trim(); if (!raw) { return ''; } if (/^<\s*(?:p|div|h[1-6]|ul|ol|pre|table|blockquote|figure|hr|li)\b/i.test(raw)) { return raw; } return '

    ' + raw + '

    '; } // Render Editor.js JSON or plain content safely. 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); } } var raw = String(value || ''); try { var 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 wrapRichTextParagraph(sanitizeRichText(raw)); } // Parse string values that look like JSON. function parseMaybeJson(value) { if (typeof value !== 'string') { return value; } var raw = value.trim(); if (!raw) { return value; } if (raw.charAt(0) !== '{' && raw.charAt(0) !== '[') { return value; } try { return JSON.parse(raw); } catch (_error) { return value; } } // Normalize a slide region's stored content value. function normalizeContentValue(value) { var normalized; if (!value || typeof value !== 'object') { return { type: 'text', value: parseMaybeJson(value) }; } normalized = {}; Object.keys(value).forEach(function (key) { normalized[key] = value[key]; }); if (normalized.value !== undefined) { normalized.value = parseMaybeJson(normalized.value); } if (normalized.font_family !== undefined && normalized.font_family !== null) { normalized.font_family = sanitizeFontFamily(normalized.font_family); } if (normalized.font_size !== undefined && normalized.font_size !== null) { normalized.font_size = sanitizeFontSize(normalized.font_size); } if (normalized.font_color !== undefined && normalized.font_color !== null) { normalized.font_color = sanitizeTextColor(normalized.font_color); } if (!normalized.type) { normalized.type = 'text'; } return normalized; } // Normalize a slide and its nested region content. function normalizeSlide(slide) { var normalized = {}; var content; Object.keys(slide || {}).forEach(function (key) { normalized[key] = slide[key]; }); normalized.content = {}; content = slide && slide.content && typeof slide.content === 'object' ? slide.content : {}; Object.keys(content).forEach(function (regionKey) { normalized.content[regionKey] = normalizeContentValue(content[regionKey]); }); return normalized; } // Parse the stored schedule-day list into numbers. function parseScheduleDays(value) { if (!value) { return []; } if (Array.isArray(value)) { return value.map(function (item) { return Number(item); }).filter(function (item) { return !Number.isNaN(item); }); } try { const parsed = JSON.parse(value); return Array.isArray(parsed) ? parsed.map(function (item) { return Number(item); }).filter(function (item) { return !Number.isNaN(item); }) : []; } catch (_error) { return []; } } // Convert a HH:MM time string to minutes since midnight. function parseTimeToMinutes(value) { const raw = String(value || '').trim(); if (!raw) { return null; } const match = raw.match(/^(\d{2}):(\d{2})/); if (!match) { return null; } return Number(match[1]) * 60 + Number(match[2]); } function normalizeScheduleRule(rule) { const input = rule && typeof rule === 'object' ? rule : {}; const startDatetime = String(input.start_datetime || input.startDateTime || '').trim(); const endDatetime = String(input.end_datetime || input.endDateTime || '').trim(); const startTime = String(input.start_time || input.startTime || '').trim(); const endTime = String(input.end_time || input.endTime || '').trim(); const days = parseScheduleDays(input.days); if (startDatetime || endDatetime) { if (!startDatetime || !endDatetime) { return null; } } if (startTime || endTime) { if (!startTime || !endTime) { return null; } } if (!startDatetime && !endDatetime && !startTime && !endTime && !days.length) { return null; } return { start_datetime: startDatetime || null, end_datetime: endDatetime || null, start_time: startTime || null, end_time: endTime || null, days: days }; } function parseScheduleRules(value) { let rawRules = []; if (Array.isArray(value)) { rawRules = value; } else { const raw = String(value || '').trim(); if (!raw) { return []; } try { const parsed = JSON.parse(raw); rawRules = Array.isArray(parsed) ? parsed : []; } catch (_error) { return []; } } return rawRules.map(normalizeScheduleRule).filter(Boolean); } function matchesScheduleRule(rule, now) { const normalized = normalizeScheduleRule(rule); if (!normalized) { return false; } if (normalized.start_datetime && normalized.end_datetime) { const start = new Date(normalized.start_datetime); const end = new Date(normalized.end_datetime); if (Number.isNaN(start.getTime()) || Number.isNaN(end.getTime())) { return false; } if (now < start || now > end) { return false; } } if (normalized.days.length && normalized.days.indexOf(now.getDay()) === -1) { return false; } if (normalized.start_time && normalized.end_time) { const startMinutes = parseTimeToMinutes(normalized.start_time); const endMinutes = parseTimeToMinutes(normalized.end_time); if (startMinutes === null || endMinutes === null) { return false; } const nowMinutes = now.getHours() * 60 + now.getMinutes(); if (startMinutes <= endMinutes) { return nowMinutes >= startMinutes && nowMinutes <= endMinutes; } return nowMinutes >= startMinutes || nowMinutes <= endMinutes; } return true; } // Determine whether a slide should be shown at the current time. function isSlideActive(slide, now) { const rules = Array.isArray(slide.scheduleRules) ? slide.scheduleRules : []; if (!rules.length) { return true; } return rules.some(function (rule) { return matchesScheduleRule(rule, now); }); } // Build the cache key for a template layout. function getTemplateLayoutCacheKey(template) { var viewportKey = window.innerWidth + 'x' + window.innerHeight; return [currentPlaylistSignature || '', template && template.id ? template.id : '', viewportKey].join('|'); } // Build or reuse layout metadata for a template. function getTemplateLayout(template) { if (!template || !template.id) { return null; } syncRenderCacheViewport(); var cacheKey = getTemplateLayoutCacheKey(template); if (Object.prototype.hasOwnProperty.call(templateLayoutCache, cacheKey)) { return templateLayoutCache[cacheKey]; } var templateCanvas = getTemplateCanvasSize(template); var canvasSize = fitCanvasSize(templateCanvas.width, templateCanvas.height, window.innerWidth, window.innerHeight); var canvasScale = canvasSize.width / templateCanvas.width; var regions = (template.regions || []).map(function (region) { var left = (Number(region.x) / templateCanvas.width) * 100; var top = (Number(region.y) / templateCanvas.height) * 100; var width = (Number(region.width) / templateCanvas.width) * 100; var height = (Number(region.height) / templateCanvas.height) * 100; var baseStyle = 'left:' + left + '%;top:' + top + '%;width:' + width + '%;height:' + height + '%;z-index:' + Number(region.z_index || 0) + ';'; var pixelWidth = Math.max(1, Math.round(Number(region.width || 0) || 1)); var pixelHeight = Math.max(1, Math.round(Number(region.height || 0) || 1)); return { regionKey: region.region_key, regionType: region.region_type, label: region.label, animationJson: region.animation_json || null, baseStyle: baseStyle, pixelWidth: pixelWidth, pixelHeight: pixelHeight, fontFamily: region.font_family || null, fontSize: region.font_size || null, fontColor: region.font_color || null, canvasScale: canvasScale }; }); var layout = { canvasWidth: canvasSize.width, canvasHeight: canvasSize.height, background: template.background_image_path ? '' : '', backgroundColor: template.background_color || '#111111', regions: regions }; templateLayoutCache[cacheKey] = layout; return layout; } // Build a dark backdrop style for template and media canvases. function buildBackdropStyle(backgroundColor, backgroundImagePath) { var color = String(backgroundColor || '#111111').trim() || '#111111'; var style = 'background-color:' + escapeHtml(color) + ';'; if (backgroundImagePath) { style += 'background-image:url("' + escapeHtml(backgroundImagePath) + '");'; style += 'background-position:center;background-size:contain;background-repeat:no-repeat;'; return style; } style += 'background-image:none;background-position:center;background-size:cover;background-repeat:no-repeat;'; return style; } // Build the cache key for a template render plan. function getTemplateRenderPlanCacheKey(template) { return getTemplateLayoutCacheKey(template); } // Build or reuse the render plan for a template. function getTemplateRenderPlan(template) { if (!template || !template.id) { return null; } syncRenderCacheViewport(); var cacheKey = getTemplateRenderPlanCacheKey(template); if (Object.prototype.hasOwnProperty.call(templateRenderPlanCache, cacheKey)) { return templateRenderPlanCache[cacheKey]; } var layout = getTemplateLayout(template); function getPlayerRegionModule(regionType) { return window.pulsePlayerRegionTypes && typeof window.pulsePlayerRegionTypes.get === 'function' ? window.pulsePlayerRegionTypes.get(regionType) : null; } var plan = { layout: layout, renderRegion: function (region, regionContent) { var regionModule = getPlayerRegionModule(region.regionType); if (regionModule && typeof regionModule.renderRegion === 'function') { return regionModule.renderRegion(region, regionContent); } if (region.regionType === 'image') { return renderImageRegion(region, regionContent); } if (region.regionType === 'video') { return renderVideoRegion(region, regionContent); } if (region.regionType === 'webpage') { return renderWebpageRegion(region, regionContent); } if (region.regionType === 'rtmp') { return renderRtmpRegion(region, regionContent); } if (region.regionType === 'rss') { return renderRssRegion(region, regionContent); } if (region.regionType === 'api') { return renderApiRegion(region, regionContent); } if (region.regionType === 'html') { return renderHtmlRegion(region, regionContent); } return renderTextRegion(region, regionContent); } }; templateRenderPlanCache[cacheKey] = plan; return plan; } // Render a template-based slide using the cached layout. function renderTemplateSlideMarkup(slide) { const template = slide.template; const content = slide.content || {}; const plan = getTemplateRenderPlan(template); const layout = plan ? plan.layout : null; const regions = layout ? layout.regions.map(function (region) { const regionContent = content[region.regionKey] || {}; const markup = plan.renderRegion(region, regionContent); return decorateRegionMarkup(markup, { animationConfig: normalizePlayerAnimationConfig(region.animationJson) }); }).join('') : ''; const stageStyle = layout ? buildBackdropStyle(layout.backgroundColor || '#111111', template.background_image_path) : ''; if (layout) { setPlayerCanvasDimensions(layout.canvasWidth, layout.canvasHeight); } return renderSlideShell(slide, '', (layout ? layout.canvasWidth : 0) + 'px', (layout ? layout.canvasHeight : 0) + 'px', '
    ' + (layout ? layout.background : '') + regions + '
    '); } // Media rendering helpers. // Build the direct media element for a slide. function renderMediaSlideContent(slide) { if (slide.kind === 'image') { return 'slide'; } return ''; } // Render a slide that contains direct media content. function renderMediaSlideMarkup(slide) { const canvasSize = fitCanvasSize(16, 9, window.innerWidth, window.innerHeight); const media = renderMediaSlideContent(slide); const canvasStyle = slide.kind === 'image' ? buildBackdropStyle('#111111', slide.media_url) : ''; setPlayerCanvasDimensions(canvasSize.width, canvasSize.height); return renderSlideShell(slide, 'slide-media', canvasSize.width + 'px', canvasSize.height + 'px', media, canvasStyle); } // Render the shared slide shell around slide-specific inner content. function renderSlideShell(slide, canvasClass, canvasWidth, canvasHeight, innerHtml, canvasStyle) { const body = slide.body ? '
    ' + escapeHtml(slide.body) + '
    ' : ''; const className = canvasClass ? 'slide-canvas ' + canvasClass : 'slide-canvas'; const style = 'width:' + canvasWidth + ';height:' + canvasHeight + ';' + (canvasStyle || ''); return '
    ' + innerHtml + body + '
    '; } // Build the cache key for rendered slide markup. function getSlideMarkupCacheKey(slide) { var viewportKey = window.innerWidth + 'x' + window.innerHeight; return [currentPlaylistSignature || '', slide && slide.id ? slide.id : '', slide && slide.template_id ? slide.template_id : '', slide && slide.modified_at ? slide.modified_at : '', viewportKey, videoRegionRenderVersion || 0].join('|'); } function restorePlayerCanvasDimensions(width, height) { if (!document || !document.documentElement) { return; } var style = document.documentElement.style; if (width) { style.setProperty('--player-canvas-width', width); } else { style.removeProperty('--player-canvas-width'); } if (height) { style.setProperty('--player-canvas-height', height); } else { style.removeProperty('--player-canvas-height'); } } function primeSlideMarkup(slide) { if (!slide) { return ''; } var cachedMarkup = getCachedSlideMarkup(slide); if (cachedMarkup) { return cachedMarkup; } var previousWidth = document && document.documentElement && document.documentElement.style ? String(document.documentElement.style.getPropertyValue('--player-canvas-width') || '') : ''; var previousHeight = document && document.documentElement && document.documentElement.style ? String(document.documentElement.style.getPropertyValue('--player-canvas-height') || '') : ''; try { return buildSlideMarkupForState(slide, false); } finally { restorePlayerCanvasDimensions(previousWidth.trim(), previousHeight.trim()); } } function notifyVideoRegionSourceReady() { if (typeof videoRegionRenderVersion === 'number') { videoRegionRenderVersion += 1; } slideMarkupCache = Object.create(null); if (typeof showCurrent === 'function' && slides && slides.length) { showCurrent(); } } window.notifyVideoRegionSourceReady = notifyVideoRegionSourceReady; // Look up a previously rendered slide in the cache. function getCachedSlideMarkup(slide) { var cacheKey = getSlideMarkupCacheKey(slide); return Object.prototype.hasOwnProperty.call(slideMarkupCache, cacheKey) ? slideMarkupCache[cacheKey] : null; } // Store rendered slide markup in the cache. function setCachedSlideMarkup(slide, markup) { syncRenderCacheViewport(); slideMarkupCache[getSlideMarkupCacheKey(slide)] = markup; } // Slide rendering and markup cache helpers. // Choose the right slide renderer and cache the result. function buildSlideMarkupForState(slide, updateCurrentState) { if (updateCurrentState) { lastRenderedSlide = slide || null; syncBlackoutState(); } var cachedMarkup = getCachedSlideMarkup(slide); if (cachedMarkup) { return cachedMarkup; } var markup = ''; if (slide.template_id && slide.template) { markup = renderTemplateSlideMarkup(slide); setCachedSlideMarkup(slide, markup); return markup; } markup = renderMediaSlideMarkup(slide); setCachedSlideMarkup(slide, markup); return markup; } function buildSlideMarkup(slide) { return buildSlideMarkupForState(slide, true); }