PluginProbe
Elementor Website Builder – more than just a page builder / 4.3.1
Elementor Website Builder – more than just a page builder v4.3.1
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 4.1.0-dev2 4.0.8 All 454 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.1, at modules/mcp/abilities/utils/style-variants-merger.php

248 lines 7.6 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 $variants = [];
79
80 foreach ( $breakpoint_blocks as $entry ) {
81 $bp = $entry['breakpoint'];
82 $blocks = $entry['blocks'];
83 $base_block_css = '';
84 $base_custom_css_parts = [];
85
86 foreach ( $blocks as $block ) {
87 $selector = $block['selector'];
88 $css = $block['css'];
89
90 if ( null === $selector ) {
91 $base_block_css = $css;
92 continue;
93 }
94
95 $state = strtolower( ltrim( $selector, ':' ) );
96
97 if ( ':' !== ( $selector[0] ?? '' ) || ! in_array( $state, self::PSEUDO_STATES, true ) ) {
98 $base_custom_css_parts[] = '&' . $selector . ' { ' . trim( $css ) . ' }';
99 continue;
100 }
101
102 $variant = self::make_variant( $bp, $state, $css, $converter );
103 if ( null !== $variant ) {
104 $variants[] = $variant;
105 }
106 }
107
108 $base_variant = self::make_variant( $bp, null, $base_block_css, $converter, $base_custom_css_parts );
109 if ( null !== $base_variant ) {
110 $variants[] = $base_variant;
111 }
112 }
113
114 return $variants;
115 }
116
117 public static function apply_mode( array $existing, array $new_variants, string $mode, array $affected_breakpoints ): array {
118 if ( 'replace' === $mode ) {
119 $kept = array_values(
120 array_filter( $existing, fn( $v ) => ! in_array( $v['meta']['breakpoint'] ?? null, $affected_breakpoints, true ) )
121 );
122 $clean_new = array_values(
123 array_filter(
124 array_map(
125 function ( $v ) {
126 unset( $v['null_props'] );
127 return $v;
128 },
129 $new_variants
130 ),
131 fn( $v ) => ! empty( $v['props'] ) || ! empty( $v['custom_css'] )
132 )
133 );
134 return array_merge( $kept, $clean_new );
135 }
136
137 $result = $existing;
138
139 foreach ( $new_variants as $new_variant ) {
140 $bp = $new_variant['meta']['breakpoint'] ?? null;
141 $state = $new_variant['meta']['state'] ?? null;
142 $match = null;
143
144 foreach ( $result as $i => $v ) {
145 if ( ( $v['meta']['breakpoint'] ?? null ) === $bp && ( $v['meta']['state'] ?? null ) === $state ) {
146 $match = $i;
147 break;
148 }
149 }
150
151 if ( null === $match ) {
152 $clean = $new_variant;
153 unset( $clean['null_props'] );
154 if ( ! empty( $clean['props'] ) || ! empty( $clean['custom_css'] ) ) {
155 $result[] = $clean;
156 }
157 continue;
158 }
159
160 $variant_null_props = $new_variant['null_props'] ?? [];
161 $wipe_all = in_array( 'all', $variant_null_props, true );
162 $props_to_remove = array_filter( $variant_null_props, fn( $k ) => 'all' !== $k );
163
164 $merged_props = $wipe_all
165 ? ( $new_variant['props'] ?? [] )
166 : array_merge( $result[ $match ]['props'] ?? [], $new_variant['props'] ?? [] );
167
168 foreach ( $props_to_remove as $key ) {
169 unset( $merged_props[ $key ] );
170 }
171
172 $merged_props = array_filter( $merged_props, fn( $v ) => null !== $v );
173
174 $merged_custom_css = $wipe_all
175 ? ( $new_variant['custom_css'] ?? null )
176 : self::merge_custom_css( $result[ $match ]['custom_css'] ?? null, $new_variant['custom_css'] ?? null );
177
178 if ( empty( $merged_props ) && null === $merged_custom_css ) {
179 unset( $result[ $match ] );
180 $result = array_values( $result );
181 continue;
182 }
183
184 $result[ $match ]['props'] = $merged_props;
185 $result[ $match ]['custom_css'] = $merged_custom_css;
186 unset( $result[ $match ]['null_props'] );
187 }
188
189 return $result;
190 }
191
192 public static function merge_custom_css( ?array $existing, ?array $incoming ): ?array {
193 $existing_raw = isset( $existing['raw'] ) ? base64_decode( $existing['raw'] ) : ''; // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode -- Decoding stored base64 custom_css for merge.
194 $incoming_raw = isset( $incoming['raw'] ) ? base64_decode( $incoming['raw'] ) : ''; // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode -- Decoding stored base64 custom_css for merge.
195
196 $merged = trim( $existing_raw . ' ' . $incoming_raw );
197
198 return '' !== $merged
199 ? [ 'raw' => base64_encode( $merged ) ] // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode -- Storing merged custom_css as base64.
200 : null;
201 }
202
203 private static function make_variant( string $breakpoint, ?string $state, string $css, Css_Converter $converter, array $extra_custom_css_parts = [] ): ?array {
204 $null_props = self::extract_null_props( $css );
205 $stripped_css = self::strip_null_declarations( $css );
206 $result = $converter->convert( $stripped_css );
207 $props = $result['props'] ?? [];
208 $custom_str = $result['customCss'] ?? '';
209
210 if ( ! empty( $extra_custom_css_parts ) ) {
211 $extra = implode( ' ', $extra_custom_css_parts );
212 $custom_str = '' !== $custom_str ? $custom_str . ' ' . $extra : $extra;
213 }
214
215 if ( empty( $props ) && '' === $custom_str && empty( $null_props ) ) {
216 return null;
217 }
218
219 return [
220 'meta' => [
221 'breakpoint' => $breakpoint,
222 'state' => $state,
223 ],
224 'props' => $props,
225 'custom_css' => '' !== $custom_str
226 ? [ 'raw' => base64_encode( $custom_str ) ] // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode -- Global class custom_css.raw is stored as base64.
227 : null,
228 'null_props' => $null_props,
229 ];
230 }
231
232 private static function extract_null_props( string $css ): array {
233 $null_props = [];
234 preg_replace_callback(
235 self::NULL_DECLARATION_PATTERN,
236 function ( $matches ) use ( &$null_props ) {
237 $null_props[] = $matches[2];
238 },
239 $css
240 );
241 return $null_props;
242 }
243
244 private static function strip_null_declarations( string $css ): string {
245 return (string) preg_replace( self::NULL_DECLARATION_PATTERN, '$1', $css );
246 }
247 }
248