PluginProbe
Elementor Website Builder – more than just a page builder / 4.3.3
Elementor Website Builder – more than just a page builder v4.3.3
4.3.3 4.3.2 4.3.1 4.3.0 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 All 456 releases
elementor / modules / mcp / abilities / appliers / style-applier.php

style-applier.php in Elementor Website Builder – more than just a page builder 4.3.3, at modules/mcp/abilities/appliers/style-applier.php

368 lines 11.4 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\Appliers;
4
5 use Elementor\Modules\AtomicWidgets\CssConverter\Css_Converter;
6 use Elementor\Modules\AtomicWidgets\Styles\Atomic_Widget_Styles;
7 use Elementor\Modules\Mcp\Abilities\Appliers\V3\V3_Style_Mapper_Factory;
8 use Elementor\Modules\Mcp\Abilities\Utils\Bulk_Operations_Result;
9 use Elementor\Modules\Mcp\Abilities\Utils\Style_Variants_Merger;
10 use Elementor\Modules\Mcp\Abilities\Utils\Widget_Context_Helper;
11 use Elementor\Modules\Variables\Utils\Variable_Type_Keys;
12 use Elementor\Plugin;
13
14 if ( ! defined( 'ABSPATH' ) ) {
15 exit;
16 }
17
18 class Style_Applier {
19
20 const LOCAL_STYLE_ID_PREFIX = 'e-';
21 const DESKTOP_BREAKPOINT = 'desktop';
22 const LOCAL_STYLE_LABEL = 'local';
23 const LOCAL_STYLE_TYPE = 'class';
24
25 const UNSUPPORTED_CSS_CODE = 'unsupported_css';
26
27 private Css_Converter $css_converter;
28 private array $active_breakpoints;
29
30 public function __construct( Css_Converter $css_converter, array $active_breakpoints = [] ) {
31 $this->css_converter = $css_converter;
32 $this->active_breakpoints = $active_breakpoints;
33 }
34
35 /**
36 * @param array<string, array&> $config_id_index Index of subtree refs.
37 * @param array<string, string> $styles Per-config-id CSS strings.
38 * @param string $style_apply_mode `patch` or `replace`.
39 * @param array<string, array> $widget_configs Optional widget_type => config map (used for V3 mapping).
40 * @return array{error: \WP_Error|null, warnings: string[], variable_connections: array<string, array<string, string>>}
41 */
42 public function apply( array $config_id_index, array $styles, string $style_apply_mode = 'patch', array $widget_configs = [] ): array {
43 if ( empty( $styles ) ) {
44 return [
45 'error' => null,
46 'warnings' => [],
47 'warning_codes' => [],
48 'variable_connections' => [],
49 ];
50 }
51
52 $active_breakpoints = $this->get_active_breakpoints();
53 $errors = [];
54 $warnings = [];
55 $warning_codes = [];
56 $variable_connections = [];
57
58 foreach ( $styles as $config_id => $css_string ) {
59 if ( ! is_string( $css_string ) ) {
60 $errors[] = sprintf( '[%s] style must be a CSS string, got %s.', $config_id, gettype( $css_string ) );
61 continue;
62 }
63
64 if ( ! isset( $config_id_index[ $config_id ] ) ) {
65 continue;
66 }
67
68 $node = &$config_id_index[ $config_id ];
69
70 if ( V3_Node_Bridge::is_v3_node( $node ) ) {
71 $v3_result = $this->apply_v3_style( $node, $css_string, $style_apply_mode, $widget_configs );
72 foreach ( $v3_result['warnings'] as $warning ) {
73 $warnings[] = sprintf( '[%s] %s', $config_id, $warning );
74 }
75 $warning_codes = array_merge( $warning_codes, $v3_result['codes'] );
76 unset( $node );
77 continue;
78 }
79
80 $is_empty_css = '' === trim( $css_string );
81
82 if ( $is_empty_css ) {
83 if ( 'replace' === $style_apply_mode ) {
84 $existing_style_id = $this->find_existing_local_style_id( $node );
85 if ( $existing_style_id ) {
86 $node['styles'][ $existing_style_id ]['variants'] = [];
87 }
88 }
89 unset( $node );
90 continue;
91 }
92
93 $parse_results = new Bulk_Operations_Result();
94 $parsed = Style_Variants_Merger::parse_css_string(
95 $css_string,
96 $active_breakpoints,
97 0,
98 'update',
99 $parse_results,
100 fn() => $this->css_converter
101 );
102
103 if ( null === $parsed ) {
104 $result_data = $parse_results->to_array();
105 $errors[] = sprintf( '[%s] %s', $config_id, $result_data['results'][0]['message'] ?? 'CSS parse error.' );
106 unset( $node );
107 continue;
108 }
109
110 $built_variants = Style_Variants_Merger::build_variants_with_fallback_css( $parsed['breakpoint_blocks'], $this->css_converter );
111 $new_variants = $built_variants['variants'];
112 $unsupported_css = $this->collect_unsupported_css( $new_variants, $built_variants['fallback_css'] );
113
114 if ( '' !== $unsupported_css ) {
115 $warnings[] = sprintf(
116 '[%s] %s',
117 $config_id,
118 sprintf(
119 /* translators: %s: CSS declarations that were not saved */
120 __( 'These CSS properties or values are not supported and were not saved: %s', 'elementor' ),
121 self::truncate_css_snippet( $unsupported_css )
122 )
123 );
124 $warning_codes[] = self::UNSUPPORTED_CSS_CODE;
125 }
126
127 $affected_bps = array_column( $parsed['breakpoint_blocks'], 'breakpoint' );
128 $removal_bps = $parsed['removal_breakpoints'];
129 $existing_style_id = $this->find_existing_local_style_id( $node );
130 $existing_variants = $node['styles'][ $existing_style_id ]['variants'] ?? [];
131 $existing_after_removal = array_values(
132 array_filter( $existing_variants, fn( $v ) => ! in_array( $v['meta']['breakpoint'] ?? null, $removal_bps, true ) )
133 );
134
135 $merged_variants = Style_Variants_Merger::apply_mode(
136 $existing_after_removal,
137 $new_variants,
138 $style_apply_mode,
139 $affected_bps
140 );
141
142 $variable_connections[ $config_id ] = $this->collect_variable_connections( $new_variants );
143
144 $this->write_variants_to_node( $node, $merged_variants, $existing_style_id );
145 unset( $node );
146 }
147
148 return [
149 'error' => $errors ? new \WP_Error(
150 'elementor_invalid_styles',
151 implode( ' ', $errors ),
152 [ 'status' => \WP_Http::BAD_REQUEST ]
153 ) : null,
154 'warnings' => $warnings,
155 'warning_codes' => array_values( array_unique( $warning_codes ) ),
156 'variable_connections' => $variable_connections,
157 ];
158 }
159
160 /**
161 * @param array $node
162 * @param string $css_string
163 * @param string $style_apply_mode
164 * @param array<string, array> $widget_configs
165 * @return array{warnings: string[], codes: string[]} Warnings (without config-id prefix) and stable codes.
166 */
167 private function apply_v3_style( array &$node, string $css_string, string $style_apply_mode = 'patch', array $widget_configs = [] ): array {
168 $warnings = [];
169 $codes = [];
170 $widget_type = $node['widgetType'] ?? '';
171 $widget_config = [];
172
173 if ( is_string( $widget_type ) && '' !== $widget_type ) {
174 $widget_config = $widget_configs[ $widget_type ]
175 ?? Widget_Context_Helper::get_widget_config( $widget_type )
176 ?? [];
177 }
178
179 $is_empty_css = '' === trim( $css_string );
180 $is_replace = 'replace' === $style_apply_mode;
181
182 if ( $is_replace ) {
183 V3_Node_Bridge::clear_style_settings( $node, (string) $widget_type, $widget_config );
184 }
185
186 if ( $is_empty_css ) {
187 return [
188 'warnings' => $warnings,
189 'codes' => $codes,
190 ];
191 }
192
193 $mapper = V3_Style_Mapper_Factory::create( $this->css_converter, $this->get_active_breakpoints() );
194 $result = $mapper->apply( $css_string, (string) $widget_type, $widget_config );
195
196 foreach ( $result['warnings'] as $warning ) {
197 $warnings[] = $warning;
198 }
199
200 if ( ! empty( $result['settings_patch'] ) ) {
201 $node['settings'] = array_merge( $node['settings'] ?? [], $result['settings_patch'] );
202 }
203
204 $unmapped = $result['unmapped_css'] ?? '';
205 $pro_warning = V3_Node_Bridge::apply_custom_css( $node, $unmapped, (string) $widget_type );
206 if ( null !== $pro_warning ) {
207 $warnings[] = $pro_warning;
208 $codes[] = 'v3_style_needs_pro';
209 }
210
211 if ( '' !== trim( $unmapped ) ) {
212 $snippet = self::truncate_css_snippet( $unmapped );
213 if ( null !== $pro_warning ) {
214 $warnings[] = sprintf(
215 /* translators: %s: CSS snippet that could not be mapped */
216 __( 'Some CSS could not be mapped to V3 settings and was dropped: %s', 'elementor' ),
217 $snippet
218 );
219 $codes[] = 'css_dropped';
220 } else {
221 $warnings[] = sprintf(
222 /* translators: %s: CSS snippet that could not be mapped */
223 __( 'Some CSS could not be mapped to V3 settings and was written to custom_css: %s', 'elementor' ),
224 $snippet
225 );
226 $codes[] = 'css_fallback_custom_css';
227 }
228 }
229
230 return [
231 'warnings' => $warnings,
232 'codes' => $codes,
233 ];
234 }
235
236 private static function truncate_css_snippet( string $css, int $max_length = 200 ): string {
237 $css = trim( preg_replace( '/\s+/', ' ', $css ) ?? $css );
238 if ( strlen( $css ) <= $max_length ) {
239 return $css;
240 }
241
242 return substr( $css, 0, $max_length - 3 ) . '...';
243 }
244
245 /**
246 * @param array[] $variants
247 * @param array<int, string> $fallback_css Per variant index, CSS the converter could not convert natively.
248 */
249 private function collect_unsupported_css( array $variants, array $fallback_css ): string {
250 if ( empty( $fallback_css ) ) {
251 return '';
252 }
253
254 $kept_variants = Atomic_Widget_Styles::get_license_based_filtered_styles(
255 [ self::LOCAL_STYLE_LABEL => [ 'variants' => array_intersect_key( $variants, $fallback_css ) ] ]
256 )[ self::LOCAL_STYLE_LABEL ]['variants'];
257
258 $dropped_css = [];
259
260 foreach ( $fallback_css as $index => $css ) {
261 if ( empty( $kept_variants[ $index ]['custom_css'] ) ) {
262 $dropped_css[] = $css;
263 }
264 }
265
266 return implode( ' ', $dropped_css );
267 }
268
269 private function get_active_breakpoints(): array {
270 if ( ! empty( $this->active_breakpoints ) ) {
271 return $this->active_breakpoints;
272 }
273 return array_keys( Plugin::$instance->breakpoints->get_active_breakpoints() );
274 }
275
276 private function write_variants_to_node( array &$node, array $merged_variants, ?string $existing_style_id ): void {
277 if ( $existing_style_id ) {
278 $node['styles'][ $existing_style_id ]['variants'] = $merged_variants;
279 return;
280 }
281
282 if ( empty( $merged_variants ) ) {
283 return;
284 }
285
286 $style_id = $this->generate_local_style_id();
287 $node['styles'] = $node['styles'] ?? [];
288 $node['styles'][ $style_id ] = [
289 'id' => $style_id,
290 'label' => self::LOCAL_STYLE_LABEL,
291 'type' => self::LOCAL_STYLE_TYPE,
292 'variants' => $merged_variants,
293 ];
294 $node['settings'] = $this->add_style_to_classes( $node['settings'] ?? [], $style_id );
295 }
296
297 private function find_existing_local_style_id( array $node ): ?string {
298 foreach ( $node['styles'] ?? [] as $style_id => $_style ) {
299 if ( str_starts_with( (string) $style_id, self::LOCAL_STYLE_ID_PREFIX ) ) {
300 return $style_id;
301 }
302 }
303 return null;
304 }
305
306 private function add_style_to_classes( array $settings, string $style_id ): array {
307 $existing = $settings['classes']['value'] ?? [];
308 if ( ! is_array( $existing ) ) {
309 $existing = [];
310 }
311 if ( ! in_array( $style_id, $existing, true ) ) {
312 $existing[] = $style_id;
313 }
314
315 $settings['classes'] = [
316 '$$type' => 'classes',
317 'value' => array_values( $existing ),
318 ];
319
320 return $settings;
321 }
322
323 private function generate_local_style_id(): string {
324 return self::LOCAL_STYLE_ID_PREFIX . strtolower( \Elementor\Utils::generate_random_string() ) . '-' . dechex( wp_rand( 0x1000, 0xffff ) );
325 }
326
327 /**
328 * @param array[] $variants
329 * @return array<int, array{variable_id: string, var_type: string, control_path: string}>
330 */
331 private function collect_variable_connections( array $variants ): array {
332 $connections = [];
333
334 foreach ( $variants as $variant ) {
335 foreach ( $variant['props'] ?? [] as $prop_key => $prop_value ) {
336 if ( ! is_array( $prop_value ) ) {
337 continue;
338 }
339
340 $type = $prop_value['$$type'] ?? '';
341
342 if ( ! Variable_Type_Keys::is_variable_type( $type ) ) {
343 continue;
344 }
345
346 $connections[] = [
347 'variable_id' => (string) ( $prop_value['value'] ?? '' ),
348 'var_type' => $this->resolve_var_type_label( $type ),
349 'control_path' => (string) $prop_key,
350 ];
351 }
352 }
353
354 return $connections;
355 }
356
357 private function resolve_var_type_label( string $type ): string {
358 $labels = [
359 'global-color-variable' => 'color',
360 'global-font-variable' => 'font',
361 'global-size-variable' => 'size',
362 'global-custom-size-variable' => 'size',
363 ];
364
365 return $labels[ $type ] ?? $type;
366 }
367 }
368