render.tsx 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180
  1. import React from "react";
  2. import {
  3. renderToBuffer as pdfRenderToBuffer,
  4. renderToStream as pdfRenderToStream,
  5. render as pdfRender,
  6. } from "@react-pdf/renderer";
  7. import type { Spec, UIElement } from "@json-render/core";
  8. import {
  9. resolveElementProps,
  10. evaluateVisibility,
  11. getByPath,
  12. type PropResolutionContext,
  13. } from "@json-render/core";
  14. import { standardComponents } from "./components/standard";
  15. // Re-export the standard components for use in custom registries
  16. export { standardComponents };
  17. export type RenderComponentRegistry = Record<string, React.ComponentType<any>>;
  18. export interface RenderOptions {
  19. registry?: RenderComponentRegistry;
  20. includeStandard?: boolean;
  21. state?: Record<string, unknown>;
  22. }
  23. const noopEmit = () => {};
  24. function renderElement(
  25. elementKey: string,
  26. spec: Spec,
  27. registry: RenderComponentRegistry,
  28. stateModel: Record<string, unknown>,
  29. repeatItem?: unknown,
  30. repeatIndex?: number,
  31. repeatBasePath?: string,
  32. ): React.ReactElement | null {
  33. const element = spec.elements[elementKey];
  34. if (!element) return null;
  35. const ctx: PropResolutionContext = {
  36. stateModel,
  37. repeatItem,
  38. repeatIndex,
  39. repeatBasePath,
  40. };
  41. if (element.visible !== undefined) {
  42. if (!evaluateVisibility(element.visible, ctx)) {
  43. return null;
  44. }
  45. }
  46. const resolvedProps = resolveElementProps(
  47. element.props as Record<string, unknown>,
  48. ctx,
  49. );
  50. const resolvedElement: UIElement = { ...element, props: resolvedProps };
  51. const Component = registry[resolvedElement.type];
  52. if (!Component) return null;
  53. if (resolvedElement.repeat) {
  54. const items =
  55. (getByPath(stateModel, resolvedElement.repeat.statePath) as
  56. | unknown[]
  57. | undefined) ?? [];
  58. const fragments = items.map((item, index) => {
  59. const key =
  60. resolvedElement.repeat!.key && typeof item === "object" && item !== null
  61. ? String(
  62. (item as Record<string, unknown>)[resolvedElement.repeat!.key!] ??
  63. index,
  64. )
  65. : String(index);
  66. const childPath = `${resolvedElement.repeat!.statePath}/${index}`;
  67. const children = resolvedElement.children?.map((childKey) =>
  68. renderElement(
  69. childKey,
  70. spec,
  71. registry,
  72. stateModel,
  73. item,
  74. index,
  75. childPath,
  76. ),
  77. );
  78. return (
  79. <Component key={key} element={resolvedElement} emit={noopEmit}>
  80. {children}
  81. </Component>
  82. );
  83. });
  84. return <>{fragments}</>;
  85. }
  86. const children = resolvedElement.children?.map((childKey) =>
  87. renderElement(
  88. childKey,
  89. spec,
  90. registry,
  91. stateModel,
  92. repeatItem,
  93. repeatIndex,
  94. repeatBasePath,
  95. ),
  96. );
  97. return (
  98. <Component key={elementKey} element={resolvedElement} emit={noopEmit}>
  99. {children && children.length > 0 ? children : undefined}
  100. </Component>
  101. );
  102. }
  103. function buildDocument(
  104. spec: Spec,
  105. options: RenderOptions = {},
  106. ): React.ReactElement {
  107. const {
  108. registry: customRegistry,
  109. includeStandard = true,
  110. state = {},
  111. } = options;
  112. const mergedState: Record<string, unknown> = {
  113. ...spec.state,
  114. ...state,
  115. };
  116. const registry: RenderComponentRegistry = {
  117. ...(includeStandard ? standardComponents : {}),
  118. ...customRegistry,
  119. };
  120. const root = renderElement(spec.root, spec, registry, mergedState);
  121. return root ?? <></>;
  122. }
  123. /**
  124. * Render a json-render spec to a PDF buffer.
  125. *
  126. * This is a standalone server-side function that resolves the spec tree
  127. * without React hooks or contexts, making it safe to import in Next.js
  128. * route handlers and other server-only environments.
  129. */
  130. export async function renderToBuffer(
  131. spec: Spec,
  132. options?: RenderOptions,
  133. ): Promise<Uint8Array> {
  134. const document = buildDocument(spec, options);
  135. return pdfRenderToBuffer(document as any);
  136. }
  137. /**
  138. * Render a json-render spec to a PDF readable stream.
  139. */
  140. export async function renderToStream(
  141. spec: Spec,
  142. options?: RenderOptions,
  143. ): Promise<ReadableStream> {
  144. const document = buildDocument(spec, options);
  145. return pdfRenderToStream(document as any);
  146. }
  147. /**
  148. * Render a json-render spec to a PDF file on disk.
  149. */
  150. export async function renderToFile(
  151. spec: Spec,
  152. filePath: string,
  153. options?: RenderOptions,
  154. ): Promise<void> {
  155. const document = buildDocument(spec, options);
  156. await pdfRender(document as any, filePath);
  157. }