// packages/embed-loader/src/params.js // 纯函数参数模块:loader(esbuild 打包内联)与 vitest 单测共用的唯一参数源。 // 取值域与缺省严格对齐契约 EmbedSlotSettings(packages/contracts/openapi.yaml), // 非法/缺省一律静默回退——loader 对宿主零报错(A11「不破坏宿主」)。 export const POSITION_VALUES = ['left', 'right']; export const SIZE_VALUES = ['sm', 'md']; export const LANG_VALUES = ['auto', 'zh-CN', 'en']; export const DEFAULTS = { position: 'right', size: 'md', intervalMs: 5000, lang: 'auto', collapsedDefault: false, }; /** size → 宿主侧 wrapper 宽度(px)。sm 为窄屏降级档。 */ export const SIZE_WIDTH = { sm: 280, md: 340 }; export const INTERVAL_MIN = 3000; export const INTERVAL_MAX = 30000; /** resize 消息高度夹取范围(px),越界/非数值返回 null 表示忽略。 */ export const HEIGHT_MIN = 60; export const HEIGHT_MAX = 640; function oneOf(allowed, value, fallback) { return allowed.indexOf(value) >= 0 ? value : fallback; } /** 轮播间隔(ms):解析失败取缺省,超界夹取到 [3000, 30000]。 */ export function normalizeInterval(value) { const n = parseInt(value, 10); if (isNaN(n)) return DEFAULTS.intervalMs; return Math.min(INTERVAL_MAX, Math.max(INTERVAL_MIN, n)); } /** * 解析宿主 script 标签 data-* 配置(dataset 形如 {slot, lang, position, interval})。 * opts.narrow=true(viewport <480px)时强制 sm 尺寸 + 默认收起(计划评审缺口:移动端可用)。 * data-slot 缺失/空白 → slot 为 null,调用方须静默 no-op。 */ export function parseEmbedConfig(dataset, opts) { const ds = dataset || {}; const slot = typeof ds.slot === 'string' ? ds.slot.replace(/^\s+|\s+$/g, '') : ''; const narrow = !!(opts && opts.narrow); return { slot: slot || null, lang: oneOf(LANG_VALUES, ds.lang, DEFAULTS.lang), position: oneOf(POSITION_VALUES, ds.position, DEFAULTS.position), intervalMs: normalizeInterval(ds.interval), size: narrow ? 'sm' : oneOf(SIZE_VALUES, ds.size, DEFAULTS.size), collapsedDefault: narrow ? true : DEFAULTS.collapsedDefault, }; } /** iframe src:{origin}/embed/{slot}?lang=&pos=&interval=(槽 token 编码防注入)。 */ export function buildEmbedUrl(origin, slot, config) { return ( origin + '/embed/' + encodeURIComponent(slot) + '?lang=' + encodeURIComponent(config.lang) + '&pos=' + encodeURIComponent(config.position) + '&interval=' + String(config.intervalMs) ); } /** resize 高度夹取:非有限数值/缺失 → null(忽略该消息)。 */ export function clampHeight(value) { if (value === null || value === undefined) return null; const n = Number(value); if (isNaN(n) || !isFinite(n)) return null; return Math.min(HEIGHT_MAX, Math.max(HEIGHT_MIN, Math.round(n))); }