| @@ -1,23 +1,9 @@ | ||
| 1 | 1 | <?php |
| 2 | 2 | |
| 3 | 3 | namespace Elementor\Modules\Mcp\Abilities; |
| 4 | 4 | |
| 5 | -use Elementor\Modules\AtomicWidgets\PropsResolver\Render_Props_Resolver; | |
| 6 | -use Elementor\Modules\AtomicWidgets\Styles\Local_Style_Serializer; | |
| 7 | -use Elementor\Modules\AtomicWidgets\Utils\Element_Structure_Title; | |
| 8 | -use Elementor\Modules\DefaultStyles\Default_Styles_Repository; | |
| 9 | -use Elementor\Modules\GlobalClasses\Utils\Atomic_Elements_Utils; | |
| 10 | -use Elementor\Modules\Interactions\Props\Interaction_Item_Prop_Type; | |
| 11 | -use Elementor\Modules\Mcp\Abilities\Appliers\V3_Node_Bridge; | |
| 12 | -use Elementor\Modules\Mcp\Abilities\Appliers\V3\V3_Non_Style_Allowlist; | |
| 13 | -use Elementor\Modules\Mcp\Abilities\Appliers\V3\V3_Style_Serializer; | |
| 14 | -use Elementor\Modules\Mcp\Abilities\Appliers\V3\V3_Widget_Bridge_Registry; | |
| 15 | -use Elementor\Modules\Mcp\Abilities\Utils\Element_Default_Styles_Builder; | |
| 16 | -use Elementor\Modules\Mcp\Abilities\Utils\Element_Tag_Resolver; | |
| 17 | -use Elementor\Modules\Mcp\Abilities\Utils\Widget_Context_Helper; | |
| 18 | 5 | use Elementor\Plugin; |
| 19 | -use Elementor\Utils; | |
| 20 | 6 | |
| 21 | 7 | if ( ! defined( 'ABSPATH' ) ) { |
| 22 | 8 | exit; |
| 23 | 9 | } |
| @@ -23,14 +9,8 @@ | ||
| 23 | 9 | } |
| 24 | 10 | |
| 25 | 11 | class Get_Structure_Ability extends Abstract_Ability { |
| 26 | 12 | |
| 27 | - private ?Default_Styles_Repository $default_styles_repository; | |
| 28 | - | |
| 29 | - public function __construct( ?Default_Styles_Repository $default_styles_repository = null ) { | |
| 30 | - $this->default_styles_repository = $default_styles_repository; | |
| 31 | - } | |
| 32 | - | |
| 33 | 13 | protected function get_ability_id(): string { |
| 34 | 14 | return 'elementor/get-page-structure'; |
| 35 | 15 | } |
| 36 | 16 | |
| @@ -36,9 +16,9 @@ | ||
| 36 | 16 | |
| 37 | 17 | protected function get_definition(): Ability_Definition { |
| 38 | 18 | return new Ability_Definition( |
| 39 | 19 | __( 'Get Elementor Page Structure', 'elementor' ), |
| 40 | - __( 'Returns a lean Elementor element tree skeleton (id, elType, widgetType, version, title, nested elements) for a single post or page ID. Each node is tagged with version=3 (legacy) or version=4 (atomic). Only version=4 nodes can be modified via elementor/manage-elements or referenced by elementor/build-composition element_config; version=3 nodes are returned for context only and must be edited directly in the Elementor editor. Optionally scope to a subtree via element_id. Set include_content=true (requires element_id) to also return each V4 node\'s settings, styles (as { __style_id, css } where css is a raw CSS string round-trippable to manage-elements.update.style / build-composition.style in replace mode), interactions, its rendered HTML tag when known, and default_styles (a raw CSS string of the widget base layer followed by the kit\'s site-wide default layer for that tag, in browser cascade order — includes selectors, @media(--breakpoint) blocks, and pseudo-states as the frontend renders them). V3 nodes are returned with empty settings and styles. Only works for posts that were saved with Elementor.', 'elementor' ), | |
| 20 | + __( 'Returns the Elementor element tree (widgets, containers, and nested content) for a single post or page ID. Use after list-pages when you need the live JSON structure to reason about layout, widget types, or to plan edits. Only works for posts that were saved with Elementor.', 'elementor' ), | |
| 41 | 21 | 'elementor', |
| 42 | 22 | [ |
| 43 | 23 | 'type' => 'object', |
| 44 | 24 | 'properties' => [ |
| @@ -43,9 +23,9 @@ | ||
| 43 | 23 | 'type' => 'object', |
| 44 | 24 | 'properties' => [ |
| 45 | 25 | 'elements' => [ |
| 46 | 26 | 'type' => 'array', |
| 47 | - 'description' => 'Skeleton of Elementor elements (id, elType, widgetType, version, title, nested elements). When include_content is true, V4 nodes also include settings, styles (as { __style_id, css } — raw CSS string with @media(--breakpoint) + &:hover/&:focus/&:active), interactions, tag (rendered HTML wrapper tag when known), and default_styles (raw CSS string: widget base layer + kit site-wide default for that tag, in cascade order). V3 nodes always have empty settings and styles.', | |
| 27 | + 'description' => 'Root-level Elementor elements for the document.', | |
| 48 | 28 | ], |
| 49 | 29 | ], |
| 50 | 30 | ], |
| 51 | 31 | [ |
| @@ -65,17 +45,8 @@ | ||
| 65 | 45 | 'post_id' => [ |
| 66 | 46 | 'type' => 'integer', |
| 67 | 47 | 'description' => 'WordPress post ID of the Elementor document.', |
| 68 | 48 | ], |
| 69 | - 'element_id' => [ | |
| 70 | - 'type' => 'string', | |
| 71 | - 'description' => 'Optional. If provided, returns only the subtree rooted at that element id.', | |
| 72 | - ], | |
| 73 | - 'include_content' => [ | |
| 74 | - 'type' => 'boolean', | |
| 75 | - 'default' => false, | |
| 76 | - 'description' => 'If true, includes each V4 node\'s settings, styles (as { __style_id, css } — raw CSS string), interactions, rendered tag, and default_styles (raw CSS string: base layer + kit default for that tag). The styles.css value is round-trippable to build-composition.style / manage-elements.update.style in replace mode. Requires element_id.', | |
| 77 | - ], | |
| 78 | 49 | ], |
| 79 | 50 | ] |
| 80 | 51 | ); |
| 81 | 52 | } |
| @@ -91,222 +62,12 @@ | ||
| 91 | 62 | return $document; |
| 92 | 63 | } |
| 93 | 64 | |
| 94 | 65 | $elements = $document->get_elements_data(); |
| 95 | - $elements = is_array( $elements ) ? $elements : []; | |
| 96 | 66 | |
| 97 | - $element_id = isset( $input['element_id'] ) ? (string) $input['element_id'] : ''; | |
| 98 | - $include_content = ! empty( $input['include_content'] ); | |
| 99 | - | |
| 100 | - if ( $include_content && '' === $element_id ) { | |
| 101 | - return new \WP_Error( | |
| 102 | - 'invalid_input', | |
| 103 | - __( 'include_content requires element_id.', 'elementor' ), | |
| 104 | - [ 'status' => \WP_Http::BAD_REQUEST ] | |
| 105 | - ); | |
| 106 | - } | |
| 107 | - | |
| 108 | - if ( '' !== $element_id ) { | |
| 109 | - $subtree = Utils::find_element_recursive( $elements, $element_id ); | |
| 110 | - | |
| 111 | - if ( ! $subtree ) { | |
| 112 | - return new \WP_Error( | |
| 113 | - 'element_not_found', | |
| 114 | - __( 'Element not found in this document.', 'elementor' ), | |
| 115 | - [ 'status' => \WP_Http::NOT_FOUND ] | |
| 116 | - ); | |
| 117 | - } | |
| 118 | - | |
| 119 | - $elements = [ $subtree ]; | |
| 120 | - } | |
| 121 | - | |
| 122 | 67 | return [ |
| 123 | - 'elements' => $this->prune_elements( $elements, $include_content ), | |
| 68 | + 'elements' => is_array( $elements ) ? $elements : [], | |
| 124 | 69 | ]; |
| 125 | - } | |
| 126 | - | |
| 127 | - private function prune_elements( array $elements, bool $include_content = false ): array { | |
| 128 | - return Plugin::$instance->db->iterate_data( $elements, function ( $node ) use ( $include_content ) { | |
| 129 | - $skeleton = [ | |
| 130 | - 'id' => $node['id'] ?? null, | |
| 131 | - 'elType' => $node['elType'] ?? null, | |
| 132 | - ]; | |
| 133 | - | |
| 134 | - if ( isset( $node['widgetType'] ) ) { | |
| 135 | - $skeleton['widgetType'] = $node['widgetType']; | |
| 136 | - } | |
| 137 | - | |
| 138 | - $title = Element_Structure_Title::resolve( $node ); | |
| 139 | - | |
| 140 | - if ( null !== $title ) { | |
| 141 | - $skeleton['title'] = $title; | |
| 142 | - } | |
| 143 | - | |
| 144 | - if ( ! empty( $node['elements'] ) ) { | |
| 145 | - $skeleton['elements'] = $node['elements']; | |
| 146 | - } | |
| 147 | - | |
| 148 | - if ( $include_content ) { | |
| 149 | - $this->populate_content( $skeleton, $node ); | |
| 150 | - } | |
| 151 | - | |
| 152 | - return $skeleton; | |
| 153 | - } ); | |
| 154 | - } | |
| 155 | - | |
| 156 | - private function populate_content( array &$skeleton, array $node ): void { | |
| 157 | - $skeleton['interactions'] = $this->normalize_interactions( $node['interactions'] ?? null ); | |
| 158 | - | |
| 159 | - if ( V3_Node_Bridge::is_v3_node( $node ) ) { | |
| 160 | - $this->populate_v3_content( $skeleton, $node ); | |
| 161 | - return; | |
| 162 | - } | |
| 163 | - | |
| 164 | - $config = $this->resolve_widget_config( $node ); | |
| 165 | - $props_schema = $config['atomic_props_schema'] ?? null; | |
| 166 | - $raw_settings = $node['settings'] ?? []; | |
| 167 | - | |
| 168 | - if ( ! $props_schema ) { | |
| 169 | - $skeleton['settings'] = (object) []; | |
| 170 | - $skeleton['styles'] = (object) []; | |
| 171 | - return; | |
| 172 | - } | |
| 173 | - | |
| 174 | - $skeleton['settings'] = $this->serialize_settings_for_llm( $props_schema, $raw_settings ); | |
| 175 | - $skeleton['styles'] = Local_Style_Serializer::serialize( $node['styles'] ?? [] ); | |
| 176 | - | |
| 177 | - $resolved_settings = is_array( $skeleton['settings'] ) ? $skeleton['settings'] : []; | |
| 178 | - $this->populate_default_styles( $skeleton, $node, $config, $resolved_settings ); | |
| 179 | - } | |
| 180 | - | |
| 181 | - private function serialize_settings_for_llm( array $props_schema, $raw_settings ) { | |
| 182 | - if ( ! is_array( $raw_settings ) ) { | |
| 183 | - return (object) []; | |
| 184 | - } | |
| 185 | - | |
| 186 | - $schema = array_intersect_key( $props_schema, $raw_settings ); | |
| 187 | - $serialized = []; | |
| 188 | - $static_schema = []; | |
| 189 | - | |
| 190 | - foreach ( $schema as $key => $prop_type ) { | |
| 191 | - $dynamic = Dynamic_Tag_Llm_Resolver::try_serialize( $raw_settings[ $key ] ); | |
| 192 | - | |
| 193 | - if ( null !== $dynamic ) { | |
| 194 | - $serialized[ $key ] = $dynamic; | |
| 195 | - continue; | |
| 196 | - } | |
| 197 | - | |
| 198 | - $static_schema[ $key ] = $prop_type; | |
| 199 | - } | |
| 200 | - | |
| 201 | - if ( ! empty( $static_schema ) ) { | |
| 202 | - $serialized += Render_Props_Resolver::for_settings()->resolve( $static_schema, $raw_settings ); | |
| 203 | - } | |
| 204 | - | |
| 205 | - return ! empty( $serialized ) ? $serialized : (object) []; | |
| 206 | - } | |
| 207 | - | |
| 208 | - private function populate_default_styles( array &$skeleton, array $node, array $config, array $resolved_settings ): void { | |
| 209 | - $base_styles = is_array( $config['base_styles'] ?? null ) ? $config['base_styles'] : []; | |
| 210 | - $props_schema = is_array( $config['atomic_props_schema'] ?? null ) ? $config['atomic_props_schema'] : []; | |
| 211 | - $tag = $this->resolve_html_tag( $node, $resolved_settings ); | |
| 212 | - | |
| 213 | - $default_styles_css = Element_Default_Styles_Builder::render( | |
| 214 | - $base_styles, | |
| 215 | - $tag, | |
| 216 | - $this->get_default_styles_repository() | |
| 217 | - ); | |
| 218 | - | |
| 219 | - if ( null !== $tag ) { | |
| 220 | - $skeleton['tag'] = $tag; | |
| 221 | - } | |
| 222 | - | |
| 223 | - if ( '' !== $default_styles_css ) { | |
| 224 | - $skeleton['default_styles'] = $default_styles_css; | |
| 225 | - } | |
| 226 | - } | |
| 227 | - | |
| 228 | - private function resolve_html_tag( array $node, array $resolved_settings ): ?string { | |
| 229 | - $type = Atomic_Elements_Utils::get_element_type( $node ); | |
| 230 | - | |
| 231 | - if ( ! $type ) { | |
| 232 | - return null; | |
| 233 | - } | |
| 234 | - | |
| 235 | - return Element_Tag_Resolver::resolve( $resolved_settings, $type ); | |
| 236 | - } | |
| 237 | - | |
| 238 | - private function get_default_styles_repository(): ?Default_Styles_Repository { | |
| 239 | - if ( null !== $this->default_styles_repository ) { | |
| 240 | - return $this->default_styles_repository; | |
| 241 | - } | |
| 242 | - | |
| 243 | - if ( ! class_exists( Default_Styles_Repository::class ) ) { | |
| 244 | - return null; | |
| 245 | - } | |
| 246 | - | |
| 247 | - $this->default_styles_repository = Default_Styles_Repository::make(); | |
| 248 | - | |
| 249 | - return $this->default_styles_repository; | |
| 250 | - } | |
| 251 | - | |
| 252 | - private function populate_v3_content( array &$skeleton, array $node ): void { | |
| 253 | - $widget_type = (string) ( $node['widgetType'] ?? '' ); | |
| 254 | - $raw_settings = is_array( $node['settings'] ?? null ) ? $node['settings'] : []; | |
| 255 | - | |
| 256 | - $allowed = V3_Widget_Bridge_Registry::get_non_style_keys( $widget_type ); | |
| 257 | - | |
| 258 | - if ( empty( $allowed ) && empty( V3_Widget_Bridge_Registry::get_style_overrides( $widget_type ) ) ) { | |
| 259 | - $skeleton['settings'] = (object) []; | |
| 260 | - $skeleton['style'] = ''; | |
| 261 | - return; | |
| 262 | - } | |
| 263 | - | |
| 264 | - $filter = V3_Non_Style_Allowlist::filter( $widget_type, $raw_settings ); | |
| 265 | - $allowed_settings = $filter['allowed']; | |
| 266 | - | |
| 267 | - $widget_config = Widget_Context_Helper::get_widget_config( $widget_type ); | |
| 268 | - $style = ( new V3_Style_Serializer() )->serialize( $raw_settings, $widget_type, $widget_config ?? [] ); | |
| 269 | - | |
| 270 | - $skeleton['settings'] = ! empty( $allowed_settings ) ? $allowed_settings : (object) []; | |
| 271 | - $skeleton['style'] = $style; | |
| 272 | - } | |
| 273 | - | |
| 274 | - private function normalize_interactions( $interactions ): array { | |
| 275 | - if ( is_string( $interactions ) ) { | |
| 276 | - $decoded = json_decode( $interactions, true ); | |
| 277 | - $interactions = ( JSON_ERROR_NONE === json_last_error() && is_array( $decoded ) ) ? $decoded : []; | |
| 278 | - } | |
| 279 | - | |
| 280 | - if ( ! is_array( $interactions ) || empty( $interactions['items'] ) ) { | |
| 281 | - return []; | |
| 282 | - } | |
| 283 | - | |
| 284 | - $resolver = Render_Props_Resolver::for_plain(); | |
| 285 | - $prop_type = Interaction_Item_Prop_Type::make(); | |
| 286 | - $plain = []; | |
| 287 | - | |
| 288 | - foreach ( $interactions['items'] as $item ) { | |
| 289 | - $serialized = $resolver->resolve_value( $item, $prop_type ); | |
| 290 | - | |
| 291 | - if ( is_array( $serialized ) && ! empty( $serialized ) ) { | |
| 292 | - $plain[] = $serialized; | |
| 293 | - } | |
| 294 | - } | |
| 295 | - | |
| 296 | - return $plain; | |
| 297 | - } | |
| 298 | - | |
| 299 | - private function resolve_widget_config( array $node ): array { | |
| 300 | - $type = Atomic_Elements_Utils::get_element_type( $node ); | |
| 301 | - | |
| 302 | - if ( ! $type ) { | |
| 303 | - return []; | |
| 304 | - } | |
| 305 | - | |
| 306 | - $config = Widget_Context_Helper::get_widget_config( (string) $type ); | |
| 307 | - | |
| 308 | - return is_array( $config ) ? $config : []; | |
| 309 | 70 | } |
| 310 | 71 | |
| 311 | 72 | private function resolve_post_id( $input ) { |
| 312 | 73 | $post_id = isset( $input['post_id'] ) ? absint( $input['post_id'] ) : 0; |