-` (unique HTML ids per instance) and the alias class * `zx-` (plus `zx--` for nested instances) so the component CSS, generated once with the * component's own ids, applies to every instance; * 4. merges the instance's own attributes (classes `zx-component zx-`, custom id / classes / motion …) * onto the component's first root — no wrapper; * 5. queues the component CSS once per request (\Zadex\Frontend\Styles, handle `zadex-css-`). * A component that (directly or through nested instances) contains itself stops rendering at the repeated id. */ final class InstanceRenderer { /** @var int[] component ids being rendered (recursion guard) */ private static array $stack = []; public const MAX_DEPTH = 12; public static function reset(): void { self::$stack = []; } /** Editor previews (?zadex=edit|preview, REST renders) show placeholders; the live site shows nothing. */ public static function is_editor(): bool { return EditorLoader::mode() !== '' || (defined('REST_REQUEST') && REST_REQUEST); } public static function render(Renderer $renderer, array $element, array $settings, string $attrs): string { $component_id = (int) ($settings['component_id'] ?? 0); $instance_id = (string) ($element['id'] ?? ''); $editor = self::is_editor(); if ($component_id <= 0 || $instance_id === '' || !ComponentPost::renderable($component_id, $editor)) { return $editor ? self::placeholder($attrs, __('Component not found', 'zadex')) : ''; } if (in_array($component_id, self::$stack, true) || count(self::$stack) >= self::MAX_DEPTH) { return $editor ? self::placeholder($attrs, __('A component cannot contain itself.', 'zadex')) : ''; } $tree = ComponentPost::tree($component_id); if (!$tree) { return $editor ? self::placeholder($attrs, __('This component is empty.', 'zadex')) : ''; } $overrides = is_array($settings['overrides'] ?? null) ? $settings['overrides'] : []; $tree = self::apply_overrides($tree, ComponentPost::props($component_id), $overrides, $renderer->registry()); $aliases = array_values(array_filter((array) ($element['_zx_alias'] ?? []), 'is_string')); $tree = self::prefix($tree, $instance_id, $aliases); $merge = $renderer->attrs_array($element, $settings); unset($merge['data-zx-id'], $merge['data-zx-type']); $merge['data-zx-component'] = (string) $component_id; foreach ($tree as $i => $root) { if (is_array($root)) { $tree[$i]['_zx_merge'] = $merge; break; } } self::$stack[] = $component_id; try { $html = $renderer->render_document($tree); } finally { array_pop(self::$stack); } self::css_once($component_id); return $html; } /** * The component tree with override values written into the exposed controls (base keys). Values are * sanitized with the inner widget's control definition; props pointing at removed elements are ignored. * * @param array $tree * @param array $props * @param array $overrides * @return array */ public static function apply_overrides(array $tree, array $props, array $overrides, WidgetRegistry $registry): array { if (!$props || !$overrides) { return $tree; } $sanitizer = new Sanitizer($registry); $values = []; foreach ($props as $prop) { if (array_key_exists($prop['id'], $overrides) && $overrides[$prop['id']] !== null) { $values[$prop['elementId']][$prop['control']] = $overrides[$prop['id']]; } } if (!$values) { return $tree; } $walk = static function (array $list, int $depth) use (&$walk, $values, $sanitizer, $registry): array { if ($depth > 60) { return $list; } foreach ($list as $i => $el) { if (!is_array($el)) { continue; } $id = (string) ($el['id'] ?? ''); if (isset($values[$id])) { $controls = $registry->controls((string) ($el['type'] ?? '')); $known = array_intersect_key($values[$id], $controls); if ($known) { $clean = $sanitizer->sanitize_settings($known, $controls); $list[$i]['settings'] = array_merge(is_array($el['settings'] ?? null) ? $el['settings'] : [], $clean); } } if (!empty($el['children']) && is_array($el['children'])) { $list[$i]['children'] = $walk($el['children'], $depth + 1); } } return $list; }; return $walk($tree, 0); } /** * Instance-prefixed ids: `x` becomes `-x`, with the aliases `x` and `-x` (for each alias of the * instance element itself, i.e. when this instance is nested in another component). * * @param array $tree * @param string[] $instance_aliases * @return array */ public static function prefix(array $tree, string $instance_id, array $instance_aliases = [], int $depth = 0): array { if ($depth > 60) { return []; } $out = []; foreach ($tree as $el) { if (!is_array($el) || !is_string($el['id'] ?? null) || $el['id'] === '') { continue; } $own = $el['id']; $el['id'] = $instance_id . '-' . $own; $el['_zx_alias'] = array_merge([$own], array_map(static fn(string $a): string => $a . '-' . $own, $instance_aliases)); $el['children'] = !empty($el['children']) && is_array($el['children']) ? self::prefix($el['children'], $instance_id, $instance_aliases, $depth + 1) : []; $out[] = $el; } return $out; } /** Queues the component's CSS (once per request; in when the page tree was scanned early). */ public static function css_once(int $component_id): void { \Zadex\Frontend\Styles::document($component_id); } private static function placeholder(string $attrs, string $message): string { return '
' . esc_html($message) . '
'; } }