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
Migrator.php
601 lines
| 1 | <?php |
| 2 | /** |
| 3 | * Style Customizer v2 — Legacy → v2 migration. |
| 4 | * |
| 5 | * Converts a legacy `everest_forms_styles[form_id]` record into a v2 record ({@see Schema} |
| 6 | * token map), keeping legacy value shapes intact and only renaming group/prop to token keys. |
| 7 | * |
| 8 | * @package EverestForms\Addons\StyleCustomizer\V2 |
| 9 | * @since x.x.x |
| 10 | */ |
| 11 | |
| 12 | namespace EverestForms\Addons\StyleCustomizer\V2; |
| 13 | |
| 14 | defined( 'ABSPATH' ) || exit; |
| 15 | |
| 16 | /** |
| 17 | * Legacy → v2 record migrator. |
| 18 | */ |
| 19 | final class Migrator { |
| 20 | |
| 21 | /** |
| 22 | * Convert one legacy record into a v2 record. |
| 23 | * |
| 24 | * @param array $legacy Legacy `everest_forms_styles[form_id]` record. |
| 25 | * @return array v2 record ( schema_version + tokens + palette ). |
| 26 | */ |
| 27 | public static function migrate_record( $legacy ) { |
| 28 | $legacy = is_array( $legacy ) ? $legacy : array(); |
| 29 | |
| 30 | // Already a v2 record — return unchanged (keeps this method idempotent). |
| 31 | if ( isset( $legacy['tokens'] ) ) { |
| 32 | return $legacy; |
| 33 | } |
| 34 | |
| 35 | $legacy = self::normalize_legacy_shape( $legacy ); |
| 36 | |
| 37 | // A prop the user never explicitly saved still renders correctly on the v1 frontend — |
| 38 | // WP_Customize_Setting resolves it to the active template's own control default. Replicate |
| 39 | // that fallback here (template values as the base, explicit legacy values still win) or a |
| 40 | // template's baked-in background/colours are invisible to the migrator even though they're |
| 41 | // live on the frontend. |
| 42 | if ( ! empty( $legacy['template'] ) ) { |
| 43 | $template_defaults = Templates::legacy_template_data( $legacy['template'] ); |
| 44 | if ( ! empty( $template_defaults ) ) { |
| 45 | $legacy = array_replace_recursive( $template_defaults, $legacy ); |
| 46 | } |
| 47 | } |
| 48 | |
| 49 | $tokens = array(); |
| 50 | |
| 51 | foreach ( self::map() as $rule ) { |
| 52 | $group = $rule['group']; |
| 53 | $prop = $rule['prop']; |
| 54 | if ( ! isset( $legacy[ $group ][ $prop ] ) ) { |
| 55 | continue; |
| 56 | } |
| 57 | $value = $legacy[ $group ][ $prop ]; |
| 58 | // An empty string means "never customized" — skip so the token stays unset. |
| 59 | if ( '' === $value ) { |
| 60 | continue; |
| 61 | } |
| 62 | $tokens[ $rule['token'] ] = self::apply_transform( $rule['transform'], $value ); |
| 63 | } |
| 64 | |
| 65 | // Palette-derived tokens go first so an explicit legacy value can override them. |
| 66 | $tokens = array_merge( self::migrate_palette( $legacy ), $tokens ); |
| 67 | |
| 68 | // NOTE: `typography.button_background_color`, `button_font_color`, |
| 69 | // `field_styles_background_color` and `field_labels_font_color` are NOT real v1 settings — |
| 70 | // no WP_Customize control is ever registered for them (confirmed live: applying a bundled |
| 71 | // template sets `form_container.background_image`/`background_preset` and |
| 72 | // `typography.button_hover_background_color` as real settings, but |
| 73 | // `wp.customize('everest_forms_styles[id][typography][button_background_color]')` etc. |
| 74 | // return no setting object at all). The bundled templates JSON carries stray values under |
| 75 | // these keys, but v1 can never actually apply them — a template like "layout-five" with no |
| 76 | // palette renders its button as WordPress's plain default (`#0073aa`/`#ffffff`), not the |
| 77 | // template's own colours, and label text stays `#333333` regardless of the background image. |
| 78 | // A previous fix (EVF-2668) migrated these dead keys into real v2 tokens, which was itself |
| 79 | // the bug: it showed the v2 panel a colour v1's frontend never rendered. Removed — the |
| 80 | // schema defaults above already equal v1's true no-palette output exactly (verified live). |
| 81 | |
| 82 | // v1 never coupled these to the palette; re-assert each at its schema default unless customized. |
| 83 | foreach ( array( |
| 84 | 'input.focusBorder' => 'field_styles_border_focus_color', |
| 85 | 'choice.checked' => 'checkbox_radio_checked_color', |
| 86 | 'file.icon' => 'file_upload_icon_color', |
| 87 | 'title.color' => 'section_title_font_color', |
| 88 | 'desc.color' => 'field_description_font_color', |
| 89 | ) as $token => $legacy_key ) { |
| 90 | if ( empty( $legacy['typography'][ $legacy_key ] ) ) { |
| 91 | $default = Schema::get( $token ); |
| 92 | if ( $default ) { |
| 93 | $tokens[ $token ] = self::apply_transform( 'pass', $default['default'] ); |
| 94 | } |
| 95 | } |
| 96 | } |
| 97 | |
| 98 | // Combine both position axes into whichever one isn't 'center'. |
| 99 | $bg_position = self::migrate_background_position( isset( $legacy['form_container'] ) ? $legacy['form_container'] : array() ); |
| 100 | if ( null !== $bg_position ) { |
| 101 | $tokens['wrap.bgPosition'] = self::apply_transform( 'pass', $bg_position ); |
| 102 | } |
| 103 | |
| 104 | $record = array( |
| 105 | 'schema_version' => Schema::version(), |
| 106 | 'tokens' => $tokens, |
| 107 | ); |
| 108 | |
| 109 | // Carry the v1 selected template across so the Templates pane shows it as applied. |
| 110 | if ( ! empty( $legacy['template'] ) ) { |
| 111 | $record['template'] = Templates::resolve_legacy_slug( $legacy['template'] ); |
| 112 | } |
| 113 | |
| 114 | /** |
| 115 | * Filter a freshly-migrated v2 record (e.g. to add standalone-engine keys). |
| 116 | * |
| 117 | * @param array $record V2 record. |
| 118 | * @param array $legacy Source legacy record. |
| 119 | */ |
| 120 | return apply_filters( 'evf_style_v2_migrated_record', $record, $legacy ); |
| 121 | } |
| 122 | |
| 123 | /** |
| 124 | * The record a form should be treated as having RIGHT NOW, whether or not it's already been |
| 125 | * explicitly opened-and-saved under v2 — migrates a still-legacy stored record on the fly |
| 126 | * (never persisting it) so every reader (the panel, the frontend enqueue) sees the same |
| 127 | * thing without one of them waiting on a save the user may never make. |
| 128 | * |
| 129 | * Note: Custom CSS is NOT part of this record — it's WP core's own site-wide Additional CSS |
| 130 | * (`wp_get_custom_css()`/`wp_update_custom_css_post()`), matching v1's actual behavior |
| 131 | * exactly (verified directly against v1, EVF-2732/EVF-2737 follow-up): one shared value |
| 132 | * every form uses, not a per-form field to migrate. {@see RestController::build_payload()} |
| 133 | * and {@see RestController::save_item()} read/write it directly. |
| 134 | * |
| 135 | * @param array $stored Raw `everest_forms_styles[form_id]` value. |
| 136 | * @return array v2-shaped record ( schema_version + tokens, + template/palette ). |
| 137 | */ |
| 138 | public static function effective_record( $stored ) { |
| 139 | $stored = is_array( $stored ) ? $stored : array(); |
| 140 | |
| 141 | if ( Engine::is_v2_record( $stored ) ) { |
| 142 | return $stored; |
| 143 | } |
| 144 | |
| 145 | if ( empty( $stored ) ) { |
| 146 | return array( |
| 147 | 'schema_version' => Schema::version(), |
| 148 | 'tokens' => array(), |
| 149 | ); |
| 150 | } |
| 151 | |
| 152 | return self::migrate_record( $stored ); |
| 153 | } |
| 154 | |
| 155 | /* --------------------------------------------------------------------- * |
| 156 | * Shape normalization (v0 / v1 / v2) |
| 157 | * --------------------------------------------------------------------- */ |
| 158 | |
| 159 | /** |
| 160 | * Normalize a legacy record to the canonical v1 shape that {@see self::map()} reads. |
| 161 | * |
| 162 | * @param array $legacy Source record (v0 or v1 shape). |
| 163 | * @return array Canonical v1 record. |
| 164 | */ |
| 165 | protected static function normalize_legacy_shape( $legacy ) { |
| 166 | if ( self::is_v0_shape( $legacy ) ) { |
| 167 | return self::v0_to_v1( $legacy ); |
| 168 | } |
| 169 | return $legacy; // Canonical v1. |
| 170 | } |
| 171 | |
| 172 | /** |
| 173 | * Detect the v0 (old standalone-plugin) shape by keys that only exist there. |
| 174 | * |
| 175 | * @param array $legacy Record. |
| 176 | * @return bool |
| 177 | */ |
| 178 | protected static function is_v0_shape( $legacy ) { |
| 179 | foreach ( array( 'wrapper', 'field_label', 'field_sublabel', 'checkbox_radio_styles', 'file_upload', 'section_title' ) as $marker ) { |
| 180 | if ( isset( $legacy[ $marker ] ) ) { |
| 181 | return true; |
| 182 | } |
| 183 | } |
| 184 | return false; |
| 185 | } |
| 186 | |
| 187 | /** |
| 188 | * Convert one v0 (standalone-plugin) record to the canonical v1 shape. |
| 189 | * |
| 190 | * @param array $s v0 record (`$settings` in evfsc_migration). |
| 191 | * @return array Canonical v1 record. |
| 192 | */ |
| 193 | protected static function v0_to_v1( $s ) { |
| 194 | $new = array(); |
| 195 | |
| 196 | if ( isset( $s['template'] ) ) { |
| 197 | $new['template'] = $s['template']; |
| 198 | } |
| 199 | |
| 200 | // Font. |
| 201 | if ( isset( $s['wrapper']['font_family'] ) ) { |
| 202 | $new['font']['font_family'] = $s['wrapper']['font_family']; |
| 203 | } |
| 204 | |
| 205 | // Form container. |
| 206 | $wrapper_keys = array( 'width', 'border_type', 'border_width', 'border_radius', 'border_color', 'background_image', 'background_preset', 'opacity', 'background_position', 'background_size', 'margin', 'padding' ); |
| 207 | foreach ( $wrapper_keys as $k ) { |
| 208 | if ( isset( $s['wrapper'][ $k ] ) ) { |
| 209 | $new['form_container'][ $k ] = $s['wrapper'][ $k ]; |
| 210 | } |
| 211 | } |
| 212 | |
| 213 | // Group-level borders (field / file-upload / button). |
| 214 | foreach ( array( 'field_styles' => 'field_styles', 'file_upload' => 'file_upload_styles', 'button' => 'button' ) as $src => $dst ) { |
| 215 | foreach ( array( 'width', 'border_type', 'border_width', 'border_radius' ) as $k ) { |
| 216 | if ( isset( $s[ $src ][ $k ] ) ) { |
| 217 | $new[ $dst ][ $k ] = $s[ $src ][ $k ]; |
| 218 | } |
| 219 | } |
| 220 | } |
| 221 | |
| 222 | // Typography — (v0 group, [ v0 prop => v1 typography prop ]). |
| 223 | $typography = array( |
| 224 | 'field_label' => array( 'font_size' => 'field_labels_font_size', 'font_style' => 'field_labels_font_style', 'text_alignment' => 'field_labels_text_alignment', 'line_height' => 'field_labels_line_height', 'margin' => 'field_labels_margin', 'padding' => 'field_labels_padding' ), |
| 225 | 'field_sublabel' => array( 'font_size' => 'field_sublabels_font_size', 'font_style' => 'field_sublabels_font_style', 'text_alignment' => 'field_sublabels_text_alignment', 'line_height' => 'field_sublabels_line_height', 'margin' => 'field_sublabels_margin', 'padding' => 'field_sublabels_padding' ), |
| 226 | 'field_styles' => array( 'font_size' => 'field_styles_font_size', 'font_color' => 'field_styles_font_color', 'placeholder_font_color' => 'field_styles_placeholder_font_color', 'font_style' => 'field_styles_font_style', 'alignment' => 'field_styles_alignment', 'border_color' => 'field_styles_border_color', 'border_focus_color' => 'field_styles_border_focus_color', 'margin' => 'field_styles_margin', 'padding' => 'field_styles_padding' ), |
| 227 | 'field_description' => array( 'font_size' => 'field_description_font_size', 'font_color' => 'field_description_font_color', 'font_style' => 'field_description_font_style', 'text_alignment' => 'field_description_text_alignment', 'line_height' => 'field_description_line_height', 'margin' => 'field_description_margin', 'padding' => 'field_description_padding' ), |
| 228 | 'section_title' => array( 'font_size' => 'section_title_font_size', 'font_color' => 'section_title_font_color', 'font_style' => 'section_title_font_style', 'text_alignment' => 'section_title_text_alignment', 'line_height' => 'section_title_line_height', 'margin' => 'section_title_margin', 'padding' => 'section_title_padding' ), |
| 229 | 'file_upload_styles' => array( 'font_size' => 'file_upload_font_size', 'font_color' => 'file_upload_font_color', 'background_color' => 'file_upload_background_color', 'icon_background_color' => 'file_upload_icon_background_color', 'icon_color' => 'file_upload_icon_color', 'border_color' => 'file_upload_border_color', 'margin' => 'file_upload_margin', 'padding' => 'file_upload_padding' ), |
| 230 | 'checkbox_radio_styles' => array( 'font_size' => 'checkbox_radio_font_size', 'font_color' => 'checkbox_radio_font_color', 'font_style' => 'checkbox_radio_font_style', 'alignment' => 'checkbox_radio_alignment', 'style_variation' => 'checkbox_radio_style_variation', 'size' => 'checkbox_radio_size', 'color' => 'checkbox_radio_color', 'checked_color' => 'checkbox_radio_checked_color', 'margin' => 'checkbox_radio_margin' ), |
| 231 | 'button' => array( 'font_size' => 'button_font_size', 'font_style' => 'button_font_style', 'hover_font_color' => 'button_hover_font_color', 'hover_background_color' => 'button_hover_background_color', 'border_color' => 'button_border_color', 'alignment' => 'button_alignment', 'border_hover_color' => 'button_border_hover_color', 'line_height' => 'button_line_height', 'margin' => 'button_margin', 'padding' => 'button_padding' ), |
| 232 | ); |
| 233 | foreach ( $typography as $group => $props ) { |
| 234 | foreach ( $props as $src => $dst ) { |
| 235 | if ( isset( $s[ $group ][ $src ] ) ) { |
| 236 | $new['typography'][ $dst ] = $s[ $group ][ $src ]; |
| 237 | } |
| 238 | } |
| 239 | } |
| 240 | |
| 241 | // Submission messages — v0 key structure is IDENTICAL to v1, so copy straight across. |
| 242 | foreach ( array( 'success_message', 'validation_message', 'error_message' ) as $group ) { |
| 243 | foreach ( array( 'show_submission_message', 'font_size', 'text_alignment', 'font_color', 'background_color', 'border_type', 'border_width', 'border_color', 'border_radius' ) as $k ) { |
| 244 | if ( isset( $s[ $group ][ $k ] ) ) { |
| 245 | $new[ $group ][ $k ] = $s[ $group ][ $k ]; |
| 246 | } |
| 247 | } |
| 248 | } |
| 249 | |
| 250 | // Palette colours — v0 stored each flat; v1 packs the six into `color_palette.color_12`. |
| 251 | $color_mappings = array( |
| 252 | 'wrapper' => array( 'background_color' => 'form_background' ), |
| 253 | 'field_styles' => array( 'background_color' => 'field_background' ), |
| 254 | 'field_label' => array( 'font_color' => 'field_label' ), |
| 255 | 'field_sublabel' => array( 'font_color' => 'field_sublabel' ), |
| 256 | 'button' => array( 'font_color' => 'button_text', 'background_color' => 'button_background' ), |
| 257 | ); |
| 258 | foreach ( $color_mappings as $group => $fields ) { |
| 259 | foreach ( $fields as $src => $slot ) { |
| 260 | if ( isset( $s[ $group ][ $src ] ) ) { |
| 261 | $new['color_palette']['color_12'][ $slot ] = $s[ $group ][ $src ]; |
| 262 | } |
| 263 | } |
| 264 | } |
| 265 | |
| 266 | return $new; |
| 267 | } |
| 268 | |
| 269 | /* --------------------------------------------------------------------- * |
| 270 | * Mapping table |
| 271 | * --------------------------------------------------------------------- */ |
| 272 | |
| 273 | /** |
| 274 | * The full legacy(group,prop) => v2 token map with transform per row. |
| 275 | * |
| 276 | * Transform keys: `pass` (scalar), `fontstyle`, `dim_resp` (responsive margin/padding), |
| 277 | * `dim` (single-value border width/radius), `opacity` (0–1 → 0–100). |
| 278 | * |
| 279 | * @return array |
| 280 | */ |
| 281 | public static function map() { |
| 282 | $rows = array(); |
| 283 | |
| 284 | // --- Font --- |
| 285 | $rows[] = self::row( 'font', 'show_theme_font', 'fonts.theme', 'pass' ); |
| 286 | $rows[] = self::row( 'font', 'font_family', 'fonts.family', 'pass' ); |
| 287 | |
| 288 | // --- Form container --- |
| 289 | $rows[] = self::row( 'form_container', 'width', 'wrap.width', 'pass' ); |
| 290 | $rows[] = self::row( 'form_container', 'border_type', 'wrap.borderStyle', 'pass' ); |
| 291 | $rows[] = self::row( 'form_container', 'border_width', 'wrap.bw', 'dim' ); |
| 292 | $rows[] = self::row( 'form_container', 'border_color', 'wrap.borderC', 'pass' ); |
| 293 | $rows[] = self::row( 'form_container', 'border_radius', 'wrap.radius', 'dim' ); |
| 294 | $rows[] = self::row( 'form_container', 'background_image', 'wrap.bgImage', 'pass' ); |
| 295 | $rows[] = self::row( 'form_container', 'background_preset', 'wrap.bgPreset', 'pass' ); |
| 296 | $rows[] = self::row( 'form_container', 'background_size', 'wrap.bgSize', 'pass' ); |
| 297 | $rows[] = self::row( 'form_container', 'background_repeat', 'wrap.bgRepeat', 'pass' ); |
| 298 | $rows[] = self::row( 'form_container', 'background_attachment', 'wrap.bgAttachment', 'pass' ); |
| 299 | // background_position_x/y are combined separately — see migrate_background_position(). |
| 300 | $rows[] = self::row( 'form_container', 'opacity', 'wrap.bgOpacity', 'pass' ); // v2 keeps the 0–1 scale (identity). |
| 301 | $rows[] = self::row( 'form_container', 'margin', 'wrap.margin', 'dim_resp' ); |
| 302 | $rows[] = self::row( 'form_container', 'padding', 'wrap.pad', 'dim_resp' ); |
| 303 | |
| 304 | // --- Typography: text roles (group `typography`, prefixed props) --- |
| 305 | $roles = array( |
| 306 | 'field_labels' => 'label', |
| 307 | 'field_sublabels' => 'sub', |
| 308 | 'field_description' => 'desc', |
| 309 | 'section_title' => 'title', |
| 310 | ); |
| 311 | foreach ( $roles as $legacy_prefix => $vp ) { |
| 312 | $rows[] = self::row( 'typography', "{$legacy_prefix}_font_size", "{$vp}.size", 'pass' ); |
| 313 | // label/sub colour is palette-driven in v1, not typography-driven — skip; desc/title |
| 314 | // colour is typography-driven so keep the mapping. |
| 315 | if ( 'label' !== $vp && 'sub' !== $vp ) { |
| 316 | $rows[] = self::row( 'typography', "{$legacy_prefix}_font_color", "{$vp}.color", 'pass' ); |
| 317 | } |
| 318 | $rows[] = self::row( 'typography', "{$legacy_prefix}_font_style", "{$vp}.fstyle", 'fontstyle' ); |
| 319 | $rows[] = self::row( 'typography', "{$legacy_prefix}_text_alignment", "{$vp}.align", 'pass' ); |
| 320 | $rows[] = self::row( 'typography', "{$legacy_prefix}_line_height", "{$vp}.line", 'pass' ); |
| 321 | $rows[] = self::row( 'typography', "{$legacy_prefix}_margin", "{$vp}.margin", 'dim_resp' ); |
| 322 | $rows[] = self::row( 'typography', "{$legacy_prefix}_padding", "{$vp}.pad", 'dim_resp' ); |
| 323 | } |
| 324 | |
| 325 | // --- Field styles (typography props + `field_styles` group borders) --- |
| 326 | $rows[] = self::row( 'typography', 'field_styles_font_size', 'input.size', 'pass' ); |
| 327 | $rows[] = self::row( 'typography', 'field_styles_font_color', 'input.color', 'pass' ); |
| 328 | $rows[] = self::row( 'typography', 'field_styles_placeholder_font_color', 'input.ph', 'pass' ); |
| 329 | $rows[] = self::row( 'typography', 'field_styles_font_style', 'input.fstyle', 'fontstyle' ); |
| 330 | $rows[] = self::row( 'typography', 'field_styles_alignment', 'input.align', 'pass' ); |
| 331 | $rows[] = self::row( 'typography', 'field_styles_border_color', 'input.borderC', 'pass' ); |
| 332 | $rows[] = self::row( 'typography', 'field_styles_border_focus_color', 'input.focusBorder', 'pass' ); |
| 333 | $rows[] = self::row( 'typography', 'field_styles_margin', 'field.margin', 'dim_resp' ); |
| 334 | $rows[] = self::row( 'typography', 'field_styles_padding', 'input.pad', 'dim_resp' ); |
| 335 | $rows[] = self::row( 'field_styles', 'border_type', 'input.borderStyle', 'pass' ); |
| 336 | $rows[] = self::row( 'field_styles', 'border_width', 'input.bw', 'dim' ); |
| 337 | $rows[] = self::row( 'field_styles', 'border_radius', 'input.radius', 'dim' ); |
| 338 | |
| 339 | // --- File upload (typography props + `file_upload_styles` group borders) --- |
| 340 | $rows[] = self::row( 'typography', 'file_upload_font_size', 'file.size', 'pass' ); |
| 341 | $rows[] = self::row( 'typography', 'file_upload_font_color', 'file.color', 'pass' ); |
| 342 | $rows[] = self::row( 'typography', 'file_upload_background_color', 'file.bg', 'pass' ); |
| 343 | $rows[] = self::row( 'typography', 'file_upload_icon_background_color', 'file.iconBg', 'pass' ); |
| 344 | $rows[] = self::row( 'typography', 'file_upload_icon_color', 'file.icon', 'pass' ); |
| 345 | $rows[] = self::row( 'typography', 'file_upload_border_color', 'file.border', 'pass' ); |
| 346 | $rows[] = self::row( 'typography', 'file_upload_margin', 'file.margin', 'dim_resp' ); |
| 347 | $rows[] = self::row( 'typography', 'file_upload_padding', 'file.pad', 'dim_resp' ); |
| 348 | $rows[] = self::row( 'file_upload_styles', 'border_type', 'file.borderStyle', 'pass' ); |
| 349 | $rows[] = self::row( 'file_upload_styles', 'border_width', 'file.bw', 'dim' ); |
| 350 | $rows[] = self::row( 'file_upload_styles', 'border_radius', 'file.radius', 'dim' ); |
| 351 | |
| 352 | // --- Radio / checkbox --- |
| 353 | $rows[] = self::row( 'typography', 'checkbox_radio_font_size', 'choice.fsize', 'pass' ); |
| 354 | $rows[] = self::row( 'typography', 'checkbox_radio_font_color', 'choice.color', 'pass' ); |
| 355 | $rows[] = self::row( 'typography', 'checkbox_radio_font_style', 'choice.fstyle', 'fontstyle' ); |
| 356 | $rows[] = self::row( 'typography', 'checkbox_radio_alignment', 'choice.align', 'pass' ); |
| 357 | $rows[] = self::row( 'typography', 'checkbox_radio_style_variation', 'choice.variation', 'pass' ); |
| 358 | $rows[] = self::row( 'typography', 'checkbox_radio_size', 'choice.size', 'pass' ); |
| 359 | $rows[] = self::row( 'typography', 'checkbox_radio_color', 'choice.border', 'pass' ); |
| 360 | $rows[] = self::row( 'typography', 'checkbox_radio_checked_color', 'choice.checked', 'pass' ); |
| 361 | $rows[] = self::row( 'typography', 'checkbox_radio_margin', 'choice.margin', 'dim_resp' ); |
| 362 | |
| 363 | // --- Button (typography props + `button` group borders) --- |
| 364 | $rows[] = self::row( 'typography', 'button_font_size', 'btn.size', 'pass' ); |
| 365 | $rows[] = self::row( 'typography', 'button_font_style', 'btn.fstyle', 'fontstyle' ); |
| 366 | $rows[] = self::row( 'typography', 'button_alignment', 'btn.align', 'pass' ); |
| 367 | $rows[] = self::row( 'typography', 'button_line_height', 'btn.line', 'pass' ); |
| 368 | $rows[] = self::row( 'typography', 'button_border_color', 'btn.borderC', 'pass' ); |
| 369 | $rows[] = self::row( 'typography', 'button_border_hover_color', 'btn.borderCHover', 'pass' ); |
| 370 | $rows[] = self::row( 'typography', 'button_hover_font_color', 'btn.colorHover', 'pass' ); |
| 371 | $rows[] = self::row( 'typography', 'button_hover_background_color', 'btn.bgHover', 'pass' ); |
| 372 | $rows[] = self::row( 'typography', 'button_margin', 'btn.margin', 'dim_resp' ); |
| 373 | $rows[] = self::row( 'typography', 'button_padding', 'btn.pad', 'dim_resp' ); |
| 374 | $rows[] = self::row( 'button', 'border_type', 'btn.borderStyle', 'pass' ); |
| 375 | $rows[] = self::row( 'button', 'border_width', 'btn.bw', 'dim' ); |
| 376 | $rows[] = self::row( 'button', 'border_radius', 'btn.radius', 'dim' ); |
| 377 | |
| 378 | // --- Submission messages (success / error / validation) --- |
| 379 | $msg_props = array( |
| 380 | 'font_size' => 'size', |
| 381 | 'font_style' => 'fstyle', |
| 382 | 'text_alignment' => 'align', |
| 383 | 'font_color' => 'color', |
| 384 | 'background_color' => 'bg', |
| 385 | 'border_type' => 'borderStyle', |
| 386 | 'border_width' => 'bw', |
| 387 | 'border_color' => 'borderC', |
| 388 | 'border_radius' => 'radius', |
| 389 | ); |
| 390 | foreach ( array( 'success', 'error', 'validation' ) as $type ) { |
| 391 | foreach ( $msg_props as $legacy_prop => $v2_prop ) { |
| 392 | $transform = 'font_style' === $legacy_prop ? 'fontstyle' : ( in_array( $legacy_prop, array( 'border_width', 'border_radius' ), true ) ? 'dim' : 'pass' ); |
| 393 | $rows[] = self::row( "{$type}_message", $legacy_prop, "msg.{$type}.{$v2_prop}", $transform ); |
| 394 | } |
| 395 | } |
| 396 | |
| 397 | /** |
| 398 | * Filter the legacy→v2 mapping rows (e.g. to append standalone-engine group aliases). |
| 399 | * |
| 400 | * @param array $rows Mapping rows. |
| 401 | */ |
| 402 | return apply_filters( 'evf_style_v2_migration_map', $rows ); |
| 403 | } |
| 404 | |
| 405 | /* --------------------------------------------------------------------- * |
| 406 | * Transforms (pure) |
| 407 | * --------------------------------------------------------------------- */ |
| 408 | |
| 409 | /** |
| 410 | * Apply a named transform, returning a per-device value bag. |
| 411 | * |
| 412 | * @param string $transform Transform key. |
| 413 | * @param mixed $value Legacy value. |
| 414 | * @return array Device bag ( at least `desktop` ). |
| 415 | */ |
| 416 | public static function apply_transform( $transform, $value ) { |
| 417 | switch ( $transform ) { |
| 418 | case 'dim_resp': |
| 419 | return self::responsive_dimension( $value ); |
| 420 | case 'dim': |
| 421 | return array( 'desktop' => self::normalize_dimension( $value ) ); |
| 422 | case 'fontstyle': |
| 423 | return array( 'desktop' => self::fontstyle( $value ) ); |
| 424 | case 'pass': |
| 425 | default: |
| 426 | return array( 'desktop' => $value ); |
| 427 | } |
| 428 | } |
| 429 | |
| 430 | /** |
| 431 | * Validate a legacy dimension, keeping its associative shape {top,right,bottom,left}. |
| 432 | * |
| 433 | * @param mixed $dim Legacy dimension. |
| 434 | * @return array |
| 435 | */ |
| 436 | public static function normalize_dimension( $dim ) { |
| 437 | if ( ! is_array( $dim ) ) { |
| 438 | return array( 'top' => 0, 'right' => 0, 'bottom' => 0, 'left' => 0 ); |
| 439 | } |
| 440 | $out = array( |
| 441 | 'top' => isset( $dim['top'] ) ? (int) $dim['top'] : 0, |
| 442 | 'right' => isset( $dim['right'] ) ? (int) $dim['right'] : 0, |
| 443 | 'bottom' => isset( $dim['bottom'] ) ? (int) $dim['bottom'] : 0, |
| 444 | 'left' => isset( $dim['left'] ) ? (int) $dim['left'] : 0, |
| 445 | ); |
| 446 | if ( isset( $dim['unit'] ) && '' !== $dim['unit'] ) { |
| 447 | $out['unit'] = $dim['unit']; |
| 448 | } |
| 449 | return $out; |
| 450 | } |
| 451 | |
| 452 | /** |
| 453 | * Responsive dimension {desktop:{…}, tablet?:{…}, mobile?:{…}} → per-device bag. |
| 454 | * |
| 455 | * @param mixed $dim Legacy responsive dimension. |
| 456 | * @return array |
| 457 | */ |
| 458 | public static function responsive_dimension( $dim ) { |
| 459 | if ( ! is_array( $dim ) ) { |
| 460 | return array( 'desktop' => self::normalize_dimension( null ) ); |
| 461 | } |
| 462 | // A legacy value may be wrapped ({desktop:{…}}) or bare ({top,right,…}). |
| 463 | if ( ! isset( $dim['desktop'] ) && ( isset( $dim['top'] ) || isset( $dim['left'] ) ) ) { |
| 464 | return array( 'desktop' => self::normalize_dimension( $dim ) ); |
| 465 | } |
| 466 | $out = array(); |
| 467 | foreach ( array( 'desktop', 'tablet', 'mobile' ) as $device ) { |
| 468 | if ( isset( $dim[ $device ] ) ) { |
| 469 | $out[ $device ] = self::normalize_dimension( $dim[ $device ] ); |
| 470 | } |
| 471 | } |
| 472 | if ( empty( $out ) ) { |
| 473 | $out['desktop'] = self::normalize_dimension( null ); |
| 474 | } |
| 475 | return $out; |
| 476 | } |
| 477 | |
| 478 | /** |
| 479 | * Font-style flags — identical key set on both sides, just coerced to booleans. |
| 480 | * |
| 481 | * @param mixed $value Legacy value. |
| 482 | * @return array |
| 483 | */ |
| 484 | public static function fontstyle( $value ) { |
| 485 | $value = is_array( $value ) ? $value : array(); |
| 486 | return array( |
| 487 | 'bold' => ! empty( $value['bold'] ), |
| 488 | 'italic' => ! empty( $value['italic'] ), |
| 489 | 'underline' => ! empty( $value['underline'] ), |
| 490 | 'uppercase' => ! empty( $value['uppercase'] ), |
| 491 | ); |
| 492 | } |
| 493 | |
| 494 | /* --------------------------------------------------------------------- * |
| 495 | * Background position |
| 496 | * --------------------------------------------------------------------- */ |
| 497 | |
| 498 | /** |
| 499 | * Combine v1's two-axis background_position_x/y into v2's single 5-value wrap.bgPosition. |
| 500 | * Prefers whichever axis isn't 'center'; the vertical axis wins if both are non-center. |
| 501 | * |
| 502 | * @param array $container Legacy `form_container` group. |
| 503 | * @return string|null The migrated position, or null if neither axis was set. |
| 504 | */ |
| 505 | protected static function migrate_background_position( $container ) { |
| 506 | $x = isset( $container['background_position_x'] ) ? (string) $container['background_position_x'] : ''; |
| 507 | $y = isset( $container['background_position_y'] ) ? (string) $container['background_position_y'] : ''; |
| 508 | if ( '' === $x && '' === $y ) { |
| 509 | return null; |
| 510 | } |
| 511 | if ( '' !== $y && 'center' !== $y ) { |
| 512 | return $y; |
| 513 | } |
| 514 | if ( '' !== $x && 'center' !== $x ) { |
| 515 | return $x; |
| 516 | } |
| 517 | return 'center'; |
| 518 | } |
| 519 | |
| 520 | /* --------------------------------------------------------------------- * |
| 521 | * Palette |
| 522 | * --------------------------------------------------------------------- */ |
| 523 | |
| 524 | /** |
| 525 | * Seed the six palette-driven token colours from a saved `color_palette` selection, plus the |
| 526 | * derived `btn.bgHover` (button_background darkened 14% toward black). |
| 527 | * |
| 528 | * @param array $legacy Legacy record. |
| 529 | * @return array token => device bag. |
| 530 | */ |
| 531 | protected static function migrate_palette( $legacy ) { |
| 532 | if ( empty( $legacy['color_palette'] ) || ! is_array( $legacy['color_palette'] ) ) { |
| 533 | return array(); |
| 534 | } |
| 535 | $colors = reset( $legacy['color_palette'] ); // color_N => { 6 colours }. |
| 536 | if ( ! is_array( $colors ) ) { |
| 537 | return array(); |
| 538 | } |
| 539 | $out = array(); |
| 540 | foreach ( Schema::palette_map() as $slot => $keys ) { |
| 541 | if ( ! isset( $colors[ $slot ] ) ) { |
| 542 | continue; |
| 543 | } |
| 544 | foreach ( $keys as $token_key ) { |
| 545 | $out[ $token_key ] = array( 'desktop' => $colors[ $slot ] ); |
| 546 | } |
| 547 | } |
| 548 | if ( ! empty( $colors['button_background'] ) ) { |
| 549 | $out['btn.bgHover'] = array( 'desktop' => self::mix_hex( $colors['button_background'], '#000000', 0.14 ) ); |
| 550 | } |
| 551 | return $out; |
| 552 | } |
| 553 | |
| 554 | /** |
| 555 | * Linear-interpolate two hex colours — PHP port of `mixHex()` in constants.ts. |
| 556 | * |
| 557 | * @param string $a First hex colour (`#rgb` or `#rrggbb`). |
| 558 | * @param string $b Second hex colour. |
| 559 | * @param float $t Mix factor, 0 = `$a`, 1 = `$b`. |
| 560 | * @return string `#rrggbb`. |
| 561 | */ |
| 562 | protected static function mix_hex( $a, $b, $t ) { |
| 563 | $parse = static function ( $hex ) { |
| 564 | $s = ltrim( (string) $hex, '#' ); |
| 565 | if ( 3 === strlen( $s ) ) { |
| 566 | $s = $s[0] . $s[0] . $s[1] . $s[1] . $s[2] . $s[2]; |
| 567 | } |
| 568 | return array( hexdec( substr( $s, 0, 2 ) ), hexdec( substr( $s, 2, 2 ) ), hexdec( substr( $s, 4, 2 ) ) ); |
| 569 | }; |
| 570 | $from = $parse( $a ); |
| 571 | $to = $parse( $b ); |
| 572 | $out = '#'; |
| 573 | foreach ( $from as $i => $x ) { |
| 574 | $out .= str_pad( dechex( (int) round( $x + ( $to[ $i ] - $x ) * $t ) ), 2, '0', STR_PAD_LEFT ); |
| 575 | } |
| 576 | return $out; |
| 577 | } |
| 578 | |
| 579 | /* --------------------------------------------------------------------- * |
| 580 | * Helpers |
| 581 | * --------------------------------------------------------------------- */ |
| 582 | |
| 583 | /** |
| 584 | * Build a mapping row. |
| 585 | * |
| 586 | * @param string $group Legacy group. |
| 587 | * @param string $prop Legacy prop. |
| 588 | * @param string $token v2 token key. |
| 589 | * @param string $transform Transform key. |
| 590 | * @return array |
| 591 | */ |
| 592 | protected static function row( $group, $prop, $token, $transform ) { |
| 593 | return array( |
| 594 | 'group' => $group, |
| 595 | 'prop' => $prop, |
| 596 | 'token' => $token, |
| 597 | 'transform' => $transform, |
| 598 | ); |
| 599 | } |
| 600 | } |
| 601 |