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 |