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 / utils / style-variants-merger.php

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

277 lines 8.5 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\CssConverter\Css_Converter;
6 use Elementor\Modules\AtomicWidgets\CssConverter\Css_Media_Splitter;
7
8 if ( ! defined( 'ABSPATH' ) ) {
9 exit;
10 }
11
12 class Style_Variants_Merger {
13
14 const PSEUDO_STATES = [ 'hover', 'focus', 'active' ];
15
16 /**
17 * Matches a property declaration `name: null;` only when it is not inside
18 * a quoted string. The boundary (^|[;{]) ensures we only capture top-level
19 * declarations, not occurrences inside values like content: "color: null;".
20 * The boundary char is captured so it can be preserved in replacements.
21 */
22 const NULL_DECLARATION_PATTERN = '/(^|[;{])\s*([a-zA-Z][a-zA-Z0-9-]*)\s*:\s*null\s*;?/';
23
24 /**
25 * $get_converter is a factory callable resolved lazily — only called if the CSS split succeeds.
26 */
27 public static function parse_css_string(
28 string $css_string,
29 array $active_breakpoints,
30 int $op_index,
31 string $op_action,
32 Bulk_Operations_Result $results,
33 callable $get_converter
34 ): ?array {
35 $splitter = new Css_Media_Splitter( $active_breakpoints );
36 $split = $splitter->split( $css_string );
37
38 if ( null !== $split['error'] ) {
39 $results->add_error(
40 $op_index,
41 $op_action,
42 'invalid_css',
43 $split['error'] . sprintf( ' Valid breakpoints: %s.', implode( ', ', $active_breakpoints ) )
44 );
45 return null;
46 }
47
48 $css_converter = $get_converter();
49 $breakpoint_blocks = [];
50 $removal_breakpoints = [];
51
52 foreach ( $split['breakpoints'] as $breakpoint => $css ) {
53 if ( '' === trim( $css ) ) {
54 $removal_breakpoints[] = $breakpoint;
55 continue;
56 }
57
58 $result = $css_converter->parse_nested( $css );
59
60 if ( isset( $result['error'] ) ) {
61 $results->add_error( $op_index, $op_action, 'invalid_css', $result['error'] );
62 return null;
63 }
64
65 $breakpoint_blocks[] = [
66 'breakpoint' => $breakpoint,
67 'blocks' => $result['blocks'],
68 ];
69 }
70
71 return [
72 'breakpoint_blocks' => $breakpoint_blocks,
73 'removal_breakpoints' => $removal_breakpoints,
74 ];
75 }
76
77 public static function build_variants( array $breakpoint_blocks, Css_Converter $converter ): array {
78 return self::build_variants_with_fallback_css( $breakpoint_blocks, $converter )['variants'];
79 }
80
81 /**
82 * @return array{variants: array[], fallback_css: array<int, string>} fallback_css holds, per variant index, the declarations the converter could not convert natively.
83 */
84 public static function build_variants_with_fallback_css( array $breakpoint_blocks, Css_Converter $converter ): array {
85 $variants = [];
86 $fallback_css = [];
87
88 foreach ( $breakpoint_blocks as $entry ) {
89 $bp = $entry['breakpoint'];
90 $blocks = $entry['blocks'];
91 $base_block_css = '';
92 $base_custom_css_parts = [];
93
94 foreach ( $blocks as $block ) {
95 $selector = $block['selector'];
96 $css = $block['css'];
97
98 if ( null === $selector ) {
99 $base_block_css = $css;
100 continue;
101 }
102
103 $state = strtolower( ltrim( $selector, ':' ) );
104
105 if ( ':' !== ( $selector[0] ?? '' ) || ! in_array( $state, self::PSEUDO_STATES, true ) ) {
106 $base_custom_css_parts[] = '&' . $selector . ' { ' . trim( $css ) . ' }';
107 continue;
108 }
109
110 self::add_variant( $variants, $fallback_css, self::make_variant( $bp, $state, $css, $converter ) );
111 }
112
113 self::add_variant( $variants, $fallback_css, self::make_variant( $bp, null, $base_block_css, $converter, $base_custom_css_parts ) );
114 }
115
116 return [
117 'variants' => $variants,
118 'fallback_css' => $fallback_css,
119 ];
120 }
121
122 private static function add_variant( array &$variants, array &$fallback_css, array $built ): void {
123 if ( null === $built['variant'] ) {
124 return;
125 }
126
127 if ( '' !== $built['fallback_css'] ) {
128 $fallback_css[ count( $variants ) ] = $built['fallback_css'];
129 }
130
131 $variants[] = $built['variant'];
132 }
133
134 public static function apply_mode( array $existing, array $new_variants, string $mode, array $affected_breakpoints ): array {
135 if ( 'replace' === $mode ) {
136 $kept = array_values(
137 array_filter( $existing, fn( $v ) => ! in_array( $v['meta']['breakpoint'] ?? null, $affected_breakpoints, true ) )
138 );
139 $clean_new = array_values(
140 array_filter(
141 array_map(
142 function ( $v ) {
143 unset( $v['null_props'] );
144 return $v;
145 },
146 $new_variants
147 ),
148 fn( $v ) => ! empty( $v['props'] ) || ! empty( $v['custom_css'] )
149 )
150 );
151 return array_merge( $kept, $clean_new );
152 }
153
154 $result = $existing;
155
156 foreach ( $new_variants as $new_variant ) {
157 $bp = $new_variant['meta']['breakpoint'] ?? null;
158 $state = $new_variant['meta']['state'] ?? null;
159 $match = null;
160
161 foreach ( $result as $i => $v ) {
162 if ( ( $v['meta']['breakpoint'] ?? null ) === $bp && ( $v['meta']['state'] ?? null ) === $state ) {
163 $match = $i;
164 break;
165 }
166 }
167
168 if ( null === $match ) {
169 $clean = $new_variant;
170 unset( $clean['null_props'] );
171 if ( ! empty( $clean['props'] ) || ! empty( $clean['custom_css'] ) ) {
172 $result[] = $clean;
173 }
174 continue;
175 }
176
177 $variant_null_props = $new_variant['null_props'] ?? [];
178 $wipe_all = in_array( 'all', $variant_null_props, true );
179 $props_to_remove = array_filter( $variant_null_props, fn( $k ) => 'all' !== $k );
180
181 $merged_props = $wipe_all
182 ? ( $new_variant['props'] ?? [] )
183 : array_merge( $result[ $match ]['props'] ?? [], $new_variant['props'] ?? [] );
184
185 foreach ( $props_to_remove as $key ) {
186 unset( $merged_props[ $key ] );
187 }
188
189 $merged_props = array_filter( $merged_props, fn( $v ) => null !== $v );
190
191 $merged_custom_css = $wipe_all
192 ? ( $new_variant['custom_css'] ?? null )
193 : self::merge_custom_css( $result[ $match ]['custom_css'] ?? null, $new_variant['custom_css'] ?? null );
194
195 if ( empty( $merged_props ) && null === $merged_custom_css ) {
196 unset( $result[ $match ] );
197 $result = array_values( $result );
198 continue;
199 }
200
201 $result[ $match ]['props'] = $merged_props;
202 $result[ $match ]['custom_css'] = $merged_custom_css;
203 unset( $result[ $match ]['null_props'] );
204 }
205
206 return $result;
207 }
208
209 public static function merge_custom_css( ?array $existing, ?array $incoming ): ?array {
210 $existing_raw = isset( $existing['raw'] ) ? base64_decode( $existing['raw'] ) : ''; // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode -- Decoding stored base64 custom_css for merge.
211 $incoming_raw = isset( $incoming['raw'] ) ? base64_decode( $incoming['raw'] ) : ''; // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode -- Decoding stored base64 custom_css for merge.
212
213 $merged = trim( $existing_raw . ' ' . $incoming_raw );
214
215 return '' !== $merged
216 ? [ 'raw' => base64_encode( $merged ) ] // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode -- Storing merged custom_css as base64.
217 : null;
218 }
219
220 /**
221 * @return array{variant: array|null, fallback_css: string}
222 */
223 private static function make_variant( string $breakpoint, ?string $state, string $css, Css_Converter $converter, array $extra_custom_css_parts = [] ): array {
224 $null_props = self::extract_null_props( $css );
225 $stripped_css = self::strip_null_declarations( $css );
226 $result = $converter->convert( $stripped_css );
227 $props = $result['props'] ?? [];
228 $fallback_css = $result['customCss'] ?? '';
229 $custom_str = $fallback_css;
230
231 if ( ! empty( $extra_custom_css_parts ) ) {
232 $extra = implode( ' ', $extra_custom_css_parts );
233 $custom_str = '' !== $custom_str ? $custom_str . ' ' . $extra : $extra;
234 }
235
236 if ( empty( $props ) && '' === $custom_str && empty( $null_props ) ) {
237 return [
238 'variant' => null,
239 'fallback_css' => '',
240 ];
241 }
242
243 $variant = [
244 'meta' => [
245 'breakpoint' => $breakpoint,
246 'state' => $state,
247 ],
248 'props' => $props,
249 'custom_css' => '' !== $custom_str
250 ? [ 'raw' => base64_encode( $custom_str ) ] // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode -- Global class custom_css.raw is stored as base64.
251 : null,
252 'null_props' => $null_props,
253 ];
254
255 return [
256 'variant' => $variant,
257 'fallback_css' => trim( $fallback_css ),
258 ];
259 }
260
261 private static function extract_null_props( string $css ): array {
262 $null_props = [];
263 preg_replace_callback(
264 self::NULL_DECLARATION_PATTERN,
265 function ( $matches ) use ( &$null_props ) {
266 $null_props[] = $matches[2];
267 },
268 $css
269 );
270 return $null_props;
271 }
272
273 private static function strip_null_declarations( string $css ): string {
274 return (string) preg_replace( self::NULL_DECLARATION_PATTERN, '$1', $css );
275 }
276 }
277