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 / Migrator.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
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