PluginProbe ʕ •ᴥ•ʔ
Everest Forms – Contact Form, Payment Form, Quiz, Survey & Custom Form Builder with AI / 3.6.0
Everest Forms – Contact Form, Payment Form, Quiz, Survey & Custom Form Builder with AI v3.6.0
3.6.0 3.5.3 3.5.2 3.5.1 3.5.0 3.4.8 3.4.7 3.4.6 1.1.0 1.1.1 1.1.2 1.1.3 1.1.4 1.1.5 1.1.5.1 1.1.6 1.1.7 1.1.8 1.1.9 1.2.0 1.2.1 1.2.2 1.2.3 1.2.4 1.3.0 1.3.1 1.3.2 1.3.3 1.3.4 1.4.0 1.4.1 1.4.2 1.4.3 1.4.4 1.4.5 1.4.6 1.4.7 1.4.8 1.4.9 1.5.0 1.5.1 1.5.10 1.5.2 1.5.3 1.5.4 1.5.5 1.5.6 1.5.7 1.5.8 1.5.9 1.6.0 1.6.1 1.6.2 1.6.3 1.6.4 1.6.5 1.6.6 1.6.6.1 1.6.7 1.7.0 1.7.0.1 1.7.0.2 1.7.0.3 1.7.1 1.7.2 1.7.2.1 1.7.2.2 1.7.3 1.7.4 1.7.5 1.7.5.1 1.7.5.2 1.7.6 1.7.7 1.7.7.1 1.7.7.2 1.7.8 1.7.9 1.8.0 1.8.0.1 1.8.1 1.8.2 1.8.2.1 1.8.2.2 1.8.2.3 1.8.3 1.8.4 1.8.5 1.8.6 1.8.7 1.8.8 1.8.9 1.9.0 1.9.0.1 1.9.1 1.9.2 1.9.3 1.9.4 1.9.4.1 1.9.5 1.9.6 1.9.7 1.9.8 1.9.9 2.0.0 2.0.0.1 2.0.1 2.0.2 2.0.3 2.0.3.1 2.0.4 2.0.4.1 2.0.5 2.0.6 2.0.7 2.0.8 2.0.8.1 2.0.9 3.0.0 3.0.0.1 3.0.1 3.0.2 3.0.3 3.0.3.1 3.0.4 3.0.4.1 3.0.4.2 3.0.5 3.0.5.1 3.0.5.2 3.0.6 3.0.6.1 3.0.7.1 3.0.8 3.0.8.1 3.0.9 3.0.9.1 3.0.9.2 3.0.9.3 3.0.9.4 3.0.9.5 3.1.0 3.1.1 3.1.2 3.2.0 3.2.1 3.2.2 3.2.3 3.2.4 3.2.5 3.2.6 3.3.0 3.4.0 3.4.1 3.4.2 3.4.2.1 3.4.3 3.4.4 3.4.5 trunk 1.0 1.0.1 1.0.2 1.0.3
everest-forms / addons / StyleCustomizer / V2 / Compiler.php
everest-forms / addons / StyleCustomizer / V2 Last commit date
assets 1 week ago BuilderPanel.php 1 week ago Compiler.php 1 week ago Engine.php 1 week ago FrontendEnqueue.php 1 week ago Migrator.php 1 week ago Palettes.php 1 week ago PreviewDraft.php 1 week ago RestController.php 1 week ago Sanitizer.php 1 week ago Schema.php 1 week ago Templates.php 1 week ago
Compiler.php
348 lines
1 <?php
2 /**
3 * Style Customizer v2 — Compiler.
4 *
5 * Turns a (sanitized) v2 record into the per-form CSS custom-property block, scoped to the
6 * form wrapper. Responsive tokens (margin/padding) additionally emit tablet/mobile overrides
7 * inside media queries.
8 *
9 * @package EverestForms\Addons\StyleCustomizer\V2
10 * @since x.x.x
11 */
12
13 namespace EverestForms\Addons\StyleCustomizer\V2;
14
15 defined( 'ABSPATH' ) || exit;
16
17 /**
18 * Record → CSS-variable compiler.
19 */
20 final class Compiler {
21
22 /**
23 * Compile a form's record into scoped CSS custom properties.
24 *
25 * @param array $record Sanitized v2 record (`tokens` map).
26 * @param int|string $form_id Form id.
27 * @return string CSS (empty string if nothing to emit).
28 */
29 public static function compile( $record, $form_id ) {
30 $selector = self::wrapper_selector( $form_id );
31 $tokens = ( isset( $record['tokens'] ) && is_array( $record['tokens'] ) ) ? $record['tokens'] : array();
32 // The global "Apply Theme Style" toggle forces theme fonts too — mirrors v1's
33 // `show_theme_font` (the only thing that toggle ever did) — regardless of the per-form
34 // Font section's own setting.
35 $apply_theme_style = 'default' !== get_post_meta( $form_id, 'everest_forms_enable_theme_style', true );
36 $theme_font = $apply_theme_style || ! empty( $tokens['fonts.theme']['desktop'] );
37 $pro_active = Engine::pro_active();
38
39 // Base (desktop) declarations. Without Pro, a pro-tier token renders at its default —
40 // never the stored value — since the rule template has no CSS fallback for the var.
41 $base = array();
42 foreach ( Schema::tokens() as $token ) {
43 $locked = ! $pro_active && isset( $token['tier'] ) && 'pro' === $token['tier'];
44 $value = $locked ? $token['default'] : self::resolve( $tokens, $token, 'desktop' );
45 $base = array_merge( $base, self::declarations( $token, $value, $theme_font ) );
46 }
47
48 $css = '';
49 if ( $base ) {
50 $css .= $selector . '{' . self::join( $base ) . '}';
51 }
52
53 foreach ( self::breakpoints() as $device => $max_width ) {
54 $decls = array();
55 foreach ( Schema::tokens() as $token ) {
56 if ( empty( $token['responsive'] ) ) {
57 continue;
58 }
59 if ( ! $pro_active && isset( $token['tier'] ) && 'pro' === $token['tier'] ) {
60 continue;
61 }
62 if ( ! isset( $tokens[ $token['key'] ][ $device ] ) ) {
63 continue; // No override → inherits desktop.
64 }
65 $value = $tokens[ $token['key'] ][ $device ];
66 $decls = array_merge( $decls, self::declarations( $token, $value, $theme_font ) );
67 }
68 if ( $decls ) {
69 $css .= '@media (max-width:' . (int) $max_width . 'px){' . $selector . '{' . self::join( $decls ) . '}}';
70 }
71 }
72
73 return $css;
74 }
75
76 /**
77 * Scope a user's custom CSS to the form wrapper so it can never leak site-wide. `@media` /
78 * `@supports` / `@container` blocks are recursed into; other at-rules pass through untouched.
79 *
80 * @param string $css Sanitized custom CSS.
81 * @param int|string $form_id Form id.
82 * @return string Scoped CSS (empty if nothing to emit).
83 */
84 public static function scope_custom_css( $css, $form_id ) {
85 $css = trim( (string) $css );
86 if ( '' === $css ) {
87 return '';
88 }
89 return self::scope_block( $css, self::wrapper_selector( $form_id ) );
90 }
91
92 /**
93 * Recursively prefix every rule selector in a CSS block with a scope selector.
94 *
95 * @param string $css CSS block body.
96 * @param string $scope Scope selector (e.g. `#evf-12`).
97 * @return string
98 */
99 protected static function scope_block( $css, $scope ) {
100 $out = '';
101 $len = strlen( $css );
102 $i = 0;
103
104 while ( $i < $len ) {
105 $brace = strpos( $css, '{', $i );
106 if ( false === $brace ) {
107 break; // Trailing junk with no block — drop it.
108 }
109 $prelude = trim( substr( $css, $i, $brace - $i ) );
110
111 // Find the matching close brace (track nesting, skipping braces inside string
112 // literals/comments so e.g. `content:"}";` can't close the block early).
113 $depth = 1;
114 $j = $brace + 1;
115 $quote = '';
116 while ( $j < $len && $depth > 0 ) {
117 $ch = $css[ $j ];
118
119 if ( '' !== $quote ) {
120 if ( $ch === $quote && '\\' !== $css[ $j - 1 ] ) {
121 $quote = '';
122 }
123 ++$j;
124 continue;
125 }
126
127 if ( '/' === $ch && $j + 1 < $len && '*' === $css[ $j + 1 ] ) {
128 $close = strpos( $css, '*/', $j + 2 );
129 $j = ( false === $close ) ? $len : $close + 2;
130 continue;
131 }
132
133 if ( '"' === $ch || "'" === $ch ) {
134 $quote = $ch;
135 } elseif ( '{' === $ch ) {
136 ++$depth;
137 } elseif ( '}' === $ch ) {
138 --$depth;
139 }
140 ++$j;
141 }
142 $body = substr( $css, $brace + 1, $j - $brace - 2 );
143
144 if ( '' !== $prelude && '@' === $prelude[0] ) {
145 $lower = strtolower( $prelude );
146 if ( 0 === strpos( $lower, '@media' ) || 0 === strpos( $lower, '@supports' ) || 0 === strpos( $lower, '@container' ) ) {
147 $out .= $prelude . '{' . self::scope_block( $body, $scope ) . '}';
148 } else {
149 // @keyframes / @font-face / @page — not selector-based, keep as-is.
150 $out .= $prelude . '{' . $body . '}';
151 }
152 } elseif ( '' !== $prelude ) {
153 $selectors = array_filter( array_map( 'trim', explode( ',', $prelude ) ) );
154 $scoped = array();
155 foreach ( $selectors as $selector ) {
156 $scoped[] = $scope . ' ' . $selector;
157 }
158 $out .= implode( ',', $scoped ) . '{' . $body . '}';
159 }
160
161 $i = $j;
162 }
163
164 return $out;
165 }
166
167 /**
168 * The wrapper selector for a form. Matches the existing engine's `#evf-{id}` (inside
169 * `.everest-forms`); custom properties inherit from it to every field.
170 *
171 * @param int|string $form_id Form id.
172 * @return string
173 */
174 public static function wrapper_selector( $form_id ) {
175 $selector = '#evf-' . $form_id;
176 /**
177 * Filter the wrapper selector the v2 variables are scoped to.
178 *
179 * @param string $selector Selector.
180 * @param int|string $form_id Form id.
181 */
182 return apply_filters( 'evf_style_v2_wrapper_selector', $selector, $form_id );
183 }
184
185 /**
186 * Responsive breakpoints (device => max-width px). Shared with the preview widths so
187 * the panel and the live site always agree.
188 *
189 * @return array
190 */
191 public static function breakpoints() {
192 /**
193 * Filter the compiled responsive breakpoints.
194 *
195 * @param array $breakpoints device => max-width.
196 */
197 return apply_filters(
198 'evf_style_v2_breakpoints',
199 array(
200 'tablet' => 768,
201 'mobile' => 480,
202 )
203 );
204 }
205
206 /* --------------------------------------------------------------------- *
207 * Emission
208 * --------------------------------------------------------------------- */
209
210 /**
211 * The CSS declarations (`--var: value`) a token contributes for one value. A font-style
212 * token expands to four; a var-less token (bg preset, choice variation…) contributes none.
213 *
214 * @param array $token Token definition.
215 * @param mixed $value Resolved value.
216 * @param bool $theme_font Whether "use theme fonts" is on.
217 * @return array List of `--var:value` strings.
218 */
219 protected static function declarations( $token, $value, $theme_font ) {
220 if ( null === $value ) {
221 return array();
222 }
223
224 if ( 'fontstyle' === $token['type'] ) {
225 $v = is_array( $value ) ? $value : array();
226 // An explicit weight (new Font weight dropdown) wins; absent/empty (every old record)
227 // falls back to the original bold-derived value — no migration needed either way.
228 $weight = ! empty( $v['weight'] ) ? $v['weight'] : ( ! empty( $v['bold'] ) ? '700' : $token['neutral_weight'] );
229 return array(
230 $token['vars']['weight'] . ':' . $weight,
231 $token['vars']['style'] . ':' . ( ! empty( $v['italic'] ) ? 'italic' : 'normal' ),
232 $token['vars']['decoration'] . ':' . ( ! empty( $v['underline'] ) ? 'underline' : 'none' ),
233 $token['vars']['transform'] . ':' . ( ! empty( $v['uppercase'] ) ? 'uppercase' : 'none' ),
234 );
235 }
236
237 if ( empty( $token['var'] ) ) {
238 return array(); // Meta/data-attribute tokens (bg preset, choice variation…).
239 }
240
241 // "Use theme fonts" overrides the family with `inherit`.
242 if ( 'fonts.family' === $token['key'] && $theme_font ) {
243 return array( $token['var'] . ':inherit' );
244 }
245
246 $formatted = self::format( $token, $value );
247 if ( '' === $formatted ) {
248 return array();
249 }
250 return array( $token['var'] . ':' . $formatted );
251 }
252
253 /**
254 * Format a single value into its CSS representation.
255 *
256 * @param array $token Token definition.
257 * @param mixed $value Value.
258 * @return string
259 */
260 protected static function format( $token, $value ) {
261 switch ( $token['type'] ) {
262 case 'slider':
263 if ( ! is_numeric( $value ) ) {
264 return '';
265 }
266 $unit = isset( $token['unit'] ) ? $token['unit'] : 'px';
267 return $value . $unit;
268
269 case 'box4':
270 return self::format_box4( $token, $value );
271
272 case 'media':
273 return $value ? 'url("' . esc_url_raw( $value ) . '")' : 'none';
274
275 case 'color':
276 case 'select':
277 default:
278 return ( '' === $value || null === $value ) ? '' : self::css_safe( (string) $value );
279 }
280 }
281
282 /**
283 * Defense-in-depth: strip anything that could break out of a CSS declaration/rule.
284 *
285 * @param string $value Value.
286 * @return string
287 */
288 protected static function css_safe( $value ) {
289 return preg_replace( '#[<>{};\\\\]|/\*|\*/#', '', $value );
290 }
291
292 /**
293 * Format a 4-side box ({top,right,bottom,left}[,unit]) into a CSS shorthand `t r b l`.
294 *
295 * @param array $token Token definition.
296 * @param mixed $value Associative box.
297 * @return string
298 */
299 protected static function format_box4( $token, $value ) {
300 if ( ! is_array( $value ) ) {
301 return '';
302 }
303 $unit = 'px';
304 if ( ! empty( $token['units'] ) ) {
305 $unit = ( isset( $value['unit'] ) && in_array( $value['unit'], $token['units'], true ) ) ? $value['unit'] : $token['units'][0];
306 }
307 $out = array();
308 foreach ( array( 'top', 'right', 'bottom', 'left' ) as $side ) {
309 if ( ! isset( $value[ $side ] ) ) {
310 return '';
311 }
312 $out[] = ( (int) $value[ $side ] ) . $unit;
313 }
314 return implode( ' ', $out );
315 }
316
317 /**
318 * Resolve a token's value for a device: the device value, else the desktop value, else
319 * the schema default (desktop only).
320 *
321 * @param array $tokens Record tokens map.
322 * @param array $token Token definition.
323 * @param string $device Device.
324 * @return mixed
325 */
326 protected static function resolve( $tokens, $token, $device ) {
327 $key = $token['key'];
328 // An empty string means "no value" — treat it like the key being absent.
329 if ( isset( $tokens[ $key ][ $device ] ) && '' !== $tokens[ $key ][ $device ] ) {
330 return $tokens[ $key ][ $device ];
331 }
332 if ( 'desktop' === $device ) {
333 return ( isset( $tokens[ $key ]['desktop'] ) && '' !== $tokens[ $key ]['desktop'] ) ? $tokens[ $key ]['desktop'] : $token['default'];
334 }
335 return null;
336 }
337
338 /**
339 * Join declarations into a rule body.
340 *
341 * @param array $decls Declarations.
342 * @return string
343 */
344 protected static function join( $decls ) {
345 return implode( ';', $decls ) . ';';
346 }
347 }
348