elementor
/
modules
/
atomic-widgets
/
css-converter
/
value-parsers
/
background-image-value-parser.php
background-image-value-parser.php in Elementor Website Builder – more than just a page builder 4.3.0, at modules/atomic-widgets/css-converter/value-parsers/background-image-value-parser.php
| 1 | <?php |
| 2 | |
| 3 | namespace Elementor\Modules\AtomicWidgets\CssConverter\ValueParsers; |
| 4 | |
| 5 | use Elementor\Modules\AtomicWidgets\PropTypes\Background_Gradient_Overlay_Prop_Type; |
| 6 | use Elementor\Modules\AtomicWidgets\PropTypes\Background_Image_Overlay_Prop_Type; |
| 7 | use Elementor\Modules\AtomicWidgets\PropTypes\Color_Prop_Type; |
| 8 | use Elementor\Modules\AtomicWidgets\PropTypes\Color_Stop_Prop_Type; |
| 9 | use Elementor\Modules\AtomicWidgets\PropTypes\Gradient_Color_Stop_Prop_Type; |
| 10 | use Elementor\Modules\AtomicWidgets\PropTypes\Image_Prop_Type; |
| 11 | use Elementor\Modules\AtomicWidgets\PropTypes\Image_Src_Prop_Type; |
| 12 | use Elementor\Modules\AtomicWidgets\PropTypes\Position_Prop_Type; |
| 13 | use Elementor\Modules\AtomicWidgets\PropTypes\Primitives\Number_Prop_Type; |
| 14 | use Elementor\Modules\AtomicWidgets\PropTypes\Primitives\String_Prop_Type; |
| 15 | use Elementor\Modules\AtomicWidgets\PropTypes\Url_Prop_Type; |
| 16 | |
| 17 | if ( ! defined( 'ABSPATH' ) ) { |
| 18 | exit; // Exit if accessed directly. |
| 19 | } |
| 20 | |
| 21 | /** |
| 22 | * Stateless parser: a raw CSS `background-image` value -> an ordered list of overlay PropValues (one |
| 23 | * per comma-separated layer), or null to decline the whole declaration (-> custom_css). |
| 24 | * |
| 25 | * Each layer token is classified: |
| 26 | * - `none` -> layer is silently skipped (not added to the list). |
| 27 | * - `url(...)` -> Background_Image_Overlay_Prop_Type PropValue. |
| 28 | * - `linear-gradient(...)` -> Background_Gradient_Overlay_Prop_Type PropValue (linear). |
| 29 | * - `radial-gradient(...)` -> Background_Gradient_Overlay_Prop_Type PropValue (radial). |
| 30 | * - anything else -> decline the entire value. |
| 31 | * |
| 32 | * Gradient parsing supports: |
| 33 | * - Linear: optional leading `Ndeg` angle, then comma-separated color stops. |
| 34 | * - Radial: optional leading `circle at <named-position>`, then color stops. |
| 35 | * - Color stops: `<color> <N>%` pairs; offset is optional. |
| 36 | * - Unsupported syntax (direction keywords, conic, complex shapes) declines. |
| 37 | */ |
| 38 | class Background_Image_Value_Parser { |
| 39 | const DEFAULT_IMAGE_SIZE = 'large'; |
| 40 | |
| 41 | /** |
| 42 | * @return array[]|null Ordered overlay PropValues per layer, or null to decline. |
| 43 | */ |
| 44 | public static function parse( string $value ): ?array { |
| 45 | $tokens = Css_Token_Splitter::split_by_comma( trim( $value ) ); |
| 46 | |
| 47 | if ( empty( $tokens ) ) { |
| 48 | return null; |
| 49 | } |
| 50 | |
| 51 | $overlays = []; |
| 52 | |
| 53 | foreach ( $tokens as $token ) { |
| 54 | $overlay = self::parse_layer( trim( $token ) ); |
| 55 | |
| 56 | if ( false === $overlay ) { |
| 57 | return null; |
| 58 | } |
| 59 | |
| 60 | if ( null !== $overlay ) { |
| 61 | $overlays[] = $overlay; |
| 62 | } |
| 63 | } |
| 64 | |
| 65 | return $overlays; |
| 66 | } |
| 67 | |
| 68 | /** |
| 69 | * @return array|null|false PropValue, null for `none`, false to decline. |
| 70 | */ |
| 71 | private static function parse_layer( string $token ) { |
| 72 | $lower = strtolower( $token ); |
| 73 | |
| 74 | if ( 'none' === $lower ) { |
| 75 | return null; |
| 76 | } |
| 77 | |
| 78 | if ( 0 === strpos( $lower, 'url(' ) ) { |
| 79 | return self::parse_url( $token ); |
| 80 | } |
| 81 | |
| 82 | if ( preg_match( '/^([\w-]+-gradient)\s*\((.+)\)$/si', $token, $m ) ) { |
| 83 | return self::parse_gradient( strtolower( $m[1] ), $m[2] ); |
| 84 | } |
| 85 | |
| 86 | return false; |
| 87 | } |
| 88 | |
| 89 | /** |
| 90 | * @return array|false |
| 91 | */ |
| 92 | private static function parse_url( string $token ) { |
| 93 | if ( ! preg_match( '/^url\s*\(\s*([\'"]?)(.+?)\1\s*\)$/i', $token, $m ) ) { |
| 94 | return false; |
| 95 | } |
| 96 | |
| 97 | $url = $m[2]; |
| 98 | |
| 99 | return Background_Image_Overlay_Prop_Type::generate( [ |
| 100 | 'image' => Image_Prop_Type::generate( [ |
| 101 | 'src' => Image_Src_Prop_Type::generate( [ |
| 102 | 'url' => Url_Prop_Type::generate( $url ), |
| 103 | ] ), |
| 104 | 'size' => String_Prop_Type::generate( self::DEFAULT_IMAGE_SIZE ), |
| 105 | ] ), |
| 106 | ] ); |
| 107 | } |
| 108 | |
| 109 | /** |
| 110 | * @return array|false |
| 111 | */ |
| 112 | private static function parse_gradient( string $func_name, string $args ) { |
| 113 | if ( 'linear-gradient' === $func_name ) { |
| 114 | return self::parse_linear_gradient( $args ); |
| 115 | } |
| 116 | |
| 117 | if ( 'radial-gradient' === $func_name ) { |
| 118 | return self::parse_radial_gradient( $args ); |
| 119 | } |
| 120 | |
| 121 | return false; |
| 122 | } |
| 123 | |
| 124 | /** |
| 125 | * @return array|false |
| 126 | */ |
| 127 | private static function parse_linear_gradient( string $args ) { |
| 128 | $tokens = Css_Token_Splitter::split_by_comma( trim( $args ) ); |
| 129 | |
| 130 | if ( empty( $tokens ) ) { |
| 131 | return false; |
| 132 | } |
| 133 | |
| 134 | $angle = null; |
| 135 | $stop_start = 0; |
| 136 | $first = trim( $tokens[0] ); |
| 137 | |
| 138 | if ( preg_match( '/^(-?\d+(?:\.\d+)?)deg$/i', $first, $m ) ) { |
| 139 | $angle = (float) $m[1]; |
| 140 | $stop_start = 1; |
| 141 | } elseif ( 0 === strpos( strtolower( $first ), 'to ' ) ) { |
| 142 | return false; |
| 143 | } |
| 144 | |
| 145 | $stops = self::parse_color_stops( array_slice( $tokens, $stop_start ) ); |
| 146 | |
| 147 | if ( null === $stops ) { |
| 148 | return false; |
| 149 | } |
| 150 | |
| 151 | $value = [ |
| 152 | 'type' => String_Prop_Type::generate( 'linear' ), |
| 153 | 'stops' => Gradient_Color_Stop_Prop_Type::generate( $stops ), |
| 154 | ]; |
| 155 | |
| 156 | if ( null !== $angle ) { |
| 157 | $value['angle'] = Number_Prop_Type::generate( $angle ); |
| 158 | } |
| 159 | |
| 160 | return Background_Gradient_Overlay_Prop_Type::generate( $value ); |
| 161 | } |
| 162 | |
| 163 | /** |
| 164 | * @return array|false |
| 165 | */ |
| 166 | private static function parse_radial_gradient( string $args ) { |
| 167 | $tokens = Css_Token_Splitter::split_by_comma( trim( $args ) ); |
| 168 | |
| 169 | if ( empty( $tokens ) ) { |
| 170 | return false; |
| 171 | } |
| 172 | |
| 173 | $positions = null; |
| 174 | $stop_start = 0; |
| 175 | $first = trim( $tokens[0] ); |
| 176 | |
| 177 | if ( preg_match( '/^circle\s+at\s+(.+)$/i', $first, $m ) ) { |
| 178 | $pos = trim( $m[1] ); |
| 179 | |
| 180 | if ( ! Position_Prop_Type::is_valid_radial_position( $pos ) ) { |
| 181 | return false; |
| 182 | } |
| 183 | |
| 184 | $positions = $pos; |
| 185 | $stop_start = 1; |
| 186 | } |
| 187 | |
| 188 | $stops = self::parse_color_stops( array_slice( $tokens, $stop_start ) ); |
| 189 | |
| 190 | if ( null === $stops ) { |
| 191 | return false; |
| 192 | } |
| 193 | |
| 194 | $value = [ |
| 195 | 'type' => String_Prop_Type::generate( 'radial' ), |
| 196 | 'stops' => Gradient_Color_Stop_Prop_Type::generate( $stops ), |
| 197 | ]; |
| 198 | |
| 199 | if ( null !== $positions ) { |
| 200 | $value['positions'] = String_Prop_Type::generate( $positions ); |
| 201 | } |
| 202 | |
| 203 | return Background_Gradient_Overlay_Prop_Type::generate( $value ); |
| 204 | } |
| 205 | |
| 206 | /** |
| 207 | |
| 208 | * @return array[]|null |
| 209 | */ |
| 210 | private static function parse_color_stops( array $tokens ): ?array { |
| 211 | if ( empty( $tokens ) ) { |
| 212 | return null; |
| 213 | } |
| 214 | |
| 215 | $parsed = []; |
| 216 | |
| 217 | foreach ( $tokens as $token ) { |
| 218 | $parts = self::parse_color_stop_parts( trim( $token ) ); |
| 219 | |
| 220 | if ( null === $parts ) { |
| 221 | return null; |
| 222 | } |
| 223 | |
| 224 | $parsed[] = $parts; |
| 225 | } |
| 226 | |
| 227 | return self::build_color_stops_with_defaults( $parsed ); |
| 228 | } |
| 229 | |
| 230 | private static function parse_color_stop_parts( string $token ): ?array { |
| 231 | $parts = Css_Token_Splitter::split_by_whitespace( $token ); |
| 232 | |
| 233 | if ( empty( $parts ) || count( $parts ) > 2 ) { |
| 234 | return null; |
| 235 | } |
| 236 | |
| 237 | $offset = null; |
| 238 | |
| 239 | if ( isset( $parts[1] ) ) { |
| 240 | if ( ! preg_match( '/^(-?\d+(?:\.\d+)?)%$/', $parts[1], $m ) ) { |
| 241 | return null; |
| 242 | } |
| 243 | |
| 244 | $offset = (float) $m[1]; |
| 245 | } |
| 246 | |
| 247 | return [ |
| 248 | 'color' => $parts[0], |
| 249 | 'offset' => $offset, |
| 250 | ]; |
| 251 | } |
| 252 | |
| 253 | private static function build_color_stops_with_defaults( array $parsed ): array { |
| 254 | $count = count( $parsed ); |
| 255 | $stops = []; |
| 256 | |
| 257 | foreach ( $parsed as $index => $part ) { |
| 258 | $offset = $part['offset'] ?? self::default_offset_for_index( $index, $count ); |
| 259 | |
| 260 | $stops[] = Color_Stop_Prop_Type::generate( [ |
| 261 | 'color' => Color_Prop_Type::generate( $part['color'] ), |
| 262 | 'offset' => Number_Prop_Type::generate( $offset ), |
| 263 | ] ); |
| 264 | } |
| 265 | |
| 266 | return $stops; |
| 267 | } |
| 268 | |
| 269 | private static function default_offset_for_index( int $index, int $count ): float { |
| 270 | if ( $count <= 1 ) { |
| 271 | return 0.0; |
| 272 | } |
| 273 | |
| 274 | return ( $index / ( $count - 1 ) ) * 100; |
| 275 | } |
| 276 | } |
| 277 |