Files
Endoleap/src/lib/canvas-core/copy-paths.ts
T

39 lines
2.4 KiB
TypeScript

/**
* 组件声明式引用路径的遍历器,供复制/解散等纯规划复用。
* 普通路径段只访问自有字段,不用字符串相似度猜 ID;这里 yield 可写 slot,调用者必须先复制数据再改。
* path 应先经组件定义校验:本遍历器不重新禁止空路径或原型敏感键,数组 * 按下标位置枚举。
*/
import type { Json } from './types.ts';
import { fault } from './values.ts';
/** Own JSON fields only. A * segment enumerates an array, never object keys. */
/** 用显式 stack 遍历 path,返回终端字段的 parent/key;缺失普通字段直接跳过。
* * 只允许数组,非数组抛错;反向压栈使最终输出仍按原数组顺序。 */
export function* copySlots(data: Json, path: readonly string[]): Generator<{parent: Record<string, Json>; key: string}> {
const work: {value: Json; depth: number}[] = [{value: data, depth: 0}];
while (work.length) {
const {value, depth} = work.pop()!, key = path[depth];
if (key === '*') {
if (!Array.isArray(value)) throw fault('invalid_copy_reference');
// 终端 * 直接枚举 0..length-1,不另用 hasOwn 检查空洞;非终端 * 继续匹配下一路径段。
if (depth === path.length - 1) { for (let i = 0; i < value.length; i++) yield {parent: value as unknown as Record<string, Json>, key: String(i)}; }
else for (let i = value.length - 1; i >= 0; i--) work.push({value: value[i], depth: depth + 1});
} else if (value && typeof value === 'object' && Object.hasOwn(value, key)) {
if (depth === path.length - 1) yield {parent: value as Record<string, Json>, key};
else work.push({value: (value as Record<string, Json>)[key], depth: depth + 1});
}
}
}
/** 在 slot 上读取引用 ID;整个 slot 为 null 表示无引用,many 要求数组,每个 ID 必须非空字符串。
* many 数组里的 null 不等于无引用,会拒绝;many 与路径中的 * 是独立的两层展开规则。 */
export function* copyReferenceIds(data: Json, ref: {path: readonly string[]; many?: boolean}): Generator<string> {
for (const {parent, key} of copySlots(data, ref.path)) {
const value = parent[key]; if (value === null) continue;
if (ref.many && !Array.isArray(value)) throw fault('invalid_copy_reference');
for (const id of ref.many ? value as Json[] : [value]) {
if (typeof id !== 'string' || !id) throw fault('invalid_copy_reference');
yield id;
}
}
}