PluginProbe
Elementor Website Builder – more than just a page builder / 4.3.0
Elementor Website Builder – more than just a page builder v4.3.0
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 4.1.0-dev2 All 455 releases
elementor / modules / atomic-widgets / css-converter / expanders / background-shorthand-expander.php

background-shorthand-expander.php in Elementor Website Builder – more than just a page builder 4.3.0, at modules/atomic-widgets/css-converter/expanders/background-shorthand-expander.php

308 lines 8.1 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\AtomicWidgets\CssConverter\Expanders;
4
5 use Elementor\Modules\AtomicWidgets\CssConverter\Css_Var_Token_Resolver;
6 use Elementor\Modules\AtomicWidgets\CssConverter\Shorthand_Expander_Base;
7 use Elementor\Modules\AtomicWidgets\CssConverter\ValueParsers\Css_Token_Splitter;
8 use Elementor\Modules\AtomicWidgets\CssConverter\ValueParsers\Size_Value_Parser;
9 use Elementor\Modules\Variables\Services\Variables_Service;
10
11 if ( ! defined( 'ABSPATH' ) ) {
12 exit; // Exit if accessed directly.
13 }
14
15 /**
16 * Expands a `background` shorthand into its constituent longhand declarations so the
17 * existing per-property converters can process each one independently.
18 *
19 * Each comma-separated layer is parsed into role slots: image, repeat, attachment, clip, position,
20 * size, and (last layer only) color. Slot values are then aggregated across layers to form the
21 * longhand values:
22 * - background-image: comma-joined per layer (none for layers without an explicit image).
23 * - background-repeat: comma-joined if all layers that have an image also specify it.
24 * - background-attachment: same.
25 * - background-position: same.
26 * - background-size: same (position must also be present when size is used).
27 * - background-color: from the last layer only.
28 * - background-clip: from the last layer only (single scalar in our model).
29 *
30 * A layer whose tokens cannot be unambiguously classified declines the entire shorthand (returns [])
31 * so the original declaration is kept and routed to custom_css.
32 */
33 class Background_Shorthand_Expander extends Shorthand_Expander_Base {
34 private ?Variables_Service $variables_service;
35
36 const REPEAT_ENUM = [ 'repeat', 'repeat-x', 'repeat-y', 'no-repeat' ];
37 const ATTACHMENT_ENUM = [ 'fixed', 'scroll' ];
38 const CLIP_ENUM = [ 'border-box', 'padding-box', 'content-box', 'text' ];
39 const POSITION_KEYWORDS = [ 'top', 'bottom', 'left', 'right', 'center' ];
40 const SIZE_KEYWORDS = [ 'cover', 'contain', 'auto' ];
41
42 public function __construct( ?Variables_Service $variables_service = null ) {
43 $this->variables_service = $variables_service;
44 }
45
46 protected function get_supported_properties(): array {
47 return [ 'background' ];
48 }
49
50 const ALL_LONGHANDS = [
51 'background-image',
52 'background-repeat',
53 'background-attachment',
54 'background-position',
55 'background-size',
56 'background-clip',
57 'background-color',
58 ];
59
60 protected function expand_null( array $rule ): array {
61 return array_map( fn( $p ) => $this->null_rule( $p ), self::ALL_LONGHANDS );
62 }
63
64 protected function do_expand( array $rule ): array {
65 $layers = Css_Token_Splitter::split_by_comma( trim( $rule['value'] ) );
66
67 if ( empty( $layers ) ) {
68 return [];
69 }
70
71 $parsed = [];
72
73 foreach ( $layers as $layer ) {
74 $result = $this->parse_layer( trim( $layer ) );
75
76 if ( null === $result ) {
77 return [];
78 }
79
80 $parsed[] = $result;
81 }
82
83 return $this->build_rules( $parsed );
84 }
85
86 /**
87 * @return array{image:string|null,repeat:string|null,attachment:string|null,clip:string|null,position:string|null,size:string|null,color:string|null}|null
88 */
89 private function parse_layer( string $layer ): ?array {
90 if ( $this->is_var_only_layer( $layer ) ) {
91 return $this->parse_var_only_layer( $layer );
92 }
93
94 $tokens = Css_Token_Splitter::split_by_whitespace( $layer );
95
96 $image = null;
97 $repeat = null;
98 $attachment = null;
99 $clip = null;
100 $color = null;
101 $position_tokens = [];
102 $size_tokens = [];
103 $after_slash = false;
104
105 $n = count( $tokens );
106
107 for ( $i = 0; $i < $n; $i++ ) {
108 $token = $tokens[ $i ];
109 $lower = strtolower( $token );
110
111 if ( '/' === $token ) {
112 $after_slash = true;
113 continue;
114 }
115
116 if ( $after_slash ) {
117 if ( $this->is_size_token( $token ) ) {
118 $size_tokens[] = $token;
119 if ( count( $size_tokens ) >= 2 ) {
120 $after_slash = false;
121 }
122 continue;
123 }
124 $after_slash = false;
125 }
126
127 if ( $this->is_image_token( $lower ) ) {
128 if ( null !== $image ) {
129 return null;
130 }
131 $image = $token;
132 continue;
133 }
134
135 if ( in_array( $lower, self::REPEAT_ENUM, true ) ) {
136 if ( null !== $repeat ) {
137 return null;
138 }
139 $repeat = $token;
140 continue;
141 }
142
143 if ( in_array( $lower, self::ATTACHMENT_ENUM, true ) ) {
144 if ( null !== $attachment ) {
145 return null;
146 }
147 $attachment = $token;
148 continue;
149 }
150
151 if ( in_array( $lower, self::CLIP_ENUM, true ) ) {
152 if ( null !== $clip ) {
153 return null;
154 }
155 $clip = $token;
156 continue;
157 }
158
159 if ( $this->is_position_token( $lower ) ) {
160 $position_tokens[] = $token;
161 if ( count( $position_tokens ) > 2 ) {
162 return null;
163 }
164 continue;
165 }
166
167 if ( null !== $color ) {
168 return null;
169 }
170
171 $color = $token;
172 }
173
174 if ( ! empty( $size_tokens ) && empty( $position_tokens ) ) {
175 return null;
176 }
177
178 $position = empty( $position_tokens ) ? null : implode( ' ', $position_tokens );
179 $size = empty( $size_tokens ) ? null : implode( ' ', $size_tokens );
180
181 return compact( 'image', 'repeat', 'attachment', 'clip', 'color', 'position', 'size' );
182 }
183
184 private function is_var_only_layer( string $layer ): bool {
185 return Css_Var_Token_Resolver::is_var_only_token( $layer );
186 }
187
188 /**
189 * @return array{image:string|null,repeat:string|null,attachment:string|null,clip:string|null,position:string|null,size:string|null,color:string|null}|null
190 */
191 private function parse_var_only_layer( string $layer ): ?array {
192 $resolved_type = Css_Var_Token_Resolver::resolve_var_only_token_type( $this->variables_service, $layer );
193 $empty_layer = $this->empty_layer();
194
195 if ( 'color' === $resolved_type ) {
196 $empty_layer['color'] = $layer;
197
198 return $empty_layer;
199 }
200
201 if ( 'size' === $resolved_type ) {
202 $empty_layer['position'] = 'center center';
203 $empty_layer['size'] = $layer;
204
205 return $empty_layer;
206 }
207
208 return null;
209 }
210
211 /**
212 * @return array{image:string|null,repeat:string|null,attachment:string|null,clip:string|null,position:string|null,size:string|null,color:string|null}
213 */
214 private function empty_layer(): array {
215 return [
216 'image' => null,
217 'repeat' => null,
218 'attachment' => null,
219 'clip' => null,
220 'position' => null,
221 'size' => null,
222 'color' => null,
223 ];
224 }
225
226 private function is_image_token( string $lower ): bool {
227 return 'none' === $lower
228 || 0 === strpos( $lower, 'url(' )
229 || false !== strpos( $lower, '-gradient(' );
230 }
231
232 private function is_size_token( string $token ): bool {
233 return in_array( strtolower( $token ), self::SIZE_KEYWORDS, true )
234 || null !== Size_Value_Parser::parse( $token );
235 }
236
237 private function is_position_token( string $lower ): bool {
238 return in_array( $lower, self::POSITION_KEYWORDS, true )
239 || null !== Size_Value_Parser::parse( $lower );
240 }
241
242 /**
243 * @param array[] $parsed
244 * @return array[]
245 */
246 private function build_rules( array $parsed ): array {
247 $layer_count = count( $parsed );
248 $rules = [];
249
250 $has_image = ! empty( array_filter( $parsed, fn( $l ) => null !== $l['image'] ) );
251
252 if ( $has_image ) {
253 $images = array_map( fn( $l ) => $l['image'] ?? 'none', $parsed );
254 $rules['background-image'] = implode( ', ', $images );
255 }
256
257 $per_layer_slots = [
258 'repeat' => 'background-repeat',
259 'attachment' => 'background-attachment',
260 'position' => 'background-position',
261 'size' => 'background-size',
262 ];
263
264 foreach ( $per_layer_slots as $slot => $property ) {
265 $values = array_column( $parsed, $slot );
266 $non_null = array_filter( $values, fn( $v ) => null !== $v );
267
268 if ( empty( $non_null ) ) {
269 continue;
270 }
271
272 if ( count( $non_null ) === 1 || count( $non_null ) === $layer_count ) {
273 $rules[ $property ] = implode( ', ', $non_null );
274 }
275 }
276
277 $last = end( $parsed );
278
279 if ( null !== $last['clip'] ) {
280 $rules['background-clip'] = $last['clip'];
281 }
282
283 if ( null !== $last['color'] ) {
284 $rules['background-color'] = $last['color'];
285 }
286
287 return $this->emit_rules( $rules );
288 }
289
290 /**
291 * @param array<string, string> $longhands
292 * @return array[]
293 */
294 private function emit_rules( array $longhands ): array {
295 $rules = [];
296
297 foreach ( $longhands as $property => $value ) {
298 $rules[] = [
299 'property' => $property,
300 'value' => $value,
301 'declaration' => $property . ': ' . $value,
302 ];
303 }
304
305 return $rules;
306 }
307 }
308