world"), limited to these tags: %s. Any other tag is stripped on save.'; /** * @return array widget_type => config, filtered to widgets eligible for LLM use. */ public static function get_llm_eligible_widgets(): array { $all_types = array_merge( Plugin::$instance->widgets_manager->get_widget_types(), Plugin::$instance->elements_manager->get_element_types() ); $eligible = []; foreach ( $all_types as $type => $instance ) { if ( self::is_v3_allowlisted( (string) $type ) && method_exists( $instance, 'get_stack' ) ) { $instance->get_stack(); } $config = $instance->get_config(); if ( self::is_widget_eligible_for_llm( $config ) ) { $eligible[ $type ] = $config; } } return $eligible; } public static function get_widget_config( string $widget_type ): ?array { $instance = Atomic_Elements_Utils::get_element_instance( $widget_type ); if ( ! $instance ) { return null; } if ( self::is_v3_allowlisted( $widget_type ) && method_exists( $instance, 'get_stack' ) ) { $instance->get_stack(); } return $instance->get_config(); } public static function is_widget_eligible_for_llm( array $config ): bool { if ( false === ( $config['meta']['llm_support'] ?? null ) ) { return false; } if ( self::EXCLUDED_WIDGET_TITLE === ( $config['title'] ?? null ) ) { return false; } if ( ! empty( $config['atomic_props_schema'] ) ) { return true; } return self::has_v3_controls( $config['controls'] ?? null ); } private static function has_v3_controls( $controls ): bool { return is_array( $controls ) && ! empty( $controls ); } public static function get_widget_version( array $config ): string { return empty( $config['atomic_props_schema'] ) ? self::VERSION_V3 : self::VERSION_V4; } public static function is_v3_allowlisted( string $widget_type ): bool { return in_array( $widget_type, self::V3_ALLOWLIST, true ); } public static function build_widget_summary( string $widget_type, array $config ): array { return self::filter_nulls( [ 'type' => $widget_type, 'version' => self::get_widget_version( $config ), 'description' => self::get_description( $config, $widget_type ), ] ); } /** * Builds a parents index for efficient allowed_parents lookup. * * @param array $all_configs All widget configs keyed by type. * @return array child_type => parent_types[]. */ public static function build_parents_index( array $all_configs ): array { $index = []; foreach ( $all_configs as $parent_type => $parent_config ) { foreach ( $parent_config['allowed_child_types'] ?? [] as $child_type ) { $index[ $child_type ][] = $parent_type; } } return $index; } /** * Builds the JSON Schema for a widget's props. * Returns null for widgets that can't be schematized at all (no atomic props and no V3 controls). * * @param string $widget_type Widget type to build the schema for. * @param array $config The widget's own config, from `get_config()`. * @param array $parents_index Precomputed child_type => parent_types[] index for nesting guidance. */ public static function build_widget_schema( string $widget_type, array $config, array $parents_index = [] ): ?array { $props_schema = $config['atomic_props_schema'] ?? null; if ( ! $props_schema ) { if ( ! self::has_v3_controls( $config['controls'] ?? null ) ) { return null; } $allowed_keys = V3_Widget_Bridge_Registry::get_non_style_keys( $widget_type ); $built = V3_Json_Schema_Builder::build( $config['controls'], $allowed_keys ); return self::filter_nulls( [ 'type' => 'object', 'widget_version' => self::VERSION_V3, 'description' => self::get_description( $config, $widget_type ), 'message' => self::V3_FALLBACK_MESSAGE, 'fields_note' => self::V3_FALLBACK_FIELDS_NOTE, 'properties' => $built['properties'], 'required' => $built['required'], 'additionalProperties' => false, ] ); } $properties = self::build_configurable_properties_schema( $props_schema, $widget_type ); return self::filter_nulls( [ 'type' => 'object', 'properties' => $properties, 'description' => self::get_description( $config, $widget_type ), 'llm_guidance' => Llm_Guidance_Builder::build( $config, $widget_type, $parents_index ), ] ); } /** * @param array $props_schema */ private static function build_configurable_properties_schema( array $props_schema, string $widget_type ): array { $properties = []; foreach ( $props_schema as $key => $prop_type ) { if ( ! $prop_type instanceof Prop_Type || ! self::is_prop_key_configurable( $key, $prop_type ) ) { continue; } $schema = self::to_plain_llm_schema_from_json( $prop_type->to_json_schema() ); $allowed_html_tags = Escaped_Html_Prop_Type::get_allowed_html_tags_for_prop( $widget_type, $key ); if ( null !== $allowed_html_tags ) { $schema = self::describe_allowed_html_tags( $schema, $allowed_html_tags ); } $properties[ $key ] = $schema; } return $properties; } /** * Keeps the machine-readable tag list while spelling out in the description that the string * itself may carry that markup — a bare `allowed_html_tags` key is non-standard JSON Schema * and reads as ambiguous next to `type: string`. */ private static function describe_allowed_html_tags( array $schema, array $allowed_html_tags ): array { $schema['allowed_html_tags'] = $allowed_html_tags; $tag_list = implode( ', ', array_map( fn( $tag ) => "<{$tag}>", $allowed_html_tags ) ); $note = sprintf( self::ALLOWED_HTML_TAGS_NOTE, $tag_list ); if ( ! isset( $schema['anyOf'] ) || ! is_array( $schema['anyOf'] ) ) { return self::append_description( $schema, $note ); } // The markup rule belongs on the static string variant only — a dynamic-tag branch // resolves its own value and never carries inline HTML from the caller. $schema['anyOf'] = array_map( fn( $branch ) => is_array( $branch ) && 'string' === ( $branch['type'] ?? null ) ? self::append_description( $branch, $note ) : $branch, $schema['anyOf'] ); return $schema; } private static function append_description( array $schema, string $note ): array { $schema['description'] = isset( $schema['description'] ) ? $schema['description'] . ' ' . $note : $note; return $schema; } public static function to_plain_llm_schema( Prop_Type $prop_type ): array { $schema = self::to_plain_llm_schema_from_json( $prop_type->to_json_schema() ); return self::refine_from_prop_type( $schema, $prop_type, Utils::has_pro() ); } private static function to_plain_llm_schema_from_json( array $schema ): array { $filtered = apply_filters( 'elementor/atomic-widgets/llm-json-schema', $schema ); return Plain_Llm_Schema_Converter::convert( $filtered ); } /** * Walks a plain LLM schema alongside its PropType tree to: * - Enrich primitive enums from `meta('enum')` when the JSON schema lacks them. * - Strip fields marked `meta('pro') === true` and enum values listed in `meta('pro')` * when Pro is inactive. */ private static function refine_from_prop_type( array $schema, Prop_Type $prop_type, bool $is_pro_active ): array { if ( $prop_type instanceof Object_Prop_Type ) { return self::refine_object( $schema, $prop_type, $is_pro_active ); } if ( $prop_type instanceof Array_Prop_Type ) { return self::refine_array( $schema, $prop_type, $is_pro_active ); } return self::refine_primitive( $schema, $prop_type, $is_pro_active ); } private static function refine_object( array $schema, Object_Prop_Type $prop_type, bool $is_pro_active ): array { if ( ! isset( $schema['properties'] ) || ! is_array( $schema['properties'] ) ) { return $schema; } $properties = $schema['properties']; foreach ( $prop_type->get_shape() as $key => $child_prop_type ) { if ( ! isset( $properties[ $key ] ) ) { continue; } if ( ! $is_pro_active && self::is_pro_only_field( $child_prop_type ) ) { unset( $properties[ $key ] ); continue; } $properties[ $key ] = self::refine_from_prop_type( $properties[ $key ], $child_prop_type, $is_pro_active ); } $schema['properties'] = $properties; return $schema; } private static function refine_array( array $schema, Array_Prop_Type $prop_type, bool $is_pro_active ): array { if ( isset( $schema['items'] ) && is_array( $schema['items'] ) ) { $schema['items'] = self::refine_from_prop_type( $schema['items'], $prop_type->get_item_type(), $is_pro_active ); } return $schema; } private static function refine_primitive( array $schema, Prop_Type $prop_type, bool $is_pro_active ): array { $enum_values = $prop_type->get_meta_item( 'enum' ); if ( is_array( $enum_values ) ) { $schema['enum'] = $is_pro_active ? array_values( $enum_values ) : self::filter_pro_enum_values( $enum_values, $prop_type ); } return $schema; } private static function filter_pro_enum_values( array $enum_values, Prop_Type $prop_type ): array { if ( self::is_pro_only_field( $prop_type ) ) { return []; } $pro_values = $prop_type->get_meta_item( 'pro' ); if ( ! is_array( $pro_values ) ) { return array_values( $enum_values ); } return array_values( array_diff( $enum_values, $pro_values ) ); } private static function is_pro_only_field( Prop_Type $prop_type ): bool { return true === $prop_type->get_meta_item( 'pro' ); } private static function is_prop_key_configurable( string $key, Prop_Type $prop_type ): bool { if ( ! in_array( $key, self::NON_CONFIGURABLE_PROP_KEYS, true ) ) { return true; } return (bool) $prop_type->get_meta_item( 'llm_configurable', false ); } private static function get_description( array $config, ?string $widget_type = null ): ?string { $description = $config['meta']['description'] ?? null; if ( is_string( $description ) && '' !== $description ) { return $description; } if ( null !== $widget_type && self::is_v3_allowlisted( $widget_type ) ) { return V3_Widget_Bridge_Registry::get_description( $widget_type ); } return null; } private static function filter_nulls( array $data ): array { return array_filter( $data, fn( $value ) => null !== $value ); } }