PluginProbe
Formidable Forms – WordPress Form Builder for Contact Forms, Calculators, Quizzes & More / 6.33.1
Formidable Forms – WordPress Form Builder for Contact Forms, Calculators, Quizzes & More v6.33.1
6.35 6.34 6.33.1 6.33 6.32.1 6.32 6.31 6.25 6.25.1 6.26 6.26.1 6.27 6.28 6.29 6.3 6.3.1 6.3.2 6.30 6.4 6.4.1 6.4.2 6.5 6.5.1 6.5.2 6.5.3 All 141 releases
formidable / classes / helpers / FrmCssScopeHelper.php

FrmCssScopeHelper.php in Formidable Forms – WordPress Form Builder for Contact Forms, Calculators, Quizzes & More 6.33.1, at classes/helpers/FrmCssScopeHelper.php

368 lines 9.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 if ( ! defined( 'ABSPATH' ) ) {
3 die( 'You are not allowed to call this page directly.' );
4 }
5
6 class FrmCssScopeHelper {
7
8 /**
9 * Nest the CSS.
10 * This function nests the CSS by adding the class name prefix to the selectors.
11 *
12 * @param string $css
13 * @param string $class_name
14 *
15 * @return string
16 */
17 public function nest( $css, $class_name ) {
18 // Remove CSS comments but preserve newlines
19 $css = preg_replace( '/\/\*.*?\*\//s', '', $css );
20
21 $output = array();
22 $css = trim( $css );
23 $length = strlen( $css );
24 $i = 0;
25 $buffer = '';
26
27 while ( $i < $length ) {
28 $char = $css[ $i ];
29
30 if ( '@' === $char ) {
31 $brace_pos = strpos( $css, '{', $i );
32
33 if ( false === $brace_pos ) {
34 $buffer .= $char;
35 ++$i;
36
37 continue;
38 }
39
40 $rule = substr( $css, $i, $brace_pos - $i );
41 $closing_brace = $this->find_matching_brace( $css, $brace_pos );
42 $inner_content = substr( $css, $brace_pos + 1, $closing_brace - $brace_pos - 1 );
43
44 // Don't nest keyframes content
45 if ( str_contains( $rule, '@keyframes' ) ) {
46 $output[] = "\n" . $rule . ' {' . $inner_content . '}' . "\n";
47 } else {
48 $output[] = "\n" . $rule . ' {';
49 $output[] = $this->nest( $inner_content, $class_name );
50 $output[] = '}' . "\n";
51 }
52
53 $i = $closing_brace + 1;
54 $buffer = '';
55
56 continue;
57 }//end if
58
59 if ( '{' === $char ) {
60 $selector = trim( $buffer );
61 $closing_brace = $this->find_matching_brace( $css, $i );
62 $declarations = substr( $css, $i + 1, $closing_brace - $i - 1 );
63
64 // Preserve indentation and formatting of declarations
65 $declarations = $this->preserve_declaration_formatting( $declarations );
66
67 if ( '' !== $selector && '' !== trim( $declarations ) ) {
68 $prefixed_selectors = $this->prefix_selectors( $selector, $class_name );
69
70 if ( $prefixed_selectors ) {
71 $output[] = "\n" . implode( ',' . "\n", $prefixed_selectors ) . ' {' . $declarations . '}' . "\n";
72 }
73 }
74
75 $i = $closing_brace + 1;
76 $buffer = '';
77
78 continue;
79 }//end if
80
81 $buffer .= $char;
82 ++$i;
83 }//end while
84
85 return implode( '', $output );
86 }
87
88 /**
89 * Unnest the CSS.
90 * This function unnests the CSS by removing the class name prefix from the selectors.
91 *
92 * @param string $css
93 * @param string $class_name
94 *
95 * @return string
96 */
97 public function unnest( $css, $class_name ) {
98 // Remove CSS comments but preserve newlines
99 $css = preg_replace( '/\/\*.*?\*\//s', '', $css );
100
101 $output = array();
102 $css = trim( $css );
103 $length = strlen( $css );
104 $i = 0;
105 $buffer = '';
106 $prefix = '.' . $class_name . ' ';
107 $prefix_length = strlen( $prefix );
108
109 while ( $i < $length ) {
110 $char = $css[ $i ];
111
112 if ( '@' === $char ) {
113 $brace_pos = strpos( $css, '{', $i );
114
115 if ( false === $brace_pos ) {
116 $buffer .= $char;
117 ++$i;
118
119 continue;
120 }
121
122 $rule = substr( $css, $i, $brace_pos - $i );
123 $closing_brace = $this->find_matching_brace( $css, $brace_pos );
124 $inner_content = substr( $css, $brace_pos + 1, $closing_brace - $brace_pos - 1 );
125
126 $output[] = "\n" . $rule . ' {';
127 $output[] = $this->unnest( $inner_content, $class_name );
128 $output[] = '}' . "\n";
129
130 $i = $closing_brace + 1;
131 $buffer = '';
132
133 continue;
134 }//end if
135
136 if ( '{' === $char ) {
137 $selector = trim( $buffer );
138 $closing_brace = $this->find_matching_brace( $css, $i );
139 $declarations = substr( $css, $i + 1, $closing_brace - $i - 1 );
140
141 // Preserve indentation and formatting of declarations
142 $declarations = $this->preserve_declaration_formatting( $declarations );
143
144 if ( '' !== $selector && '' !== trim( $declarations ) ) {
145 $unprefixed_selectors = $this->unprefix_selectors( $selector, $class_name, $prefix, $prefix_length );
146
147 if ( $unprefixed_selectors ) {
148 $output[] = "\n" . implode( ',' . "\n", $unprefixed_selectors ) . ' {' . $declarations . '}' . "\n";
149 }
150 }
151
152 $i = $closing_brace + 1;
153 $buffer = '';
154
155 continue;
156 }//end if
157
158 $buffer .= $char;
159 ++$i;
160 }//end while
161
162 return implode( '', $output );
163 }
164
165 /**
166 * Preserve declaration formatting with proper indentation.
167 *
168 * @param string $declarations
169 *
170 * @return string
171 */
172 private function preserve_declaration_formatting( $declarations ) {
173 // Trim the entire block but keep internal structure
174 $declarations = trim( $declarations );
175
176 if ( '' === $declarations ) {
177 return '';
178 }
179
180 // Check if declarations are already on multiple lines
181 if ( str_contains( $declarations, "\n" ) ) {
182 // Already formatted - preserve it
183 $lines = explode( "\n", $declarations );
184 $formatted_lines = array();
185
186 foreach ( $lines as $line ) {
187 $trimmed = trim( $line );
188
189 if ( '' !== $trimmed ) {
190 $formatted_lines[] = "\n\t" . $trimmed;
191 }
192 }
193
194 return implode( '', $formatted_lines ) . "\n";
195 }
196
197 // Single line - add minimal formatting
198 return ' ' . $declarations . ' ';
199 }
200
201 /**
202 * Build the list of prefixed selectors for a given selector string.
203 * Generates both descendant (.scope selector) and direct (selector.scope) forms.
204 *
205 * @param string $selector The comma-separated selector string.
206 * @param string $class_name The scope class name.
207 *
208 * @return array The list of prefixed selectors.
209 */
210 private function prefix_selectors( $selector, $class_name ) {
211 $selectors = array_map( 'trim', explode( ',', $selector ) );
212 $prefixed_selectors = array();
213
214 foreach ( $selectors as $single_selector ) {
215 if ( '' === $single_selector ) {
216 continue;
217 }
218
219 $prefixed_selectors[] = '.' . $class_name . ' ' . $single_selector;
220 $direct = $this->add_direct_scope( $single_selector, $class_name );
221
222 if ( null !== $direct ) {
223 $prefixed_selectors[] = $direct;
224 }
225 }
226
227 return $prefixed_selectors;
228 }
229
230 /**
231 * Build the list of unprefixed selectors for a given selector string.
232 * Handles both descendant (.scope selector) and direct (selector.scope) forms.
233 *
234 * @param string $selector The comma-separated selector string.
235 * @param string $class_name The scope class name.
236 * @param string $prefix The descendant prefix string (.class_name followed by space).
237 * @param int $prefix_length The length of the descendant prefix.
238 *
239 * @return array The list of unprefixed selectors (deduplicated).
240 */
241 private function unprefix_selectors( $selector, $class_name, $prefix, $prefix_length ) {
242 $selectors = array_filter(
243 array_map( 'trim', explode( ',', $selector ) ),
244 function ( $s ) {
245 return '' !== $s;
246 }
247 );
248 $unprefixed_selectors = array();
249
250 foreach ( $selectors as $single_selector ) {
251 if ( str_starts_with( $single_selector, $prefix ) ) {
252 $unprefixed_selectors[] = trim( substr( $single_selector, $prefix_length ) );
253 } else {
254 $unprefixed_selectors[] = $this->remove_direct_scope( $single_selector, $class_name ) ?? $single_selector;
255 }
256 }
257
258 return array_values( array_unique( $unprefixed_selectors ) );
259 }
260
261 /**
262 * Add the scope class directly to the first segment of a selector.
263 * For example, "h2" becomes "h2.scope" and ".foo .bar" becomes ".foo.scope .bar".
264 *
265 * @param string $selector The CSS selector.
266 * @param string $class_name The scope class name.
267 *
268 * @return string|null The direct-scoped selector, or null if not applicable.
269 */
270 private function add_direct_scope( $selector, $class_name ) {
271 $parts = preg_split( '/(\s+)/', $selector, 2, PREG_SPLIT_DELIM_CAPTURE );
272 $first = $parts[0];
273
274 // Don't add direct scope to selectors starting with combinators or universal selector.
275 if ( preg_match( '/^[>+~*]/', $first ) ) {
276 return null;
277 }
278
279 $rest = '';
280
281 if ( count( $parts ) > 1 ) {
282 $rest = $parts[1] . $parts[2];
283 }
284
285 // Capture all trailing pseudo-classes/pseudo-elements ( :before, :hover, etc. )
286 // Capture the chained pseudos wraps (.form_field:nth-child(2):focus )
287 $pseudo = '';
288
289 if ( preg_match( '/^(.*?)((?::{1,2}[a-zA-Z-]+(?:\([^)]*\))?)+)$/', $first, $pseudo_parts ) ) {
290 $first = $pseudo_parts[1];
291 $pseudo = $pseudo_parts[2];
292 }
293
294 return $first . '.' . $class_name . $pseudo . $rest;
295 }
296
297 /**
298 * Remove the scope class from the first segment of a direct-scoped selector.
299 * For example, "h2.scope" becomes "h2" and ".foo.scope .bar" becomes ".foo .bar".
300 *
301 * @param string $selector The CSS selector.
302 * @param string $class_name The scope class name.
303 *
304 * @return string|null The unscoped selector, or null if the scope was not found.
305 */
306 private function remove_direct_scope( $selector, $class_name ) {
307 $scope_pattern = '/\.' . preg_quote( $class_name, '/' ) . '(?![a-zA-Z0-9_-])/';
308 $parts = preg_split( '/(\s+)/', $selector, 2, PREG_SPLIT_DELIM_CAPTURE );
309
310 if ( false === $parts || ! preg_match( $scope_pattern, $parts[0] ) ) {
311 return null;
312 }
313
314 $parts[0] = preg_replace( $scope_pattern, '', $parts[0], 1 );
315 $result = trim( implode( '', $parts ) );
316
317 return '' !== $result ? $result : null;
318 }
319
320 /**
321 * Find the matching brace in the CSS.
322 *
323 * @param string $css
324 * @param int $open_pos
325 *
326 * @return int
327 */
328 private function find_matching_brace( $css, $open_pos ) {
329 $level = 1;
330 $length = strlen( $css );
331 $in_string = false;
332 $string_char = '';
333
334 for ( $i = $open_pos + 1; $i < $length; $i++ ) {
335 $char = $css[ $i ];
336
337 // Handle string literals to avoid matching braces inside strings
338 if ( ( '"' === $char || "'" === $char ) && ( 0 === $i || '\\' !== $css[ $i - 1 ] ) ) {
339 if ( ! $in_string ) {
340 $in_string = true;
341 $string_char = $char;
342 } elseif ( $char === $string_char ) {
343 $in_string = false;
344 }
345
346 continue;
347 }
348
349 // Skip braces inside strings
350 if ( $in_string ) {
351 continue;
352 }
353
354 if ( '{' === $char ) {
355 ++$level;
356 } elseif ( '}' === $char ) {
357 --$level;
358
359 if ( 0 === $level ) {
360 return $i;
361 }
362 }
363 }//end for
364
365 return $length - 1;
366 }
367 }//end class
368