` (WordPress prints it as the inline style `#zadex-css--inline-css`): * its `_zadex_css` plus the page-settings CSS. Each handle is added once per request. * * - `wp_enqueue_scripts`: Frontend / ThemeBuilder / Popups call document() + scan() for the documents they already * know will render (queried post, main-query posts, header / footer / location templates, popups) and for the * documents those trees reference (components, embedded templates, loop-item templates), so the CSS is in . * - While rendering, the renderers call document() for what they print. A document found only then (e.g. a * component inside a loop item) is enqueued late and WordPress prints it in the footer (print_late_styles()). * - Order: handles queued before `wp_enqueue_scripts` finished (block themes and classic header templates render * before wp_head) wait until flush() on `wp_enqueue_scripts` (priority 9999), so the document CSS always comes * after the theme and the widget base CSS (frontend.css) it overrides. * - REST responses are not pages: capture() collects the CSS of a render as `[handle => css]` instead, returned * as the `css` field of the response; the frontend / editor runtime inserts it with JavaScript. */ final class Styles { public const PREFIX = 'zadex-css-'; /** @var array handles enqueued on this request */ private static array $added = []; /** @var array> open capture() buffers */ private static array $captures = []; /** @var array documents scanned by scan_document() */ private static array $scanned = []; /** @var array handle => css waiting for flush() */ private static array $pending = []; private static bool $flushed = false; private static bool $hooked = false; /** false: enqueue at once (tests / contexts without a page). */ private static bool $defer = true; /** Handle of a document's inline style. */ public static function handle(int $id): string { return self::PREFIX . $id; } /** Queues the generated CSS of a document (element CSS + page-settings CSS). */ public static function document(int $id): void { if ($id <= 0) { return; } $handle = self::handle($id); if (self::has($handle)) { return; } $css = Document::get_css($id); $page = Document::get_page_settings($id); if (!empty($page['css']) && is_string($page['css'])) { $css .= ($css !== '' ? "\n" : '') . Document::sanitize_css($page['css']); } self::add($handle, $css); } /** Queues a block of CSS under a handle (once per request, or once per capture). */ public static function add(string $handle, string $css): void { $css = self::tag_safe(trim($css)); if ($handle === '' || $css === '' || self::has($handle)) { return; } if (self::$captures) { self::$captures[count(self::$captures) - 1][$handle] = $css; return; } self::$added[$handle] = true; if (self::$defer && !self::$flushed && !self::enqueue_done()) { self::$pending[$handle] = $css; if (!self::$hooked && function_exists('add_action')) { self::$hooked = true; add_action('wp_enqueue_scripts', [self::class, 'flush'], 9999); } return; } self::enqueue($handle, $css); } /** `wp_enqueue_scripts` (9999): enqueues what was queued so far; later handles are enqueued at once. */ public static function flush(): void { self::$flushed = true; $pending = self::$pending; self::$pending = []; foreach ($pending as $handle => $css) { self::enqueue($handle, $css); } } /** `wp_enqueue_scripts` already ran completely (content rendered after wp_head): nothing to wait for. */ private static function enqueue_done(): bool { if (!function_exists('did_action') || !did_action('wp_enqueue_scripts')) { return false; } return !(function_exists('doing_action') && doing_action('wp_enqueue_scripts')); } private static function enqueue(string $handle, string $css): void { if (!function_exists('wp_register_style')) { return; } wp_register_style($handle, false, [], ZADEX_VERSION); wp_add_inline_style($handle, wp_strip_all_tags($css)); wp_enqueue_style($handle); } /** Whether the handle is already queued (in the current capture when one is open). */ public static function has(string $handle): bool { if (self::$captures) { return isset(self::$captures[count(self::$captures) - 1][$handle]); } return isset(self::$added[$handle]); } /** * Runs $fn collecting the CSS it queues instead of enqueuing it (REST responses). * * @template T * @param callable(): T $fn * @return array{0: T, 1: array} [$fn's result, handle => css] */ public static function capture(callable $fn): array { self::$captures[] = []; try { $result = $fn(); } finally { $css = array_pop(self::$captures); } return [$result, $css]; } /** * Queues the CSS of a document and of every document its tree references (see scan()). Used before rendering, * on `wp_enqueue_scripts`, so the styles land in . */ public static function document_tree(int $id, ?Renderer $renderer = null): void { if ($id <= 0 || isset(self::$scanned[$id]) || count(self::$scanned) > 100) { return; } self::$scanned[$id] = true; self::document($id); $data = Document::get_data($id); self::scan(is_array($data['elements'] ?? null) ? $data['elements'] : [], $renderer); } /** * Walks an element tree and queues the CSS it will need: components (`component_id`), embedded templates * (`template_id` of the template widget), loop-item templates of loop widgets, and the device rules of * display conditions. * * @param array $elements */ public static function scan(array $elements, ?Renderer $renderer = null, int $depth = 0): void { if ($depth > 40) { return; } foreach ($elements as $el) { if (!is_array($el) || !empty($el['hidden'])) { continue; } $type = (string) ($el['type'] ?? ''); $settings = is_array($el['settings'] ?? null) ? $el['settings'] : []; if ($renderer && $type !== '') { $settings = $renderer->settings($el); } if ($type === 'component') { self::document_tree((int) ($settings['component_id'] ?? 0), $renderer); } elseif ($type === 'template') { self::document_tree((int) ($settings['template_id'] ?? 0), $renderer); } elseif (class_exists('\Zadex\Loop\LoopRenderer') && \Zadex\Loop\LoopRenderer::is_loop($type)) { foreach (\Zadex\Loop\LoopRenderer::template_ids($settings) as $tpl) { self::document_tree((int) $tpl, $renderer); } } if (ElementConditions::hidden_devices($settings)) { self::device_css(); } if (!empty($el['children']) && is_array($el['children'])) { self::scan($el['children'], $renderer, $depth + 1); } } } /** The `zx-cond-hide-` media queries of display conditions (T165). */ public static function device_css(): void { if (self::has('zadex-cond')) { return; } self::add('zadex-cond', ElementConditions::device_css((new \Zadex\Settings())->breakpoints())); } /** CSS is generated by the editor from sanitized settings; only make sure it can never close its tag. */ public static function tag_safe(string $css): string { return str_ireplace('