p_error( $builder_result ) ) { return $builder_result; } $settings['overridable_props'] = $builder_result; return null; } /** * Rewrites `overridable_props[*].target` from source-document ids to the freshly assigned * ids, so callers can keep referencing the elements by the ids they observed on the source. * Unknown targets pass through untouched so the xml_structure `configuration-id` path (which * resolves via `editor_settings.title` in `Overridable_Props_Builder::find_element_ref`) and * regular id targets still work. * * @param array $definitions * @param array $source_id_map */ private function remap_overridable_targets( array $definitions, array $source_id_map ): array { if ( empty( $source_id_map ) ) { return $definitions; } foreach ( $definitions as $override_key => &$definition ) { if ( ! is_array( $definition ) ) { continue; } $target = $definition['target'] ?? null; if ( is_string( $target ) && isset( $source_id_map[ $target ] ) ) { $definition['target'] = $source_id_map[ $target ]; } } return $definitions; } private function assign_element_ids( array $elements ): array { return array_map( fn( array $element ) => $this->assign_element_id( $element ), $elements ); } private function assign_element_id( array $element ): array { $element['id'] = Document_Mutator::instance()->generate_id(); if ( ! empty( $element['elements'] ) && is_array( $element['elements'] ) ) { $element['elements'] = array_map( fn( array $child ) => $this->assign_element_id( $child ), $element['elements'] ); } return $element; } /** * Regenerates ids like `assign_element_ids`, but records the mapping from each element's * old id to its new id so callers can keep addressing the tree by pre-regeneration ids. * * @param array[] $elements * @param array $source_id_map Populated by reference. */ private function assign_element_ids_recording_source_ids( array $elements, array &$source_id_map ): array { $result = []; foreach ( $elements as $element ) { $result[] = $this->assign_element_id_recording_source_id( $element, $source_id_map ); } return $result; } private function assign_element_id_recording_source_id( array $element, array &$source_id_map ): array { $old_id = isset( $element['id'] ) ? (string) $element['id'] : ''; $element['id'] = Document_Mutator::instance()->generate_id(); if ( '' !== $old_id ) { $source_id_map[ $old_id ] = $element['id']; } if ( ! empty( $element['elements'] ) && is_array( $element['elements'] ) ) { $element['elements'] = $this->assign_element_ids_recording_source_ids( $element['elements'], $source_id_map ); } return $element; } private function document_links( Component_Document $component ): array { $editor_url = $component->get_edit_url(); return [ 'edit_url' => $editor_url, 'llm_instructions' => sprintf( /* translators: %s: Component editor URL. */ __( 'You MUST show the user this link to review the component: %s', 'elementor' ), $editor_url ), ]; } private function resolve_status( array $input ): string { return 'draft' === ( $input['publish_status'] ?? 'publish' ) ? Document::STATUS_DRAFT : Document::STATUS_PUBLISH; } private function generate_uid(): string { return 'component-' . wp_generate_uuid4(); } private function normalize_ids( $raw ): array { if ( ! is_array( $raw ) ) { return []; } $ids = []; foreach ( $raw as $value ) { $id = (int) $value; if ( $id > 0 ) { $ids[ $id ] = $id; } } return array_values( $ids ); } private function check_access( string $action, callable $can ): ?\WP_Error { return $can() ? null : Insufficient_Permissions_Error::for_action( $action ); } private function invalid_input( string $message ): \WP_Error { return new \WP_Error( 'invalid_input', $message, [ 'status' => \WP_Http::BAD_REQUEST ] ); } private function not_found( string $message ): \WP_Error { return new \WP_Error( 'elementor_not_found', $message, [ 'status' => \WP_Http::NOT_FOUND ] ); } private function get_repository(): Components_Repository { if ( ! $this->repository ) { $this->repository = new Components_Repository(); } return $this->repository; } private function get_output_schema(): array { return [ 'type' => 'object', 'properties' => [ 'success' => [ 'type' => 'boolean' ], 'component_id' => [ 'type' => 'integer' ], 'uid' => [ 'type' => 'string', 'description' => 'Server-generated component uid, only present for create.', ], 'success_ids' => [ 'type' => 'array', 'items' => [ 'type' => 'integer' ], 'description' => 'archive only.', ], 'failed_ids' => [ 'type' => 'array', 'items' => [ 'type' => 'integer' ], 'description' => 'archive only.', ], 'edit_url' => [ 'type' => 'string', 'description' => 'Component documents have no public permalink; this is the editor URL to review the change.', ], 'llm_instructions' => [ 'type' => 'string', 'description' => 'Mandatory next step: include this text (with the editor link) in your reply to the user.', ], 'warnings' => [ 'type' => 'array', 'items' => [ 'type' => 'string' ], 'description' => 'Non-fatal notices from XML compilation, e.g. props skipped or CSS that fell back to custom_css.', ], ], ]; } private function get_input_schema(): array { return [ 'type' => 'object', 'required' => [ 'action' ], 'properties' => [ 'action' => [ 'type' => 'string', 'enum' => [ self::ACTION_CREATE, self::ACTION_UPDATE, self::ACTION_RENAME, self::ACTION_ARCHIVE, self::ACTION_PUBLISH ], ], 'title' => [ 'type' => 'string', 'minLength' => 2, 'maxLength' => 200, 'description' => 'create: component title. rename: the new title.', ], 'source_post_id' => [ 'type' => 'integer', 'description' => 'create: WordPress post id to copy an existing element subtree from. Requires element_id. Mutually exclusive with xml_structure.', ], 'element_id' => [ 'type' => 'string', 'description' => 'create: id of the element (within source_post_id, from elementor/get-page-structure) to copy as the component root.', ], 'xml_structure' => [ 'type' => 'string', 'description' => 'create/update: same tag language as elementor/build-composition, with exactly one root element. Mutually exclusive with source_post_id/element_id.', ], 'element_config' => [ 'type' => 'object', 'default' => (object) [], 'description' => 'Same shape as elementor/build-composition element_config. Only used with xml_structure.', ], 'classes' => [ 'type' => 'object', 'default' => (object) [], 'description' => 'Same shape as elementor/build-composition classes. Only used with xml_structure.', ], 'style' => [ 'type' => 'object', 'default' => (object) [], 'description' => 'Same shape as elementor/build-composition style. Only used with xml_structure.', ], 'interactions' => [ 'type' => 'object', 'default' => (object) [], 'description' => 'Same shape as elementor/build-composition interactions. Only used with xml_structure.', ], 'overridable_props' => [ 'type' => 'object', 'description' => 'Caller-supplied record mapping override-key → { target, prop_key, label, group? }. target identifies the element to expose: for xml_structure, use the same configuration-id you set on that element; for source_post_id/element_id (create) or an update without xml_structure, use the real element id (from elementor/get-page-structure). Group IDs and origin values are generated server-side.', ], 'publish_status' => [ 'type' => 'string', 'enum' => [ 'publish', 'draft' ], 'default' => 'publish', 'description' => 'create: initial document status. update/rename/archive: "draft" edits an autosave copy instead of the live document; the change is not visible on pages using this component until you publish it.', ], 'component_id' => [ 'type' => 'integer', 'description' => 'update/rename/publish target component id.', ], 'component_ids' => [ 'type' => 'array', 'items' => [ 'type' => 'integer' ], 'description' => 'archive targets.', ], ], ]; } private function emit_component_created_event( string $title, int $component_id, array $elements ): void { [ 'elements_count' => $nested_elements_count, 'components_count' => $nested_components_count ] = $this->count_nested_elements( $elements ); $top_element_type = $elements[0]['elType'] ?? ( $elements[0]['widgetType'] ?? '' ); Mcp_Event_Dispatcher::emit( 'component_created', [ 'id' => (string) $component_id, 'name' => $title, 'nested_elements_count' => $nested_elements_count, 'nested_components_count' => $nested_components_count, 'top_element_type' => $top_element_type, ] ); } private function count_nested_elements( array $elements ): array { $elements_count = count( $elements ); $components_count = 0; foreach ( $elements as $element ) { if ( 'e-component' === ( $element['widgetType'] ?? '' ) ) { $components_count++; } $children = $element['elements'] ?? []; if ( ! empty( $children ) ) { $child_counts = $this->count_nested_elements( $children ); $elements_count += $child_counts['elements_count']; $components_count += $child_counts['components_count']; } } return [ 'elements_count' => $elements_count, 'components_count' => $components_count, ]; } }