| 1 |
<?php |
| 2 |
|
| 3 |
namespace Elementor\Modules\Mcp\Abilities\Appliers\V3; |
| 4 |
|
| 5 |
use Elementor\Modules\AtomicWidgets\CssConverter\Css_Converter; |
| 6 |
use Elementor\Modules\AtomicWidgets\CssConverter\Css_Media_Splitter; |
| 7 |
use Elementor\Modules\Mcp\Abilities\Appliers\V3\Converter\V3_Context_Meta; |
| 8 |
use Elementor\Modules\Mcp\Abilities\Appliers\V3\Converter\V3_Conversion_Context; |
| 9 |
use Elementor\Modules\Mcp\Abilities\Appliers\V3\Converter\V3_Converter_Registry; |
| 10 |
use Elementor\Modules\Mcp\Abilities\Appliers\V3\Mapper\Css_Declaration_Parser; |
| 11 |
use Elementor\Modules\Mcp\Abilities\Appliers\V3\Mapper\Responsive_Key_Resolver; |
| 12 |
use Elementor\Modules\Mcp\Abilities\Appliers\V3\Mapper\Unmapped_Css_Serializer; |
| 13 |
|
| 14 |
if ( ! defined( 'ABSPATH' ) ) { |
| 15 |
exit; |
| 16 |
} |
| 17 |
|
| 18 |
/** |
| 19 |
* Maps LLM CSS strings onto legacy V3 style settings; unmapped rules become custom_css. |
| 20 |
* |
| 21 |
* Breakpoints: |
| 22 |
* The input CSS is split by `Css_Media_Splitter` using the site's active breakpoint names. |
| 23 |
* Only `desktop` writes to bare setting keys. Non-desktop breakpoints look up |
| 24 |
* `<setting>_<breakpoint>` on the widget config; if the responsive variant does not exist |
| 25 |
* and the base setting does, the rule is dropped (to avoid overwriting desktop with mobile). |
| 26 |
* |
| 27 |
* Overrides (per-widget CSS -> V3 setting map, see {@see V3_Widget_Bridge_Registry}): |
| 28 |
* Four mutually-exclusive shapes are dispatched by the {@see V3_Converter_Registry}, |
| 29 |
* one converter class per shape. A fallback `Generic_Index_Converter` uses |
| 30 |
* {@see V3_Style_Settings_Index} for auto-discovered mappings. |
| 31 |
*/ |
| 32 |
class V3_Style_Mapper { |
| 33 |
|
| 34 |
private Css_Converter $css_converter; |
| 35 |
private array $active_breakpoints; |
| 36 |
private V3_Converter_Registry $converter_registry; |
| 37 |
private Css_Declaration_Parser $declaration_parser; |
| 38 |
private Unmapped_Css_Serializer $unmapped_serializer; |
| 39 |
private Responsive_Key_Resolver $responsive_resolver; |
| 40 |
|
| 41 |
public function __construct( |
| 42 |
Css_Converter $css_converter, |
| 43 |
array $active_breakpoints, |
| 44 |
V3_Converter_Registry $converter_registry, |
| 45 |
Css_Declaration_Parser $declaration_parser, |
| 46 |
Unmapped_Css_Serializer $unmapped_serializer, |
| 47 |
Responsive_Key_Resolver $responsive_resolver |
| 48 |
) { |
| 49 |
$this->css_converter = $css_converter; |
| 50 |
$this->active_breakpoints = $active_breakpoints; |
| 51 |
$this->converter_registry = $converter_registry; |
| 52 |
$this->declaration_parser = $declaration_parser; |
| 53 |
$this->unmapped_serializer = $unmapped_serializer; |
| 54 |
$this->responsive_resolver = $responsive_resolver; |
| 55 |
} |
| 56 |
|
| 57 |
/** |
| 58 |
* @param string $css_string |
| 59 |
* @param string $widget_type |
| 60 |
* @param array $widget_config From Widget_Context_Helper::get_widget_config(). |
| 61 |
* @return array{settings_patch: array<string, mixed>, unmapped_css: string, warnings: string[]} |
| 62 |
*/ |
| 63 |
public function apply( string $css_string, string $widget_type, array $widget_config ): array { |
| 64 |
$css_string = trim( $css_string ); |
| 65 |
|
| 66 |
if ( '' === $css_string ) { |
| 67 |
return $this->empty_result(); |
| 68 |
} |
| 69 |
|
| 70 |
$meta = $this->build_meta( $widget_type, $widget_config ); |
| 71 |
$ctx = new V3_Conversion_Context(); |
| 72 |
|
| 73 |
$split = ( new Css_Media_Splitter( $this->get_active_breakpoints() ) )->split( $css_string ); |
| 74 |
if ( null !== $split['error'] ) { |
| 75 |
$ctx->warn( $split['error'] ); |
| 76 |
$ctx->mark_unmapped( $css_string ); |
| 77 |
|
| 78 |
return $this->finalize( $ctx, $meta ); |
| 79 |
} |
| 80 |
|
| 81 |
foreach ( $split['breakpoints'] as $breakpoint => $css ) { |
| 82 |
if ( '' === trim( $css ) ) { |
| 83 |
continue; |
| 84 |
} |
| 85 |
|
| 86 |
$this->process_breakpoint( $ctx, $meta, (string) $breakpoint, (string) $css ); |
| 87 |
} |
| 88 |
|
| 89 |
if ( ! empty( $split['custom_css'] ) ) { |
| 90 |
$ctx->mark_unmapped( (string) $split['custom_css'] ); |
| 91 |
} |
| 92 |
|
| 93 |
return $this->finalize( $ctx, $meta ); |
| 94 |
} |
| 95 |
|
| 96 |
private function process_breakpoint( V3_Conversion_Context $ctx, V3_Context_Meta $meta, string $breakpoint, string $css ): void { |
| 97 |
$parsed = $this->css_converter->parse_nested( $css ); |
| 98 |
|
| 99 |
if ( isset( $parsed['error'] ) ) { |
| 100 |
$ctx->mark_unmapped( $this->unmapped_serializer->serialize_breakpoint_block( $breakpoint, $css ) ); |
| 101 |
$ctx->warn( (string) $parsed['error'] ); |
| 102 |
|
| 103 |
return; |
| 104 |
} |
| 105 |
|
| 106 |
foreach ( $parsed['blocks'] as $block ) { |
| 107 |
$this->process_block( $ctx, $meta, $breakpoint, $block ); |
| 108 |
} |
| 109 |
} |
| 110 |
|
| 111 |
private function process_block( V3_Conversion_Context $ctx, V3_Context_Meta $meta, string $breakpoint, array $block ): void { |
| 112 |
$selector = $block['selector'] ?? null; |
| 113 |
$block_css = (string) ( $block['css'] ?? '' ); |
| 114 |
$state = $this->declaration_parser->normalize_state( $selector ); |
| 115 |
|
| 116 |
if ( null !== $selector && null === $state ) { |
| 117 |
$ctx->mark_unmapped( $this->unmapped_serializer->serialize_nested_block( $breakpoint, (string) $selector, $block_css ) ); |
| 118 |
|
| 119 |
return; |
| 120 |
} |
| 121 |
|
| 122 |
foreach ( $this->declaration_parser->parse_declarations( $block_css ) as $declaration ) { |
| 123 |
$rule = [ |
| 124 |
'property' => $declaration['property'], |
| 125 |
'value' => $declaration['value'], |
| 126 |
'state' => $state, |
| 127 |
'breakpoint' => $breakpoint, |
| 128 |
]; |
| 129 |
|
| 130 |
if ( ! $this->dispatch_rule( $ctx, $meta, $rule ) ) { |
| 131 |
$ctx->mark_unmapped( $this->unmapped_serializer->serialize_declaration( |
| 132 |
$breakpoint, |
| 133 |
$state, |
| 134 |
$rule['property'], |
| 135 |
$rule['value'] |
| 136 |
) ); |
| 137 |
} |
| 138 |
} |
| 139 |
} |
| 140 |
|
| 141 |
private function dispatch_rule( V3_Conversion_Context $ctx, V3_Context_Meta $meta, array $rule ): bool { |
| 142 |
foreach ( $this->converter_registry->all() as $converter ) { |
| 143 |
if ( ! $converter->is_supported( $rule, $meta ) ) { |
| 144 |
continue; |
| 145 |
} |
| 146 |
|
| 147 |
return $converter->convert( $ctx, $rule, $meta ); |
| 148 |
} |
| 149 |
|
| 150 |
return false; |
| 151 |
} |
| 152 |
|
| 153 |
private function build_meta( string $widget_type, array $widget_config ): V3_Context_Meta { |
| 154 |
$overrides = V3_Widget_Bridge_Registry::get_style_overrides( $widget_type ); |
| 155 |
$controls = $widget_config['controls'] ?? []; |
| 156 |
$generic_index = V3_Style_Settings_Index::build( is_array( $controls ) ? $controls : [], $overrides ); |
| 157 |
|
| 158 |
return new V3_Context_Meta( $widget_type, $widget_config, $overrides, $generic_index ); |
| 159 |
} |
| 160 |
|
| 161 |
private function finalize( V3_Conversion_Context $ctx, V3_Context_Meta $meta ): array { |
| 162 |
$settings_patch = $ctx->settings_patch(); |
| 163 |
|
| 164 |
foreach ( $ctx->typography_buckets() as $bucket ) { |
| 165 |
$group_patch = V3_Value_Resolvers::resolve_typography_group( |
| 166 |
$bucket['declarations'], |
| 167 |
$bucket['prefix'] |
| 168 |
); |
| 169 |
|
| 170 |
if ( ! empty( $bucket['responsive'] ) && Responsive_Key_Resolver::BASE_BREAKPOINT !== $bucket['breakpoint'] ) { |
| 171 |
$group_patch = $this->responsive_resolver->suffix_patch( $group_patch, $bucket['breakpoint'], $meta ); |
| 172 |
} |
| 173 |
|
| 174 |
$settings_patch = array_merge( $settings_patch, $group_patch ); |
| 175 |
} |
| 176 |
|
| 177 |
return [ |
| 178 |
'settings_patch' => $settings_patch, |
| 179 |
'unmapped_css' => $this->unmapped_serializer->join( $ctx->unmapped_parts() ), |
| 180 |
'warnings' => $ctx->warnings(), |
| 181 |
]; |
| 182 |
} |
| 183 |
|
| 184 |
private function empty_result(): array { |
| 185 |
return [ |
| 186 |
'settings_patch' => [], |
| 187 |
'unmapped_css' => '', |
| 188 |
'warnings' => [], |
| 189 |
]; |
| 190 |
} |
| 191 |
|
| 192 |
private function get_active_breakpoints(): array { |
| 193 |
if ( ! empty( $this->active_breakpoints ) ) { |
| 194 |
return $this->active_breakpoints; |
| 195 |
} |
| 196 |
|
| 197 |
return [ Responsive_Key_Resolver::BASE_BREAKPOINT ]; |
| 198 |
} |
| 199 |
} |
| 200 |
|