PluginProbe
Elementor Website Builder – more than just a page builder / 4.3.0-beta3
Elementor Website Builder – more than just a page builder v4.3.0-beta3
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 4.1.0-beta1 4.1.0-dev1 All 452 releases
elementor / modules / atomic-widgets / css-converter / css-media-splitter.php

css-media-splitter.php in Elementor Website Builder – more than just a page builder 4.3.0-beta3, at modules/atomic-widgets/css-converter/css-media-splitter.php

183 lines 4.4 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;
4
5 if ( ! defined( 'ABSPATH' ) ) {
6 exit;
7 }
8
9 class Css_Media_Splitter {
10
11 use Css_Block_Scanner_Trait;
12
13 const DESKTOP_ALIASES = [ 'desktop', 'default' ];
14 const DESKTOP_KEY = 'desktop';
15 const MEDIA_AT_LENGTH = 6;
16
17 /** @var array */
18 private array $known_breakpoints;
19
20 public function __construct( array $known_breakpoints ) {
21 $this->known_breakpoints = $known_breakpoints;
22 }
23
24 /**
25 * @return array{breakpoints: array<string, string>, custom_css: string, error: string|null}
26 */
27 public function split( string $css ): array {
28 $breakpoints = [];
29 $custom_blocks = [];
30 $root_segments = [];
31
32 $len = strlen( $css );
33 $i = 0;
34 $depth = 0;
35 $in_string = false;
36 $string_char = '';
37 $segment_start = 0;
38
39 while ( $i < $len ) {
40 $char = $css[ $i ];
41
42 if ( $in_string ) {
43 if ( $string_char === $char && ! $this->is_escaped( $css, $i ) ) {
44 $in_string = false;
45 }
46 ++$i;
47 continue;
48 }
49
50 if ( '"' === $char || "'" === $char ) {
51 $in_string = true;
52 $string_char = $char;
53 ++$i;
54 continue;
55 }
56
57 if ( '{' === $char ) {
58 ++$depth;
59 ++$i;
60 continue;
61 }
62
63 if ( '}' === $char ) {
64 --$depth;
65 if ( $depth < 0 ) {
66 return $this->split_error( 'Unexpected closing bracket in CSS.' );
67 }
68 ++$i;
69 continue;
70 }
71
72 if ( $depth > 0 || '@' !== $char ) {
73 ++$i;
74 continue;
75 }
76
77 if ( ! $this->is_media_at( $css, $i ) ) {
78 ++$i;
79 continue;
80 }
81
82 $root_segments[] = substr( $css, $segment_start, $i - $segment_start );
83
84 $j = $i + self::MEDIA_AT_LENGTH;
85 while ( $j < $len && '{' !== $css[ $j ] ) {
86 ++$j;
87 }
88
89 if ( $j >= $len ) {
90 ++$i;
91 $segment_start = $i;
92 continue;
93 }
94
95 $raw_selector = trim( substr( $css, $i + self::MEDIA_AT_LENGTH, $j - $i - self::MEDIA_AT_LENGTH ) );
96 $block_end = $this->find_block_end( $css, $j + 1, $len );
97
98 if ( null === $block_end ) {
99 return $this->split_error( 'Unclosed @media block.' );
100 }
101
102 $block_content = trim( substr( $css, $j + 1, $block_end - $j - 2 ) );
103 $segment_start = $block_end;
104 $i = $block_end;
105
106 if ( preg_match( '/^\(\s*--([a-z0-9_-]+)\s*\)$/', $raw_selector, $matches ) ) {
107 $result = $this->resolve_alias( $matches[1] );
108
109 if ( isset( $result['error'] ) ) {
110 return $this->split_error( $result['error'] );
111 }
112
113 if ( $this->contains_nested_breakpoint( $block_content ) ) {
114 return $this->split_error( 'Nested breakpoints are not allowed.' );
115 }
116
117 $target = $result['breakpoint'];
118 $breakpoints[ $target ] = isset( $breakpoints[ $target ] )
119 ? $breakpoints[ $target ] . ' ' . $block_content
120 : $block_content;
121 } else {
122 $custom_blocks[] = '@media ' . $raw_selector . ' { ' . $block_content . ' }';
123 }
124 }
125
126 $root_segments[] = substr( $css, $segment_start );
127 $root_css = trim( implode( '', $root_segments ) );
128
129 if ( '' !== $root_css ) {
130 $breakpoints[ self::DESKTOP_KEY ] = isset( $breakpoints[ self::DESKTOP_KEY ] )
131 ? $root_css . ' ' . $breakpoints[ self::DESKTOP_KEY ]
132 : $root_css;
133 }
134
135 return [
136 'breakpoints' => $breakpoints,
137 'custom_css' => implode( ' ', $custom_blocks ),
138 'error' => null,
139 ];
140 }
141
142 private function split_error( string $message ): array {
143 return [
144 'breakpoints' => [],
145 'custom_css' => '',
146 'error' => $message,
147 ];
148 }
149
150 private function contains_nested_breakpoint( string $css ): bool {
151 $all_aliases = array_merge( self::DESKTOP_ALIASES, $this->known_breakpoints );
152 $pattern = '/@media\s*\(\s*--(?:' . implode( '|', array_map( 'preg_quote', $all_aliases ) ) . ')\s*\)/';
153
154 return (bool) preg_match( $pattern, $css );
155 }
156
157 private function is_media_at( string $css, int $pos ): bool {
158 if ( 0 !== strncmp( substr( $css, $pos, self::MEDIA_AT_LENGTH ), '@media', self::MEDIA_AT_LENGTH ) ) {
159 return false;
160 }
161
162 $after = $pos + self::MEDIA_AT_LENGTH;
163 $len = strlen( $css );
164
165 return $after >= $len || ! ctype_alpha( $css[ $after ] );
166 }
167
168 /**
169 * @return array{breakpoint: string}|array{error: string}
170 */
171 private function resolve_alias( string $alias ): array {
172 if ( in_array( $alias, self::DESKTOP_ALIASES, true ) ) {
173 return [ 'breakpoint' => self::DESKTOP_KEY ];
174 }
175
176 if ( in_array( $alias, $this->known_breakpoints, true ) ) {
177 return [ 'breakpoint' => $alias ];
178 }
179
180 return [ 'error' => sprintf( 'Unknown breakpoint alias: --%s.', $alias ) ];
181 }
182 }
183