38 lines
4.4 KiB
TypeScript
38 lines
4.4 KiB
TypeScript
/**
|
|
* 可移植的 renderer 描述表,只声明哪种元素由哪个显示实现处理,不保存 React 组件或元素实例状态。
|
|
* 按 componentId/dataVersion/structure 精确匹配;模型 registry 能解释数据,不代表宿主一定安装了对应 renderer。
|
|
*/
|
|
import type { ElementRecord,Registry } from '../canvas-core/types.ts';
|
|
import { fault } from '../canvas-core/values.ts';
|
|
/** rendererId 是宿主显示类型名;record-v1 读取原记录,compat-v1 使用旧卡片投影,默认采用前者。 */
|
|
export type RendererDescriptor={readonly componentId:string;readonly dataVersion:number;readonly structure:'node'|'connection';readonly rendererId:string;readonly projection?:'compat-v1'|'record-v1'};
|
|
/** 冻结定义列表与只读解析入口;没有匹配项返回 undefined,交给上层选择降级外壳。 */
|
|
export type RendererCatalog={readonly definitions:readonly RendererDescriptor[];resolve(element:Pick<ElementRecord,'componentId'|'dataVersion'|'structure'>):RendererDescriptor|undefined};
|
|
/** JSON 元组键避免拼接分隔符歧义,节点与连线即使 componentId 相同也独立匹配。 */
|
|
const key=(value:Pick<RendererDescriptor,'componentId'|'dataVersion'|'structure'>)=>JSON.stringify([value.componentId,value.dataVersion,value.structure]);
|
|
/** 校验并复制描述项,拒绝重复定义、危险/保留 renderer 名和未知投影协议。
|
|
* 工厂按声明顺序重建标准标量字段/默认 projection,未知扩展字段不复制,Map 键是精确三元组。
|
|
* 不加载 React/插件代码、不执行 schema 校验、不自动迁移版本;resolve 不授予编辑或网络权限。 */
|
|
export function createRendererCatalog(definitions:readonly RendererDescriptor[]):RendererCatalog{
|
|
const entries=new Map<string,RendererDescriptor>();
|
|
for(const value of definitions){
|
|
if(!value||typeof value.componentId!=='string'||!value.componentId||!Number.isSafeInteger(value.dataVersion)||value.dataVersion<1||!['node','connection'].includes(value.structure)||typeof value.rendererId!=='string'||!value.rendererId||['unsupported','__proto__','prototype','constructor'].includes(value.rendererId)||value.projection!==undefined&&!['compat-v1','record-v1'].includes(value.projection))throw fault('invalid_renderer_definition');
|
|
const id=key(value);if(entries.has(id))throw fault('duplicate_renderer_definition',[value.componentId]);entries.set(id,Object.freeze({componentId:value.componentId,dataVersion:value.dataVersion,structure:value.structure,rendererId:value.rendererId,projection:value.projection??'record-v1'}));
|
|
}
|
|
return Object.freeze({definitions:Object.freeze([...entries.values()]),resolve:(element:Pick<ElementRecord,'componentId'|'dataVersion'|'structure'>)=>entries.get(key(element))});
|
|
}
|
|
/** 内置旧卡片 renderer 的版本1映射,明确保留 compat-v1,避免默认原生投影破坏旧组件输入。 */
|
|
export const builtinRendererDefinitions:readonly RendererDescriptor[]=Object.freeze([
|
|
...['text','image','script'].map(componentId=>Object.freeze({componentId,dataVersion:1,structure:'node' as const,rendererId:'material',projection:'compat-v1' as const})),
|
|
...Object.entries({'plain-text':'plainText','plain-image':'plainImage',frame:'frame',icon:'icon'}).map(([componentId,rendererId])=>Object.freeze({componentId,dataVersion:1,structure:'node' as const,rendererId,projection:'compat-v1' as const})),
|
|
Object.freeze({componentId:'connection',dataVersion:1,structure:'connection',rendererId:'default',projection:'compat-v1'}),
|
|
]);
|
|
/** 可复用的不可变内置描述目录,不含任何 editor 的可变状态。 */
|
|
export const builtinRendererCatalog=createRendererCatalog(builtinRendererDefinitions);
|
|
|
|
/** Installed renderers without a reference declaration wait for full context. */
|
|
/** 渐进加载仅开放 registry 中声明 copy 引用规则的组件 renderer;缺声明先走降级等待完整上下文。
|
|
* 保留原 catalog.definitions 供装配检查,resolve 每次先查 registry 中 component 是否有 copy 声明。
|
|
* 这仅是局部加载门禁,不验证声明覆盖所有引用、schema 或源记录版本,也不证明依赖已加载;精确 renderer 匹配仍由 catalog 执行。 */
|
|
export function createLoadingRendererCatalog(registry:Registry,catalog:RendererCatalog):RendererCatalog{return Object.freeze({definitions:catalog.definitions,resolve:(element:Parameters<RendererCatalog['resolve']>[0])=>registry.components.get(element.componentId)?.copy?catalog.resolve(element):undefined});}
|