$v ) { if ( is_int( $k ) ) { $options_arr[ $v ] = $v; $raw_options .= $v . "\r\n"; } else { $options_arr[ $k ] = $v; $raw_options .= $k . ':' . $v . "\r\n"; } } $raw_options = rtrim( $raw_options ); } elseif ( ! empty( $field['options_raw'] ) && is_string( $field['options_raw'] ) ) { $raw_options = $field['options_raw']; } $sub_fields = array(); if ( ! empty( $field['sub_fields'] ) && is_array( $field['sub_fields'] ) ) { foreach ( $field['sub_fields'] as $sf ) { $sub_fields[] = $this->normalize_field( $sf ); } } $conditions = array(); if ( ! empty( $field['conditions'] ) && is_array( $field['conditions'] ) ) { foreach ( $field['conditions'] as $cond ) { $conditions[] = array( 'field' => sanitize_text_field( $cond['field'] ?? '' ), 'operator' => sanitize_text_field( $cond['operator'] ?? '==' ), 'value' => sanitize_text_field( $cond['value'] ?? '' ), ); } } $type = sanitize_text_field( $field['type'] ?? 'text' ); if ( function_exists( 'metaforge_is_pro_active' ) && ! metaforge_is_pro_active() && $type === 'repeater' ) { $type = 'group'; } return array( 'name' => sanitize_text_field( $field['name'] ?? '' ), 'label' => sanitize_text_field( $field['label'] ?? '' ), 'type' => $type, 'options' => $options_arr, 'options_raw' => $raw_options, 'sub_fields' => $sub_fields, 'conditional_logic' => intval( $field['conditional_logic'] ?? 0 ), 'conditions' => $conditions, 'layout' => sanitize_text_field( $field['layout'] ?? 'vertical' ), 'sub_layout' => sanitize_text_field( $field['sub_layout'] ?? 'block' ), 'image_return_format' => sanitize_text_field( $field['image_return_format'] ?? 'array' ), ); } private function condense_response_json( $json_str ) { $json_str_cleaned = stripslashes( $json_str ); $decoded = json_decode( $json_str_cleaned, true ); if ( ! $decoded || empty( $decoded['fields'] ) ) { $decoded = json_decode( $json_str, true ); } if ( ! $decoded || empty( $decoded['fields'] ) ) { return '[Previous Fields Layout]'; } $format_fields = function ( $fields ) use ( &$format_fields ) { $parts = array(); foreach ( $fields as $field ) { $type = $field['type'] ?? 'text'; $name = $field['name'] ?? ''; $label = $field['label'] ?? ''; $desc = "{$type} (Label: {$label}, Name: {$name})"; if ( ! empty( $field['sub_fields'] ) && is_array( $field['sub_fields'] ) ) { $desc .= ' [Sub-fields: ' . implode( ', ', $format_fields( $field['sub_fields'] ) ) . ']'; } $parts[] = $desc; } return $parts; }; return 'Generated fields: ' . implode( ', ', $format_fields( $decoded['fields'] ) ); } // ── AJAX: Import JSON directly (no AI call) ────────────────────────────── public function import_json() { check_ajax_referer( 'metaforge_ai_layout_nonce', 'nonce' ); if ( ! current_user_can( 'manage_metaforge' ) ) { wp_send_json_error( array( 'message' => 'Unauthorized' ) ); } // Rate limiting: prevent request spam (3-second lock per user) $rate_key = 'metaforge_rate_' . get_current_user_id(); if ( get_transient( $rate_key ) ) { wp_send_json_error( array( 'message' => 'Too many requests. Please wait a moment before trying again.' ) ); } set_transient( $rate_key, true, 3 ); $raw_json = sanitize_textarea_field( wp_unslash( $_POST['json'] ?? '' ) ); $start_index = isset( $_POST['start_index'] ) ? intval( wp_unslash( $_POST['start_index'] ) ) : 0; $prefix = sanitize_text_field( wp_unslash( $_POST['prefix'] ?? 'fields' ) ); if ( empty( $raw_json ) ) { wp_send_json_error( array( 'message' => 'No JSON provided.' ) ); } $decoded = json_decode( $raw_json, true ); if ( json_last_error() !== JSON_ERROR_NONE ) { wp_send_json_error( array( 'message' => 'Invalid JSON: ' . json_last_error_msg() ) ); } if ( is_array( $decoded ) ) { $decoded = map_deep( $decoded, 'sanitize_textarea_field' ); } $fields = null; $group_name = ''; // Format 1: { "metaforge_field_groups": [...] } if ( isset( $decoded['metaforge_field_groups'][0]['fields'] ) ) { $fields = $decoded['metaforge_field_groups'][0]['fields']; $group_name = $decoded['metaforge_field_groups'][0]['group_name'] ?? ''; } // Format 2: { "fields": [...] } elseif ( isset( $decoded['fields'] ) && is_array( $decoded['fields'] ) ) { $fields = $decoded['fields']; } // Format 3: [ {...}, {...} ] — raw field array elseif ( is_array( $decoded ) && isset( $decoded[0]['name'] ) ) { $fields = $decoded; } if ( empty( $fields ) || ! is_array( $fields ) ) { wp_send_json_error( array( 'message' => 'Could not find a "fields" array in the JSON. Please paste the full metaforge_groups export.' ) ); } global $metaforge_loader; if ( empty( $metaforge_loader ) ) { $metaforge_loader = new MetaForge_Loader(); } ob_start(); foreach ( $fields as $i => $raw_field ) { $field = $this->normalize_field( $raw_field ); $metaforge_loader->render_builder_field( $field, $prefix . '[' . ( $start_index + $i ) . ']', $start_index + $i, 0 ); } $html = ob_get_clean(); wp_send_json_success( array( 'html' => $html, 'count' => count( $fields ), 'group_name' => $group_name, ) ); } // ── AJAX: Generate layout via AI ───────────────────────────────────────── public function generate_layout() { check_ajax_referer( 'metaforge_ai_layout_nonce', 'nonce' ); if ( ! current_user_can( 'manage_metaforge' ) ) { wp_send_json_error( array( 'message' => 'Unauthorized' ) ); } // Rate limiting: prevent API credit abuse (5-second lock per user for AI generation) $rate_key = 'metaforge_ai_rate_' . get_current_user_id(); if ( get_transient( $rate_key ) ) { wp_send_json_error( array( 'message' => 'Please wait a few seconds before generating again.' ) ); } set_transient( $rate_key, true, 5 ); $prompt = sanitize_text_field( wp_unslash( $_POST['prompt'] ?? '' ) ); $group_name = sanitize_text_field( wp_unslash( $_POST['group_name'] ?? '' ) ); $schema_array = array(); $existing_schema = sanitize_textarea_field( wp_unslash( $_POST['existing_schema'] ?? '' ) ); $search_context = trim( $prompt . ' ' . $group_name ); if ( ! empty( $existing_schema ) && $existing_schema !== '[]' ) { $schema_array = json_decode( $existing_schema, true ); if ( is_array( $schema_array ) ) { $schema_array = map_deep( $schema_array, 'sanitize_textarea_field' ); $cleaned_fields = $this->clean_schema_fields( $schema_array, $search_context ); $existing_schema = wp_json_encode( $cleaned_fields, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE ); } } if ( empty( $prompt ) && empty( $group_name ) ) { wp_send_json_error( array( 'message' => 'Please provide a prompt or a group name.' ) ); } $client = new MetaForge_APIClient(); $is_pro = function_exists( 'metaforge_is_pro_active' ) && metaforge_is_pro_active(); if ( $is_pro ) { $layout_directives = "CRITICAL DIRECTIVES FOR LAYOUT BUILDER & FLEXIBLE CONTENT ARCHITECTURE: If the user asks to add a new layout, section, or block (or if a layout structure already exists in the CURRENT BUILDER FIELDS SCHEMA), you MUST build or update that Flexible Content architecture: 1. Ensure there is a parent 'repeater' field (usually named 'layouts', 'page_layouts', 'sections', or similar). 2. Inside that repeater's 'sub_fields', there must be a 'select' field (named 'layout_type' or ending with 'layout_type') containing choices for all the layout blocks. 3. For each choice in that 'layout_type' select field, there must be a corresponding sibling 'group' field inside the repeater's 'sub_fields' representing that block's contents. 4. Each group field MUST have 'conditional_logic' set to 1 and a condition rule targeting 'layout_type' is equal to that group field's name/slug. Example condition rule: [{\"field\": \"layout_type\", \"operator\": \"==\", \"value\": \"beauty_layout\"}] 5. When the user requests a new layout block (e.g., 'Add a Slider layout' or 'Create a Testimonial section'), you MUST: a. Keep all existing layouts/fields in the schema untouched. b. Add the new layout choice key and label (e.g. 'slider_layout:Slider Layout') to the 'layout_type' select field's options. c. Append the new corresponding 'group' field (e.g. name: 'slider_layout') to the repeater's 'sub_fields' containing all appropriate fields for that layout. d. Set the new group field's conditional logic to target the 'layout_type' select field with the new choice key (e.g., show if 'layout_type' is equal to 'slider_layout')."; $theme_directives = "CRITICAL DIRECTIVES FOR THEME OPTIONS & OPTIONS PAGES: If the user requests settings for 'Theme Options', 'Theme Settings', 'Header/Footer Settings', 'Social Media Links', or general site configurations: 1. Generate logical, organized option groups or fields matching typical theme features: - Header: Logo (image), Sticky Header (true_false), Header Navigation Options. - Footer: Copyright text (textarea), Columns/Widgets configurations. - Social Media Links: Repeater field containing Platform (select) and Profile URL (text). - Styling: Primary Color (text/colorpicker), Secondary Color, custom branding options. 2. Structure repeaters and groups carefully with clear and developer-friendly field slugs."; $allowed_types = 'text|textarea|image|wysiwyg|select|checkbox|radio|true_false|repeater|group|post_type_selector|taxonomy|relationship|date_time|oembed'; $pro_rule = ''; } else { $layout_directives = "CRITICAL DIRECTIVES FOR LAYOUT BUILDER & FLEXIBLE CONTENT ARCHITECTURE (FREE VERSION): IMPORTANT: This site is running the Free version of MetaForge Engine (MetaForge Pro is NOT active). The 'repeater' field type is a Pro-only feature and is strictly NOT available. You MUST NOT generate any 'repeater' fields. All page layouts and section containers MUST use 'group' fields instead of 'repeater'. 1. Ensure there is a parent 'group' field (usually named 'layouts', 'page_layouts', 'sections', or similar). 2. Inside that parent group's 'sub_fields', there must be a 'select' field (named 'layout_type' or ending with 'layout_type') containing choices for all the layout blocks. 3. For each choice in that 'layout_type' select field, there must be a corresponding sibling 'group' field inside the parent group's 'sub_fields' representing that block's contents. 4. Each group field MUST have 'conditional_logic' set to 1 and a condition rule targeting 'layout_type' is equal to that group field's name/slug. Example condition rule: [{\"field\": \"layout_type\", \"operator\": \"==\", \"value\": \"beauty_layout\"}] 5. When the user requests a new layout block (e.g., 'Add a Slider layout' or 'Create a Testimonial section'), you MUST: a. Keep all existing layouts/fields in the schema untouched. b. Add the new layout choice key and label (e.g. 'slider_layout:Slider Layout') to the 'layout_type' select field's options. c. Append the new corresponding 'group' field (e.g. name: 'slider_layout') to the parent group's 'sub_fields' containing all appropriate fields for that layout. d. Set the new group field's conditional logic to target the 'layout_type' select field with the new choice key (e.g., show if 'layout_type' is equal to 'slider_layout'). 6. For multi-item or repeating content (such as testimonials, client logos, feature lists), use a 'group' field with individual fields instead of a repeater."; $theme_directives = "CRITICAL DIRECTIVES FOR THEME OPTIONS & OPTIONS PAGES: If the user requests settings for 'Theme Options', 'Theme Settings', 'Header/Footer Settings', 'Social Media Links', or general site configurations: 1. Generate logical, organized option groups or fields matching typical theme features: - Header: Logo (image), Sticky Header (true_false), Header Navigation Options. - Footer: Copyright text (textarea), Columns/Widgets configurations. - Social Media Links: Group containing individual platform URL fields (facebook_url, twitter_url, instagram_url, linkedin_url). Do NOT use repeater. - Styling: Primary Color (text/colorpicker), Secondary Color, custom branding options. 2. Structure groups carefully with clear and developer-friendly field slugs."; $allowed_types = 'text|textarea|image|wysiwyg|select|checkbox|radio|true_false|group|post_type_selector|taxonomy|relationship|date_time|oembed'; $pro_rule = "\n- PRO vs FREE RESTRICTION: The 'repeater' field is disabled in the Free version. You MUST use 'group' instead of 'repeater'. Never output 'type': 'repeater'."; } $system_prompt = "You are an expert field schema designer for MetaForge Engine. Your task is to generate a highly detailed, professional list of fields appropriate for the user's request. We will provide you with the CURRENT BUILDER FIELDS SCHEMA (if any exist). {$layout_directives} {$theme_directives} CRITICAL FORMATTING RULES: - If you create or modify a 'select', 'checkbox', or 'radio' field, you MUST populate its 'options' string with a newline-separated list of values (e.g., 'hero_banner:Hero Banner\\ntext_block:Text Block'). The keys in this 'options' string MUST exactly match the 'value' used in the 'conditions' array of the dependent groups! - Respond ONLY with a raw JSON object containing an array of field definitions. - Do not wrap your response in markdown blocks.{$pro_rule} - Return format: { \"fields\": [ { \"type\": \"{$allowed_types}\", \"label\": \"Field Label\", \"name\": \"field_name_slug\", \"options\": \"value1:Label 1\\nvalue2:Label 2\", \"sub_fields\": [], \"conditional_logic\": 1, \"conditions\": [ {\"field\": \"layout_type\", \"operator\": \"==\", \"value\": \"value1\"} ] } ] }"; $user_prompt = ''; if ( ! empty( $existing_schema ) && $existing_schema !== '[]' ) { $user_prompt .= "CURRENT BUILDER FIELDS SCHEMA:\n" . $existing_schema . "\n\n"; } if ( class_exists( 'MetaForge_Memory' ) ) { $history = MetaForge_Memory::get_recent_history( get_current_user_id(), $group_name, 1 ); if ( ! empty( $history ) ) { $user_prompt .= "PREVIOUS CONVERSATION CONTEXT:\n"; foreach ( $history as $h ) { $user_prompt .= 'User: ' . $h['prompt'] . "\nAssistant: " . $this->condense_response_json( $h['response_json'] ) . "\n\n"; } $user_prompt .= "NEW REQUEST TO IMPLEMENT:\n"; } } $user_prompt .= 'Generate realistic fields for: ' . ( $prompt ? $prompt : $group_name ); $response = $client->generate( $system_prompt, $user_prompt, true ); if ( is_wp_error( $response ) ) { $error_data = $response->get_error_data(); $retry_after = isset( $error_data['retry_after'] ) ? intval( $error_data['retry_after'] ) : 0; wp_send_json_error( array( 'message' => $response->get_error_message(), 'code' => $response->get_error_code(), 'retry_after' => $retry_after, ) ); } $response = trim( $response ); if ( strpos( $response, '```json' ) === 0 ) { $response = trim( substr( $response, 7, -3 ) ); } elseif ( strpos( $response, '```' ) === 0 ) { $response = trim( substr( $response, 3, -3 ) ); } $decoded = json_decode( $response, true ); if ( ! $decoded || ! isset( $decoded['fields'] ) || ! is_array( $decoded['fields'] ) ) { wp_send_json_error( array( 'message' => 'Failed to parse AI response.' ) ); } $decoded = map_deep( $decoded, 'sanitize_textarea_field' ); if ( ! empty( $schema_array ) ) { $decoded['fields'] = $this->restore_pruned_fields( $decoded['fields'], $schema_array ); } // Log interaction to local database for conversational context if ( class_exists( 'MetaForge_Memory' ) ) { MetaForge_Memory::log_interaction( get_current_user_id(), $group_name, $user_prompt, $response ); } global $metaforge_loader; if ( empty( $metaforge_loader ) ) { $metaforge_loader = new MetaForge_Loader(); } $start_index = isset( $_POST['start_index'] ) ? intval( wp_unslash( $_POST['start_index'] ) ) : 0; $prefix = sanitize_text_field( wp_unslash( $_POST['prefix'] ?? 'fields' ) ); // Helper to collect all conditional values associated with a specific field name $collect_conditions = function ( $sibling_fields, $target_field_name ) { $values = array(); foreach ( $sibling_fields as $sf ) { if ( ! empty( $sf['conditional_logic'] ) && ! empty( $sf['conditions'] ) && is_array( $sf['conditions'] ) ) { foreach ( $sf['conditions'] as $cond ) { if ( isset( $cond['field'] ) && $cond['field'] === $target_field_name && ! empty( $cond['value'] ) ) { // Use the label of the group if available, otherwise just use the condition value $label = ! empty( $sf['label'] ) ? $sf['label'] : ucwords( str_replace( '_', ' ', $cond['value'] ) ); $values[ $cond['value'] ] = $label; } } } } return $values; }; // Recursive processor $repeater_converted = false; $process_fields = function ( &$fields ) use ( &$process_fields, $collect_conditions, $is_pro, &$repeater_converted ) { if ( ! is_array( $fields ) ) { return; } // Enforce Free vs Pro: If free version, convert any repeater to group if ( ! $is_pro ) { foreach ( $fields as &$f ) { if ( ( $f['type'] ?? '' ) === 'repeater' ) { $f['type'] = 'group'; $repeater_converted = true; } } unset( $f ); } // First pass: collect potential syncing layout data across siblings $sync_map = array(); foreach ( $fields as $field ) { if ( ( $field['type'] ?? '' ) === 'select' && ! empty( $field['name'] ) ) { $sync_map[ $field['name'] ] = $collect_conditions( $fields, $field['name'] ); } } foreach ( $fields as $i => &$field ) { if ( ! $is_pro && ( $field['type'] ?? '' ) === 'repeater' ) { $field['type'] = 'group'; $repeater_converted = true; } $field['layout'] = $field['layout'] ?? 'vertical'; $field['sub_layout'] = $field['sub_layout'] ?? 'block'; $field['image_return_format'] = $field['image_return_format'] ?? 'array'; // Sync select options with sibling conditions if we found matching dependent conditions! if ( ( $field['type'] ?? '' ) === 'select' && ! empty( $field['name'] ) && ! empty( $sync_map[ $field['name'] ] ) ) { $existing_opts = array(); // attempt to merge existing options with newly discovered ones if ( ! empty( $field['options'] ) ) { $opts_raw = is_array( $field['options'] ) ? implode( "\n", $field['options'] ) : $field['options']; foreach ( explode( "\n", $opts_raw ) as $line ) { $line = trim( $line ); if ( empty( $line ) ) { continue; } $parts = explode( ':', $line, 2 ); $k = trim( $parts[0] ); if ( count( $parts ) > 1 ) { $existing_opts[ $k ] = trim( $parts[1] ); } else { $existing_opts[ $k ] = $k; } } } // overwrite/merge with actual required conditions foreach ( $sync_map[ $field['name'] ] as $val => $label ) { $existing_opts[ $val ] = $label; } $built_options = array(); foreach ( $existing_opts as $k => $var ) { $built_options[] = $k . ' : ' . $var; } $field['options'] = implode( "\n", $built_options ); } // Format options strings safely and remove excessive blank lines if ( ! empty( $field['options'] ) ) { if ( is_array( $field['options'] ) ) { $field['options'] = implode( "\n", $field['options'] ); } if ( is_string( $field['options'] ) ) { $lines = array_filter( array_map( 'trim', explode( "\n", $field['options'] ) ) ); $field['options'] = implode( "\n", $lines ); } } $field['options_raw'] = $field['options'] ?? ''; if ( ! empty( $field['sub_fields'] ) && is_array( $field['sub_fields'] ) ) { $process_fields( $field['sub_fields'] ); } } }; $process_fields( $decoded['fields'] ); ob_start(); foreach ( $decoded['fields'] as $i => $field ) { $metaforge_loader->render_builder_field( $field, $prefix . '[' . ( $start_index + $i ) . ']', $start_index + $i, 0 ); } $html = ob_get_clean(); $response_data = array( 'html' => $html, 'count' => count( $decoded['fields'] ), ); if ( $repeater_converted ) { $response_data['notice'] = __( '⚠️ Repeater fields are not available in the Free version. We\'ve automatically used Group fields instead. Upgrade to MetaForge Pro to unlock Repeater fields.', 'metaforge-engine' ); $response_data['notice_type'] = 'upgrade'; } wp_send_json_success( $response_data ); } private function collect_conditional_target_fields( $fields, &$targets ) { if ( ! is_array( $fields ) ) { return; } foreach ( $fields as $field ) { if ( ! empty( $field['conditional_logic'] ) && ! empty( $field['conditions'] ) && is_array( $field['conditions'] ) ) { foreach ( $field['conditions'] as $cond ) { if ( ! empty( $cond['field'] ) ) { $targets[ $cond['field'] ] = true; } } } if ( ! empty( $field['sub_fields'] ) && is_array( $field['sub_fields'] ) ) { $this->collect_conditional_target_fields( $field['sub_fields'], $targets ); } } } private function clean_schema_fields( $fields, $prompt = '', $conditional_targets = null ) { if ( ! is_array( $fields ) ) { return array(); } if ( $conditional_targets === null ) { $conditional_targets = array(); $this->collect_conditional_target_fields( $fields, $conditional_targets ); } $cleaned = array(); foreach ( $fields as $field ) { if ( ! is_array( $field ) ) { continue; } $f = array(); if ( ! empty( $field['type'] ) ) { $f['type'] = $field['type']; } if ( ! empty( $field['name'] ) ) { $f['name'] = $field['name']; } if ( ! empty( $field['label'] ) ) { $f['label'] = $field['label']; } $name = $field['name'] ?? ''; $label = $field['label'] ?? ''; $type = $field['type'] ?? ''; // Keep options only if necessary (to save tokens): // 1. It is a conditional target field (like layout_type). // 2. Or it has layout/type in the name (heuristic for controllers). // 3. Or the field name/label is mentioned in the prompt. $keep_options = false; if ( ! empty( $conditional_targets[ $name ] ) ) { $keep_options = true; } elseif ( stripos( $name, 'layout' ) !== false || stripos( $name, 'type' ) !== false ) { $keep_options = true; } elseif ( ! empty( $prompt ) ) { if ( stripos( $prompt, $name ) !== false || stripos( $prompt, $label ) !== false || stripos( $prompt, str_replace( '_', ' ', $name ) ) !== false ) { $keep_options = true; } } if ( $keep_options && ! empty( $field['options'] ) ) { $f['options'] = $field['options']; } // Keep conditional logic if ( ! empty( $field['conditional_logic'] ) && ! empty( $field['conditions'] ) ) { $f['conditional_logic'] = 1; $f['conditions'] = $field['conditions']; } // Sub fields (recursively clean or prune to save tokens) if ( ! empty( $field['sub_fields'] ) && is_array( $field['sub_fields'] ) ) { $should_prune = false; if ( $type === 'group' || $type === 'repeater' ) { $should_prune = true; if ( ! empty( $prompt ) ) { if ( stripos( $prompt, $name ) !== false || stripos( $prompt, $label ) !== false || stripos( $prompt, str_replace( '_', ' ', $name ) ) !== false ) { $should_prune = false; } } } if ( $should_prune ) { $f['sub_fields'] = array( array( 'type' => 'text', 'name' => 'omitted_placeholder', 'label' => 'Sub-fields omitted to save tokens. Ask to modify this layout specifically if needed.', ), ); } else { $cleaned_sub = $this->clean_schema_fields( $field['sub_fields'], $prompt, $conditional_targets ); if ( ! empty( $cleaned_sub ) ) { $f['sub_fields'] = $cleaned_sub; } } } $cleaned[] = $f; } return $cleaned; } private function restore_pruned_fields( $generated_fields, $original_fields ) { if ( ! is_array( $generated_fields ) || ! is_array( $original_fields ) ) { return $generated_fields; } // Build a lookup map of original fields by name $original_map = array(); foreach ( $original_fields as $orig ) { if ( is_array( $orig ) && isset( $orig['name'] ) ) { $original_map[ $orig['name'] ] = $orig; } } foreach ( $generated_fields as &$gen ) { if ( ! is_array( $gen ) || ! isset( $gen['name'] ) ) { continue; } $name = $gen['name']; if ( ! isset( $original_map[ $name ] ) ) { continue; } $orig = $original_map[ $name ]; // 1. Restore option strings if they were pruned/emptied if ( empty( $gen['options'] ) && ! empty( $orig['options'] ) ) { $gen['options'] = $orig['options']; } // 2. Restore subfields if they were pruned to omitted_placeholder or left empty $is_pruned = false; if ( empty( $gen['sub_fields'] ) ) { $is_pruned = true; } elseif ( is_array( $gen['sub_fields'] ) ) { foreach ( $gen['sub_fields'] as $sub ) { if ( isset( $sub['name'] ) && $sub['name'] === 'omitted_placeholder' ) { $is_pruned = true; break; } } } if ( $is_pruned && ! empty( $orig['sub_fields'] ) ) { $gen['sub_fields'] = $orig['sub_fields']; } elseif ( ! empty( $gen['sub_fields'] ) && ! empty( $orig['sub_fields'] ) ) { // Recursively restore nested sub_fields $gen['sub_fields'] = $this->restore_pruned_fields( $gen['sub_fields'], $orig['sub_fields'] ); } } return $generated_fields; } }