import JSONQuery from '@sagold/json-query'; import { JSONSchema } from '@squiz/json-schema-library'; import { ComponentInputSchema } from '../..'; import { BaseFormattedNodes, ComponentNode, FormattedText, FormattedTextFragment, FormattedTextTag, HigherOrderFormattedNodes, TextNode, } from './formattedText'; import { FORMATTED_TEXT_SCHEMA_ID } from './formattedTextConstants'; export { FORMATTED_TEXT_SCHEMA_ID }; export interface FormattedNodeWithChildren { children: FormattedNode[]; } type FormattedNode = { type: string }; type ResolutionOutput = string | T; type Resolver> = ( node: Nodes, blockIndex?: number, ) => Promise; export type ResolverConfig> = { [N in Nodes as N['type']]: Resolver; } & { default?: Resolver }; export type ResolvedChildNode< DEFAULT_NODES extends FormattedNode, CHILD_NODE extends ResolutionOutput, > = DEFAULT_NODES extends { children: any[]; } ? Omit & { children: CHILD_NODE[] } : DEFAULT_NODES; export type FullyResolvedNodes = ResolvedChildNode< TextNode | FormattedTextTag | FormattedTextFragment | ComponentNode, string >; export type BaseResolvedNodes = ResolvedChildNode; export type HigherOrderResolvedNodes = ResolvedChildNode; export type HigherOrderFormattedNodesMap = ResolverConfig; export type FullyResolvedNodesMap = ResolverConfig; interface ResolveOptions> { withJoin?: string; handleError?: (error: unknown, pointer: string) => O | never; } export async function resolveFormattedTextNodes>( contentItem: any, contentSchema: JSONSchema, resolvers: Partial>, { withJoin, handleError }: ResolveOptions = {}, ) { const resolvedDataSetters: Promise<(newItem: typeof contentItem) => typeof contentItem>[] = []; ComponentInputSchema.each( contentItem, (schema, data, pointer) => { if (schema.type === 'FormattedText' || schema.$id === FORMATTED_TEXT_SCHEMA_ID) { if (!Array.isArray(data)) { return; } const formattedText = data as FormattedText; const textResolvers = formattedText.map((t) => resolveFormattedText(t, resolvers)); const resolvedItem = Promise.all(textResolvers) .catch((error) => { if (handleError) { return [handleError(error, pointer)]; } return [{ type: 'text', value: 'Error resolving formatted text at ' + pointer + ': ' + error?.message }]; }) .then((resolvedData: unknown[]) => (newItem: typeof contentItem) => { let joinedData: string | null = null; if (resolvedData.every((d): d is string => typeof d === 'string') && typeof withJoin === 'string') { joinedData = resolvedData.join(withJoin); } return JSONQuery.set(newItem, pointer, joinedData ?? resolvedData); }); resolvedDataSetters.push(resolvedItem); } }, ComponentInputSchema.compileSchema(contentSchema), ); for (const setResolvedData of await Promise.all(resolvedDataSetters)) { contentItem = setResolvedData(contentItem); } return contentItem; } export async function resolveFormattedText>>( node: I, resolvers: RC, blockIndex?: number, ): Promise> ? O : unknown> { if (shouldResolveChildren(node)) { const updatedChildren = await Promise.all( node.children.map((child) => typeof child === 'string' ? child : resolveFormattedText(child, resolvers, blockIndex), ), ); node = { ...node, children: updatedChildren }; } const resolver = resolvers[node.type as keyof typeof resolvers] || resolvers['default']; if (resolver) { return await resolver(node, blockIndex); } return node as any; } function shouldResolveChildren(node: I): node is I & { children: Array> } { return !!node && typeof node == 'object' && 'children' in node && Array.isArray(node.children); }