Release v2.2.0

This commit is contained in:
2026-08-01 21:41:44 +01:00
parent c643d2fb07
commit d6417b667c
673 changed files with 146752 additions and 7389 deletions
+82 -5
View File
@@ -1,3 +1,5 @@
// Slide data access helpers, including rich-text normalization and payload building.
const { fetchTemplateById } = require('./templates');
const { parseJsonSafe } = require('./utils');
@@ -25,6 +27,25 @@ function sanitizeRichText(html) {
});
}
function normalizePlainText(value) {
return String(value === undefined || value === null ? '' : value)
.replace(/<\s*br\s*\/?\s*>/gi, '\n')
.replace(/<[^>]*>/g, '')
.replace(/&nbsp;/gi, ' ')
.trim();
}
function stripEditorOnlyMarkup(value) {
return String(value || '')
.replace(/<pre[^>]*class="[^"]*api-region-sample-preview[^"]*"[^>]*>[\s\S]*?<\/pre>/gi, '')
.replace(/<details[^>]*class="[^"]*api-region-sample-accordion[^"]*"[^>]*>[\s\S]*?<\/details>/gi, '')
.trim();
}
function normalizeEditorMarkup(value) {
return String(value === undefined || value === null ? '' : value).trim();
}
async function fetchSlideById(pool, id) {
const [slides] = await pool.query(`
SELECT s.id, s.title, s.template_id, s.content_json, s.thumbnail_path, s.created_at, s.modified_at,
@@ -53,6 +74,14 @@ function getFilesByField(files) {
return map;
}
function getSubmittedValue(body, key, fallback) {
if (body && Object.prototype.hasOwnProperty.call(body, key)) {
return String(body[key] || '').trim();
}
return fallback;
}
function sanitizeTextColor(value, fallback) {
const raw = String(value || '').trim();
if (/^#[0-9a-fA-F]{6}$/.test(raw) || /^#[0-9a-fA-F]{3}$/.test(raw)) {
@@ -88,9 +117,10 @@ function buildTemplateContent(template, body, filesByField, existingContent) {
if (region.region_type === 'image') {
const uploaded = filesByField[`region_image_${region.id}`];
const existing = body[`existing_region_image_${region.id}`];
const current = existingContent && existingContent[region.region_key] ? existingContent[region.region_key].value : '';
content[region.region_key] = {
type: 'image',
value: uploaded ? `/media/uploads/${uploaded.filename}` : String(existing || '').trim() || ((existingContent && existingContent[region.region_key]) ? existingContent[region.region_key].value : '')
value: uploaded ? `/media/uploads/${uploaded.filename}` : getSubmittedValue(body, `existing_region_image_${region.id}`, current)
};
} else if (region.region_type === 'video') {
const uploaded = filesByField[`region_video_${region.id}`];
@@ -99,9 +129,10 @@ function buildTemplateContent(template, body, filesByField, existingContent) {
const existingDuration = existingContent && existingContent[region.region_key] ? Number(existingContent[region.region_key].duration_seconds || 0) : 0;
const parsedDuration = Number(durationValue || existingDuration || 0);
const normalizedDuration = Math.round(parsedDuration * 1000) / 1000;
const current = existingContent && existingContent[region.region_key] ? existingContent[region.region_key].value : '';
content[region.region_key] = {
type: 'video',
value: uploaded ? `/media/uploads/${uploaded.filename}` : String(existing || '').trim() || ((existingContent && existingContent[region.region_key]) ? existingContent[region.region_key].value : ''),
value: uploaded ? `/media/uploads/${uploaded.filename}` : getSubmittedValue(body, `existing_region_video_${region.id}`, current),
duration_seconds: Number.isFinite(normalizedDuration) && normalizedDuration > 0 ? normalizedDuration : null
};
} else if (region.region_type === 'webpage') {
@@ -126,6 +157,15 @@ function buildTemplateContent(template, body, filesByField, existingContent) {
type: 'html',
value: submitted === undefined ? current : String(submitted || '')
};
} else if (region.region_type === 'time-date') {
const submitted = body[`region_text_${region.id}`];
const current = existingContent && existingContent[region.region_key] ? existingContent[region.region_key] : {};
const timezoneValue = body[`region_timezone_${region.id}`];
content[region.region_key] = {
type: 'time-date',
value: submitted === undefined ? String(current.value !== undefined ? current.value : current.text !== undefined ? current.text : '') : String(submitted || ''),
timezone: timezoneValue === undefined || timezoneValue === null ? String(current.timezone || current.time_zone || '') : String(timezoneValue || '').trim()
};
} else if (region.region_type === 'rss') {
const submitted = body[`region_text_${region.id}`];
const current = existingContent && existingContent[region.region_key] ? existingContent[region.region_key] : {};
@@ -135,7 +175,7 @@ function buildTemplateContent(template, body, filesByField, existingContent) {
const parsedItemNumber = Math.max(1, Number(itemNumber || current.item_number || 1));
content[region.region_key] = {
type: 'rss',
value: submitted === undefined ? String(current.value || '') : String(submitted || ''),
value: stripEditorOnlyMarkup(normalizeEditorMarkup(submitted === undefined ? String(current.value || '') : String(submitted || ''))),
feed_id: feedId === undefined || feedId === null || feedId === '' ? (current.feed_id || null) : Number(feedId),
item_number: Number.isFinite(parsedItemNumber) && parsedItemNumber > 0 ? parsedItemNumber : 1,
variable_name: 'item',
@@ -149,24 +189,61 @@ function buildTemplateContent(template, body, filesByField, existingContent) {
const style = getTextRegionStyle(body, region, existingContent);
const sourceId = body[`region_api_source_id_${region.id}`];
const itemNumber = body[`region_api_item_number_${region.id}`];
const itemsPath = body[`region_api_items_path_${region.id}`];
const parsedItemNumber = Math.max(1, Number(itemNumber || current.item_number || 1));
content[region.region_key] = {
type: 'api',
value: submitted === undefined ? String(current.value || '') : String(submitted || ''),
value: stripEditorOnlyMarkup(normalizeEditorMarkup(submitted === undefined ? String(current.value || '') : String(submitted || ''))),
source_id: sourceId === undefined || sourceId === null || sourceId === '' ? (current.source_id || null) : Number(sourceId),
item_number: Number.isFinite(parsedItemNumber) && parsedItemNumber > 0 ? parsedItemNumber : 1,
items_path: itemsPath === undefined || itemsPath === null ? (current.items_path === undefined || current.items_path === null ? '' : String(current.items_path)) : String(itemsPath || '').trim(),
variable_name: 'item',
font_family: style.font_family,
font_size: style.font_size,
font_color: style.font_color
};
} else if (!['text', 'image', 'video', 'webpage', 'html', 'rtmp', 'rss', 'api'].includes(String(region.region_type || '').trim())) {
const current = existingContent && existingContent[region.region_key] && typeof existingContent[region.region_key] === 'object' ? existingContent[region.region_key] : {};
const suffix = '_' + region.id;
const generic = {};
Object.keys(body || {}).forEach((key) => {
if (!key.startsWith('region_') || !key.endsWith(suffix)) {
return;
}
const field = key.slice('region_'.length, -suffix.length);
if (!field || field === 'type' || field === 'key' || field === 'name' || field === 'label') {
return;
}
generic[field] = body[key];
});
Object.keys(filesByField || {}).forEach((fieldName) => {
if (!fieldName.startsWith('region_') || !fieldName.endsWith(suffix)) {
return;
}
const field = fieldName.slice('region_'.length, -suffix.length);
if (!field) {
return;
}
generic[field] = `/media/uploads/${filesByField[fieldName].filename}`;
});
Object.keys(current).forEach((key) => {
if (generic[key] === undefined) {
generic[key] = current[key];
}
});
generic.type = region.region_type;
content[region.region_key] = generic;
} else {
const submitted = body[`region_text_${region.id}`];
const current = existingContent && existingContent[region.region_key] ? existingContent[region.region_key].value : '';
const style = getTextRegionStyle(body, region, existingContent);
content[region.region_key] = {
type: 'text',
value: submitted === undefined ? current : String(submitted),
value: stripEditorOnlyMarkup(normalizeEditorMarkup(submitted === undefined ? current : String(submitted || ''))),
font_family: style.font_family,
font_size: style.font_size,
font_color: style.font_color