import { nitrogen } from '@nitrogenbuilder/client-core'; import type { BuilderModule, Category, ComponentSettings, ComponentSettingsToProps, Group, Prop, PropArray, } from '@nitrogenbuilder/types'; import NitrogenContentSlot, { nitrogenContentSlotSettings, } from './NitrogenContentSlot.js'; import { getDefault } from './utils/getDefault.js'; import { renderTemplate } from './utils/renderTemplate.js'; import { resolve } from './utils/resolve.js'; import { set } from './utils/set.js'; const isBuilderMode = typeof window !== 'undefined' && new URLSearchParams(window.location.search).has('nitrogen-builder'); // Auto-register built-in content slot module nitrogen.registerModule( 'Nitrogen Content Slot', NitrogenContentSlot, nitrogenContentSlotSettings ); type NitrogenRendererProps = { page: BuilderModule[]; dynamicData?: any; requestedData?: any; contentPage?: BuilderModule[]; }; function renderChildren( mod: BuilderModule, dynamicData: any = {}, requestedData: any = {}, contentPage?: BuilderModule[], contentSlotRef?: { filled: boolean } ) { if (!mod.props.children) return undefined; if (Array.isArray(mod.props.children)) { return mod.props.children.flatMap((child: any) => { return renderModule( child, dynamicData, requestedData, contentPage, contentSlotRef ); }); } else if (typeof mod.props.children === 'object') { return Object.keys(mod.props.children).reduce((acc, slot) => { if (Array.isArray(mod.props.children[slot])) { return { ...acc, [slot]: mod.props.children[slot].flatMap((child: any) => { return renderModule( child, dynamicData, requestedData, contentPage, contentSlotRef ); }), }; } return acc; }, {}); } return undefined; } type DeepPartial = T extends object ? { [P in keyof T]?: DeepPartial; } : T; export function renderModuleProps( props: T extends ComponentSettings ? DeepPartial> : Record, settings: T, dynamicData: any = {}, requestedData: any = {} ): T extends ComponentSettings ? ComponentSettingsToProps : any { let _settings: ComponentSettings; if (typeof settings === 'string') { const module = nitrogen.getModule(settings); if (!module) return {} as any; _settings = { categories: module.categories, options: module.options, } as ComponentSettings; } else { _settings = settings; } let spreadProps = { ...(props ?? {}) } as any; delete spreadProps.children; function recursiveArrayPropDefault(prop: PropArray, keys: string[]) { if (prop.props) { Object.entries(prop.props).forEach(([subPropK, subPropV]) => { if (subPropV.type === 'array') { let indexes = Object.keys(resolve(keys.join('.'), spreadProps) ?? {}); for (let i = 0; i < indexes.length; i++) { recursiveArrayPropDefault(subPropV as PropArray, [ ...keys, `${indexes[i]}`, subPropK, ]); } } else { let indexes = Object.keys(resolve(keys.join('.'), spreadProps) ?? {}); for (let i = 0; i < indexes.length; i++) { set( spreadProps, getDefault( resolve( [...keys, `${indexes[i]}`, subPropK].join('.'), spreadProps ), subPropV ), [...keys, `${indexes[i]}`, subPropK] ); } } }); } } Object.values(_settings.categories).forEach((category) => { Object.entries(category.groups).forEach(([groupK, group]) => { Object.entries(group.props).forEach(([propK, propV]) => { if (spreadProps[groupK] === undefined) { spreadProps[groupK] = {}; } if (propV.type === 'array') { recursiveArrayPropDefault(propV as PropArray, [groupK, propK]); } else { spreadProps[groupK][propK] = getDefault( spreadProps[groupK][propK], propV ); } }); }); }); function recursiveDynamicProps(keys: string[]) { let props = resolve(keys.join('.'), spreadProps); if (!props) return; let propChildrenKeys = Object.keys(props); for (let j = 0; j < propChildrenKeys.length; j++) { let propChildrenKey = propChildrenKeys[j]; let propChild = props[propChildrenKey]; if (propChildrenKey.endsWith('___dynamic')) { const dynamicVal = resolve(propChild.key, dynamicData) || propChild.fallback || ''; const displayVal = typeof dynamicVal === 'boolean' ? dynamicVal : (propChild.before || '') + (isBuilderMode && typeof dynamicVal === 'string' ? `{{ ${dynamicVal} }}` : dynamicVal) + (propChild.after || ''); props[propChildrenKey.replace('___dynamic', '')] = displayVal; } else if (typeof props[propChildrenKey] === 'object') { recursiveDynamicProps([...keys, propChildrenKey]); } else { props[propChildrenKey] = renderTemplate( props[propChildrenKey], dynamicData, isBuilderMode ); } } } let groupKeys = Object.keys(spreadProps); for (let i = 0; i < groupKeys.length; i++) { let groupKey = groupKeys[i]; if (typeof groupKey === 'string') { if (typeof spreadProps[groupKey] === 'object') { // check children properties for if the end in ___dynamic recursiveDynamicProps([groupKey]); } } } return spreadProps as any; } function renderModule( mod: BuilderModule, dynamicData: any = {}, requestedData: any = {}, contentPage?: BuilderModule[], contentSlotRef?: { filled: boolean } ) { const moduleName = typeof mod.module === 'object' ? mod.module.name : mod.module; // Intercept content slot: replace with content modules when contentPage is provided if ( moduleName === 'Nitrogen Content Slot' && contentPage && contentSlotRef && !contentSlotRef.filled ) { contentSlotRef.filled = true; return (
{contentPage.map((block) => renderModule(block, dynamicData, requestedData) )}
); } const Component = nitrogen.getModuleComponent(moduleName); if (!Component) { console.groupCollapsed('NitrogenRenderer/renderModule/Component'); console.warn( 'Component not found for module. Possibly removed or renamed.', mod ); console.groupEnd(); return null; } const module = typeof mod.module === 'object' ? mod.module : nitrogen.getModule(mod.module); const hasSlots = typeof mod.props?.children === 'object' && !Array.isArray(mod.props?.children); let spreadProps = { ...(mod?.props ?? {}) }; delete spreadProps.children; if (typeof module === 'object') { function recursiveArrayPropDefault(prop: PropArray, keys: string[]) { if (prop.props) { Object.entries(prop.props).forEach(([subPropK, subPropV]) => { if (subPropV.type === 'array') { let indexes = Object.keys( resolve(keys.join('.'), spreadProps) ?? {} ); for (let i = 0; i < indexes.length; i++) { recursiveArrayPropDefault(subPropV as PropArray, [ ...keys, `${indexes[i]}`, subPropK, ]); } } else { let indexes = Object.keys( resolve(keys.join('.'), spreadProps) ?? {} ); for (let i = 0; i < indexes.length; i++) { set( spreadProps, getDefault( resolve( [...keys, `${indexes[i]}`, subPropK].join('.'), spreadProps ), subPropV ), [...keys, `${indexes[i]}`, subPropK] ); } } }); } } Object.values(module.categories).forEach((category: Category) => { Object.entries(category.groups).forEach(([groupK, group]: [string, Group]) => { Object.entries(group.props).forEach(([propK, propV]: [string, Prop]) => { if (spreadProps[groupK] === undefined) { spreadProps[groupK] = {}; } if (propV.type === 'array') { recursiveArrayPropDefault(propV as PropArray, [groupK, propK]); } else { spreadProps[groupK][propK] = getDefault( spreadProps[groupK][propK], propV ); } }); }); }); } function recursiveDynamicProps(keys: string[]) { let props = resolve(keys.join('.'), spreadProps); if (!props) return; let propChildrenKeys = Object.keys(props); for (let j = 0; j < propChildrenKeys.length; j++) { let propChildrenKey = propChildrenKeys[j]; let propChild = props[propChildrenKey]; if (propChildrenKey.endsWith('___dynamic')) { const dynamicVal = resolve(propChild.key, dynamicData) || propChild.fallback || ''; const displayVal = typeof dynamicVal === 'boolean' ? dynamicVal : (propChild.before || '') + (isBuilderMode && typeof dynamicVal === 'string' ? `{{ ${dynamicVal} }}` : dynamicVal) + (propChild.after || ''); props[propChildrenKey.replace('___dynamic', '')] = displayVal; } else if (typeof props[propChildrenKey] === 'object') { recursiveDynamicProps([...keys, propChildrenKey]); } else { props[propChildrenKey] = renderTemplate( props[propChildrenKey], dynamicData, isBuilderMode ); } } } let groupKeys = Object.keys(spreadProps); for (let i = 0; i < groupKeys.length; i++) { let groupKey = groupKeys[i]; if (typeof groupKey === 'string') { if (typeof spreadProps[groupKey] === 'object') { // check children properties for if the end in ___dynamic recursiveDynamicProps([groupKey]); } } } return ( {hasSlots ? null : renderChildren( mod, dynamicData, requestedData, contentPage, contentSlotRef )} ); } export default function NitrogenRenderer({ page, dynamicData, requestedData, contentPage, }: NitrogenRendererProps) { const contentSlotRef = contentPage ? { filled: false } : undefined; console.log('NitrogenRenderer page', page, contentSlotRef); return ( <> {page.map((block: BuilderModule) => { return renderModule( block, dynamicData, requestedData, contentPage, contentSlotRef ); })} ); }