PluginProbe
WDesignKit – AI Templates, Widget Builder & MCP Workflow / trunk
WDesignKit – AI Templates, Widget Builder & MCP Workflow vtrunk
2.6.6 2.6.5 2.6.4 2.6.3 2.6.2 2.6.1 2.6.0 2.5.5 2.5.4 2.5.3 2.5.2 2.5.1 2.5.0 2.4.0 2.3.3 2.3.2 2.3.1 1.0.10 1.0.11 1.0.12 1.0.13 1.0.14 1.0.15 1.0.16 1.0.17 All 128 releases
wdesignkit / includes / abilities / widgets / wdesignkit-sync-widget-code.php

wdesignkit-sync-widget-code.php in WDesignKit – AI Templates, Widget Builder & MCP Workflow trunk, at includes/abilities/widgets/wdesignkit-sync-widget-code.php

755 lines 33.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Ability: Sync and report drift between WDesignKit widget PHP code and JSON section_data.
4 */
5
6 declare(strict_types=1);
7
8 if (!defined('ABSPATH')) {
9 exit();
10 }
11
12 wp_register_ability('wdesignkit/sync-widget-code', [
13 'label' => __('Sync WDesignKit Widget Code', 'wdesignkit'),
14 'description' => __(
15 'Reports whether a widget\'s PHP register_controls() and JSON section_data are in sync, and on request regenerates section_data from PHP (code_to_section) or PHP register_controls() from section_data (section_to_code). Supports dry_run preview.',
16 'wdesignkit',
17 ),
18 'category' => 'wdesignkit',
19 'input_schema' => [
20 'type' => 'object',
21 'properties' => [
22 'builder' => [
23 'type' => 'string',
24 'description' => 'Builder type the widget belongs to (elementor, gutenberg, gutenberg_core, bricks).',
25 'enum' => ['elementor', 'gutenberg', 'gutenberg_core', 'bricks'],
26 ],
27 'folder' => [
28 'type' => 'string',
29 'description' => 'Widget folder name (from wdesignkit/list-widgets). Required if widget_id is omitted.',
30 ],
31 'widget_id' => [
32 'type' => 'string',
33 'description' => 'Widget unique ID. Used to locate the widget folder if folder is omitted.',
34 ],
35 'direction' => [
36 'type' => 'string',
37 'description' => 'Sync operation direction: "check_only" (report status), "code_to_section" (regenerate section_data from PHP), or "section_to_code" (regenerate PHP register_controls from section_data). Default "check_only".',
38 'enum' => ['check_only', 'code_to_section', 'section_to_code'],
39 ],
40 'dry_run' => [
41 'type' => 'boolean',
42 'description' => 'When true, returns preview of proposed changes without writing to disk. Default false.',
43 ],
44 ],
45 'additionalProperties' => false,
46 ],
47 'output_schema' => [
48 'type' => 'object',
49 'properties' => [
50 'success' => ['type' => 'boolean'],
51 'message' => ['type' => 'string'],
52 'in_sync' => ['type' => 'boolean'],
53 'direction' => ['type' => 'string'],
54 'dry_run' => ['type' => 'boolean'],
55 'builder' => ['type' => 'string'],
56 'folder' => ['type' => 'string'],
57 'widget_id' => ['type' => 'string'],
58 'php_controls_count' => ['type' => 'integer'],
59 'section_data_controls_count' => ['type' => 'integer'],
60 'diff' => ['type' => 'object'],
61 'preview' => ['type' => 'object'],
62 ],
63 ],
64 'execute_callback' => 'wdesignkit_mcp_sync_widget_code',
65 'permission_callback' => 'wdesignkit_mcp_permission_callback',
66 'meta' => [
67 'public' => true,
68 'show_in_rest' => true,
69 'mcp' => ['public' => true],
70 'annotations' => [
71 'instructions' => implode("\n", [
72 'Reports drift between widget PHP register_controls() and JSON section_data.',
73 'Use direction="check_only" to inspect sync status.',
74 'Use direction="code_to_section" after updating raw php_code to update JSON section_data.',
75 'Use direction="section_to_code" to regenerate PHP register_controls() from JSON section_data.',
76 'Use dry_run=true to preview proposed updates before writing to disk.',
77 ]),
78 'readonly' => false,
79 'destructive' => false,
80 'idempotent' => false,
81 ],
82 ],
83 ]);
84
85 function wdesignkit_mcp_sync_widget_code(array $input): array {
86 set_time_limit(90);
87
88 if (!defined('WDKIT_BUILDER_PATH')) {
89 return ['success' => false, 'message' => 'WDesignKit plugin is not active.'];
90 }
91
92 // Ensure create-widget helper functions are loaded
93 if (!function_exists('wdesignkit_mcp_parse_php_section_data')) {
94 $cw_path = __DIR__ . '/wdesignkit-create-widget.php';
95 if (file_exists($cw_path)) {
96 require_once $cw_path;
97 }
98 }
99
100 $builder = sanitize_text_field((string) ($input['builder'] ?? ''));
101 $folder = sanitize_file_name((string) ($input['folder'] ?? ''));
102 $widget_id = sanitize_text_field((string) ($input['widget_id'] ?? ''));
103 $direction = sanitize_text_field((string) ($input['direction'] ?? 'check_only'));
104 $dry_run = !empty($input['dry_run']);
105
106 $allowed_builders = ['elementor', 'gutenberg', 'gutenberg_core', 'bricks'];
107 $allowed_directions = ['check_only', 'code_to_section', 'section_to_code'];
108
109 if (!in_array($direction, $allowed_directions, true)) {
110 $direction = 'check_only';
111 }
112
113 // Locate widget folder
114 $widget_dir = null;
115
116 if ($folder !== '') {
117 if ($builder !== '' && !in_array($builder, $allowed_builders, true)) {
118 return ['success' => false, 'message' => 'Invalid builder type.'];
119 }
120 $builders_to_check = ($builder !== '') ? [$builder] : $allowed_builders;
121 foreach ($builders_to_check as $b) {
122 $candidate_dir = WDKIT_BUILDER_PATH . '/' . $b . '/' . $folder;
123 if (is_dir($candidate_dir)) {
124 $widget_dir = $candidate_dir;
125 $builder = $b;
126 break;
127 }
128 }
129 } elseif ($widget_id !== '') {
130 $builders_to_check = ($builder !== '' && in_array($builder, $allowed_builders, true)) ? [$builder] : $allowed_builders;
131 foreach ($builders_to_check as $b) {
132 $b_dir = WDKIT_BUILDER_PATH . '/' . $b;
133 if (!is_dir($b_dir)) {
134 continue;
135 }
136 $subfolders = array_diff(@scandir($b_dir) ?: [], ['.', '..']);
137 foreach ($subfolders as $sub) {
138 $dir_path = $b_dir . '/' . $sub;
139 if (!is_dir($dir_path)) {
140 continue;
141 }
142 $files = array_diff(@scandir($dir_path) ?: [], ['.', '..']);
143 foreach ($files as $f) {
144 if (pathinfo($f, PATHINFO_EXTENSION) === 'json') {
145 $raw = @file_get_contents($dir_path . '/' . $f);
146 $data = ($raw !== false) ? json_decode($raw, true) : null;
147 $wid = $data['widget_data']['widgetdata']['widget_id'] ?? '';
148 if ($wid === $widget_id) {
149 $widget_dir = $dir_path;
150 $builder = $b;
151 $folder = $sub;
152 break 3;
153 }
154 }
155 }
156 }
157 }
158 }
159
160 if (!$widget_dir || !is_dir($widget_dir)) {
161 return [
162 'success' => false,
163 'message' => 'Widget folder not found. Specify a valid builder and folder or widget_id.',
164 ];
165 }
166
167 // Path safety check
168 $real_widget = realpath($widget_dir);
169 $real_base = realpath(WDKIT_BUILDER_PATH);
170 if (!$real_widget || !$real_base || strpos($real_widget, $real_base . DIRECTORY_SEPARATOR) !== 0) {
171 return ['success' => false, 'message' => 'Invalid widget path.'];
172 }
173
174 // Read JSON and PHP files
175 $files = array_diff(@scandir($widget_dir) ?: [], ['.', '..']);
176 $json_path = null;
177 $json_data = null;
178 $php_path = null;
179 $php_code = '';
180
181 foreach ($files as $f) {
182 $ext = strtolower(pathinfo($f, PATHINFO_EXTENSION));
183 if ($ext === 'json' && $json_path === null) {
184 $json_path = $widget_dir . '/' . $f;
185 $raw = @file_get_contents($json_path);
186 $json_data = ($raw !== false) ? json_decode($raw, true) : null;
187 } elseif ($ext === 'php' && $php_path === null) {
188 $php_path = $widget_dir . '/' . $f;
189 $raw = @file_get_contents($php_path);
190 $php_code = ($raw !== false) ? $raw : '';
191 }
192 }
193
194 if (!is_array($json_data) || !$json_path) {
195 return ['success' => false, 'message' => "Could not read JSON config in widget folder {$builder}/{$folder}."];
196 }
197
198 $wd_name = (string) ($json_data['widget_data']['widgetdata']['name'] ?? $folder);
199 if ($widget_id === '') {
200 $widget_id = (string) ($json_data['widget_data']['widgetdata']['widget_id'] ?? '');
201 }
202
203 $_slug = sanitize_title($wd_name);
204 $widget_css_class = 'wdkit-' . $_slug . (substr($_slug, -7) === '-widget' ? '' : '-widget');
205
206 $stored_section_data = $json_data['section_data'] ?? [];
207 if (!is_array($stored_section_data)) {
208 $stored_section_data = [];
209 }
210
211 // Parse PHP section data
212 $parsed_section_data = null;
213 if ($php_code !== '' && function_exists('wdesignkit_mcp_parse_php_section_data')) {
214 $parsed_section_data = wdesignkit_mcp_parse_php_section_data($php_code, $widget_css_class);
215 }
216
217 // Extract flat control lists for comparison
218 $php_flat = wdesignkit_mcp_extract_flat_controls($parsed_section_data ?: []);
219 $stored_flat = wdesignkit_mcp_extract_flat_controls($stored_section_data);
220
221 // Compute diff
222 $missing_in_section_data = [];
223 $missing_in_php = [];
224 $mismatched_controls = [];
225
226 $php_by_name = [];
227 foreach ($php_flat as $ctrl) {
228 $php_by_name[$ctrl['name']] = $ctrl;
229 }
230
231 $stored_by_name = [];
232 foreach ($stored_flat as $ctrl) {
233 $stored_by_name[$ctrl['name']] = $ctrl;
234 }
235
236 $checked_stored_names = [];
237 $php_count = count($php_flat);
238 $stored_count = count($stored_flat);
239 $max_count = max($php_count, $stored_count);
240
241 for ($i = 0; $i < $max_count; $i++) {
242 $p = $php_flat[$i] ?? null;
243 $s = $stored_flat[$i] ?? null;
244
245 if ($p !== null && $s !== null) {
246 if ($p['name'] === $s['name']) {
247 $checked_stored_names[$s['name']] = true;
248 if ($p['type'] !== $s['type'] || $p['section'] !== $s['section'] || $p['default'] !== $s['default']) {
249 $mismatched_controls[] = [
250 'name' => $p['name'],
251 'php_name' => $p['name'],
252 'section_data_name' => $s['name'],
253 'php_type' => $p['type'],
254 'section_data_type' => $s['type'],
255 'php_section' => $p['section'],
256 'section_data_section' => $s['section'],
257 'php_default' => $p['default'],
258 'section_data_default' => $s['default'],
259 ];
260 }
261 } else {
262 $p_in_stored = isset($stored_by_name[$p['name']]);
263 $s_in_php = isset($php_by_name[$s['name']]);
264
265 if (!$p_in_stored && !$s_in_php) {
266 $mismatched_controls[] = [
267 'name' => $p['name'] . ' vs ' . $s['name'],
268 'php_name' => $p['name'],
269 'section_data_name' => $s['name'],
270 'php_type' => $p['type'],
271 'section_data_type' => $s['type'],
272 'php_section' => $p['section'],
273 'section_data_section' => $s['section'],
274 'php_default' => $p['default'],
275 'section_data_default' => $s['default'],
276 ];
277 $checked_stored_names[$s['name']] = true;
278 } elseif ($p_in_stored) {
279 $matched_s = $stored_by_name[$p['name']];
280 $checked_stored_names[$matched_s['name']] = true;
281 if ($p['type'] !== $matched_s['type'] || $p['section'] !== $matched_s['section'] || $p['default'] !== $matched_s['default']) {
282 $mismatched_controls[] = [
283 'name' => $p['name'],
284 'php_name' => $p['name'],
285 'section_data_name' => $matched_s['name'],
286 'php_type' => $p['type'],
287 'section_data_type' => $matched_s['type'],
288 'php_section' => $p['section'],
289 'section_data_section' => $matched_s['section'],
290 'php_default' => $p['default'],
291 'section_data_default' => $matched_s['default'],
292 ];
293 }
294 } else {
295 $missing_in_section_data[] = [
296 'name' => $p['name'],
297 'label' => $p['label'],
298 'type' => $p['type'],
299 'section' => $p['section'],
300 ];
301 }
302 }
303 } elseif ($p !== null && $s === null) {
304 if (isset($stored_by_name[$p['name']])) {
305 $matched_s = $stored_by_name[$p['name']];
306 $checked_stored_names[$matched_s['name']] = true;
307 if ($p['type'] !== $matched_s['type'] || $p['section'] !== $matched_s['section'] || $p['default'] !== $matched_s['default']) {
308 $mismatched_controls[] = [
309 'name' => $p['name'],
310 'php_name' => $p['name'],
311 'section_data_name' => $matched_s['name'],
312 'php_type' => $p['type'],
313 'section_data_type' => $matched_s['type'],
314 'php_section' => $p['section'],
315 'section_data_section' => $matched_s['section'],
316 'php_default' => $p['default'],
317 'section_data_default' => $matched_s['default'],
318 ];
319 }
320 } else {
321 $missing_in_section_data[] = [
322 'name' => $p['name'],
323 'label' => $p['label'],
324 'type' => $p['type'],
325 'section' => $p['section'],
326 ];
327 }
328 } elseif ($p === null && $s !== null) {
329 if (!isset($checked_stored_names[$s['name']]) && !isset($php_by_name[$s['name']])) {
330 $missing_in_php[] = [
331 'name' => $s['name'],
332 'label' => $s['label'],
333 'type' => $s['type'],
334 'section' => $s['section'],
335 ];
336 }
337 }
338 }
339
340 $in_sync = empty($missing_in_section_data) && empty($missing_in_php) && empty($mismatched_controls);
341
342 $diff_summary = [
343 'missing_in_section_data' => $missing_in_section_data,
344 'missing_in_php' => $missing_in_php,
345 'mismatched_controls' => $mismatched_controls,
346 ];
347
348 // Handle check_only
349 if ($direction === 'check_only') {
350 return [
351 'success' => true,
352 'message' => $in_sync ? "Widget '{$wd_name}' PHP and section_data are in sync." : "Widget '{$wd_name}' PHP and section_data have drifted.",
353 'in_sync' => $in_sync,
354 'direction' => 'check_only',
355 'dry_run' => $dry_run,
356 'builder' => $builder,
357 'folder' => $folder,
358 'widget_id' => $widget_id,
359 'php_controls_count' => count($php_flat),
360 'section_data_controls_count' => count($stored_flat),
361 'diff' => $diff_summary,
362 ];
363 }
364
365 // Handle code_to_section
366 if ($direction === 'code_to_section') {
367 if (!is_array($parsed_section_data)) {
368 return [
369 'success' => false,
370 'message' => "Could not parse register_controls() from PHP code for widget '{$wd_name}'.",
371 ];
372 }
373
374 $file_name = pathinfo($json_path, PATHINFO_FILENAME);
375 $new_editor_html = '';
376 if (function_exists('wdesignkit_mcp_generate_editor_html_from_section_data')) {
377 $new_editor_html = wdesignkit_mcp_generate_editor_html_from_section_data(
378 $widget_id, $widget_css_class, $file_name, $parsed_section_data
379 );
380 }
381
382 if ($dry_run) {
383 return [
384 'success' => true,
385 'message' => "Dry run: Proposed update of section_data from PHP code for '{$wd_name}'.",
386 'in_sync' => false,
387 'direction' => 'code_to_section',
388 'dry_run' => true,
389 'builder' => $builder,
390 'folder' => $folder,
391 'widget_id' => $widget_id,
392 'php_controls_count' => count($php_flat),
393 'section_data_controls_count' => count($stored_flat),
394 'diff' => $diff_summary,
395 'preview' => [
396 'new_section_data' => $parsed_section_data,
397 'new_editor_html' => $new_editor_html,
398 ],
399 ];
400 }
401
402 $json_data['section_data'] = $parsed_section_data;
403 if (!isset($json_data['Editor_data']) || !is_array($json_data['Editor_data'])) {
404 $json_data['Editor_data'] = [];
405 }
406 $json_data['Editor_data']['html'] = $new_editor_html;
407
408 $written = @file_put_contents(
409 $json_path,
410 wp_json_encode($json_data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)
411 );
412
413 if ($written === false) {
414 return ['success' => false, 'message' => "Failed to write updated JSON config to disk for {$builder}/{$folder}."];
415 }
416
417 return [
418 'success' => true,
419 'message' => "Successfully synced section_data from PHP code for widget '{$wd_name}'.",
420 'in_sync' => true,
421 'direction' => 'code_to_section',
422 'dry_run' => false,
423 'builder' => $builder,
424 'folder' => $folder,
425 'widget_id' => $widget_id,
426 'php_controls_count' => count($php_flat),
427 'section_data_controls_count' => count($php_flat),
428 'diff' => [
429 'missing_in_section_data' => [],
430 'missing_in_php' => [],
431 'mismatched_controls' => [],
432 ],
433 ];
434 }
435
436 // Handle section_to_code
437 if ($direction === 'section_to_code') {
438 if (empty($stored_section_data)) {
439 return [
440 'success' => false,
441 'message' => "section_data is empty in JSON config for widget '{$wd_name}'. Cannot generate PHP code.",
442 ];
443 }
444
445 if (!$php_path || $php_code === '') {
446 return [
447 'success' => false,
448 'message' => "PHP file not found for widget '{$wd_name}'.",
449 ];
450 }
451
452 $new_controls_body = wdesignkit_mcp_generate_php_controls_from_section_data($stored_section_data);
453 $new_php_code = wdesignkit_mcp_replace_register_controls($php_code, $new_controls_body);
454
455 if ($dry_run) {
456 return [
457 'success' => true,
458 'message' => "Dry run: Proposed update of PHP register_controls() from section_data for '{$wd_name}'.",
459 'in_sync' => false,
460 'direction' => 'section_to_code',
461 'dry_run' => true,
462 'builder' => $builder,
463 'folder' => $folder,
464 'widget_id' => $widget_id,
465 'php_controls_count' => count($php_flat),
466 'section_data_controls_count' => count($stored_flat),
467 'diff' => $diff_summary,
468 'preview' => [
469 'new_php_code' => $new_php_code,
470 ],
471 ];
472 }
473
474 // Site-level opt-out for generated widget PHP (ClickUp 86d41zavc). This path regenerates
475 // register_controls() and rewrites the widget's .php, so it is a PHP write like any other
476 // and has to honour the same filter. Checked after the dry_run return above, so a preview
477 // still works on a locked-down site.
478 if (function_exists('wdesignkit_widget_php_write_allowed') && !wdesignkit_widget_php_write_allowed()) {
479 return [
480 'success' => false,
481 'message' => "PHP was not rewritten for {$builder}/{$folder}: generated widget PHP writes are disabled on this site via the wdesignkit_allow_widget_php_write filter. Use dry_run: true to preview the change.",
482 ];
483 }
484
485 $written = @file_put_contents($php_path, $new_php_code);
486 if ($written === false) {
487 return ['success' => false, 'message' => "Failed to write updated PHP file to disk for {$builder}/{$folder}."];
488 }
489
490 return [
491 'success' => true,
492 'message' => "Successfully synced PHP register_controls() from section_data for widget '{$wd_name}'.",
493 'in_sync' => true,
494 'direction' => 'section_to_code',
495 'dry_run' => false,
496 'builder' => $builder,
497 'folder' => $folder,
498 'widget_id' => $widget_id,
499 'php_controls_count' => count($stored_flat),
500 'section_data_controls_count' => count($stored_flat),
501 'diff' => [
502 'missing_in_section_data' => [],
503 'missing_in_php' => [],
504 'mismatched_controls' => [],
505 ],
506 ];
507 }
508
509 return ['success' => false, 'message' => 'Invalid direction.'];
510 }
511
512 /**
513 * Extract a flat map of controls from section_data array.
514 */
515 function wdesignkit_mcp_extract_flat_controls(array $section_data): array {
516 // section_data's real shape is [{ "layout": [<sections>], "style": [<sections>] }] —
517 // each wrapper entry holds layout/style arrays of sections, NOT a section itself.
518 // Flatten to a plain list of sections (each with inner_sec) before extracting controls;
519 // this also tolerates a flat array of sections directly, for defensiveness.
520 $sections = [];
521 foreach ($section_data as $entry) {
522 if (!is_array($entry)) {
523 continue;
524 }
525 if (isset($entry['layout']) || isset($entry['style'])) {
526 foreach (['layout', 'style'] as $key) {
527 if (is_array($entry[$key] ?? null)) {
528 foreach ($entry[$key] as $sec) {
529 if (is_array($sec)) {
530 $sections[] = $sec;
531 }
532 }
533 }
534 }
535 } elseif (isset($entry['inner_sec'])) {
536 $sections[] = $entry;
537 }
538 }
539
540 $flat = [];
541 foreach ($sections as $group) {
542 $sec_title = (string) ($group['section'] ?? $group['name'] ?? 'Section');
543 $inner = $group['inner_sec'] ?? [];
544 if (!is_array($inner)) {
545 continue;
546 }
547
548 foreach ($inner as $ctrl) {
549 if (!is_array($ctrl)) {
550 continue;
551 }
552 $c_type = strtolower((string) ($ctrl['type'] ?? ''));
553 if ($c_type === 'normalhover') {
554 $tabs = $ctrl['inner_sec'] ?? $ctrl['fields'] ?? [];
555 if (is_array($tabs)) {
556 foreach ($tabs as $tab) {
557 $tab_ctrls = $tab['inner_sec'] ?? $tab['fields'] ?? [];
558 if (is_array($tab_ctrls)) {
559 foreach ($tab_ctrls as $tc) {
560 $c_name = (string) ($tc['name'] ?? '');
561 if ($c_name !== '') {
562 $flat[] = [
563 'name' => $c_name,
564 'label' => (string) ($tc['lable'] ?? $tc['label'] ?? $c_name),
565 'type' => strtolower((string) ($tc['type'] ?? 'text')),
566 'default' => (string) ($tc['defaultValue'] ?? $tc['default'] ?? ''),
567 'section' => $sec_title,
568 ];
569 }
570 }
571 }
572 }
573 }
574 } else {
575 $c_name = (string) ($ctrl['name'] ?? '');
576 if ($c_name !== '') {
577 $flat[] = [
578 'name' => $c_name,
579 'label' => (string) ($ctrl['lable'] ?? $ctrl['label'] ?? $c_name),
580 'type' => strtolower((string) ($ctrl['type'] ?? 'text')),
581 'default' => (string) ($ctrl['defaultValue'] ?? $ctrl['default'] ?? ''),
582 'section' => $sec_title,
583 ];
584 }
585 }
586 }
587 }
588 return $flat;
589 }
590
591 /**
592 * Generate PHP register_controls() code from section_data array.
593 */
594 function wdesignkit_mcp_generate_php_controls_from_section_data(array $section_data): string {
595 // section_data's real shape is [{ "layout": [<sections>], "style": [<sections>] }] —
596 // flatten to a plain list of sections first, tagging each with which wrapper it came
597 // from so the TAB_CONTENT/TAB_STYLE choice below doesn't have to guess from the name.
598 $sections = [];
599 foreach ($section_data as $entry) {
600 if (!is_array($entry)) {
601 continue;
602 }
603 if (isset($entry['layout']) || isset($entry['style'])) {
604 foreach (['layout' => false, 'style' => true] as $key => $is_style_wrapper) {
605 if (is_array($entry[$key] ?? null)) {
606 foreach ($entry[$key] as $sec) {
607 if (is_array($sec)) {
608 $sec['_is_style'] = $is_style_wrapper;
609 $sections[] = $sec;
610 }
611 }
612 }
613 }
614 } elseif (isset($entry['inner_sec'])) {
615 $sections[] = $entry;
616 }
617 }
618
619 $out = [];
620 foreach ($sections as $sec) {
621 $sec_name = (string) ($sec['name'] ?? 'section');
622 $sec_label = (string) ($sec['section'] ?? 'Section');
623 $is_style = isset($sec['_is_style'])
624 ? (bool) $sec['_is_style']
625 : (!empty($sec['is_style']) || (strpos($sec_name, 'style') !== false) || (strtolower($sec_label) === 'style' || strtolower($sec_label) === 'widget style'));
626 $tab_const = $is_style ? 'Controls_Manager::TAB_STYLE' : 'Controls_Manager::TAB_CONTENT';
627
628 $out[] = " \$this->start_controls_section(";
629 $out[] = " '" . addslashes($sec_name) . "',";
630 $out[] = " array(";
631 $out[] = " 'label' => esc_html__( '" . addslashes($sec_label) . "', 'wdesignkit' ),";
632 $out[] = " 'tab' => {$tab_const},";
633 $out[] = " )";
634 $out[] = " );";
635 $out[] = "";
636
637 $inner_sec = $sec['inner_sec'] ?? [];
638 if (is_array($inner_sec)) {
639 foreach ($inner_sec as $ctrl) {
640 if (!is_array($ctrl)) {
641 continue;
642 }
643 if (($ctrl['type'] ?? '') === 'normalhover') {
644 $out[] = " \$this->start_controls_tabs( '" . addslashes((string) ($ctrl['name'] ?? 'tabs')) . "' );";
645 $out[] = "";
646 $tabs = $ctrl['inner_sec'] ?? [];
647 if (is_array($tabs)) {
648 foreach ($tabs as $tab) {
649 $tab_type = (string) ($tab['type'] ?? 'normal');
650 $tab_label = ucfirst($tab_type);
651 $out[] = " \$this->start_controls_tab(";
652 $out[] = " '" . addslashes((string) ($tab['name'] ?? ('tab_' . $tab_type))) . "',";
653 $out[] = " array( 'label' => esc_html__( '" . $tab_label . "', 'wdesignkit' ) )";
654 $out[] = " );";
655 $out[] = "";
656 $tab_ctrls = $tab['inner_sec'] ?? [];
657 if (is_array($tab_ctrls)) {
658 foreach ($tab_ctrls as $tc) {
659 $out[] = wdesignkit_mcp_generate_single_control_php($tc);
660 }
661 }
662 $out[] = " \$this->end_controls_tab();";
663 $out[] = "";
664 }
665 }
666 $out[] = " \$this->end_controls_tabs();";
667 $out[] = "";
668 } else {
669 $out[] = wdesignkit_mcp_generate_single_control_php($ctrl);
670 }
671 }
672 }
673
674 $out[] = " \$this->end_controls_section();";
675 $out[] = "";
676 }
677 return implode("\n", $out);
678 }
679
680 /**
681 * Helper to generate PHP code for a single control definition.
682 */
683 function wdesignkit_mcp_generate_single_control_php(array $ctrl): string {
684 $c_type = strtolower((string) ($ctrl['type'] ?? 'text'));
685 $c_name = (string) ($ctrl['name'] ?? 'control');
686 $c_label = (string) ($ctrl['lable'] ?? $ctrl['label'] ?? ucfirst($c_name));
687
688 $type_map = [
689 'text' => 'Controls_Manager::TEXT',
690 'textarea' => 'Controls_Manager::TEXTAREA',
691 'wysiwyg' => 'Controls_Manager::WYSIWYG',
692 'number' => 'Controls_Manager::NUMBER',
693 'select' => 'Controls_Manager::SELECT',
694 'switcher' => 'Controls_Manager::SWITCHER',
695 'color' => 'Controls_Manager::COLOR',
696 'dimension' => 'Controls_Manager::DIMENSIONS',
697 'slider' => 'Controls_Manager::SLIDER',
698 'media' => 'Controls_Manager::MEDIA',
699 'choose' => 'Controls_Manager::CHOOSE',
700 'code' => 'Controls_Manager::CODE',
701 ];
702
703 if ($c_type === 'typography') {
704 $lines = [];
705 $lines[] = " \$this->add_group_control(";
706 $lines[] = " Group_Control_Typography::get_type(),";
707 $lines[] = " array(";
708 $lines[] = " 'name' => '" . addslashes($c_name) . "',";
709 $lines[] = " 'label' => esc_html__( '" . addslashes($c_label) . "', 'wdesignkit' ),";
710 if (!empty($ctrl['selector'])) {
711 $lines[] = " 'selector' => '{{WRAPPER}} " . addslashes((string) $ctrl['selector']) . "',";
712 }
713 $lines[] = " )";
714 $lines[] = " );";
715 $lines[] = "";
716 return implode("\n", $lines);
717 }
718
719 $cm_type = $type_map[$c_type] ?? 'Controls_Manager::TEXT';
720
721 $lines = [];
722 $lines[] = " \$this->add_control(";
723 $lines[] = " '" . addslashes($c_name) . "',";
724 $lines[] = " array(";
725 $lines[] = " 'label' => esc_html__( '" . addslashes($c_label) . "', 'wdesignkit' ),";
726 $lines[] = " 'type' => {$cm_type},";
727 if (isset($ctrl['defaultValue']) && $ctrl['defaultValue'] !== '') {
728 $lines[] = " 'default' => esc_html__( '" . addslashes((string) $ctrl['defaultValue']) . "', 'wdesignkit' ),";
729 }
730 if (!empty($ctrl['selector_value']) && !empty($ctrl['selectors'])) {
731 $sel_target = addslashes((string) $ctrl['selectors']);
732 $sel_prop = addslashes((string) $ctrl['selector_value']);
733 $lines[] = " 'selectors' => array(";
734 $lines[] = " '{{WRAPPER}} {$sel_target}' => '{$sel_prop}: {{VALUE}};',";
735 $lines[] = " ),";
736 }
737 $lines[] = " )";
738 $lines[] = " );";
739 $lines[] = "";
740
741 return implode("\n", $lines);
742 }
743
744 /**
745 * Replace register_controls() method body in PHP code.
746 */
747 function wdesignkit_mcp_replace_register_controls(string $php_code, string $new_controls_body): string {
748 $pattern = '/(protected|public)\s+function\s+register_controls\s*\(\s*\)\s*\{.*?\n\s*\}\n/s';
749 $replacement = " protected function register_controls() {\n" . $new_controls_body . " }\n";
750 if (preg_match($pattern, $php_code)) {
751 return (string) preg_replace($pattern, $replacement, $php_code, 1);
752 }
753 return $php_code;
754 }
755