diff --git a/src/adf/document.ts b/src/adf/document.ts index aa67faa..772dd34 100644 --- a/src/adf/document.ts +++ b/src/adf/document.ts @@ -1,5 +1,5 @@ import type { ConvertFault } from '../result.ts' -import { everyInTree, isJsonValue, overNested, type JsonValue } from '../json-value.ts' +import { isJsonValue, overNested, type JsonValue } from '../json-value.ts' import { largestNesting } from '../nesting.ts' import { serializeCanonicalJson } from '../canonical-json.ts' @@ -123,16 +123,21 @@ export function nodeMarks(node: { marks?: AdfMark[] }): readonly AdfMark[] { } function isNodeArray(value: readonly unknown[]): value is readonly AdfNode[] { - return everyInTree(value, (node) => { - if (!isRecord(node) || !holdsOnly(node, nodeKeys)) return undefined - if (typeof node['type'] !== 'string') return undefined - if ('attrs' in node && !isAttributes(node['attrs'])) return undefined - if ('marks' in node && !isArrayOf(node['marks'], isAdfMark)) return undefined - if ('text' in node && typeof node['text'] !== 'string') return undefined - if (!('content' in node)) return [] - const content = node['content'] - return Array.isArray(content) ? [...content] : undefined - }) + const pending: unknown[] = [...value] + while (pending.length > 0) { + const node = pending.pop() + if (!isRecord(node) || !holdsOnly(node, nodeKeys)) return false + if (typeof node['type'] !== 'string') return false + if ('attrs' in node && !isAttributes(node['attrs'])) return false + if ('marks' in node && !isArrayOf(node['marks'], isAdfMark)) return false + if ('text' in node && typeof node['text'] !== 'string') return false + if ('content' in node) { + const content = node['content'] + if (!Array.isArray(content)) return false + for (const child of content) pending.push(child) + } + } + return true } function marksKey(marks: readonly AdfMark[]): string { diff --git a/src/json-value.ts b/src/json-value.ts index f444ee1..6448b4d 100644 --- a/src/json-value.ts +++ b/src/json-value.ts @@ -2,36 +2,23 @@ import { largestNesting } from './nesting.ts' export type JsonValue = JsonValue[] | boolean | null | number | string | { [key: string]: JsonValue } -// Tracks ancestors alone: a tree may share an object, but one holding itself would walk forever. -export function everyInTree(roots: readonly unknown[], children: (item: unknown) => readonly unknown[] | undefined): boolean { - const pending = Array.from(roots, (item) => ({ depth: 0, item })) - const ancestors: unknown[] = [] - const onPath = new Set() +export function isJsonValue(value: unknown): value is JsonValue { + const pending: unknown[] = [value] while (pending.length > 0) { - const entry = pending.pop() - if (entry === undefined) continue - while (ancestors.length > entry.depth) onPath.delete(ancestors.pop()) - if (onPath.has(entry.item)) return false - const held = children(entry.item) - if (held === undefined) return false - if (held.length === 0) continue - ancestors.push(entry.item) - onPath.add(entry.item) - for (const child of held) pending.push({ depth: entry.depth + 1, item: child }) + const item = pending.pop() + if (item === null || typeof item === 'boolean' || typeof item === 'string') continue + if (typeof item === 'number') { + if (!Number.isFinite(item)) return false + continue + } + // A hole is not a JSON value, and Array.prototype methods skip holes — spreading materialises them. + if (Array.isArray(item)) for (const child of [...item]) pending.push(child) + else if (typeof item === 'object') for (const child of Object.values(item)) pending.push(child) + else return false } return true } -export function isJsonValue(value: unknown): value is JsonValue { - return everyInTree([value], (item) => { - if (item === null || typeof item === 'boolean' || typeof item === 'string') return [] - if (typeof item === 'number') return Number.isFinite(item) ? [] : undefined - // A hole is not a JSON value, and Array.prototype methods skip holes — spreading materialises them. - if (Array.isArray(item)) return [...item] - return typeof item === 'object' ? Object.values(item) : undefined - }) -} - export function nestingDepth(value: unknown): number { const pending: { depth: number; item: unknown }[] = [{ depth: 0, item: value }] let deepest = 0