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
← All changes | modules/atomic-widgets/css-converter/css-converter.php +215 -4 4.2.0-beta1 → 4.3.1 View file →
@@ -10,8 +10,11 @@
10 10 exit; // Exit if accessed directly.
11 11 }
12 12
13 13 class Css_Converter {
14 +
15 + use Css_Block_Scanner_Trait;
16 +
14 17 const BLOCKED_PROPERTIES = [ 'behavior', '-moz-binding' ];
15 18 const BLOCKED_VALUE_NEEDLES = [ 'expression(', 'javascript:' ];
16 19
17 20 private Converter_Registry $registry;
@@ -34,12 +37,120 @@
34 37 $this->variable_transformer = $variable_transformer;
35 38 }
36 39
37 40 /**
41 + * @return array{blocks: array<int, array{selector: string|null, css: string}>}|array{blocks: array, error: string}
42 + */
43 + public function parse_nested( string $css ): array {
44 + $result = $this->scan_ampersand_blocks( $css );
45 +
46 + if ( isset( $result['error'] ) ) {
47 + return [
48 + 'blocks' => [],
49 + 'error' => $result['error'],
50 + ];
51 + }
52 +
53 + return [
54 + 'blocks' => array_merge(
55 + [
56 + [
57 + 'selector' => null,
58 + 'css' => $result['base_css'],
59 + ],
60 + ],
61 + $result['nested_blocks']
62 + ),
63 + ];
64 + }
65 +
66 + private function scan_ampersand_blocks( string $css ): array {
67 + $nested_blocks = [];
68 + $base_css = '';
69 + $len = strlen( $css );
70 + $i = 0;
71 + $segment_start = 0;
72 + $in_string = false;
73 + $string_char = '';
74 +
75 + while ( $i < $len ) {
76 + $char = $css[ $i ];
77 +
78 + if ( $in_string ) {
79 + if ( $string_char === $char && ! $this->is_escaped( $css, $i ) ) {
80 + $in_string = false;
81 + }
82 + ++$i;
83 + continue;
84 + }
85 +
86 + if ( '"' === $char || "'" === $char ) {
87 + $in_string = true;
88 + $string_char = $char;
89 + ++$i;
90 + continue;
91 + }
92 +
93 + if ( '{' === $char ) {
94 + return [ 'error' => 'Unsupported nested selector. Only &:hover, &:focus, and &:active pseudo-class blocks are allowed.' ];
95 + }
96 +
97 + if ( '}' === $char ) {
98 + return [ 'error' => 'Unclosed brace detected in CSS input.' ];
99 + }
100 +
101 + if ( '&' !== $char ) {
102 + ++$i;
103 + continue;
104 + }
105 +
106 + $j = $i + 1;
107 + while ( $j < $len && '{' !== $css[ $j ] && ';' !== $css[ $j ] && '}' !== $css[ $j ] ) {
108 + ++$j;
109 + }
110 +
111 + if ( $j >= $len || ';' === $css[ $j ] || '}' === $css[ $j ] ) {
112 + ++$i;
113 + continue;
114 + }
115 +
116 + $selector = rtrim( substr( $css, $i + 1, $j - $i - 1 ) );
117 +
118 + if ( '' === $selector ) {
119 + return [ 'error' => 'Bare & block without a selector is not valid CSS.' ];
120 + }
121 +
122 + $block_end = $this->find_block_end( $css, $j + 1, $len );
123 +
124 + if ( null === $block_end ) {
125 + return [ 'error' => 'Unclosed brace detected in CSS input.' ];
126 + }
127 +
128 + $block_content = substr( $css, $j + 1, $block_end - $j - 2 );
129 + $base_css .= substr( $css, $segment_start, $i - $segment_start );
130 + $segment_start = $block_end;
131 +
132 + $nested_blocks[] = [
133 + 'selector' => $selector,
134 + 'css' => $block_content,
135 + ];
136 +
137 + $i = $block_end;
138 + }
139 +
140 + $base_css .= substr( $css, $segment_start );
141 +
142 + return [
143 + 'base_css' => $base_css,
144 + 'nested_blocks' => $nested_blocks,
145 + ];
146 + }
147 +
148 + /**
38 149 * @return array{props: array, customCss: string, rejected: string[]}
39 150 */
40 151 public function convert( string $css ): array {
41 - $rules = $this->expand_shorthands( $this->parse( $css ) );
152 + $rules = $this->dedupe( $this->expand_shorthands( $this->parse( $css ) ) );
42 153 $context = new Conversion_Context( $rules );
43 154 $leftover = [];
44 155
45 156 foreach ( $rules as $rule ) {
@@ -58,11 +169,18 @@
58 169 $ejected = $this->variable_transformer->eject_unresolved_var_props( $props, $schema, $rules );
59 170 $props = $ejected['props'];
60 171 $leftover = array_merge( $leftover, $ejected['custom_css'] );
61 172 $rejected = array_merge( $rejected, $ejected['rejected'] );
173 +
174 + // Post-processing: handle edge cases that the general transform pipeline cannot cover.
175 + $gradient_color_stops_leftovers = $this->variable_transformer->normalize_gradient_color_stops( $props, $rules );
176 + $leftover = array_merge( $leftover, $gradient_color_stops_leftovers );
177 +
62 178 $props = $this->validate_props( $props, $schema );
63 179 }
64 180
181 + $props = $this->cleanup_props( $props );
182 +
65 183 return [
66 184 'props' => $props,
67 185 'customCss' => implode( ' ', $leftover ),
68 186 'rejected' => $rejected,
@@ -73,11 +191,90 @@
73 191 if ( empty( $props ) ) {
74 192 return [];
75 193 }
76 194
77 - return Props_Parser::make( $schema )->validate( $props )->unwrap();
195 + $null_resets = array_filter( $props, fn( $v ) => null === $v || $this->has_null_leaf( $v ) );
196 + $value_props = array_filter( $props, fn( $v ) => null !== $v && ! $this->has_null_leaf( $v ) );
197 +
198 + $validated = empty( $value_props )
199 + ? []
200 + : Props_Parser::make( $schema )->validate( $value_props )->unwrap();
201 +
202 + return array_merge( $validated, $null_resets );
78 203 }
79 204
205 + private function has_null_leaf( $value ): bool {
206 + if ( ! is_array( $value ) || ! is_array( $value['value'] ?? null ) ) {
207 + return false;
208 + }
209 +
210 + foreach ( $value['value'] as $v ) {
211 + if ( null === $v || $this->has_null_leaf( $v ) ) {
212 + return true;
213 + }
214 + }
215 +
216 + return false;
217 + }
218 +
219 + /**
220 + * Recursively collapses prop values where all present sub-values are null or empty arrays into a
221 + * single null. This propagates null resets up the tree so the client receives a clean signal:
222 + * e.g. a Dimensions object where every side was set to null becomes just null at the prop level.
223 + */
224 + private function cleanup_props( array $props ): array {
225 + $result = [];
226 +
227 + foreach ( $props as $key => $value ) {
228 + $result[ $key ] = $this->cleanup_value( $value );
229 + }
230 +
231 + return $result;
232 + }
233 +
234 + private function cleanup_value( $value ) {
235 + if ( null === $value || ! is_array( $value ) ) {
236 + return $value;
237 + }
238 +
239 + $inner = $value['value'] ?? $value;
240 +
241 + if ( ! is_array( $inner ) ) {
242 + return $value;
243 + }
244 +
245 + $cleaned = [];
246 +
247 + foreach ( $inner as $k => $v ) {
248 + $cleaned[ $k ] = $this->cleanup_value( $v );
249 + }
250 +
251 + if ( $this->is_empty_or_all_null( $cleaned ) ) {
252 + return null;
253 + }
254 +
255 + return isset( $value['$$type'] )
256 + ? [
257 + '$$type' => $value['$$type'],
258 + 'value' => $cleaned,
259 + ]
260 + : $cleaned;
261 + }
262 +
263 + private function is_empty_or_all_null( array $arr ): bool {
264 + if ( empty( $arr ) ) {
265 + return false;
266 + }
267 +
268 + foreach ( $arr as $v ) {
269 + if ( null !== $v ) {
270 + return false;
271 + }
272 + }
273 +
274 + return true;
275 + }
276 +
80 277 private function style_schema(): array {
81 278 if ( function_exists( 'apply_filters' ) ) {
82 279 return Style_Schema::get();
83 280 }
@@ -93,8 +290,20 @@
93 290 *
94 291 * @param array<int, array{property: string, value: string, declaration: string}> $rules
95 292 * @return array<int, array{property: string, value: string, declaration: string}>
96 293 */
294 + private function dedupe( array $rules ): array {
295 + $last_index = [];
296 +
297 + foreach ( $rules as $i => $rule ) {
298 + $last_index[ $rule['property'] ] = $i;
299 + }
300 +
301 + return array_values(
302 + array_filter( $rules, fn( $rule, $i ) => $last_index[ $rule['property'] ] === $i, ARRAY_FILTER_USE_BOTH )
303 + );
304 + }
305 +
97 306 private function expand_shorthands( array $rules ): array {
98 307 $expanded = [];
99 308
100 309 foreach ( $rules as $rule ) {
@@ -181,13 +390,15 @@
181 390 continue;
182 391 }
183 392
184 393 $property = strtolower( trim( substr( $declaration, 0, $separator ) ) );
185 - $value = trim( substr( $declaration, $separator + 1 ) );
394 + $raw_value = trim( substr( $declaration, $separator + 1 ) );
186 395
187 - if ( '' === $property || '' === $value || $this->is_blocked( $property, $value ) ) {
396 + if ( '' === $property || '' === $raw_value || $this->is_blocked( $property, $raw_value ) ) {
188 397 continue;
189 398 }
399 +
400 + $value = 'null' === $raw_value ? null : $raw_value;
190 401
191 402 $rules[] = [
192 403 'property' => $property,
193 404 'value' => $value,