26 lines
3.0 KiB
TypeScript
26 lines
3.0 KiB
TypeScript
export type Slot={id:string;kind:string;role:string;box:number[];direction:string;text:string|null;reading:string};
|
||
export type Layout={id:string;name:string;width:number;height:number;elements:Slot[]};
|
||
export type TextValue={value:string;color?:string;scale?:number;align?:'start'|'middle'|'end'};
|
||
export type ImageValue={assetId?:string;color?:string;fit?:'cover'|'contain'};
|
||
export type DecorationValue={color?:string;hidden?:boolean};
|
||
export type Parameters={texts:Record<string,TextValue>;images:Record<string,ImageValue>;decorations:Record<string,DecorationValue>;paper?:string};
|
||
export class LayoutParameterError extends Error{}
|
||
export const uuid=(v:unknown):v is string=>typeof v==='string'&&/^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(v);
|
||
function fail(message:string):never{throw new LayoutParameterError(message)}
|
||
function object(v:unknown):Record<string,unknown>{if(!v||typeof v!=='object'||Array.isArray(v))fail('参数必须为 JSON 对象');return v as Record<string,unknown>}
|
||
function keys(v:Record<string,unknown>,allowed:string[]){if(Object.keys(v).some(k=>!allowed.includes(k)))fail('未知参数或构件 ID')}
|
||
function color(v:unknown){if(typeof v!=='string'||!/^#[a-f0-9]{6}$/i.test(v))fail('颜色使用 #RRGGBB');return v}
|
||
export function parameters(layout:Layout,input:unknown):Parameters{
|
||
const raw=object(input);keys(raw,['texts','images','decorations','paper']);const result:Parameters={texts:{},images:{},decorations:{}};
|
||
if(raw.paper!==undefined)result.paper=color(raw.paper);
|
||
for(const [group,kind] of [['texts','text'],['images','image'],['decorations','decoration']] as const){
|
||
const entries=object(raw[group]??{});keys(entries,layout.elements.filter(e=>e.kind===kind).map(e=>e.id));
|
||
for(const [id,value] of Object.entries(entries)){const v=object(value);
|
||
if(group==='texts'){keys(v,['value','color','scale','align']);if(typeof v.value!=='string'||Array.from(v.value).length>500||v.value.split('\n').length>8)fail('文字最多 500 字 / 8 行');const t:TextValue={value:v.value};if(v.color!==undefined)t.color=color(v.color);if(v.scale!==undefined){if(typeof v.scale!=='number'||!Number.isFinite(v.scale)||v.scale<.25||v.scale>2)fail('scale 范围 0.25–2');t.scale=v.scale}if(v.align!==undefined){if(!['start','middle','end'].includes(String(v.align)))fail('无效 align');t.align=v.align as TextValue['align']}result.texts[id]=t;
|
||
}else if(group==='images'){keys(v,['assetId','color','fit']);const image:ImageValue={};if(v.assetId!==undefined){if(!uuid(v.assetId))fail('assetId 必须为已上传图片 UUID');image.assetId=v.assetId}if(v.color!==undefined)image.color=color(v.color);if(v.fit!==undefined){if(v.fit!=='cover'&&v.fit!=='contain')fail('fit 使用 cover 或 contain');image.fit=v.fit}result.images[id]=image;
|
||
}else{keys(v,['color','hidden']);const d:DecorationValue={};if(v.color!==undefined)d.color=color(v.color);if(v.hidden!==undefined){if(typeof v.hidden!=='boolean')fail('hidden 必须为布尔值');d.hidden=v.hidden}result.decorations[id]=d}
|
||
}
|
||
}
|
||
return result;
|
||
}
|