import React from "react"; import { renderToBuffer as pdfRenderToBuffer, renderToStream as pdfRenderToStream, render as pdfRender, } from "@react-pdf/renderer"; import type { Spec, UIElement } from "@json-render/core"; import { resolveElementProps, evaluateVisibility, getByPath, type PropResolutionContext, } from "@json-render/core"; import { standardComponents } from "./components/standard"; // Re-export the standard components for use in custom registries export { standardComponents }; export type RenderComponentRegistry = Record>; export interface RenderOptions { registry?: RenderComponentRegistry; includeStandard?: boolean; state?: Record; } const noopEmit = () => {}; function renderElement( elementKey: string, spec: Spec, registry: RenderComponentRegistry, stateModel: Record, repeatItem?: unknown, repeatIndex?: number, repeatBasePath?: string, ): React.ReactElement | null { const element = spec.elements[elementKey]; if (!element) return null; const ctx: PropResolutionContext = { stateModel, repeatItem, repeatIndex, repeatBasePath, }; if (element.visible !== undefined) { if (!evaluateVisibility(element.visible, ctx)) { return null; } } const resolvedProps = resolveElementProps( element.props as Record, ctx, ); const resolvedElement: UIElement = { ...element, props: resolvedProps }; const Component = registry[resolvedElement.type]; if (!Component) return null; if (resolvedElement.repeat) { const items = (getByPath(stateModel, resolvedElement.repeat.statePath) as | unknown[] | undefined) ?? []; const fragments = items.map((item, index) => { const key = resolvedElement.repeat!.key && typeof item === "object" && item !== null ? String( (item as Record)[resolvedElement.repeat!.key!] ?? index, ) : String(index); const childPath = `${resolvedElement.repeat!.statePath}/${index}`; const children = resolvedElement.children?.map((childKey) => renderElement( childKey, spec, registry, stateModel, item, index, childPath, ), ); return ( {children} ); }); return <>{fragments}; } const children = resolvedElement.children?.map((childKey) => renderElement( childKey, spec, registry, stateModel, repeatItem, repeatIndex, repeatBasePath, ), ); return ( {children && children.length > 0 ? children : undefined} ); } function buildDocument( spec: Spec, options: RenderOptions = {}, ): React.ReactElement { const { registry: customRegistry, includeStandard = true, state = {}, } = options; const mergedState: Record = { ...spec.state, ...state, }; const registry: RenderComponentRegistry = { ...(includeStandard ? standardComponents : {}), ...customRegistry, }; const root = renderElement(spec.root, spec, registry, mergedState); return root ?? <>; } /** * Render a json-render spec to a PDF buffer. * * This is a standalone server-side function that resolves the spec tree * without React hooks or contexts, making it safe to import in Next.js * route handlers and other server-only environments. */ export async function renderToBuffer( spec: Spec, options?: RenderOptions, ): Promise { const document = buildDocument(spec, options); return pdfRenderToBuffer(document as any); } /** * Render a json-render spec to a PDF readable stream. */ export async function renderToStream( spec: Spec, options?: RenderOptions, ): Promise { const document = buildDocument(spec, options); return pdfRenderToStream(document as any); } /** * Render a json-render spec to a PDF file on disk. */ export async function renderToFile( spec: Spec, filePath: string, options?: RenderOptions, ): Promise { const document = buildDocument(spec, options); await pdfRender(document as any, filePath); }