resolve( $post_id ); $store = new SLBUILD_DataStore(); $parser = new SLBUILD_SchemaParser(); $post_id = $ctx['post_id']; $value_source = $ctx['value_source'] ?? ''; if ( 'draft_meta' === $value_source ) { $layout = $store->get_draft_layout( $post_id ); } else { $layout = $store->get_layout( $post_id ); } foreach ( $layout as $item ) { if ( empty( $item['instance'] ) || empty( $item['type'] ) ) { continue; } // Skip hidden sections. if ( ! empty( $item['hidden'] ) ) { continue; } // Skip sections excluded from this page. if ( $post_id && ! empty( $item['excluded_pages'] ) && is_array( $item['excluded_pages'] ) ) { if ( in_array( (int) $post_id, array_map( 'intval', $item['excluded_pages'] ), true ) ) { continue; } } $this->render_instance( $item['instance'], $item['type'], $post_id, $ctx ); } } /** * Render all section instances for a named global region (header, footer, etc.). * * @param string $region Region slug e.g. 'header', 'footer'. */ public function render_global_region( string $region ): void { $ctx = SLBUILD_Resolver::instance()->resolve( null ); $store = new SLBUILD_DataStore(); $raw = $store->get_global( $region ); $layout = is_array( $raw ) && isset( $raw['layout'] ) ? $raw['layout'] : array(); $instances = is_array( $raw ) && isset( $raw['instances'] ) ? $raw['instances'] : array(); foreach ( $layout as $item ) { if ( empty( $item['instance'] ) || empty( $item['type'] ) ) { continue; } // Pass saved settings + blocks directly from the global region blob. $inst_data = $instances[ $item['instance'] ] ?? array(); $saved = $inst_data['settings'] ?? array(); $blocks = $inst_data['blocks'] ?? array(); $this->render_instance_with_values( $item['instance'], $item['type'], $saved, $blocks, $ctx ); } } /** * Render a section instance using pre-supplied values (bypasses DataStore lookup). * Used for global regions where values live in wp_options, not post meta. */ private function render_instance_with_values( string $instance_id, string $section_type, array $saved, array $blocks, array $ctx ): void { $parser = new SLBUILD_SchemaParser(); $file = trailingslashit( $ctx['sections_dir'] ) . $section_type . '.php'; if ( ! file_exists( $file ) && ! empty( $ctx['starter_sections_dir'] ) ) { $file = trailingslashit( $ctx['starter_sections_dir'] ) . $section_type . '.php'; } if ( ! file_exists( $file ) ) { return; } $schema = $parser->parse_file( $file ); $defaults = array(); if ( $schema && ! empty( $schema['settings'] ) ) { foreach ( $schema['settings'] as $field ) { if ( isset( $field['id'] ) && isset( $field['default'] ) ) { $defaults[ $field['id'] ] = $field['default']; } } } $values = array_merge( $defaults, $saved ); $this->current_section = array( 'instance_id' => $instance_id, 'type' => $section_type, 'values' => $values, 'blocks' => $blocks, 'post_id' => 0, 'schema' => $schema ?: array(), ); include $file; $this->current_section = array(); } /** * Render a single section instance. * * @param string $instance_id * @param string $section_type Section slug (matches filename without .php) * @param int $post_id * @param array $ctx Resolved context from Resolver. */ public function render_instance( $instance_id, $section_type, $post_id, $ctx ): void { $store = new SLBUILD_DataStore(); $parser = new SLBUILD_SchemaParser(); // Theme /sections/ takes priority; fall back to plugin starter-sections. $file = trailingslashit( $ctx['sections_dir'] ) . $section_type . '.php'; if ( ! file_exists( $file ) && ! empty( $ctx['starter_sections_dir'] ) ) { $file = trailingslashit( $ctx['starter_sections_dir'] ) . $section_type . '.php'; } if ( ! file_exists( $file ) ) { return; } $value_source = $ctx['value_source'] ?? ''; if ( 'draft_meta' === $value_source ) { $saved = $store->get_draft_instance_values( $post_id, $instance_id ); $blocks = $store->get_draft_instance_blocks( $post_id, $instance_id ); } else { $saved = $store->get_instance_values( $post_id, $instance_id ); $blocks = $store->get_instance_blocks( $post_id, $instance_id ); } // Merge schema defaults under saved values so templates always get a value. $schema = $parser->parse_file( $file ); $defaults = array(); if ( $schema && ! empty( $schema['settings'] ) ) { foreach ( $schema['settings'] as $field ) { if ( isset( $field['id'] ) && isset( $field['default'] ) ) { $defaults[ $field['id'] ] = $field['default']; } } } $values = array_merge( $defaults, $saved ); // Advanced CSS overrides - wrap section output only when values are set. // Sanitize spacing values: only CSS spacing characters allowed (digits, dots, spaces, // unit letters, %) to prevent CSS property injection via the style attribute. $sanitize_spacing = static function( $val ) { return preg_replace( '/[^0-9a-zA-Z.% ]/', '', (string) $val ); }; $adv_margin = isset( $values['slbuild_margin'] ) && $values['slbuild_margin'] !== '' ? $sanitize_spacing( $values['slbuild_margin'] ) : ''; $adv_padding = isset( $values['slbuild_padding'] ) && $values['slbuild_padding'] !== '' ? $sanitize_spacing( $values['slbuild_padding'] ) : ''; $adv_z_index = isset( $values['slbuild_z_index'] ) && $values['slbuild_z_index'] !== '' ? (int) $values['slbuild_z_index'] : ''; $has_advanced = $adv_margin !== '' || $adv_padding !== '' || $adv_z_index !== ''; // Set the context so slbuild_field() / slbuild_raw() / slbuild_meta() resolve correctly. $this->current_section = array( 'instance_id' => $instance_id, 'type' => $section_type, 'values' => $values, 'blocks' => $blocks, 'post_id' => $post_id, 'schema' => $schema ?: array(), ); if ( $has_advanced ) { $style_parts = array(); if ( $adv_margin !== '' ) $style_parts[] = 'margin:' . esc_attr( $adv_margin ); if ( $adv_padding !== '' ) $style_parts[] = 'padding:' . esc_attr( $adv_padding ); if ( $adv_z_index !== '' ) { $style_parts[] = 'position:relative'; $style_parts[] = 'z-index:' . $adv_z_index; } ob_start(); include $file; $html = ob_get_clean(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped echo '
' . $html . '
'; } else { include $file; } $this->current_section = array(); } /** * Get the value of a field in the currently-rendering section. * Returns the raw stored value (escaping is the caller's job via helpers). * * @param string $field_id * @return mixed */ public function get_field_value( $field_id ) { return $this->current_section['values'][ $field_id ] ?? null; } /** * Get the parsed schema array for the currently-rendering section. * Used by slbuild_meta() to read schema-level keys (heading_level, above_fold, etc.). * * @return array */ public function get_schema(): array { return $this->current_section['schema'] ?? array(); } /** * Get the blocks array for the currently-rendering section. * * @return array */ public function get_blocks(): array { return $this->current_section['blocks'] ?? array(); } /** * Set the active block context (called inside a block loop in a template). * * @param array $block_data Single block instance: [ 'block' => id, 'type' => type, 'settings' => [] ] */ public function set_current_block( $block_data ): void { $this->current_block = $block_data; } public function clear_current_block(): void { $this->current_block = array(); } /** * Get the value of a field in the currently-active block. * * @param string $field_id * @return mixed */ public function get_block_field_value( $field_id ) { return $this->current_block['settings'][ $field_id ] ?? null; } /** * Enqueue scripts and styles declared in section schemas for the current page's layout. * Hooked to wp_enqueue_scripts at priority 20 - fires before template output, after theme assets. * WP's enqueue system deduplicates by handle, so repeated section types enqueue once. */ public function maybe_enqueue_assets(): void { $post_id = get_queried_object_id(); if ( ! $post_id ) { return; } $ctx = SLBUILD_Resolver::instance()->resolve( $post_id ); $store = new SLBUILD_DataStore(); $value_source = $ctx['value_source'] ?? ''; if ( 'draft_meta' === $value_source ) { $layout = $store->get_draft_layout( $post_id ); } else { $layout = $store->get_layout( $post_id ); } if ( empty( $layout ) ) { return; } $parser = new SLBUILD_SchemaParser(); $theme_uri = get_stylesheet_directory_uri(); $seen = array(); foreach ( $layout as $item ) { $slug = $item['type'] ?? ''; if ( ! $slug || isset( $seen[ $slug ] ) ) { continue; } $seen[ $slug ] = true; $file = trailingslashit( $ctx['sections_dir'] ) . $slug . '.php'; if ( ! file_exists( $file ) && ! empty( $ctx['starter_sections_dir'] ) ) { $file = trailingslashit( $ctx['starter_sections_dir'] ) . $slug . '.php'; } if ( ! file_exists( $file ) ) { continue; } $schema = $parser->parse_file( $file ); if ( ! $schema || empty( $schema['assets'] ) ) { continue; } foreach ( $schema['assets']['scripts'] ?? array() as $s ) { if ( empty( $s['handle'] ) || empty( $s['src'] ) ) { continue; } wp_enqueue_script( $s['handle'], str_replace( '{{theme}}', $theme_uri, $s['src'] ), $s['deps'] ?? array(), $s['version'] ?? false, ! empty( $s['footer'] ) ); } foreach ( $schema['assets']['styles'] ?? array() as $s ) { if ( empty( $s['handle'] ) || empty( $s['src'] ) ) { continue; } wp_enqueue_style( $s['handle'], str_replace( '{{theme}}', $theme_uri, $s['src'] ), $s['deps'] ?? array(), $s['version'] ?? false ); } } } }