| |
| |
| |
| |
|
|
| export interface TraverseContext { |
| node: unknown; |
| path: (string | number)[]; |
| parent: TraverseContext | undefined; |
| key: string | number | undefined; |
| isLeaf: boolean; |
| notLeaf: boolean; |
| isRoot: boolean; |
| notRoot: boolean; |
| level: number; |
| circular: TraverseContext | null; |
| update: (value: unknown, stopHere?: boolean) => void; |
| remove: () => void; |
| } |
|
|
| type ForEachCallback = (this: TraverseContext, value: unknown) => void; |
|
|
| |
| type TraversableObject = Record<string | number, unknown> | unknown[]; |
|
|
| function isObject(value: unknown): value is TraversableObject { |
| if (value === null || typeof value !== 'object') { |
| return false; |
| } |
|
|
| |
| if (value instanceof Date) return false; |
| if (value instanceof RegExp) return false; |
| if (value instanceof Error) return false; |
| if (value instanceof URL) return false; |
|
|
| |
| if (typeof Buffer !== 'undefined' && Buffer.isBuffer(value)) return false; |
|
|
| |
| if (ArrayBuffer.isView(value)) return false; |
| if (value instanceof ArrayBuffer) return false; |
| if (value instanceof SharedArrayBuffer) return false; |
|
|
| |
| if (value instanceof Promise) return false; |
| if (value instanceof WeakMap) return false; |
| if (value instanceof WeakSet) return false; |
| if (value instanceof Map) return false; |
| if (value instanceof Set) return false; |
|
|
| |
| const stringTag = Object.prototype.toString.call(value); |
| if ( |
| stringTag === '[object Boolean]' || |
| stringTag === '[object Number]' || |
| stringTag === '[object String]' |
| ) { |
| return false; |
| } |
|
|
| return true; |
| } |
|
|
| |
| function setProperty(obj: TraversableObject, key: string | number, value: unknown): void { |
| if (Array.isArray(obj) && typeof key === 'number') { |
| obj[key] = value; |
| } else if (!Array.isArray(obj) && typeof key === 'string') { |
| obj[key] = value; |
| } else if (!Array.isArray(obj) && typeof key === 'number') { |
| |
| obj[key] = value; |
| } |
| } |
|
|
| |
| function deleteProperty(obj: TraversableObject, key: string | number): void { |
| if (Array.isArray(obj) && typeof key === 'number') { |
| |
| |
| return; |
| } |
|
|
| if (!Array.isArray(obj)) { |
| delete obj[key]; |
| } |
| } |
|
|
| function forEach(obj: unknown, callback: ForEachCallback): void { |
| const visited = new WeakSet<object>(); |
|
|
| function walk(node: unknown, path: (string | number)[] = [], parent?: TraverseContext): void { |
| |
| let circular: TraverseContext | null = null; |
| if (isObject(node)) { |
| if (visited.has(node)) { |
| |
| let p = parent; |
| while (p) { |
| if (p.node === node) { |
| circular = p; |
| break; |
| } |
| p = p.parent; |
| } |
| return; |
| } |
| visited.add(node); |
| } |
|
|
| const key = path.length > 0 ? path[path.length - 1] : undefined; |
| const isRoot = path.length === 0; |
| const level = path.length; |
|
|
| |
| const isLeaf = |
| !isObject(node) || |
| (Array.isArray(node) && node.length === 0) || |
| Object.keys(node).length === 0; |
|
|
| |
| const context: TraverseContext = { |
| node, |
| path: [...path], |
| parent, |
| key, |
| isLeaf, |
| notLeaf: !isLeaf, |
| isRoot, |
| notRoot: !isRoot, |
| level, |
| circular, |
| update(value: unknown) { |
| if (!isRoot && parent && key !== undefined && isObject(parent.node)) { |
| setProperty(parent.node, key, value); |
| } |
| this.node = value; |
| }, |
| remove() { |
| if (!isRoot && parent && key !== undefined && isObject(parent.node)) { |
| if (Array.isArray(parent.node) && typeof key === 'number') { |
| parent.node.splice(key, 1); |
| } else { |
| deleteProperty(parent.node, key); |
| } |
| } |
| }, |
| }; |
|
|
| |
| callback.call(context, node); |
|
|
| |
| if (!circular && isObject(node) && !isLeaf) { |
| if (Array.isArray(node)) { |
| for (let i = 0; i < node.length; i++) { |
| walk(node[i], [...path, i], context); |
| } |
| } else { |
| for (const [childKey, childValue] of Object.entries(node)) { |
| walk(childValue, [...path, childKey], context); |
| } |
| } |
| } |
| } |
|
|
| walk(obj); |
| } |
|
|
| |
| export default function traverse(obj: unknown) { |
| return { |
| forEach(callback: ForEachCallback): void { |
| forEach(obj, callback); |
| }, |
| }; |
| } |
|
|