> */ private static $modules = []; /** * Register one complete server-side module definition. * * Required: type, renderer. Optional metadata includes label, category, * icon, owner, tier, settings_schema, builder, dependencies, * frontend_css, capability_callback, available, and navigator. */ public static function register_module( $definition ) { if ( ! is_array( $definition ) ) { return false; } $normalized_type = self::normalize_type( $definition['type'] ?? '' ); $owner = self::normalize_owner( $definition['owner'] ?? 'extension' ); $renderer = $definition['renderer'] ?? null; if ( ! $normalized_type || ! $owner || ! is_callable( $renderer ) ) { return false; } if ( in_array( $normalized_type, self::CORE_TYPES, true ) && 'core' !== $owner ) { return false; } if ( isset( self::$modules[ $normalized_type ] ) ) { return false; } if ( isset( $definition['frontend_css'] ) && ! is_callable( $definition['frontend_css'] ) ) return false; if ( isset( $definition['capability_callback'] ) && ! is_callable( $definition['capability_callback'] ) ) return false; if ( isset( $definition['settings_schema'] ) && ! is_array( $definition['settings_schema'] ) ) return false; $metadata = [ 'label' => sanitize_text_field( $definition['label'] ?? $normalized_type ), 'category' => sanitize_key( $definition['category'] ?? 'general' ) ?: 'general', 'icon' => sanitize_html_class( $definition['icon'] ?? '' ), 'owner' => $owner, 'tier' => self::normalize_owner( $definition['tier'] ?? 'free' ) ?: 'free', 'available' => ! isset( $definition['available'] ) || (bool) $definition['available'], 'catalog_only' => ! empty( $definition['catalog_only'] ), 'settings_schema' => $definition['settings_schema'] ?? [], 'builder' => is_string( $definition['builder'] ?? null ) ? sanitize_key( $definition['builder'] ) : '', 'dependencies' => self::normalize_dependencies( $definition['dependencies'] ?? [] ), 'frontend_css' => $definition['frontend_css'] ?? null, 'capability_callback' => $definition['capability_callback'] ?? null, 'navigator' => self::normalize_navigator( $definition['navigator'] ?? [] ), 'capabilities' => array_values( array_unique( array_filter( array_map( 'sanitize_key', (array) ( $definition['capabilities'] ?? [] ) ) ) ) ), ]; self::$modules[ $normalized_type ] = [ 'type' => $normalized_type, 'owner' => $owner, 'renderer' => $renderer, 'metadata' => $metadata ]; do_action( 'nyxonn_fx_module_registered', $normalized_type, self::$modules[ $normalized_type ] ); do_action( 'nyxonn_fx_module_renderer_registered', $normalized_type, self::$modules[ $normalized_type ] ); return true; } /** * Register an installed module frontend renderer. * * The callback receives the widget, settings, and normalized type. It must * return markup and escape all dynamic output for its output contexts. * * @param string $type Module type. * @param callable $renderer Render callback. * @param array $metadata Optional runtime metadata. * @return bool */ public static function register_renderer( $type, $renderer, $metadata = [] ) { if ( ! is_array( $metadata ) ) { return false; } return self::register_module( array_merge( $metadata, [ 'type' => $type, 'renderer' => $renderer ] ) ); } public static function unregister_renderer( $type ) { $normalized_type = self::normalize_type( $type ); if ( ! $normalized_type || ! isset( self::$modules[ $normalized_type ] ) ) { return false; } unset( self::$modules[ $normalized_type ] ); return true; } public static function has_renderer( $type ) { $normalized_type = self::normalize_type( $type ); return $normalized_type && isset( self::$modules[ $normalized_type ] ); } public static function get( $type ) { $normalized_type = self::normalize_type( $type ); return $normalized_type && isset( self::$modules[ $normalized_type ] ) ? self::$modules[ $normalized_type ] : null; } public static function all() { return self::$modules; } /** * Return trusted, registered frontend asset handles for used module types. * * Addons register handles in renderer metadata under * dependencies.styles and dependencies.scripts. Saved layout data can * select a registered module type, but can never provide asset handles. */ public static function get_frontend_dependencies( $types ) { $dependencies = [ 'styles' => [], 'scripts' => [], ]; foreach ( array_unique( array_map( 'sanitize_key', (array) $types ) ) as $type ) { $definition = self::get( $type ); if ( ! self::is_available( $definition ) ) continue; $registered = $definition['metadata']['dependencies'] ?? []; if ( ! is_array( $registered ) ) { continue; } foreach ( [ 'styles', 'scripts' ] as $kind ) { $handles = isset( $registered[ $kind ] ) && is_array( $registered[ $kind ] ) ? $registered[ $kind ] : []; foreach ( $handles as $handle ) { if ( ! is_string( $handle ) ) { continue; } $normalized_handle = sanitize_key( $handle ); if ( $normalized_handle && $normalized_handle === $handle ) { $dependencies[ $kind ][ $normalized_handle ] = true; } } } } return [ 'styles' => array_keys( $dependencies['styles'] ), 'scripts' => array_keys( $dependencies['scripts'] ), ]; } /** Return all trusted registered dependencies for compatibility mode. */ public static function get_all_frontend_dependencies() { return self::get_frontend_dependencies( array_keys( self::$modules ) ); } /** * Generate trusted dynamic CSS for registered module instances in a layout. * Saved data selects only a type; the executable callback always comes from * installed renderer metadata. */ public static function generate_frontend_css( $layout ) { $rules = []; self::collect_frontend_css( is_array( $layout ) ? $layout : [], $rules ); return implode( "\n", array_filter( $rules ) ); } private static function collect_frontend_css( $items, &$rules ) { foreach ( (array) $items as $item ) { if ( ! is_array( $item ) ) { continue; } if ( 'section' === ( $item['type'] ?? '' ) && class_exists( Render::class ) ) { $item = Render::resolve_linked_section( $item ); } $type = self::normalize_type( $item['type'] ?? '' ); $definition = $type ? self::get( $type ) : null; $generator = $definition['metadata']['frontend_css'] ?? null; $settings = isset( $item['settings'] ) && is_array( $item['settings'] ) ? $item['settings'] : []; if ( self::is_available( $definition ) && is_callable( $generator ) ) { try { $css = call_user_func( $generator, $item, $settings, $type ); if ( is_string( $css ) && '' !== trim( $css ) ) { $rules[] = $css; } } catch ( \Throwable $error ) { // Ignore a module-specific CSS failure and continue with the remaining modules. } } if ( in_array( $type, [ 'heading', 'iconbox' ], true ) ) { try { $extension_css = apply_filters( 'nyxonn_fx_core_module_frontend_css', '', $item, $settings, $type ); if ( is_string( $extension_css ) && '' !== trim( $extension_css ) ) $rules[] = $extension_css; } catch ( \Throwable $error ) { // A Pro style extension must not prevent the rest of the page CSS from rendering. } } foreach ( [ 'rows', 'columns', 'modules', 'children' ] as $child_key ) { if ( isset( $item[ $child_key ] ) && is_array( $item[ $child_key ] ) ) { self::collect_frontend_css( $item[ $child_key ], $rules ); } } } } /** Return null when the type has no explicit registered renderer. */ public static function render( $type, $widget ) { $definition = self::get( $type ); if ( ! self::is_available( $definition ) ) { return null; } $settings = isset( $widget['settings'] ) && is_array( $widget['settings'] ) ? $widget['settings'] : []; ob_start(); try { $result = call_user_func( $definition['renderer'], $widget, $settings, $definition['type'] ); $buffer = ob_get_clean(); } catch ( \Throwable $error ) { ob_end_clean(); return ''; } return $buffer . ( is_string( $result ) ? $result : '' ); } private static function normalize_type( $type ) { if ( ! is_string( $type ) ) { return null; } $type = trim( $type ); if ( '' === $type || sanitize_key( $type ) !== $type || ! preg_match( '/^[a-z][a-z0-9_-]*$/', $type ) ) { return null; } return $type; } private static function normalize_owner( $owner ) { if ( ! is_string( $owner ) ) { return null; } $owner = trim( $owner ); return $owner && sanitize_key( $owner ) === $owner && preg_match( '/^[a-z][a-z0-9_-]*$/', $owner ) ? $owner : null; } private static function normalize_dependencies( $dependencies ) { $clean = [ 'styles' => [], 'scripts' => [] ]; if ( ! is_array( $dependencies ) ) return $clean; foreach ( [ 'styles', 'scripts' ] as $kind ) { foreach ( (array) ( $dependencies[ $kind ] ?? [] ) as $handle ) { $handle = is_string( $handle ) ? sanitize_key( $handle ) : ''; if ( $handle ) $clean[ $kind ][ $handle ] = $handle; } $clean[ $kind ] = array_values( $clean[ $kind ] ); } return $clean; } private static function normalize_navigator( $navigator ) { if ( ! is_array( $navigator ) ) return []; return [ 'label' => sanitize_text_field( $navigator['label'] ?? '' ), 'icon' => sanitize_html_class( $navigator['icon'] ?? '' ), 'status_badge' => sanitize_text_field( $navigator['status_badge'] ?? '' ), ]; } private static function is_available( $definition ) { if ( ! is_array( $definition ) || empty( $definition['metadata']['available'] ) || ! empty( $definition['metadata']['catalog_only'] ) ) return false; $callback = $definition['metadata']['capability_callback'] ?? null; if ( ! is_callable( $callback ) ) return true; try { return (bool) call_user_func( $callback, $definition ); } catch ( \Throwable $error ) { return false; } } }