import type { SemanticNode } from "@reactive-resume/resume/stylesheet"; import type { StylesheetMode } from "@reactive-resume/schema/resume/stylesheet"; import type { ReactNode } from "react"; import type { ResolvedPdfNodePresentation } from "./adapter"; import { createContext, use, useMemo } from "react"; import { semanticNodeKeys } from "./node-keys"; export type ResolvedResumePresentation = Readonly>; type SemanticRenderProviderProps = { presentation: ResolvedResumePresentation; mode: StylesheetMode; sourceTree: SemanticNode; renderTree: SemanticNode; children: ReactNode; }; const EMPTY_NODE = Object.freeze({}) satisfies ResolvedPdfNodePresentation; const SemanticRenderContext = createContext<{ presentation: ResolvedResumePresentation; mode: StylesheetMode; renderedNodeKeys: ReadonlySet; sourceNodeKeys: ReadonlySet; renderedChildKeys: ReadonlyMap; sourceChildKeys: ReadonlyMap; sourceNodes: readonly SemanticNode[]; renderOrder: ReadonlyMap; } | null>(null); const SemanticNodeKeyContext = createContext(undefined); const flattenTree = (root: SemanticNode): SemanticNode[] => { const nodes: SemanticNode[] = []; const visit = (node: SemanticNode) => { nodes.push(node); for (const child of node.children) visit(child); }; visit(root); return nodes; }; const indexRenderTree = (root: SemanticNode) => { const keys = new Set(); const childKeys = new Map(); const order = new Map(); const visit = (node: SemanticNode) => { keys.add(node.key); order.set(node.key, order.size); childKeys.set( node.key, node.children.map(({ key }) => key), ); for (const child of node.children) visit(child); }; visit(root); return { renderedNodeKeys: keys as ReadonlySet, renderedChildKeys: childKeys as ReadonlyMap, renderOrder: order as ReadonlyMap, }; }; export function SemanticRenderProvider({ presentation, mode, sourceTree, renderTree, children, }: SemanticRenderProviderProps) { const treeIndex = useMemo(() => indexRenderTree(renderTree), [renderTree]); const sourceNodes = useMemo(() => flattenTree(sourceTree), [sourceTree]); const sourceNodeKeys = useMemo( () => new Set(sourceNodes.map(({ key }) => key)) as ReadonlySet, [sourceNodes], ); const sourceChildKeys = useMemo( () => new Map( sourceNodes.map(({ key, children: sourceChildren }) => [key, sourceChildren.map(({ key }) => key)]), ) as ReadonlyMap, [sourceNodes], ); const value = useMemo( () => ({ presentation, mode, sourceNodeKeys, sourceChildKeys, sourceNodes, ...treeIndex }), [mode, presentation, sourceChildKeys, sourceNodeKeys, sourceNodes, treeIndex], ); return {children}; } export function useResolvedNode(nodeKey: string | undefined): ResolvedPdfNodePresentation { const context = use(SemanticRenderContext); if (!context || !nodeKey) return EMPTY_NODE; return context.presentation[nodeKey] ?? EMPTY_NODE; } export const useSemanticRenderMode = (): StylesheetMode => use(SemanticRenderContext)?.mode ?? "legacy"; export const useSemanticNodeVisible = (nodeKey: string | undefined): boolean => { const context = use(SemanticRenderContext); if (context?.mode !== "semantic" || !nodeKey) return true; return context.renderedNodeKeys.has(nodeKey); }; export const useSemanticNodeExists = (nodeKey: string | undefined): boolean => { const context = use(SemanticRenderContext); return Boolean(nodeKey && context?.sourceNodeKeys.has(nodeKey)); }; export const useRenderedChildKeys = (nodeKey: string | undefined): readonly string[] | undefined => { const context = use(SemanticRenderContext); if (context?.mode !== "semantic" || !nodeKey) return undefined; const rendered = context.renderedChildKeys.get(nodeKey) ?? []; const source = context.sourceChildKeys.get(nodeKey) ?? []; return rendered.length === source.length && rendered.every((key, index) => key === source[index]) ? undefined : rendered; }; export type RenderedChildEntry = { nodeKey: string; value: T; }; export const projectRenderedChildren = ( renderedChildKeys: readonly string[] | undefined, entries: readonly RenderedChildEntry[], ): T[] => { if (!renderedChildKeys) return entries.map(({ value }) => value); const valuesByNodeKey = new Map(entries.map(({ nodeKey, value }) => [nodeKey, value])); return renderedChildKeys.flatMap((nodeKey) => { const value = valuesByNodeKey.get(nodeKey); return value === undefined ? [] : [value]; }); }; export const useSemanticSectionNodeKey = (pageNodeKey: string, sectionId: string): string => { const context = use(SemanticRenderContext); const section = context?.sourceNodes.find( (node) => node.kind === "section" && node.id === sectionId && node.key.startsWith(`${pageNodeKey}/`), ); return section?.key ?? semanticNodeKeys.section(semanticNodeKeys.region(pageNodeKey, "main"), sectionId); }; export const useRenderedSectionIds = (pageNodeKey: string, authoredIds: readonly string[]): string[] => { const context = use(SemanticRenderContext); if (context?.mode !== "semantic") return [...authoredIds]; const authored = new Set(authoredIds); return context.sourceNodes .filter( (node) => node.kind === "section" && node.id !== undefined && authored.has(node.id) && node.key.startsWith(`${pageNodeKey}/`) && context.renderedNodeKeys.has(node.key), ) .sort( (left, right) => (context.renderOrder.get(left.key) ?? Number.MAX_SAFE_INTEGER) - (context.renderOrder.get(right.key) ?? Number.MAX_SAFE_INTEGER), ) .flatMap(({ id }) => (id ? [id] : [])); }; export const useSemanticNodeBindings = () => { const context = use(SemanticRenderContext); return { resolveNode: (nodeKey: string | undefined): ResolvedPdfNodePresentation => { if (!context || !nodeKey) return EMPTY_NODE; return context.presentation[nodeKey] ?? EMPTY_NODE; }, isNodeVisible: (nodeKey: string | undefined): boolean => { if (context?.mode !== "semantic" || !nodeKey) return true; return context.renderedNodeKeys.has(nodeKey); }, renderedChildKeysFor: (nodeKey: string | undefined): readonly string[] | undefined => { if (context?.mode !== "semantic" || !nodeKey) return undefined; const rendered = context.renderedChildKeys.get(nodeKey) ?? []; const source = context.sourceChildKeys.get(nodeKey) ?? []; return rendered.length === source.length && rendered.every((key, index) => key === source[index]) ? undefined : rendered; }, }; }; export function SemanticNodeKeyProvider({ nodeKey, children }: { nodeKey: string | undefined; children: ReactNode }) { return {children}; } export const useSemanticNodeKey = (): string | undefined => use(SemanticNodeKeyContext); export function SemanticItemNodeKeyProvider({ itemId, children }: { itemId: string; children: ReactNode }) { const parentNodeKey = useSemanticNodeKey(); const nodeKey = parentNodeKey ? semanticNodeKeys.item(parentNodeKey, itemId) : undefined; return {children}; }