// Shared placeholder parsing and resolution helpers for browser-rendered API content. (function () { var root = window; var transformPattern = /^([a-z_][a-z0-9_]*)\((.*)\)$/i; var monthNamesShort = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; var monthNamesLong = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']; var dayNamesShort = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']; var dayNamesLong = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']; function resolvePath(value, path) { var current = value; if (!path) { return current; } String(path).split('.').forEach(function (segment) { if (current === undefined || current === null) { current = ''; return; } current = current[segment]; }); return current === undefined || current === null ? '' : current; } function splitExpressionSegments(value) { var segments = []; var current = ''; var depth = 0; String(value || '').split('').forEach(function (character) { if (character === '(') { depth += 1; } else if (character === ')' && depth > 0) { depth -= 1; } if (character === '.' && depth === 0) { segments.push(current); current = ''; return; } current += character; }); segments.push(current); return segments; } function parsePlaceholderExpression(expression) { var raw = String(expression || '').trim(); var segments = raw ? splitExpressionSegments(raw) : []; var transforms = []; while (segments.length) { var candidate = String(segments[segments.length - 1] || '').trim(); var match = candidate.match(transformPattern); if (!match) { break; } transforms.unshift({ name: String(match[1] || '').trim().toLowerCase(), args: match[2] ? splitTransformArgs(match[2]) : [] }); segments.pop(); } return { path: segments.join('.'), transforms: transforms }; } function splitTransformArgs(value) { var source = String(value || '').trim(); if (!source) { return []; } var args = []; var current = ''; var quote = ''; source.split('').forEach(function (character) { if ((character === '"' || character === '\'') && (!quote || quote === character)) { quote = quote ? '' : character; current += character; return; } if (character === ',' && !quote) { if (current.trim()) { args.push(current.trim()); } current = ''; return; } current += character; }); if (current.trim()) { args.push(current.trim()); } return args.map(function (item) { var normalized = String(item || '').trim(); if ((normalized[0] === '"' && normalized[normalized.length - 1] === '"') || (normalized[0] === '\'' && normalized[normalized.length - 1] === '\'')) { return normalized.slice(1, -1); } return normalized; }).filter(Boolean); } function padNumber(value, size) { var text = String(Math.abs(Number(value || 0))); while (text.length < size) { text = '0' + text; } return text; } function getDefaultTimeZone() { try { return Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC'; } catch (_error) { return 'UTC'; } } function getDateValue(value) { return value instanceof Date ? value : new Date(value); } function resolveTimeZone(value) { var raw = String(value || '').trim(); if (!raw) { return getDefaultTimeZone(); } try { new Intl.DateTimeFormat('en-GB', { timeZone: raw }).format(new Date()); return raw; } catch (_error) { return getDefaultTimeZone(); } } function getDatePartsFromLocalTime(date) { var dayPeriod = date.getHours() >= 12 ? 'PM' : 'AM'; return { year: String(date.getFullYear()), month: padNumber(date.getMonth() + 1, 2), day: padNumber(date.getDate(), 2), hour24: padNumber(date.getHours(), 2), hour12: padNumber(date.getHours() % 12 || 12, 2), minute: padNumber(date.getMinutes(), 2), second: padNumber(date.getSeconds(), 2), weekdayLong: dayNamesLong[date.getDay()], weekdayShort: dayNamesShort[date.getDay()], monthLong: monthNamesLong[date.getMonth()], monthShort: monthNamesShort[date.getMonth()], dayPeriod: dayPeriod }; } function getDatePartsFromTimeZone(date, timeZone) { var resolvedTimeZone = resolveTimeZone(timeZone); var baseFormatter = new Intl.DateTimeFormat('en-GB', { timeZone: resolvedTimeZone, hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit', day: '2-digit', month: '2-digit', year: 'numeric' }); var weekdayLongFormatter = new Intl.DateTimeFormat('en-GB', { timeZone: resolvedTimeZone, weekday: 'long' }); var weekdayShortFormatter = new Intl.DateTimeFormat('en-GB', { timeZone: resolvedTimeZone, weekday: 'short' }); var monthLongFormatter = new Intl.DateTimeFormat('en-GB', { timeZone: resolvedTimeZone, month: 'long' }); var monthShortFormatter = new Intl.DateTimeFormat('en-GB', { timeZone: resolvedTimeZone, month: 'short' }); var ampmFormatter = new Intl.DateTimeFormat('en-GB', { timeZone: resolvedTimeZone, hour12: true, hour: '2-digit', minute: '2-digit' }); var numericParts = baseFormatter.formatToParts(date); var weekdayLong = weekdayLongFormatter.formatToParts(date); var weekdayShort = weekdayShortFormatter.formatToParts(date); var monthLong = monthLongFormatter.formatToParts(date); var monthShort = monthShortFormatter.formatToParts(date); var ampm = ampmFormatter.formatToParts(date); function getPart(parts, type) { var match = parts.find(function (part) { return part && part.type === type; }); return match ? String(match.value || '') : ''; } var dayPeriod = getPart(ampm, 'dayPeriod'); return { year: getPart(numericParts, 'year'), month: getPart(numericParts, 'month'), day: getPart(numericParts, 'day'), hour24: getPart(numericParts, 'hour'), hour12: getPart(ampm, 'hour'), minute: getPart(numericParts, 'minute'), second: getPart(numericParts, 'second'), weekdayLong: getPart(weekdayLong, 'weekday'), weekdayShort: getPart(weekdayShort, 'weekday'), monthLong: getPart(monthLong, 'month'), monthShort: getPart(monthShort, 'month'), dayPeriod: String(dayPeriod || '').toUpperCase(), timeZone: resolvedTimeZone }; } function formatDateValue(value, pattern, timeZone) { var date = getDateValue(value); if (Number.isNaN(date.getTime())) { return ''; } var format = String(pattern || 'YYYY-MM-DD HH:mm').trim() || 'YYYY-MM-DD HH:mm'; var parts = timeZone ? getDatePartsFromTimeZone(date, timeZone) : getDatePartsFromLocalTime(date); var hours24 = parts.hour24; var hours12 = parts.hour12; var tokenMap = { YYYY: parts.year, YY: parts.year.slice(-2), MMMM: parts.monthLong, MMM: parts.monthShort, MM: parts.month, M: String(Number(parts.month) || 0), DD: parts.day, D: String(Number(parts.day) || 0), dddd: parts.weekdayLong, ddd: parts.weekdayShort, HH: hours24, H: String(Number(hours24) || 0), hh: hours12, h: String(Number(hours12) || 0), mm: parts.minute, m: String(Number(parts.minute) || 0), ss: parts.second, s: String(Number(parts.second) || 0), A: parts.dayPeriod, a: String(parts.dayPeriod || '').toLowerCase() }; return format.replace(/\[([^\]]+)\]|YYYY|YY|MMMM|MMM|MM|M|DD|D|dddd|ddd|HH|H|hh|h|mm|m|ss|s|A|a/g, function (match, literal) { return literal || tokenMap[match] || match; }); } function getTimeZoneShortName(value, timeZone) { var date = getDateValue(value); if (Number.isNaN(date.getTime())) { return ''; } var resolvedTimeZone = resolveTimeZone(timeZone); try { var parts = new Intl.DateTimeFormat('en-GB', { timeZone: resolvedTimeZone, timeZoneName: 'short' }).formatToParts(date); var match = parts.find(function (part) { return part && part.type === 'timeZoneName'; }); return match ? String(match.value || '') : resolvedTimeZone; } catch (_error) { return resolvedTimeZone; } } function getTimeZoneLongName(timeZone) { return getTransformTimeZone([], { timeZone: timeZone }); } function getTransformTimeZone(args, options) { return resolveTimeZone((args && args[0]) || (options && options.timeZone) || ''); } function toNumericValue(value) { if (value && typeof value === 'object') { var numericKeys = ['value', 'amount', 'current', 'total', 'goal', 'raised']; for (var keyIndex = 0; keyIndex < numericKeys.length; keyIndex += 1) { var nestedValue = value[numericKeys[keyIndex]]; if (nestedValue !== undefined && nestedValue !== null && nestedValue !== value) { var nestedNumber = toNumericValue(nestedValue); if (Number.isFinite(nestedNumber)) { return nestedNumber; } } } return NaN; } var normalized = String(value === undefined || value === null ? '' : value).replace(/[^0-9.eE+-]/g, ''); var number = Number(normalized); return Number.isFinite(number) ? number : NaN; } function applyTransform(value, transform, options) { var text = String(value === undefined || value === null ? '' : value); var name = String(transform && transform.name || '').trim().toLowerCase(); var args = Array.isArray(transform && transform.args) ? transform.args : []; if (name === 'lower') { return text.toLowerCase(); } if (name === 'upper') { return text.toUpperCase(); } if (name === 'title') { return text.toLowerCase().replace(/\b([a-z])/g, function (match, letter) { return String(letter || '').toUpperCase(); }); } if (name === 'format' || name === 'date' || name === 'datetime' || name === 'time') { return formatDateValue(value, args[0] || (name === 'time' ? 'h:mm A' : name === 'date' ? 'MMM D, YYYY' : 'MMM D, YYYY h:mm A'), getTransformTimeZone(args.slice(1), options)); } if (name === 'tz') { return getTimeZoneShortName(value, getTransformTimeZone(args, options)); } if (name === 'tz_long') { return getTimeZoneLongName(getTransformTimeZone(args, options)); } if (name === 'add' || name === 'subtract' || name === 'multiply' || name === 'divide') { var arithmeticValue = toNumericValue(value); var operand = toNumericValue(args[0]); if (!Number.isFinite(arithmeticValue) || !Number.isFinite(operand) || (name === 'divide' && operand === 0)) { return value; } if (name === 'add') return arithmeticValue + operand; if (name === 'subtract') return arithmeticValue - operand; if (name === 'multiply') return arithmeticValue * operand; return arithmeticValue / operand; } return text; } function resolvePlaceholderExpression(value, expression, options) { var parsed = parsePlaceholderExpression(expression); var resolved = resolvePath(value, parsed.path); parsed.transforms.forEach(function (transform) { resolved = applyTransform(resolved, transform, options || {}); }); return resolved; } function isImagePlaceholderExpression(expression) { var parsed = parsePlaceholderExpression(expression); return parsed.transforms.some(function (transform) { return transform && transform.name === 'image'; }); } function getImagePlaceholderConfig(expression) { var parsed = parsePlaceholderExpression(expression); var imageTransform = parsed.transforms.find(function (transform) { return transform && transform.name === 'image'; }); var args = imageTransform && Array.isArray(imageTransform.args) ? imageTransform.args : []; var width = Number(args[0]); var height = Number(args[1]); return { width: Number.isFinite(width) && width > 0 ? Math.round(width) : 0, height: Number.isFinite(height) && height > 0 ? Math.round(height) : 0 }; } function isProgressPlaceholderExpression(expression) { var parsed = parsePlaceholderExpression(expression); return parsed.transforms.some(function (transform) { return transform && transform.name === 'progress'; }); } function renderProgressPlaceholder(value, expression) { var parsed = parsePlaceholderExpression(expression); var transform = parsed.transforms.find(function (candidate) { return candidate && candidate.name === 'progress'; }); if (!transform || !value || typeof value !== 'object' || transform.args.length < 2) { return ''; } function toNumber(raw) { if (raw && typeof raw === 'object') { var numericKeys = ['value', 'amount', 'current', 'total', 'goal', 'raised']; for (var keyIndex = 0; keyIndex < numericKeys.length; keyIndex += 1) { var nestedValue = raw[numericKeys[keyIndex]]; if (nestedValue !== undefined && nestedValue !== null && nestedValue !== raw) { var nestedNumber = toNumber(nestedValue); if (Number.isFinite(nestedNumber)) { return nestedNumber; } } } return 0; } var normalized = String(raw === undefined || raw === null ? '' : raw).replace(/[^0-9.eE+-]/g, ''); var number = Number(normalized); return Number.isFinite(number) ? number : 0; } function resolveProgressValue(argument) { var raw = String(argument === undefined || argument === null ? '' : argument).trim(); var literal = raw.replace(/^(?:"([\s\S]*)"|'([\s\S]*)')$/, '$1$2'); if (/^[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:e[+-]?\d+)?$/i.test(literal)) { return literal; } return resolvePath(value, raw); } var startValue = resolveProgressValue(transform.args[0]); var endValue = resolveProgressValue(transform.args[1]); var startDate = startValue instanceof Date ? startValue : new Date(startValue); var endDate = endValue instanceof Date ? endValue : new Date(endValue); var percentage; if (typeof startValue === 'string' && typeof endValue === 'string' && /[-T]/.test(startValue) && /[-T]/.test(endValue) && !Number.isNaN(startDate.getTime()) && !Number.isNaN(endDate.getTime()) && endDate.getTime() > startDate.getTime()) { percentage = Math.max(0, Math.min(100, ((Date.now() - startDate.getTime()) / (endDate.getTime() - startDate.getTime())) * 100)); } else { var current = toNumber(startValue); var goal = toNumber(endValue); percentage = goal > 0 ? Math.max(0, Math.min(100, (current / goal) * 100)) : 0; } var roundedPercentage = Math.round(percentage * 10) / 10; var label = roundedPercentage + '%'; var variants = ['primary', 'secondary', 'success', 'danger', 'warning', 'info', 'light', 'dark']; var variantColors = { primary: '#0d6efd', secondary: '#6c757d', success: '#198754', danger: '#dc3545', warning: '#ffc107', info: '#0dcaf0', light: '#f8f9fa', dark: '#212529' }; var announcementColors = { orange: '#c84e10', amber: '#a56710', olive: '#5f7f0f', teal: '#12827d', sky: '#127caf', indigo: '#6f60ea', violet: '#9553db', fuchsia: '#b347be', pink: '#cd388d', navy: '#1d2d4c', steel: '#3a4860', slate: '#566577', graphite: '#32363c', midnight: '#1e1d2d' }; var variant = 'primary'; var barVariant = ''; var customColor = ''; var backgroundColor = ''; var colorCount = 0; var modifiers = []; var textless = false; var borderRadius = 'var(--bs-border-radius)'; transform.args.slice(2).forEach(function (argument) { var option = String(argument || '').trim().toLowerCase(); var isNamedColor = variants.indexOf(option) !== -1 || Object.prototype.hasOwnProperty.call(announcementColors, option); var isHexColor = /^#[0-9a-f]{3}(?:[0-9a-f]{3})?$/i.test(option); if (isNamedColor || isHexColor) { var color = isHexColor ? option : (variantColors[option] || announcementColors[option]); if (colorCount === 0) { if (variants.indexOf(option) !== -1) { variant = option; barVariant = option; } customColor = color; } else if (colorCount === 1) { backgroundColor = color; } colorCount += 1; } if (option === 'striped' || option === 'animated') { modifiers.push('progress-bar-' + option); } if (option === 'textless') { textless = true; } if (option === 'square') { borderRadius = '0'; } else if (option === 'pill') { borderRadius = '50rem'; } else if (option === 'rounded') { borderRadius = 'var(--bs-border-radius)'; } else { var radiusMatch = option.match(/^radius\((0|[0-9]+(?:\.[0-9]+)?(?:px|rem|em|%)?)\)$/); if (radiusMatch) { borderRadius = radiusMatch[1]; } } }); var progressClass = 'progress'; var barClass = 'progress-bar' + (barVariant ? ' bg-' + barVariant : '') + (modifiers.length ? ' ' + modifiers.join(' ') : ''); var barStyle = 'width:' + roundedPercentage + '%;color:inherit;'; if (customColor) { barStyle += 'background-color:' + customColor + ';'; } var progressStyleValue = (backgroundColor ? 'background-color:' + backgroundColor + ';' : '') + 'font-size:inherit;--bs-progress-font-size:inherit;--bs-progress-height:1em;height:1em;border-radius:' + borderRadius + ';'; var progressStyle = progressStyleValue ? ' style="' + progressStyleValue + '"' : ''; return '' + '' + (textless ? '' : label) + ''; } function formatPlaceholderValue(value) { if (value === undefined || value === null) { return ''; } if (typeof value === 'object') { try { return JSON.stringify(value); } catch (_error) { return ''; } } return String(value); } function collectPlaceholderFieldPaths(value) { var output = []; function walk(currentValue, prefix) { if (!currentValue || typeof currentValue !== 'object') { return; } if (Array.isArray(currentValue)) { return; } Object.keys(currentValue).forEach(function (key) { var nextPath = prefix ? prefix + '.' + key : key; var nextValue = currentValue[key]; if (nextValue && typeof nextValue === 'object') { walk(nextValue, nextPath); return; } if (output.indexOf(nextPath) === -1) { output.push(nextPath); } }); } walk(value, ''); return output; } root.placeholderUtils = { resolvePath: resolvePath, parsePlaceholderExpression: parsePlaceholderExpression, resolvePlaceholderExpression: resolvePlaceholderExpression, isImagePlaceholderExpression: isImagePlaceholderExpression, getImagePlaceholderConfig: getImagePlaceholderConfig, isProgressPlaceholderExpression: isProgressPlaceholderExpression, renderProgressPlaceholder: renderProgressPlaceholder, formatPlaceholderValue: formatPlaceholderValue, collectPlaceholderFieldPaths: collectPlaceholderFieldPaths }; }());