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 '