PluginProbe
Elementor Website Builder – more than just a page builder / 4.3.0-beta3
Elementor Website Builder – more than just a page builder v4.3.0-beta3
4.3.0-beta3 4.3.0-beta2 4.3.0-beta1 4.2.4 4.2.3 4.2.2 4.2.1 4.2.0 4.1.5 4.2.0-beta2 4.2.0-dev2 4.2.0-beta1 4.1.4 4.1.3 4.1.2 4.1.1 4.1.0 4.1.0-beta3 4.1.0-dev3 4.0.9 4.1.0-beta2 4.1.0-dev2 4.0.8 4.1.0-beta1 4.1.0-dev1 All 452 releases
← All changes | modules/mcp/abilities/get-structure-ability.php +242 -3 4.2.24.3.0-beta3 View file →
@@ -1,9 +1,23 @@
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;
5 18 use Elementor\Plugin;
19 +use Elementor\Utils;
6 20
7 21 if ( ! defined( 'ABSPATH' ) ) {
8 22 exit;
9 23 }
@@ -9,8 +23,14 @@
9 23 }
10 24
11 25 class Get_Structure_Ability extends Abstract_Ability {
12 26
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 +
13 33 protected function get_ability_id(): string {
14 34 return 'elementor/get-page-structure';
15 35 }
16 36
@@ -16,9 +36,9 @@
16 36
17 37 protected function get_definition(): Ability_Definition {
18 38 return new Ability_Definition(
19 39 __( 'Get Elementor Page Structure', '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' ),
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' ),
21 41 'elementor',
22 42 [
23 43 'type' => 'object',
24 44 'properties' => [
@@ -23,9 +43,9 @@
23 43 'type' => 'object',
24 44 'properties' => [
25 45 'elements' => [
26 46 'type' => 'array',
27 - 'description' => 'Root-level Elementor elements for the document.',
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.',
28 48 ],
29 49 ],
30 50 ],
31 51 [
@@ -45,8 +65,17 @@
45 65 'post_id' => [
46 66 'type' => 'integer',
47 67 'description' => 'WordPress post ID of the Elementor document.',
48 68 ],
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 + ],
49 78 ],
50 79 ]
51 80 );
52 81 }
@@ -62,12 +91,222 @@
62 91 return $document;
63 92 }
64 93
65 94 $elements = $document->get_elements_data();
95 + $elements = is_array( $elements ) ? $elements : [];
66 96
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 +
67 122 return [
68 - 'elements' => is_array( $elements ) ? $elements : [],
123 + 'elements' => $this->prune_elements( $elements, $include_content ),
69 124 ];
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 : [];
70 309 }
71 310
72 311 private function resolve_post_id( $input ) {
73 312 $post_id = isset( $input['post_id'] ) ? absint( $input['post_id'] ) : 0;