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
| 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 |