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
elementor / modules / mcp / abilities / utils / widget-context-helper.php

widget-context-helper.php in Elementor Website Builder – more than just a page builder 4.3.0-beta3, at modules/mcp/abilities/utils/widget-context-helper.php

361 lines 11.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace Elementor\Modules\Mcp\Abilities\Utils;
4
5 use Elementor\Modules\AtomicWidgets\PropTypes\Base\Array_Prop_Type;
6 use Elementor\Modules\AtomicWidgets\PropTypes\Base\Object_Prop_Type;
7 use Elementor\Modules\AtomicWidgets\PropTypes\Contracts\Prop_Type;
8 use Elementor\Modules\AtomicWidgets\PropTypes\Escaped_Html_Prop_Type;
9 use Elementor\Modules\AtomicWidgets\PropTypes\Utils\Plain_Llm_Schema_Converter;
10 use Elementor\Modules\GlobalClasses\Utils\Atomic_Elements_Utils;
11 use Elementor\Modules\Mcp\Abilities\Appliers\V3\V3_Widget_Bridge_Registry;
12 use Elementor\Plugin;
13 use Elementor\Utils;
14
15 if ( ! defined( 'ABSPATH' ) ) {
16 exit;
17 }
18
19 /**
20 * Provides widget metadata for MCP abilities: eligibility checks, summaries, and JSON schemas
21 * for LLM consumption. The schema output includes property types and LLM guidance.
22 */
23 class Widget_Context_Helper {
24
25 const NON_CONFIGURABLE_PROP_KEYS = [ '_cssid', 'classes', 'attributes', 'display-conditions' ];
26
27 const EXCLUDED_WIDGET_TITLE = 'Component';
28
29 const VERSION_V3 = 'v3';
30
31 const VERSION_V4 = 'v4';
32
33 const V3_ALLOWLIST = [
34 'nav-menu',
35 'theme-post-content',
36 'theme-post-title',
37 'theme-post-featured-image',
38 'theme-post-excerpt',
39 'theme-archive-title',
40 ];
41
42 const V3_FALLBACK_MESSAGE = '`properties` lists the only keys accepted in `element_config` / `manage-elements.settings` for this widget. Put all visual styling in the `style` (CSS) input.';
43
44 const V3_FALLBACK_FIELDS_NOTE = 'All properties are optional. Object-typed properties describe common shapes but do not include exhaustive inner validation.';
45
46 const ALLOWED_HTML_TAGS_NOTE = 'May contain inline HTML written directly in the string (e.g. "Hello <strong>world</strong>"), limited to these tags: %s. Any other tag is stripped on save.';
47
48 /**
49 * @return array<string, array> widget_type => config, filtered to widgets eligible for LLM use.
50 */
51 public static function get_llm_eligible_widgets(): array {
52 $all_types = array_merge(
53 Plugin::$instance->widgets_manager->get_widget_types(),
54 Plugin::$instance->elements_manager->get_element_types()
55 );
56
57 $eligible = [];
58
59 foreach ( $all_types as $type => $instance ) {
60 if ( self::is_v3_allowlisted( (string) $type ) && method_exists( $instance, 'get_stack' ) ) {
61 $instance->get_stack();
62 }
63
64 $config = $instance->get_config();
65
66 if ( self::is_widget_eligible_for_llm( $config ) ) {
67 $eligible[ $type ] = $config;
68 }
69 }
70
71 return $eligible;
72 }
73
74 public static function get_widget_config( string $widget_type ): ?array {
75 $instance = Atomic_Elements_Utils::get_element_instance( $widget_type );
76
77 if ( ! $instance ) {
78 return null;
79 }
80
81 if ( self::is_v3_allowlisted( $widget_type ) && method_exists( $instance, 'get_stack' ) ) {
82 $instance->get_stack();
83 }
84
85 return $instance->get_config();
86 }
87
88 public static function is_widget_eligible_for_llm( array $config ): bool {
89 if ( false === ( $config['meta']['llm_support'] ?? null ) ) {
90 return false;
91 }
92
93 if ( self::EXCLUDED_WIDGET_TITLE === ( $config['title'] ?? null ) ) {
94 return false;
95 }
96
97 if ( ! empty( $config['atomic_props_schema'] ) ) {
98 return true;
99 }
100
101 return self::has_v3_controls( $config['controls'] ?? null );
102 }
103
104 private static function has_v3_controls( $controls ): bool {
105 return is_array( $controls ) && ! empty( $controls );
106 }
107
108 public static function get_widget_version( array $config ): string {
109 return empty( $config['atomic_props_schema'] ) ? self::VERSION_V3 : self::VERSION_V4;
110 }
111
112 public static function is_v3_allowlisted( string $widget_type ): bool {
113 return in_array( $widget_type, self::V3_ALLOWLIST, true );
114 }
115
116 public static function build_widget_summary( string $widget_type, array $config ): array {
117 return self::filter_nulls( [
118 'type' => $widget_type,
119 'version' => self::get_widget_version( $config ),
120 'description' => self::get_description( $config, $widget_type ),
121 ] );
122 }
123
124 /**
125 * Builds a parents index for efficient allowed_parents lookup.
126 *
127 * @param array<string, array> $all_configs All widget configs keyed by type.
128 * @return array<string, string[]> child_type => parent_types[].
129 */
130 public static function build_parents_index( array $all_configs ): array {
131 $index = [];
132
133 foreach ( $all_configs as $parent_type => $parent_config ) {
134 foreach ( $parent_config['allowed_child_types'] ?? [] as $child_type ) {
135 $index[ $child_type ][] = $parent_type;
136 }
137 }
138
139 return $index;
140 }
141
142 /**
143 * Builds the JSON Schema for a widget's props.
144 * Returns null for widgets that can't be schematized at all (no atomic props and no V3 controls).
145 *
146 * @param string $widget_type Widget type to build the schema for.
147 * @param array $config The widget's own config, from `get_config()`.
148 * @param array $parents_index Precomputed child_type => parent_types[] index for nesting guidance.
149 */
150 public static function build_widget_schema( string $widget_type, array $config, array $parents_index = [] ): ?array {
151 $props_schema = $config['atomic_props_schema'] ?? null;
152
153 if ( ! $props_schema ) {
154 if ( ! self::has_v3_controls( $config['controls'] ?? null ) ) {
155 return null;
156 }
157
158 $allowed_keys = V3_Widget_Bridge_Registry::get_non_style_keys( $widget_type );
159 $built = V3_Json_Schema_Builder::build( $config['controls'], $allowed_keys );
160
161 return self::filter_nulls( [
162 'type' => 'object',
163 'widget_version' => self::VERSION_V3,
164 'description' => self::get_description( $config, $widget_type ),
165 'message' => self::V3_FALLBACK_MESSAGE,
166 'fields_note' => self::V3_FALLBACK_FIELDS_NOTE,
167 'properties' => $built['properties'],
168 'required' => $built['required'],
169 'additionalProperties' => false,
170 ] );
171 }
172
173 $properties = self::build_configurable_properties_schema( $props_schema, $widget_type );
174
175 return self::filter_nulls( [
176 'type' => 'object',
177 'properties' => $properties,
178 'description' => self::get_description( $config, $widget_type ),
179 'llm_guidance' => Llm_Guidance_Builder::build( $config, $widget_type, $parents_index ),
180 ] );
181 }
182
183 /**
184 * @param array<string, Prop_Type> $props_schema
185 */
186 private static function build_configurable_properties_schema( array $props_schema, string $widget_type ): array {
187 $properties = [];
188
189 foreach ( $props_schema as $key => $prop_type ) {
190 if ( ! $prop_type instanceof Prop_Type || ! self::is_prop_key_configurable( $key, $prop_type ) ) {
191 continue;
192 }
193
194 $schema = self::to_plain_llm_schema_from_json( $prop_type->to_json_schema() );
195 $allowed_html_tags = Escaped_Html_Prop_Type::get_allowed_html_tags_for_prop( $widget_type, $key );
196
197 if ( null !== $allowed_html_tags ) {
198 $schema = self::describe_allowed_html_tags( $schema, $allowed_html_tags );
199 }
200
201 $properties[ $key ] = $schema;
202 }
203
204 return $properties;
205 }
206
207 /**
208 * Keeps the machine-readable tag list while spelling out in the description that the string
209 * itself may carry that markup — a bare `allowed_html_tags` key is non-standard JSON Schema
210 * and reads as ambiguous next to `type: string`.
211 */
212 private static function describe_allowed_html_tags( array $schema, array $allowed_html_tags ): array {
213 $schema['allowed_html_tags'] = $allowed_html_tags;
214
215 $tag_list = implode( ', ', array_map( fn( $tag ) => "<{$tag}>", $allowed_html_tags ) );
216 $note = sprintf( self::ALLOWED_HTML_TAGS_NOTE, $tag_list );
217
218 if ( ! isset( $schema['anyOf'] ) || ! is_array( $schema['anyOf'] ) ) {
219 return self::append_description( $schema, $note );
220 }
221
222 // The markup rule belongs on the static string variant only — a dynamic-tag branch
223 // resolves its own value and never carries inline HTML from the caller.
224 $schema['anyOf'] = array_map(
225 fn( $branch ) => is_array( $branch ) && 'string' === ( $branch['type'] ?? null )
226 ? self::append_description( $branch, $note )
227 : $branch,
228 $schema['anyOf']
229 );
230
231 return $schema;
232 }
233
234 private static function append_description( array $schema, string $note ): array {
235 $schema['description'] = isset( $schema['description'] )
236 ? $schema['description'] . ' ' . $note
237 : $note;
238
239 return $schema;
240 }
241
242 public static function to_plain_llm_schema( Prop_Type $prop_type ): array {
243 $schema = self::to_plain_llm_schema_from_json( $prop_type->to_json_schema() );
244
245 return self::refine_from_prop_type( $schema, $prop_type, Utils::has_pro() );
246 }
247
248 private static function to_plain_llm_schema_from_json( array $schema ): array {
249 $filtered = apply_filters( 'elementor/atomic-widgets/llm-json-schema', $schema );
250
251 return Plain_Llm_Schema_Converter::convert( $filtered );
252 }
253
254 /**
255 * Walks a plain LLM schema alongside its PropType tree to:
256 * - Enrich primitive enums from `meta('enum')` when the JSON schema lacks them.
257 * - Strip fields marked `meta('pro') === true` and enum values listed in `meta('pro')`
258 * when Pro is inactive.
259 */
260 private static function refine_from_prop_type( array $schema, Prop_Type $prop_type, bool $is_pro_active ): array {
261 if ( $prop_type instanceof Object_Prop_Type ) {
262 return self::refine_object( $schema, $prop_type, $is_pro_active );
263 }
264
265 if ( $prop_type instanceof Array_Prop_Type ) {
266 return self::refine_array( $schema, $prop_type, $is_pro_active );
267 }
268
269 return self::refine_primitive( $schema, $prop_type, $is_pro_active );
270 }
271
272 private static function refine_object( array $schema, Object_Prop_Type $prop_type, bool $is_pro_active ): array {
273 if ( ! isset( $schema['properties'] ) || ! is_array( $schema['properties'] ) ) {
274 return $schema;
275 }
276
277 $properties = $schema['properties'];
278
279 foreach ( $prop_type->get_shape() as $key => $child_prop_type ) {
280 if ( ! isset( $properties[ $key ] ) ) {
281 continue;
282 }
283
284 if ( ! $is_pro_active && self::is_pro_only_field( $child_prop_type ) ) {
285 unset( $properties[ $key ] );
286 continue;
287 }
288
289 $properties[ $key ] = self::refine_from_prop_type( $properties[ $key ], $child_prop_type, $is_pro_active );
290 }
291
292 $schema['properties'] = $properties;
293
294 return $schema;
295 }
296
297 private static function refine_array( array $schema, Array_Prop_Type $prop_type, bool $is_pro_active ): array {
298 if ( isset( $schema['items'] ) && is_array( $schema['items'] ) ) {
299 $schema['items'] = self::refine_from_prop_type( $schema['items'], $prop_type->get_item_type(), $is_pro_active );
300 }
301
302 return $schema;
303 }
304
305 private static function refine_primitive( array $schema, Prop_Type $prop_type, bool $is_pro_active ): array {
306 $enum_values = $prop_type->get_meta_item( 'enum' );
307
308 if ( is_array( $enum_values ) ) {
309 $schema['enum'] = $is_pro_active
310 ? array_values( $enum_values )
311 : self::filter_pro_enum_values( $enum_values, $prop_type );
312 }
313
314 return $schema;
315 }
316
317 private static function filter_pro_enum_values( array $enum_values, Prop_Type $prop_type ): array {
318 if ( self::is_pro_only_field( $prop_type ) ) {
319 return [];
320 }
321
322 $pro_values = $prop_type->get_meta_item( 'pro' );
323
324 if ( ! is_array( $pro_values ) ) {
325 return array_values( $enum_values );
326 }
327
328 return array_values( array_diff( $enum_values, $pro_values ) );
329 }
330
331 private static function is_pro_only_field( Prop_Type $prop_type ): bool {
332 return true === $prop_type->get_meta_item( 'pro' );
333 }
334
335 private static function is_prop_key_configurable( string $key, Prop_Type $prop_type ): bool {
336 if ( ! in_array( $key, self::NON_CONFIGURABLE_PROP_KEYS, true ) ) {
337 return true;
338 }
339
340 return (bool) $prop_type->get_meta_item( 'llm_configurable', false );
341 }
342
343 private static function get_description( array $config, ?string $widget_type = null ): ?string {
344 $description = $config['meta']['description'] ?? null;
345
346 if ( is_string( $description ) && '' !== $description ) {
347 return $description;
348 }
349
350 if ( null !== $widget_type && self::is_v3_allowlisted( $widget_type ) ) {
351 return V3_Widget_Bridge_Registry::get_description( $widget_type );
352 }
353
354 return null;
355 }
356
357 private static function filter_nulls( array $data ): array {
358 return array_filter( $data, fn( $value ) => null !== $value );
359 }
360 }
361