571 lines
20 KiB
JavaScript
571 lines
20 KiB
JavaScript
// 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)
|
|
};
|
|
}
|
|
|
|
const ALLOWED_RICH_TEXT_TAGS = ['a', 'b', 'blockquote', 'br', 'code', 'div', 'em', 'figure', 'figcaption', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'hr', 'i', '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'],
|
|
li: ['class', 'style'],
|
|
ol: ['class', 'style', 'start'],
|
|
p: ['class', 'style'],
|
|
pre: ['class', 'style'],
|
|
span: ['class', 'style'],
|
|
table: ['class', 'style'],
|
|
td: ['class', 'style', 'colspan', 'rowspan'],
|
|
th: ['class', 'style', 'colspan', 'rowspan', 'scope'],
|
|
tr: ['class', 'style'],
|
|
ul: ['class', 'style']
|
|
};
|
|
const allowed = allowedAttributes[tagName] || [];
|
|
if (!allowed.length) {
|
|
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 CKEditor formatting.
|
|
function sanitizeRichText(html) {
|
|
var output = String(html || '');
|
|
output = output.replace(/<script[\s\S]*?<\/script>/gi, '');
|
|
output = output.replace(/<style[\s\S]*?<\/style>/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 '</' + name + '>';
|
|
}
|
|
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 '<h' + level + '>' + sanitizeRichText(block.data.text || '') + '</h' + level + '>';
|
|
}
|
|
|
|
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('') + '</' + tag + '>';
|
|
}
|
|
|
|
if (block.type === 'delimiter') {
|
|
return '<hr />';
|
|
}
|
|
|
|
if (block.type === 'code') {
|
|
return '<pre><code>' + escapeHtml(block.data.code || '') + '</code></pre>';
|
|
}
|
|
|
|
if (block.type === 'table') {
|
|
return renderEditorJsTable(block.data);
|
|
}
|
|
|
|
if (block.type === 'paragraph') {
|
|
return '<p>' + sanitizeRichText(block.data.text || '') + '</p>';
|
|
}
|
|
|
|
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('') + '</' + tag + '>' : '';
|
|
return '<li>' + sanitizeRichText(content || '') + nested + '</li>';
|
|
}
|
|
return '<li>' + sanitizeRichText(item || '') + '</li>';
|
|
}
|
|
|
|
// 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 '<tr>' + cells.map(function (cell) {
|
|
return '<' + cellTag + cellAttrs + '>' + sanitizeRichText(cell || '') + '</' + cellTag + '>';
|
|
}).join('') + '</tr>';
|
|
}).join('');
|
|
return '<table class="ck-content-table">' + tableRows + '</table>';
|
|
}
|
|
|
|
// 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 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]);
|
|
}
|
|
|
|
// Determine whether a slide should be shown at the current time.
|
|
function isSlideActive(slide, now) {
|
|
const mode = String(slide.schedule_mode || 'always');
|
|
if (mode === 'always') {
|
|
return true;
|
|
}
|
|
if (mode === 'dates') {
|
|
const start = slide.schedule_start_datetime ? new Date(slide.schedule_start_datetime) : null;
|
|
const end = slide.schedule_end_datetime ? new Date(slide.schedule_end_datetime) : null;
|
|
if (!start || !end || Number.isNaN(start.getTime()) || Number.isNaN(end.getTime())) {
|
|
return false;
|
|
}
|
|
return now >= start && now <= end;
|
|
}
|
|
if (mode === 'times') {
|
|
const days = parseScheduleDays(slide.schedule_days_json);
|
|
if (!days.length) {
|
|
return false;
|
|
}
|
|
const day = now.getDay();
|
|
if (days.indexOf(day) === -1) {
|
|
return false;
|
|
}
|
|
const startMinutes = parseTimeToMinutes(slide.schedule_start_time);
|
|
const endMinutes = parseTimeToMinutes(slide.schedule_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;
|
|
}
|
|
// 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,
|
|
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 ? '<img class="template-background" src="' + escapeHtml(template.background_image_path) + '" alt="" />' : '',
|
|
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) + ';';
|
|
var gradient = 'linear-gradient(rgba(0, 0, 0, 0.42), rgba(0, 0, 0, 0.42))';
|
|
|
|
if (backgroundImagePath) {
|
|
style += 'background-image:' + gradient + ',url("' + escapeHtml(backgroundImagePath) + '");';
|
|
style += 'background-position:center,center;background-size:100% 100%,cover;background-repeat:no-repeat,no-repeat;';
|
|
return style;
|
|
}
|
|
|
|
style += 'background-image:' + gradient + ';background-position:center;background-size:100% 100%;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);
|
|
var plan = {
|
|
layout: layout,
|
|
renderRegion: function (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] || {};
|
|
return plan.renderRegion(region, regionContent);
|
|
}).join('') : '';
|
|
const stageStyle = layout ? buildBackdropStyle(layout.backgroundColor || '#111111', template.background_image_path) : '';
|
|
return renderSlideShell(slide, '', (layout ? layout.canvasWidth : 0) + 'px', (layout ? layout.canvasHeight : 0) + 'px', '<div class="template-stage" style="' + stageStyle + '">' + (layout ? layout.background : '') + regions + '</div>');
|
|
}
|
|
|
|
// Media rendering helpers.
|
|
// Build the direct media element for a slide.
|
|
function renderMediaSlideContent(slide) {
|
|
if (slide.kind === 'image') {
|
|
return '<img src="' + escapeHtml(slide.media_url) + '" alt="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) : '';
|
|
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 ? '<div class="body">' + escapeHtml(slide.body) + '</div>' : '';
|
|
const className = canvasClass ? 'slide-canvas ' + canvasClass : 'slide-canvas';
|
|
const style = 'width:' + canvasWidth + ';height:' + canvasHeight + ';' + (canvasStyle || '');
|
|
return '<div class="slide"><div class="' + className + '" style="' + style + '">' + innerHtml + body + '</div></div>';
|
|
}
|
|
|
|
// 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 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 buildSlideMarkup(slide) {
|
|
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;
|
|
}
|