/** * 组件声明式引用路径的遍历器,供复制/解散等纯规划复用。 * 只访问自有字段,不用字符串相似度猜 ID;这里 yield 可写 slot,调用者必须先复制数据再改。 */ 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; 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'); // 终端 * 直接枚举数组项位置;非终端 * 继续匹配下一路径段。 if (depth === path.length - 1) { for (let i = 0; i < value.length; i++) yield {parent: value as unknown as Record, 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, key}; else work.push({value: (value as Record)[key], depth: depth + 1}); } } } /** 在 slot 上读取引用 ID;null 代表无引用,many 要求数组,每个 ID 必须非空字符串。 */ export function* copyReferenceIds(data: Json, ref: {path: readonly string[]; many?: boolean}): Generator { 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; } } }