Files
Endoleap/src/lib/canvas-bindings/renderers.ts
T

36 lines
4.1 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 名和未知投影协议。
* 工厂不加载代码、不自动迁移 dataVersion,也不把高版本当成兼容旧版本。 */
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;缺声明先走降级等待完整上下文。
* 保留 definitions 供装配检查,但 resolve 才决定当前是否允许显示实现。 */
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});}