PluginProbe
WDesignKit – AI Templates, Widget Builder & MCP Workflow / 2.5.0
WDesignKit – AI Templates, Widget Builder & MCP Workflow v2.5.0
2.7.0 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 All 129 releases
wdesignkit / includes / abilities / widgets / wdesignkit-create-widget.php

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

2,698 lines 116.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Ability: Create a new WDesignKit widget for a specific page builder.
4 */
5
6 declare(strict_types=1);
7
8 if (!defined('ABSPATH')) {
9 exit();
10 }
11
12 wp_register_ability('wdesignkit/create-widget', [
13 'label' => __('Create WDesignKit Widget', 'sprout-mcp'),
14 'description' => __(
15 'Creates a new widget for Elementor, Gutenberg, Gutenberg Core, or Bricks builder. Generates all required files (PHP, JSON config, CSS, JS) with proper boilerplate code. You can provide custom code for each file or use defaults.',
16 'sprout-mcp',
17 ),
18 'category' => 'wdesignkit',
19 'input_schema' => [
20 'type' => 'object',
21 'properties' => [
22 'name' => [
23 'type' => 'string',
24 'description' => 'Widget display name (e.g. "My Custom Card"). Max 64 characters. Only letters, numbers, spaces, hyphens, and underscores allowed.',
25 'minLength' => 1,
26 'maxLength' => 64,
27 ],
28 'builder' => [
29 'type' => 'string',
30 'description' => 'Target page builder.',
31 'enum' => ['elementor', 'gutenberg', 'gutenberg_core', 'bricks'],
32 ],
33 'description' => [
34 'type' => 'string',
35 'description' => 'Short description of the widget. Used in the JSON config and as placeholder text in the default boilerplate. When omitted, a fallback description is auto-generated from the widget name.',
36 ],
37 'category' => [
38 'type' => 'string',
39 'description' => 'Widget category label shown in the WDesignKit library UI. Defaults to "WDesignKit". MUST match an existing category from wdesignkit/list-categories — arbitrary strings are rejected. Use manage-categories to add a new category first if needed.',
40 'maxLength' => 64,
41 'pattern' => '^[a-zA-Z0-9 \\-_.]+$',
42 ],
43 'helper_link' => [
44 'type' => 'string',
45 'description' => 'Optional documentation URL for this widget (e.g. "https://learn.wdesignkit.com/docs/my-widget/"). Stored in widget JSON metadata and surfaced in the WDesignKit UI as a help/docs link. Leave empty if no documentation page exists yet.',
46 'format' => 'uri',
47 'maxLength' => 512,
48 ],
49 'icon' => [
50 'type' => 'string',
51 'description' => 'Elementor icon class shown next to the widget name in the WDesignKit library (e.g. "eicon-info-box", "eicon-button", "eicon-gallery-grid"). Defaults to "eicon-code". See Elementor icon list for valid values.',
52 'maxLength' => 64,
53 ],
54 'keywords' => [
55 'type' => 'array',
56 'description' => 'Search keywords for the widget in the WDesignKit library (e.g. ["card", "info", "icon"]). Helps users find the widget by keyword.',
57 'items' => ['type' => 'string'],
58 'maxItems' => 20,
59 ],
60 'php_code' => [
61 'type' => 'string',
62 'description' => 'Complete PHP file content for the widget. REPLACES the entire generated boilerplate. Must include the opening <?php tag. If omitted, correct builder-specific boilerplate is generated automatically.',
63 ],
64 'css_code' => [
65 'type' => 'string',
66 'description' => 'Complete CSS file content for the widget. If omitted, an empty placeholder stylesheet is generated.',
67 ],
68 'js_code' => [
69 'type' => 'string',
70 'description' => 'JavaScript for the widget frontend. Executed inside the Elementor element_ready scope — use $scope[0].querySelector() to access DOM elements directly. No jQuery wrapper needed. Leave empty if the widget needs no JavaScript. Example: "let btn = $scope[0].querySelector(\'.wdkit-my-widget__btn\'); btn.addEventListener(\'click\', () => { ... });". For Gutenberg, this replaces the full block registration JS.',
71 ],
72 'cdn_js' => [
73 'type' => 'array',
74 'description' => 'CDN JavaScript URLs required by the widget (e.g. a GSAP or Swiper CDN link). Stored in Editor_Link so the builder loads them before running the widget JS. Example: ["https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/gsap.min.js"]. Leave empty when no third-party JS is needed.',
75 'items' => ['type' => 'string'],
76 'maxItems' => 10,
77 ],
78 'cdn_css' => [
79 'type' => 'array',
80 'description' => 'CDN CSS URLs required by the widget (e.g. a Swiper or Font Awesome CDN link). Stored in Editor_Link. Leave empty when no third-party CSS is needed.',
81 'items' => ['type' => 'string'],
82 'maxItems' => 10,
83 ],
84 'version' => [
85 'type' => 'string',
86 'description' => 'Widget version in semver format (e.g. "1.0.0"). Defaults to "1.0.0".',
87 'pattern' => '^\d+\.\d+\.\d+$',
88 ],
89 'section_data' => [
90 'type' => 'array',
91 'description' => 'Custom section_data for the widget JSON config — defines every control shown in the WDesignKit builder panel. When provided, used verbatim (overrides auto-parsing). When omitted (recommended), the ability auto-parses register_controls() from php_code and builds section_data automatically. Each element must be an object with "layout" and "style" arrays.',
92 ],
93 'editor_html' => [
94 'type' => 'string',
95 'description' => 'Custom HTML template for the WDesignKit builder canvas preview. Uses {{control_name}} placeholders that must match the section_data control names. If omitted, a default <h3>+<p> template is generated using the standard title/description control hashes.',
96 ],
97 ],
98 'required' => ['name', 'builder'],
99 'additionalProperties' => false,
100 ],
101 'output_schema' => [
102 'type' => 'object',
103 'properties' => [
104 'success' => ['type' => 'boolean'],
105 'message' => ['type' => 'string'],
106 'widget' => [
107 'type' => 'object',
108 'properties' => [
109 'name' => ['type' => 'string'],
110 'builder' => ['type' => 'string'],
111 'folder' => ['type' => 'string'],
112 'widget_id' => ['type' => 'string', 'description' => '8-char unique hash. Use with activate-widget / deactivate-widget.'],
113 'files' => ['type' => 'array'],
114 'version' => ['type' => 'string'],
115 'css_class' => ['type' => 'string'],
116 ],
117 ],
118 ],
119 ],
120 'execute_callback' => 'wdesignkit_mcp_create_widget',
121 'permission_callback' => 'sprout_mcp_permission_callback',
122 'meta' => [
123 'show_in_rest' => true,
124 'mcp' => ['public' => true],
125 'annotations' => [
126 'instructions' => implode("\n", [
127 '## wdesignkit/create-widget — Usage Rules',
128 '',
129 'Creates a new WDesignKit widget with all required files.',
130 '',
131 '### ⚠ ALWAYS ask before calling — do NOT call this tool speculatively',
132 'Before invoking create-widget, confirm BOTH required fields with the user.',
133 'If either is missing or ambiguous from the user\'s message, ASK first.',
134 '',
135 'Ask about builder if not stated:',
136 ' "Which page builder should this widget be for?"',
137 ' Options: Elementor | Gutenberg | Gutenberg Core | Bricks',
138 '',
139 'Ask about name if ambiguous:',
140 ' "What should the widget be named?"',
141 ' (Only letters, numbers, spaces, hyphens, underscores — max 64 chars)',
142 '',
143 'Only call the tool once you have both confirmed answers.',
144 'Never guess the builder — a wrong builder creates files in the wrong directory.',
145 '',
146 '### Required inputs',
147 '- name: 1–64 chars. Letters, numbers, spaces, hyphens, underscores ONLY.',
148 ' Special characters are REJECTED with an error — do not pass them. Names must be unique per builder.',
149 '- builder: one of elementor | gutenberg | gutenberg_core | bricks',
150 '',
151 '### Code inputs (php_code, css_code, js_code, cdn_js, cdn_css, section_data, editor_html)',
152 '- php_code / css_code / js_code: COMPLETE file replacements, not partial snippets.',
153 '- STRONGLY RECOMMENDED: omit php_code and let the ability generate correct boilerplate.',
154 ' The auto-generated boilerplate includes all required methods and correct naming.',
155 '- If you provide php_code, the ability auto-corrects common mistakes (class name,',
156 ' get_name, missing asset methods), but providing wrong code still risks errors.',
157 '',
158 '### js_code — Elementor widget JS rules',
159 '- Omit js_code entirely when the widget needs no JavaScript (no empty boilerplate written).',
160 '- When JS is needed, use $scope[0].querySelector() to access DOM elements.',
161 ' Do NOT wrap in (function($){...})(jQuery) or $(window).on(elementor/...) —',
162 ' WDesignKit executes widget JS in the element_ready scope automatically.',
163 'Correct pattern:',
164 ' let btn = $scope[0].querySelector(\'.wdkit-my-widget__btn\');',
165 ' btn.addEventListener(\'click\', function() { ... });',
166 '- cdn_js: array of CDN URLs for third-party JS libraries the widget depends on.',
167 ' These are added to Editor_Link so the builder loads them before widget JS.',
168 ' Example: ["https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/gsap.min.js"]',
169 '- cdn_css: same for CSS libraries (e.g. Swiper CSS CDN).',
170 '',
171 '### section_data — auto-generated from php_code; provide only to override',
172 'When you provide php_code, the ability AUTO-PARSES register_controls() and builds',
173 'matching section_data automatically — no manual JSON work needed.',
174 'Every add_control / add_responsive_control / add_group_control call is extracted',
175 'and mapped to the correct WDesignKit JSON control type. Normal/Hover tabs are',
176 'detected and wrapped in a normalhover container automatically.',
177 '',
178 'Control name format in auto-parsed JSON: {wdk_type}_{php_control_name}',
179 ' e.g. PHP "button_text" (TEXT) → JSON "text_button_text"',
180 ' e.g. PHP "button_border" (Group Border) → JSON "border_button_border"',
181 '',
182 'You can still provide section_data manually to override the auto-parsed result.',
183 'editor_html is also auto-generated from the section_data — button-like widgets',
184 '(with a URL control) get a full <a> template; others get an <h3>+<p> template.',
185 '',
186 '### CRITICAL: Elementor PHP rules (applies when providing php_code)',
187 '⚠ WRONG CLASS NAME = PHP Fatal Error = entire site crashes.',
188 ' Auto-corrected for ALL naming conventions (WDK_*, My_Widget_*, etc.) — but ONLY if',
189 ' your php_code contains exactly one class declaration. Avoid providing php_code',
190 ' with multiple class definitions.',
191 '',
192 'The PHP class name is derived from the generated filename (which includes the hash):',
193 ' file_name = snake_case(widget_name) + "_" + widget_hash',
194 ' class = "Wdkit_" + file_name',
195 'Example: name="Normal Button", hash="eb2ace"',
196 ' → file: normal_button_eb2ace.php',
197 ' → class: Wdkit_normal_button_eb2ace',
198 '',
199 'get_name() MUST return "wb-{widget_hash}" — e.g. return \'wb-eb2ace\';',
200 ' ⚠ Duplicate get_name() across widgets causes silent conflicts.',
201 '',
202 'MUST include get_script_depends() — loads the widget .js via wp_upload_dir().',
203 'MUST include get_style_depends() — loads the widget .css via wp_upload_dir().',
204 ' ⚠ Missing these = CSS/JS never loads on the frontend.',
205 '',
206 'Use "use Elementor\\..." imports at the top of the file.',
207 '',
208 '### Minimum required Elementor class structure',
209 'class Wdkit_{name_snake}_{hash} extends Widget_Base {',
210 ' public function get_name() { return \'wb-{hash}\'; }',
211 ' public function get_title() { return esc_html__(\'...\', \'wdesignkit\'); }',
212 ' public function get_icon() { return \'eicon-code tpae-wdkit-logo\'; }',
213 ' public function get_categories() { /* dynamic: reads wkit_builder option; fallback array(\'WDesignKit\') */ }',
214 ' public function get_script_depends() { /* wp_enqueue_script(); return array(handle); */ }',
215 ' public function get_style_depends() { /* wp_enqueue_style(); return array(handle); */ }',
216 ' protected function register_controls() { /* ... */ }',
217 ' protected function render() { /* echo HTML */ }',
218 '}',
219 '',
220 '### Bricks PHP rules',
221 '- Class must extend \\Bricks\\Element.',
222 '',
223 '### Gutenberg PHP rules',
224 '- Must call register_block_type() inside an init action.',
225 '',
226 '### Storage',
227 'Widgets stored in: wp-content/uploads/wdesignkit/{builder}/{folder}/',
228 'folder = {name-kebab-case}_{8-char-hash}',
229 'file = {name-snake-case}_{8-char-hash} (same hash, underscores)',
230 '',
231 '### Widget ID',
232 'The returned widget_id is the 8-char hash (e.g. "kmrd6l24").',
233 'Use widget_id with activate-widget / deactivate-widget.',
234 'Use folder with get-widget / update-widget.',
235 'The widget_id never changes after creation.',
236 '',
237 '### After creation',
238 '- Use wdesignkit/get-widget to read back the generated files.',
239 '- Use wdesignkit/update-widget to modify code.',
240 '- Does NOT require WDesignKit cloud login.',
241 ]),
242 'readonly' => false,
243 'destructive' => false,
244 'idempotent' => false,
245 ],
246 ],
247 ]);
248
249 function wdesignkit_mcp_create_widget(array $input): array {
250 if (!defined('WDKIT_BUILDER_PATH')) {
251 return [
252 'success' => false,
253 'message' => 'WDesignKit plugin is not active.',
254 ];
255 }
256
257 // --- Validate name ---
258 $raw_name = $input['name'] ?? '';
259 if (!is_string($raw_name) || trim($raw_name) === '') {
260 return ['success' => false, 'message' => 'Widget name is required.'];
261 }
262
263 // Reject names containing invalid characters — do NOT silently strip.
264 // Schema declares: only letters, numbers, spaces, hyphens, and underscores allowed.
265 // Stripping silently mutates the stored name without the caller knowing (QA #86d2p0anc).
266 $name = sanitize_text_field($raw_name);
267 if (preg_match('/[^a-zA-Z0-9 \-_]/', $name)) {
268 return ['success' => false, 'message' => 'Widget name contains invalid characters. Only letters, numbers, spaces, hyphens, and underscores are allowed.'];
269 }
270 $name = trim($name);
271
272 if (empty($name)) {
273 return ['success' => false, 'message' => 'Widget name is required.'];
274 }
275 if (strlen($name) > 64) {
276 return ['success' => false, 'message' => 'Widget name is too long. Maximum 64 characters allowed.'];
277 }
278
279 // --- Validate builder ---
280 $builder = sanitize_text_field($input['builder'] ?? '');
281 if (empty($builder)) {
282 return ['success' => false, 'message' => 'Builder type is required.'];
283 }
284
285 $allowed_builders = ['elementor', 'gutenberg', 'gutenberg_core', 'bricks'];
286 if (!in_array($builder, $allowed_builders, true)) {
287 return [
288 'success' => false,
289 'message' => 'Invalid builder. Must be one of: ' . implode(', ', $allowed_builders),
290 ];
291 }
292
293 // --- Validate optional fields ---
294 $description = sanitize_text_field($input['description'] ?? '');
295 // Auto-generate a fallback description from the widget name so the field is never empty
296 // (prevents the "description is empty" metadata warning in get-widget).
297 if ($description === '') {
298 $description = 'A ' . $name . ' widget';
299 }
300 $category = sanitize_text_field($input['category'] ?? 'WDesignKit');
301 $helper_link = esc_url_raw((string) ($input['helper_link'] ?? ''));
302 $icon = sanitize_text_field((string) ($input['icon'] ?? 'eicon-code'));
303 $keywords = array_values(array_filter(array_map('sanitize_text_field', (array) ($input['keywords'] ?? []))));
304 $version = sanitize_text_field($input['version'] ?? '1.0.0');
305
306 // Server-side validation for category (mirrors the input_schema pattern + maxLength constraints).
307 if (strlen($category) > 64) {
308 return ['success' => false, 'message' => 'Category name is too long. Maximum 64 characters.'];
309 }
310 if (!preg_match('/^[a-zA-Z0-9 \-_.]+$/', $category)) {
311 return ['success' => false, 'message' => 'Category name contains invalid characters. Use letters, numbers, spaces, hyphens, underscores, and dots only.'];
312 }
313
314 // Validate category against registered categories (fixes QA #86d2zp7bd).
315 // Reject any category not present in wkit_builder option — mirrors list-categories source of truth.
316 $valid_categories = get_option('wkit_builder', ['WDesignKit']);
317 if (!is_array($valid_categories)) {
318 $valid_categories = ['WDesignKit'];
319 }
320 if (!in_array('WDesignKit', $valid_categories, true)) {
321 array_unshift($valid_categories, 'WDesignKit');
322 }
323 $valid_lower = array_map('strtolower', $valid_categories);
324 if (!in_array(strtolower($category), $valid_lower, true)) {
325 return [
326 'success' => false,
327 'message' => "Category '{$category}' does not exist. Use wdesignkit/list-categories to see available categories.",
328 ];
329 }
330 // Normalize to canonical casing as stored in the option (e.g. "wdesignkit" → "WDesignKit")
331 $canonical_index = array_search(strtolower($category), $valid_lower, true);
332 if ($canonical_index !== false) {
333 $category = $valid_categories[$canonical_index];
334 }
335
336 if (!preg_match('/^\d+\.\d+\.\d+$/', $version)) {
337 return ['success' => false, 'message' => 'Invalid version format. Use semver format like "1.0.0".'];
338 }
339
340 // --- Generate folder and file names matching WDesignKit convention ---
341 // folder: kebab-case (e.g. "my-custom-card")
342 // file: snake_case (e.g. "my_custom_card")
343 // Normalize ALL separators: folder is pure kebab-case, file is pure snake_case.
344 // Without this, a name like "My-Custom Widget" produces "my-custom_widget" (mixed).
345 $folder_base = sanitize_file_name(strtolower(str_replace([' ', '_'], '-', $name)));
346 $file_base = sanitize_file_name(strtolower(str_replace([' ', '-'], '_', $name)));
347
348 // 8-character unique hex hash — matches the real WDesignKit widget format (e.g. "kmrd6l24")
349 $widget_hash = substr(md5(uniqid('wdkit', true)), 0, 8);
350 $folder_name_full = $folder_base . '_' . $widget_hash;
351 $file_name_full = $file_base . '_' . $widget_hash;
352
353 // widget_id = JUST the hash (matches the real widget JSON format: "widget_id":"20hzyv26")
354 $widget_id = $widget_hash;
355
356 $builder_path = trailingslashit(WDKIT_BUILDER_PATH) . $builder;
357 $widget_dir = $builder_path . '/' . $folder_name_full;
358
359 // --- Check for duplicate widget names in same builder (case-insensitive) ---
360 if (is_dir($builder_path)) {
361 $existing_folders = @scandir($builder_path);
362 if (is_array($existing_folders)) {
363 foreach ($existing_folders as $ef) {
364 if (in_array($ef, ['.', '..'], true) || !is_dir($builder_path . '/' . $ef)) {
365 continue;
366 }
367 $ef_files = @scandir($builder_path . '/' . $ef);
368 if (!is_array($ef_files)) {
369 continue;
370 }
371 foreach ($ef_files as $ef_file) {
372 if (pathinfo($ef_file, PATHINFO_EXTENSION) !== 'json') {
373 continue;
374 }
375 $ef_json_raw = @file_get_contents($builder_path . '/' . $ef . '/' . $ef_file);
376 if ($ef_json_raw !== false) {
377 $ef_json = json_decode($ef_json_raw, true);
378 $ef_name = $ef_json['widget_data']['widgetdata']['name'] ?? '';
379 if (strtolower($ef_name) === strtolower($name)) {
380 return [
381 'success' => false,
382 'message' => "A widget named '{$name}' already exists for {$builder} (folder: {$ef}). Use a different name or delete the existing widget first.",
383 ];
384 }
385 }
386 break; // Only one JSON per folder
387 }
388 }
389 }
390 }
391
392 if (is_dir($widget_dir)) {
393 return [
394 'success' => false,
395 'message' => "Widget folder already exists: {$builder}/{$folder_name_full}",
396 ];
397 }
398
399 // --- Path traversal guard ---
400 $real_base = realpath(WDKIT_BUILDER_PATH);
401 if (!$real_base) {
402 return ['success' => false, 'message' => 'Widget base path does not exist. Check WDesignKit upload directory.'];
403 }
404 $real_base_norm = rtrim(str_replace('\\', '/', $real_base), '/');
405 $widget_dir_norm = rtrim(str_replace('\\', '/', $builder_path . '/' . $folder_name_full), '/');
406 if (strpos($widget_dir_norm, $real_base_norm . '/') !== 0) {
407 return ['success' => false, 'message' => 'Invalid widget path.'];
408 }
409
410 // --- Create directories ---
411 if (!is_dir($builder_path)) {
412 wp_mkdir_p($builder_path);
413 }
414 wp_mkdir_p($widget_dir);
415
416 // PHP class name: PascalCase (e.g. "My Custom Card" → "MyCustomCard")
417 $class_name = preg_replace('/[^a-zA-Z0-9]/', '', ucwords($name, ' -_'));
418
419 // --- Generate or use provided code ---
420 $php_code = (string) ($input['php_code'] ?? '');
421 if (empty($php_code)) {
422 $php_code = wdesignkit_mcp_generate_php(
423 $builder, $class_name, $name, $folder_name_full, $file_name_full,
424 $description, $category, $widget_hash, $version
425 );
426 } elseif ($builder === 'elementor') {
427 // Auto-correct common mistakes in AI-provided Elementor PHP:
428 // wrong class name, wrong get_name() return value, missing asset-enqueue methods.
429 $php_code = wdesignkit_mcp_fix_elementor_php(
430 $php_code, $file_name_full, $widget_hash, $folder_name_full, $version
431 );
432 }
433
434 $js_code = (string) ($input['js_code'] ?? '');
435 if (empty($js_code)) {
436 $js_code = wdesignkit_mcp_generate_js(
437 $builder, $class_name, $name, $folder_name_full, $file_name_full,
438 $description, $category, $widget_hash
439 );
440 }
441
442 // CDN links for Editor_Link — sanitise and normalise
443 $raw_cdn_js = $input['cdn_js'] ?? [];
444 $raw_cdn_css = $input['cdn_css'] ?? [];
445 if (is_string($raw_cdn_js)) { $raw_cdn_js = json_decode($raw_cdn_js, true) ?: []; }
446 if (is_string($raw_cdn_css)) { $raw_cdn_css = json_decode($raw_cdn_css, true) ?: []; }
447 $cdn_js = array_values(array_filter(array_map('esc_url_raw', (array) $raw_cdn_js)));
448 $cdn_css = array_values(array_filter(array_map('esc_url_raw', (array) $raw_cdn_css)));
449 // Editor_Link always needs at least one entry; use '' as placeholder when empty
450 if (empty($cdn_js)) { $cdn_js = ['']; }
451 if (empty($cdn_css)) { $cdn_css = ['']; }
452
453 // Compute the canonical widget CSS class once — used in both the generated CSS and the response.
454 // Avoids double '-widget' suffix when the name itself ends with 'Widget'
455 // (e.g. "My Card Widget" → slug "my-card-widget" → class "wdkit-my-card-widget", not "wdkit-my-card-widget-widget").
456 $_css_slug = sanitize_title($name);
457 $widget_css_class = 'wdkit-' . $_css_slug . (substr($_css_slug, -7) === '-widget' ? '' : '-widget');
458
459 // Always write a CSS file so the registered style handle resolves without 404.
460 // Default CSS stubs match the PHP render() output and section_data style selectors:
461 // .{widget_css_class} → widget container
462 // .{widget_css_class}__title → <h3> element
463 // .{widget_css_class}__desc → <p> element
464 $css_code = (string) ($input['css_code'] ?? '');
465 if (empty($css_code)) {
466 $css_code = ".{$widget_css_class} {\n /* Widget container styles */\n}\n\n"
467 . ".{$widget_css_class}__title {\n /* Title styles */\n}\n\n"
468 . ".{$widget_css_class}__desc {\n /* Description styles */\n}\n";
469 }
470
471 // Custom section_data / editor_html — used verbatim when provided so the JSON
472 // panel controls perfectly mirror the caller's custom PHP register_controls().
473 // MCP frameworks may deliver array parameters as JSON strings rather than PHP arrays,
474 // so we accept both: a real PHP array OR a JSON-encoded string that decodes to an array.
475 $custom_section_data = null;
476 if (isset($input['section_data'])) {
477 $sd = $input['section_data'];
478 if (is_array($sd)) {
479 $custom_section_data = $sd;
480 } elseif (is_string($sd) && $sd !== '') {
481 $decoded = json_decode($sd, true);
482 if (is_array($decoded)) {
483 $custom_section_data = $decoded;
484 }
485 }
486 }
487 $custom_editor_html = isset($input['editor_html']) && is_string($input['editor_html']) && $input['editor_html'] !== ''
488 ? $input['editor_html']
489 : null;
490
491 // Pass the PHP source for auto-parsing ONLY when the caller provided custom php_code.
492 // When using default boilerplate, generate_section_data() already covers it correctly.
493 $php_for_parse = !empty($input['php_code']) ? $php_code : '';
494
495 // JSON config — widget_id is JUST the 8-char hash (matching real widget format)
496 $json_config = wdesignkit_mcp_generate_json_config(
497 $name, $builder, $widget_id, $folder_name_full, $file_name_full,
498 $description, $version, $category, $helper_link, $icon, $keywords,
499 $css_code, $js_code, $widget_css_class, $custom_section_data, $custom_editor_html,
500 $php_for_parse, $cdn_js, $cdn_css
501 );
502
503 // --- Write files ---
504 $widget_file_base = $widget_dir . '/' . $file_name_full;
505 $write_errors = [];
506
507 // Use file_put_contents() directly — WP_Filesystem() requires admin credentials
508 // and silently fails in the REST/AJAX context that MCP abilities run under.
509 // The uploads directory is always writable by PHP, so this is safe.
510 if (file_put_contents($widget_file_base . '.php', $php_code) === false) {
511 $write_errors[] = 'PHP';
512 }
513 if (file_put_contents($widget_file_base . '.json', wp_json_encode($json_config, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)) === false) {
514 $write_errors[] = 'JSON';
515 }
516 if (file_put_contents($widget_file_base . '.js', $js_code) === false) {
517 $write_errors[] = 'JS';
518 }
519 if (file_put_contents($widget_file_base . '.css', $css_code) === false) {
520 $write_errors[] = 'CSS';
521 }
522
523 if (!empty($write_errors)) {
524 return [
525 'success' => false,
526 'message' => 'Failed to write files: ' . implode(', ', $write_errors) . '. Check directory permissions.',
527 ];
528 }
529
530 return [
531 'success' => true,
532 'message' => "Widget '{$name}' created successfully for {$builder}.",
533 'widget' => [
534 'name' => $name,
535 'builder' => $builder,
536 'folder' => $folder_name_full,
537 'widget_id' => $widget_id, // 8-char hash — use with activate/deactivate-widget
538 'files' => [
539 $file_name_full . '.php',
540 $file_name_full . '.json',
541 $file_name_full . '.js',
542 $file_name_full . '.css',
543 ],
544 'version' => $version,
545 'css_class' => $widget_css_class,
546 ],
547 ];
548 }
549
550 /**
551 * Dispatch to the correct PHP boilerplate generator for the given builder.
552 */
553 function wdesignkit_mcp_generate_php(
554 string $builder,
555 string $class_name,
556 string $name,
557 string $folder_name,
558 string $file_name,
559 string $description,
560 string $category,
561 string $widget_hash,
562 string $version = '1.0.0'
563 ): string {
564 switch ($builder) {
565 case 'elementor':
566 return wdesignkit_mcp_elementor_php(
567 $class_name, $name, $description, $category,
568 $file_name, $folder_name, $widget_hash, $version
569 );
570 case 'gutenberg':
571 case 'gutenberg_core':
572 return wdesignkit_mcp_gutenberg_php(
573 $builder, $class_name, $name, $folder_name, $file_name,
574 $description, $widget_hash, $version
575 );
576 case 'bricks':
577 return wdesignkit_mcp_bricks_php(
578 $class_name, $name, $folder_name, $description,
579 $category, $file_name, $widget_hash, $version
580 );
581 default:
582 return "<?php\n// Widget PHP code\n";
583 }
584 }
585
586 /**
587 * Generate Elementor widget PHP boilerplate.
588 *
589 * Mirrors the real WDesignKit widget pattern:
590 * - get_name() → 'wb-{hash}' (unique, avoids slug collisions)
591 * - get_icon() → 'eicon-code tpae-wdkit-logo'
592 * - get_categories()→ reads wkit_builder option dynamically
593 * - Asset URLs → wp_upload_dir() + set_url_scheme() (SSL-safe)
594 * - Enqueue inside get_script_depends() / get_style_depends()
595 * - render() → outer wrapper with data-wdkitunique attribute
596 */
597 function wdesignkit_mcp_elementor_php(
598 string $class_name,
599 string $name,
600 string $description,
601 string $category,
602 string $file_name,
603 string $folder_name,
604 string $widget_hash,
605 string $version = '1.0.0'
606 ): string {
607 $slug = sanitize_title($name);
608 // Avoid double '-widget' suffix when name ends with 'Widget'
609 $widget_class = 'wdkit-' . $slug . (substr($slug, -7) === '-widget' ? '' : '-widget');
610
611 // Loader derives class from filename: 'Wdkit_' + filename (hyphens → underscores)
612 $php_class = 'Wdkit_' . str_replace('-', '_', $file_name);
613
614 // Asset handle IDs — match real widget pattern: wkit_child_script_{hash}, wkit_css_1_{hash}
615 $script_handle = 'wkit_child_script_' . $widget_hash;
616 $style_handle = 'wkit_css_1_' . $widget_hash;
617
618 // Escape for PHP single-quoted string literals in the generated file
619 $safe_name = str_replace(["\\", "'"], ["\\\\", "\\'"], $name);
620 $safe_desc = str_replace(["\\", "'"], ["\\\\", "\\'"], $description);
621
622 return <<<PHP
623 <?php
624 /*
625 * Widget Name: {$name}
626 * Author: POSIMYTH
627 * Author URI: https://posimyth.com
628 *
629 * @package wdesignkit
630 */
631
632 use Elementor\Widget_Base;
633 use Elementor\Controls_Manager;
634 use Elementor\Group_Control_Typography;
635 use Elementor\Group_Control_Border;
636 use Elementor\Group_Control_Background;
637 use Elementor\Group_Control_Box_Shadow;
638 use Elementor\Group_Control_Text_Shadow;
639 use Elementor\Group_Control_Image_Size;
640
641 if ( ! defined( 'ABSPATH' ) ) {
642 exit;
643 }
644
645 /**
646 * Class {$php_class}
647 */
648 class {$php_class} extends Widget_Base {
649
650 /**
651 * Get Widget Name.
652 *
653 * @since 1.0.0
654 */
655 public function get_name() {
656 return 'wb-{$widget_hash}';
657 }
658
659 /**
660 * Get Widget Title.
661 *
662 * @since 1.0.0
663 */
664 public function get_title() {
665 return esc_html__('{$safe_name}', 'wdesignkit');
666 }
667
668 /**
669 * Get Widget Icon.
670 *
671 * @since 1.0.0
672 */
673 public function get_icon() {
674 return 'eicon-code tpae-wdkit-logo';
675 }
676
677 /**
678 * Get Widget Categories.
679 *
680 * @since 1.0.0
681 */
682 public function get_categories() {
683 \$categories = get_option( 'wkit_builder' );
684 if ( ! empty( \$categories ) && is_array( \$categories ) ) {
685 return \$categories;
686 }
687 return array( 'WDesignKit' );
688 }
689
690 /**
691 * Get Widget Keywords.
692 *
693 * @since 1.0.0
694 */
695 public function get_keywords() {
696 return [ 'WDesignKit' ];
697 }
698
699 /**
700 * Get Widget Scripts.
701 *
702 * Enqueues the widget's JS file and returns the handle.
703 *
704 * @since 1.0.0
705 */
706 public function get_script_depends() {
707 \$upload_dir = wp_upload_dir();
708 \$baseurl = set_url_scheme( \$upload_dir['baseurl'], is_ssl() ? 'https' : 'http' );
709 wp_enqueue_script(
710 '{$script_handle}',
711 \$baseurl . '/wdesignkit/elementor/{$folder_name}/{$file_name}.js',
712 [ 'jquery' ],
713 '{$version}',
714 true
715 );
716 return [ '{$script_handle}' ];
717 }
718
719 /**
720 * Get Widget Styles.
721 *
722 * Enqueues the widget's CSS file and returns the handle.
723 *
724 * @since 1.0.0
725 */
726 public function get_style_depends() {
727 \$upload_dir = wp_upload_dir();
728 \$baseurl = set_url_scheme( \$upload_dir['baseurl'], is_ssl() ? 'https' : 'http' );
729 wp_enqueue_style(
730 '{$style_handle}',
731 \$baseurl . '/wdesignkit/elementor/{$folder_name}/{$file_name}.css',
732 false,
733 '{$version}',
734 'all'
735 );
736 return [ '{$style_handle}' ];
737 }
738
739 /**
740 * Register Controls.
741 *
742 * @since 1.0.0
743 */
744 protected function register_controls() {
745
746 // ── Content tab ──────────────────────────────────────────────────────
747 // Section name "Content" matches the JSON section_data layout section
748 // so both the Elementor panel and WDesignKit builder show the same structure.
749 \$this->start_controls_section(
750 'content_section',
751 array(
752 'label' => esc_html__( 'Content', 'wdesignkit' ),
753 'tab' => Controls_Manager::TAB_CONTENT,
754 )
755 );
756
757 \$this->add_control(
758 'title',
759 array(
760 'label' => esc_html__( 'Title', 'wdesignkit' ),
761 'type' => Controls_Manager::TEXT,
762 'default' => esc_html__( '{$safe_name}', 'wdesignkit' ),
763 'ai' => array( 'active' => false ),
764 )
765 );
766
767 \$this->add_control(
768 'description',
769 array(
770 'label' => esc_html__( 'Description', 'wdesignkit' ),
771 'type' => Controls_Manager::TEXTAREA,
772 'default' => esc_html__( '{$safe_desc}', 'wdesignkit' ),
773 'ai' => array( 'active' => false ),
774 )
775 );
776
777 \$this->end_controls_section();
778
779 // ── Style tab ─────────────────────────────────────────────────────────
780 // Section name "Widget Style" matches the JSON section_data style section.
781 // Controls mirror the JSON style controls (dimension, typography, color)
782 // using Elementor's {{WRAPPER}} selector to scope to this widget instance.
783 \$this->start_controls_section(
784 'style_section',
785 array(
786 'label' => esc_html__( 'Widget Style', 'wdesignkit' ),
787 'tab' => Controls_Manager::TAB_STYLE,
788 )
789 );
790
791 \$this->add_control(
792 'widget_padding',
793 array(
794 'label' => esc_html__( 'Widget Padding', 'wdesignkit' ),
795 'type' => Controls_Manager::DIMENSIONS,
796 'size_units' => array( 'px', 'em', 'rem', '%' ),
797 'selectors' => array(
798 '{{WRAPPER}} .{$widget_class}' => 'padding: {{TOP}}{{UNIT}} {{RIGHT}}{{UNIT}} {{BOTTOM}}{{UNIT}} {{LEFT}}{{UNIT}};',
799 ),
800 )
801 );
802
803 \$this->add_group_control(
804 Group_Control_Typography::get_type(),
805 array(
806 'name' => 'title_typography',
807 'label' => esc_html__( 'Title Typography', 'wdesignkit' ),
808 'selector' => '{{WRAPPER}} .{$widget_class}__title',
809 )
810 );
811
812 \$this->add_control(
813 'title_color',
814 array(
815 'label' => esc_html__( 'Title Color', 'wdesignkit' ),
816 'type' => Controls_Manager::COLOR,
817 'default' => '#374151',
818 'selectors' => array(
819 '{{WRAPPER}} .{$widget_class}__title' => 'color: {{VALUE}};',
820 ),
821 )
822 );
823
824 \$this->add_group_control(
825 Group_Control_Typography::get_type(),
826 array(
827 'name' => 'description_typography',
828 'label' => esc_html__( 'Description Typography', 'wdesignkit' ),
829 'selector' => '{{WRAPPER}} .{$widget_class}__desc',
830 )
831 );
832
833 \$this->add_control(
834 'description_color',
835 array(
836 'label' => esc_html__( 'Description Color', 'wdesignkit' ),
837 'type' => Controls_Manager::COLOR,
838 'default' => '#6b7280',
839 'selectors' => array(
840 '{{WRAPPER}} .{$widget_class}__desc' => 'color: {{VALUE}};',
841 ),
842 )
843 );
844
845 \$this->end_controls_section();
846 }
847
848 /**
849 * Render widget output.
850 *
851 * @since 1.0.0
852 */
853 protected function render() {
854 \$settings = \$this->get_settings_for_display();
855 \$title = ! empty( \$settings['title'] ) ? \$settings['title'] : '';
856 \$description = ! empty( \$settings['description'] ) ? \$settings['description'] : '';
857
858 \$output = '';
859 \$output .= '<div class="{$widget_class} wkit-wb-{$file_name}" data-wdkitunique="{$widget_hash}">';
860 \$output .= '<h3 class="{$widget_class}__title">' . esc_html( \$title ) . '</h3>';
861 \$output .= '<p class="{$widget_class}__desc">' . esc_html( \$description ) . '</p>';
862 \$output .= '</div>';
863
864 echo \$output;
865 }
866 }
867 PHP;
868 }
869
870 /**
871 * Auto-correct common AI mistakes in a user-provided Elementor widget PHP file.
872 *
873 * Fixes applied (in order):
874 * 1. Class name — ensures it is exactly Wdkit_{file_name} (includes hash).
875 * 2. get_name() — ensures it returns 'wb-{hash}'.
876 * 3. get_script_depends() — injects the method if absent.
877 * 4. get_style_depends() — injects the method if absent.
878 *
879 * All replacements are safe no-ops when the correct value is already present.
880 */
881 function wdesignkit_mcp_fix_elementor_php(
882 string $php_code,
883 string $file_name,
884 string $widget_hash,
885 string $folder_name,
886 string $version
887 ): string {
888 $expected_class = 'Wdkit_' . str_replace('-', '_', $file_name);
889 $script_handle = 'wkit_child_script_' . $widget_hash;
890 $style_handle = 'wkit_css_1_' . $widget_hash;
891
892 // 1. Fix class name — replace ANY class declaration that is not the expected Wdkit_ name.
893 // The loader always derives the class as 'Wdkit_' + php_filename_without_extension.
894 // Previously this only caught Wdkit_* prefixed names — custom PHP code with any other
895 // naming convention (WDK_*, My_Widget_Widget, etc.) was silently left unchanged,
896 // causing a PHP Fatal "Class not found" that crashed the entire site (bug: info-box_9c307c).
897 if (!preg_match('/\bclass\s+' . preg_quote($expected_class, '/') . '\s+extends\b/', $php_code)) {
898 $php_code = preg_replace(
899 '/\bclass\s+(\w+)\s+extends\b/',
900 'class ' . $expected_class . ' extends',
901 $php_code,
902 1 // replace first occurrence only — safe since widget PHP files have one main class
903 ) ?? $php_code;
904 }
905
906 // 2. Fix get_name() return value — must be 'wb-{hash}'.
907 if (strpos($php_code, "'wb-{$widget_hash}'") === false && strpos($php_code, "\"wb-{$widget_hash}\"") === false) {
908 $php_code = preg_replace(
909 '/(function\s+get_name\s*\(\s*\)\s*\{[^}]*?\breturn\s+)[\'"][^\'"]*[\'"]/s',
910 "$1'wb-{$widget_hash}'",
911 $php_code,
912 1
913 ) ?? $php_code;
914 }
915
916 // 3. Inject get_script_depends() before register_controls if absent.
917 if (strpos($php_code, 'get_script_depends') === false) {
918 $method = "\n\tpublic function get_script_depends() {\n"
919 . "\t\t\$upload_dir = wp_upload_dir();\n"
920 . "\t\t\$baseurl = set_url_scheme( \$upload_dir['baseurl'], is_ssl() ? 'https' : 'http' );\n"
921 . "\t\twp_enqueue_script( '{$script_handle}', \$baseurl . '/wdesignkit/elementor/{$folder_name}/{$file_name}.js', array(), '{$version}', true );\n"
922 . "\t\treturn array( '{$script_handle}' );\n"
923 . "\t}\n";
924 $php_code = preg_replace(
925 '/(\bprotected\s+function\s+register_controls\b)/',
926 $method . "\tprotected function register_controls",
927 $php_code,
928 1
929 ) ?? $php_code;
930 }
931
932 // 4. Inject get_style_depends() before register_controls if absent.
933 if (strpos($php_code, 'get_style_depends') === false) {
934 $method = "\n\tpublic function get_style_depends() {\n"
935 . "\t\t\$upload_dir = wp_upload_dir();\n"
936 . "\t\t\$baseurl = set_url_scheme( \$upload_dir['baseurl'], is_ssl() ? 'https' : 'http' );\n"
937 . "\t\twp_enqueue_style( '{$style_handle}', \$baseurl . '/wdesignkit/elementor/{$folder_name}/{$file_name}.css', false, '{$version}', 'all' );\n"
938 . "\t\treturn array( '{$style_handle}' );\n"
939 . "\t}\n";
940 $php_code = preg_replace(
941 '/(\bprotected\s+function\s+register_controls\b)/',
942 $method . "\tprotected function register_controls",
943 $php_code,
944 1
945 ) ?? $php_code;
946 }
947
948 return $php_code;
949 }
950
951 /**
952 * Generate Gutenberg / Gutenberg Core block PHP boilerplate.
953 *
954 * Uses wp_upload_dir() for SSL-safe asset URLs.
955 * Uses $file_name (includes unique hash) for function name — no collision possible.
956 */
957 function wdesignkit_mcp_gutenberg_php(
958 string $builder,
959 string $class_name,
960 string $name,
961 string $folder_name,
962 string $file_name,
963 string $description,
964 string $widget_hash,
965 string $version = '1.0.0'
966 ): string {
967 $slug = sanitize_title($name);
968
969 // Collision-safe function name: derived from full $file_name (includes unique hash)
970 $func_name = str_replace(['-', '.'], '_', $file_name);
971
972 return <<<PHP
973 <?php
974 /*
975 * Widget Name: {$name}
976 * Author: POSIMYTH
977 * Author URI: https://posimyth.com
978 *
979 * @package wdesignkit
980 */
981
982 if ( ! defined( 'ABSPATH' ) ) {
983 exit;
984 }
985
986 /**
987 * {$name} - Gutenberg Block
988 * {$description}
989 */
990 if ( ! function_exists( 'wdkit_{$func_name}_register_block' ) ) {
991 function wdkit_{$func_name}_register_block() {
992 \$upload_dir = wp_upload_dir();
993 \$baseurl = set_url_scheme( \$upload_dir['baseurl'], is_ssl() ? 'https' : 'http' );
994
995 wp_register_script(
996 'wdkit-{$slug}-editor',
997 \$baseurl . '/wdesignkit/{$builder}/{$folder_name}/{$file_name}.js',
998 [ 'wp-blocks', 'wp-element', 'wp-editor', 'wp-components', 'wp-i18n' ],
999 '{$version}',
1000 true
1001 );
1002
1003 wp_register_style(
1004 'wdkit-{$slug}-style',
1005 \$baseurl . '/wdesignkit/{$builder}/{$folder_name}/{$file_name}.css',
1006 [],
1007 '{$version}'
1008 );
1009
1010 register_block_type( 'wdkit/{$slug}', [
1011 'editor_script' => 'wdkit-{$slug}-editor',
1012 'editor_style' => 'wdkit-{$slug}-style',
1013 'style' => 'wdkit-{$slug}-style',
1014 ] );
1015 }
1016 }
1017 add_action( 'init', 'wdkit_{$func_name}_register_block' );
1018 PHP;
1019 }
1020
1021 /**
1022 * Generate Bricks builder element PHP boilerplate.
1023 *
1024 * Uses wp_upload_dir() for SSL-safe asset URLs.
1025 */
1026 function wdesignkit_mcp_bricks_php(
1027 string $class_name,
1028 string $name,
1029 string $folder_name,
1030 string $description,
1031 string $category,
1032 string $file_name,
1033 string $widget_hash,
1034 string $version = '1.0.0'
1035 ): string {
1036 $slug = sanitize_title($name);
1037 // Avoid double '-widget' suffix when name ends with 'Widget'
1038 $widget_class = 'wdkit-' . $slug . (substr($slug, -7) === '-widget' ? '' : '-widget');
1039
1040 // Escape for PHP single-quoted string literals in the generated file
1041 $safe_name = str_replace(["\\", "'"], ["\\\\", "\\'"], $name);
1042 $safe_desc = str_replace(["\\", "'"], ["\\\\", "\\'"], $description);
1043
1044 return <<<PHP
1045 <?php
1046 /*
1047 * Widget Name: {$name}
1048 * Author: POSIMYTH
1049 * Author URI: https://posimyth.com
1050 *
1051 * @package wdesignkit
1052 */
1053
1054 if ( ! defined( 'ABSPATH' ) ) {
1055 exit;
1056 }
1057
1058 class {$class_name}_Bricks extends \\Bricks\\Element {
1059 public \$category = '{$category}';
1060 public \$name = 'wdkit-{$slug}';
1061 public \$icon = 'ti-widget';
1062
1063 public function get_label() {
1064 return esc_html__( '{$safe_name}', 'wdesignkit' );
1065 }
1066
1067 public function enqueue_scripts() {
1068 \$upload_dir = wp_upload_dir();
1069 \$baseurl = set_url_scheme( \$upload_dir['baseurl'], is_ssl() ? 'https' : 'http' );
1070 wp_enqueue_style(
1071 'wdkit-{$slug}-style',
1072 \$baseurl . '/wdesignkit/bricks/{$folder_name}/{$file_name}.css',
1073 [],
1074 '{$version}'
1075 );
1076 }
1077
1078 public function set_controls() {
1079 // ── Content controls ──────────────────────────────────────────────────
1080 // "Content" matches the JSON section_data layout section name.
1081 \$this->controls['title'] = [
1082 'tab' => 'content',
1083 'label' => esc_html__( 'Title', 'wdesignkit' ),
1084 'type' => 'text',
1085 'default' => '{$safe_name}',
1086 ];
1087
1088 \$this->controls['description'] = [
1089 'tab' => 'content',
1090 'label' => esc_html__( 'Description', 'wdesignkit' ),
1091 'type' => 'textarea',
1092 'default' => '{$safe_desc}',
1093 ];
1094
1095 // ── Style controls ────────────────────────────────────────────────────
1096 // "Widget Style" matches the JSON section_data style section name.
1097 // Selectors target the classes output by render() — must stay in sync.
1098 \$this->controls['widget_padding'] = [
1099 'tab' => 'style',
1100 'label' => esc_html__( 'Widget Padding', 'wdesignkit' ),
1101 'type' => 'spacing',
1102 'css' => [ [ 'selector' => '', 'property' => 'padding' ] ],
1103 ];
1104
1105 \$this->controls['title_typography'] = [
1106 'tab' => 'style',
1107 'label' => esc_html__( 'Title Typography', 'wdesignkit' ),
1108 'type' => 'typography',
1109 'css' => [ [ 'selector' => '.{$widget_class}__title' ] ],
1110 ];
1111
1112 \$this->controls['title_color'] = [
1113 'tab' => 'style',
1114 'label' => esc_html__( 'Title Color', 'wdesignkit' ),
1115 'type' => 'color',
1116 'default' => [ 'hex' => '#374151' ],
1117 'css' => [ [ 'selector' => '.{$widget_class}__title', 'property' => 'color' ] ],
1118 ];
1119
1120 \$this->controls['description_typography'] = [
1121 'tab' => 'style',
1122 'label' => esc_html__( 'Description Typography', 'wdesignkit' ),
1123 'type' => 'typography',
1124 'css' => [ [ 'selector' => '.{$widget_class}__desc' ] ],
1125 ];
1126
1127 \$this->controls['description_color'] = [
1128 'tab' => 'style',
1129 'label' => esc_html__( 'Description Color', 'wdesignkit' ),
1130 'type' => 'color',
1131 'default' => [ 'hex' => '#6b7280' ],
1132 'css' => [ [ 'selector' => '.{$widget_class}__desc', 'property' => 'color' ] ],
1133 ];
1134 }
1135
1136 public function render() {
1137 \$title = ! empty( \$this->settings['title'] ) ? \$this->settings['title'] : '';
1138 \$desc = ! empty( \$this->settings['description'] ) ? \$this->settings['description'] : '';
1139
1140 echo '<div class="{$widget_class} wkit-wb-{$file_name}" data-wdkitunique="{$widget_hash}">';
1141 echo '<h3 class="{$widget_class}__title">' . esc_html( \$title ) . '</h3>';
1142 echo '<p class="{$widget_class}__desc">' . esc_html( \$desc ) . '</p>';
1143 echo '</div>';
1144 }
1145 }
1146 PHP;
1147 }
1148
1149 /**
1150 * Generate builder-appropriate JS boilerplate.
1151 */
1152 function wdesignkit_mcp_generate_js(
1153 string $builder,
1154 string $class_name,
1155 string $name,
1156 string $folder_name,
1157 string $file_name,
1158 string $description,
1159 string $category = 'WDesignKit',
1160 string $widget_hash = ''
1161 ): string {
1162 $slug = sanitize_title($name);
1163
1164 // Escape values for embedding in JS single-quoted string literals
1165 $safe_name = str_replace(["\\", "'", "\n", "\r"], ["\\\\", "\\'", "\\n", "\\r"], $name);
1166 $safe_desc = str_replace(["\\", "'", "\n", "\r"], ["\\\\", "\\'", "\\n", "\\r"], $description);
1167 $safe_cat = str_replace(["\\", "'", "\n", "\r"], ["\\\\", "\\'", "\\n", "\\r"], $category);
1168
1169 if ($builder === 'elementor') {
1170 // No JS boilerplate for Elementor — most widgets need no JavaScript.
1171 // When JS is needed, callers provide js_code using $scope[0].querySelector() directly.
1172 // WDesignKit executes widget JS in the element_ready scope automatically.
1173 return '';
1174 }
1175
1176 if ($builder === 'gutenberg' || $builder === 'gutenberg_core') {
1177 return <<<JS
1178 /**
1179 * {$name} - Gutenberg Block
1180 */
1181 (function(blocks, element, blockEditor, components, i18n) {
1182 var el = element.createElement;
1183 var __ = i18n.__;
1184 var useBlockProps = blockEditor.useBlockProps;
1185 var InspectorControls = blockEditor.InspectorControls;
1186 var TextControl = components.TextControl;
1187 var TextareaControl = components.TextareaControl;
1188 var PanelBody = components.PanelBody;
1189
1190 blocks.registerBlockType('wdkit/{$slug}', {
1191 title: __('{$safe_name}', 'wdesignkit'),
1192 description: __('{$safe_desc}', 'wdesignkit'),
1193 icon: 'block-default',
1194 category: '{$safe_cat}',
1195 attributes: {
1196 title: { type: 'string', default: '{$safe_name}' },
1197 description: { type: 'string', default: '{$safe_desc}' }
1198 },
1199
1200 edit: function(props) {
1201 var blockProps = useBlockProps();
1202 return el('div', blockProps,
1203 el(InspectorControls, {},
1204 el(PanelBody, { title: __('{$safe_name} Settings', 'wdesignkit') },
1205 el(TextControl, {
1206 label: __('Title', 'wdesignkit'),
1207 value: props.attributes.title,
1208 onChange: function(val) { props.setAttributes({ title: val }); }
1209 }),
1210 el(TextareaControl, {
1211 label: __('Description', 'wdesignkit'),
1212 value: props.attributes.description,
1213 onChange: function(val) { props.setAttributes({ description: val }); }
1214 })
1215 )
1216 ),
1217 el('div', { className: 'wdkit-{$slug}-widget' },
1218 el('h3', {}, props.attributes.title),
1219 el('p', {}, props.attributes.description)
1220 )
1221 );
1222 },
1223
1224 save: function(props) {
1225 var blockProps = useBlockProps.save();
1226 return el('div', blockProps,
1227 el('div', { className: 'wdkit-{$slug}-widget' },
1228 el('h3', {}, props.attributes.title),
1229 el('p', {}, props.attributes.description)
1230 )
1231 );
1232 }
1233 });
1234 })(
1235 window.wp.blocks,
1236 window.wp.element,
1237 window.wp.blockEditor,
1238 window.wp.components,
1239 window.wp.i18n
1240 );
1241 JS;
1242 }
1243
1244 if ($builder === 'bricks') {
1245 return <<<JS
1246 /**
1247 * {$name} - Bricks Builder Widget JS
1248 */
1249 (function() {
1250 'use strict';
1251 document.addEventListener('DOMContentLoaded', function() {
1252 // Bricks frontend logic for {$name}
1253 });
1254 })();
1255 JS;
1256 }
1257
1258 return "// Widget JS\n";
1259 }
1260
1261 /**
1262 * Generate the complete JSON config that WDesignKit uses to identify, manage,
1263 * and render the widget in the builder UI.
1264 *
1265 * Matches the full structure of real marketplace/downloaded widgets:
1266 * - section_data : WDesignKit builder panel definitions (layout + style sections)
1267 * - widget_data : all metadata fields (matches real widget format per builder)
1268 * - Editor_Link : JS/CSS asset links for the builder
1269 * - Editor_data : html/css/js template used by the WDesignKit builder renderer
1270 *
1271 * IMPORTANT: $widget_id must be the 8-char hash only (e.g. "kmrd6l24"),
1272 * NOT the full folder name. This matches the real widget JSON format.
1273 * The loaders read widget_id from JSON for deactivation checks.
1274 *
1275 * Builder differences (verified against real marketplace widgets):
1276 * - Elementor/Bricks : key_words=array, w_image=URL, img_ext="png", r_id=0, allow_push=false
1277 * - Gutenberg : key_words="", w_image="", no img_ext/r_id/allow_push, build_widget="standard"
1278 * - Bricks : Editor_Link entry has extra external_cdn:[] field
1279 * - version_detail : always array of plain strings (never array of objects)
1280 */
1281 function wdesignkit_mcp_generate_json_config(
1282 string $name,
1283 string $builder,
1284 string $widget_id, // 8-char hash only, e.g. "kmrd6l24"
1285 string $folder_name,
1286 string $file_name,
1287 string $description,
1288 string $version,
1289 string $category,
1290 string $helper_link = '',
1291 string $icon = 'eicon-code',
1292 array $keywords = [],
1293 string $css_code = '',
1294 string $js_code = '',
1295 string $widget_css_class = '',
1296 ?array $section_data = null, // caller-provided section_data; null = auto-generate
1297 ?string $editor_html = null, // caller-provided canvas HTML; null = auto-generate
1298 string $php_code = '', // PHP source — auto-parsed when section_data is null
1299 array $cdn_js = [''], // JS CDN URLs for Editor_Link ('' = no external JS)
1300 array $cdn_css = [''] // CSS CDN URLs for Editor_Link ('' = no external CSS)
1301 ): array {
1302 $is_gutenberg = ($builder === 'gutenberg' || $builder === 'gutenberg_core');
1303
1304 // Compute widget CSS class if not provided by caller
1305 if (empty($widget_css_class)) {
1306 $_slug = sanitize_title($name);
1307 $widget_css_class = 'wdkit-' . $_slug . (substr($_slug, -7) === '-widget' ? '' : '-widget');
1308 }
1309
1310 // w_image: gutenberg always empty; elementor/bricks use full URL with .png extension
1311 $image_url = '';
1312 if (!$is_gutenberg && defined('WDKIT_SERVER_PATH')) {
1313 $image_url = WDKIT_SERVER_PATH . '/' . $builder . '/' . $folder_name . '/' . $file_name . '.png';
1314 }
1315
1316 $wkit_version = defined('WDKIT_VERSION') ? WDKIT_VERSION : '2.3.3';
1317
1318 // ── widgetdata ─────────────────────────────────────────────────────────────
1319 // Common fields present in all builders
1320 $widgetdata = [
1321 'name' => $name,
1322 'description' => $description,
1323 'category' => $category,
1324 'helper_link' => $helper_link,
1325 'type' => $builder,
1326 'w_icon' => $icon,
1327 'w_image' => $image_url,
1328 'publish_type' => 'Publish',
1329 // key_words: real gutenberg widgets use "" (empty string); elementor/bricks use an array
1330 'key_words' => $is_gutenberg ? '' : $keywords,
1331 'css_parent_node' => true,
1332 'widget_id' => $widget_id,
1333 'widget_version' => $version,
1334 // version_detail: real marketplace widgets always use plain string array, NOT array of objects
1335 'version_detail' => ['Initial Release'],
1336 'wkit-version' => $wkit_version,
1337 ];
1338
1339 if ($is_gutenberg) {
1340 // Gutenberg: add build_widget; omit allow_push, img_ext, r_id (not present in real gutenberg widgets)
1341 $widgetdata['build_widget'] = 'standard';
1342 } else {
1343 // Elementor/Bricks: add allow_push, img_ext (always "png"), r_id
1344 $widgetdata['allow_push'] = false;
1345 $widgetdata['img_ext'] = 'png';
1346 $widgetdata['r_id'] = 0;
1347 }
1348
1349 // ── Editor_Link ────────────────────────────────────────────────────────────
1350 // js/css arrays hold CDN URLs the builder loads before running the widget.
1351 // '' means no external dependency; real URLs are passed via cdn_js / cdn_css params.
1352 // Bricks widgets have an extra external_cdn:[] field inside the links object.
1353 $link_entry = ['js' => $cdn_js, 'css' => $cdn_css];
1354 if ($builder === 'bricks') {
1355 $link_entry['external_cdn'] = [];
1356 }
1357
1358 // ── section_data ───────────────────────────────────────────────────────────
1359 // Priority order:
1360 // 1. Caller-provided section_data (verbatim — exact override)
1361 // 2. Auto-parsed from php_code — extracts every register_controls() call so
1362 // the JSON perfectly mirrors the PHP without any manual work
1363 // 3. Default generic text+textarea+style template (fallback for default boilerplate)
1364 if ($section_data === null && !empty($php_code)) {
1365 $parsed = wdesignkit_mcp_parse_php_section_data($php_code, $widget_css_class);
1366 if ($parsed !== null) {
1367 $section_data = $parsed;
1368 }
1369 }
1370 if ($section_data === null) {
1371 $section_data = wdesignkit_mcp_generate_section_data(
1372 $builder, $name, $description, $widget_id, $widget_css_class
1373 );
1374 }
1375
1376 // ── Editor_data.html ───────────────────────────────────────────────────────
1377 // Auto-generated from the final section_data so placeholder names always match.
1378 // Button-like widgets (URL control present) get a full <a>-wrapped template.
1379 // Other widgets get an <h3>+<p> template using the first text/textarea controls.
1380 if ($editor_html === null) {
1381 $editor_html = wdesignkit_mcp_generate_editor_html_from_section_data(
1382 $widget_id, $widget_css_class, $file_name, $section_data
1383 );
1384 }
1385
1386 return [
1387 'section_data' => $section_data,
1388 'widget_data' => ['widgetdata' => $widgetdata],
1389 'Editor_Link' => ['links' => [$link_entry]],
1390 'Editor_data' => [
1391 'html' => $editor_html,
1392 'css' => $css_code,
1393 'js' => $js_code,
1394 ],
1395 ];
1396 }
1397
1398 /**
1399 * Generate the "Need Help?" rawhtml control content — matches the static
1400 * support-links HTML used in every real marketplace widget.
1401 *
1402 * @param string $helper_link Optional docs URL. If non-empty, a "Read Documentation"
1403 * link pointing to it is inserted.
1404 */
1405 function wdesignkit_mcp_help_html(string $helper_link = ''): string {
1406 // Inline styles match the real marketplace widget format exactly (no vendor prefixes, no spaces in CSS values)
1407 $ls = 'color:var(--e-a-color-txt-accent);text-decoration:none;border-color:transparent';
1408 $bs = 'margin:auto;color:#fff;background:#8072fc;padding:10px 20px;border-radius:5px;'
1409 . 'font-weight:400;font-size:13px;letter-spacing:0.4px;border:1px solid #8072fc;'
1410 . 'box-shadow:0 2px 7px 0 rgba(0,0,0,0.3);';
1411
1412 $docs_html = '';
1413 if (!empty($helper_link)) {
1414 $safe_url = esc_url($helper_link);
1415 $docs_html = '<div class="wdk-help" style="margin-bottom:15px">'
1416 . '<a class="wdk-docs-link" style="' . $ls . '" href="' . $safe_url . '" target="_blank" rel="noopener noreferrer">Read Documentation</a>'
1417 . '</div>';
1418 }
1419
1420 return '<div class="wdk-help-main" style="height:300px">'
1421 . '<div class="wdk-help" style="margin-bottom:15px">'
1422 . '<a class="wdk-docs-link" style="' . $ls . '" href="https://store.posimyth.com/helpdesk/" target="_blank" rel="noopener noreferrer">Raise a Ticket</a>'
1423 . '</div>'
1424 . $docs_html
1425 . '<div class="wdk-help" style="margin-bottom:15px">'
1426 . '<a class="wdk-docs-link" style="' . $ls . '" href="https://roadmap.wdesignkit.com/boards/feature-requests" target="_blank" rel="noopener noreferrer">Suggest Feature</a>'
1427 . '</div>'
1428 . '<div class="wdk-help" style="margin-bottom:15px">'
1429 . '<a class="wdk-docs-link" style="' . $ls . '" href="https://roadmap.wdesignkit.com/roadmap" target="_blank" rel="noopener noreferrer">Plugin Roadmap</a>'
1430 . '</div>'
1431 . '<div class="wdk-help" style="margin-bottom:15px">'
1432 . '<a class="wdk-docs-link" style="' . $ls . '" href="https://www.facebook.com/wdesignkit" target="_blank" rel="noopener noreferrer">Join Facebook Community</a>'
1433 . '</div>'
1434 . '<div class="need-help" id="elementor-panel__editor__help">'
1435 . '<a id="elementor-panel__editor__help__link" href="https://wordpress.org/support/plugin/wdesignkit/" target="_blank" style="' . $bs . '">Need Help <i class="eicon-help-o" aria-hidden="true"></i></a>'
1436 . '</div>'
1437 . '</div>';
1438 }
1439
1440 /**
1441 * Generate the Editor_data HTML template for the WDesignKit builder canvas.
1442 *
1443 * Uses {{control_name}} placeholders that must match section_data control names exactly.
1444 * Control hashes are derived deterministically from widget_id so they are always in sync.
1445 *
1446 * @param string $widget_id 8-char widget hash
1447 * @param string $widget_css_class e.g. "wdkit-pricing-table-widget"
1448 * @param string $file_name e.g. "pricing_table_ab12cd34"
1449 */
1450 function wdesignkit_mcp_generate_editor_html(
1451 string $widget_id,
1452 string $widget_css_class,
1453 string $file_name
1454 ): string {
1455 // Deterministic hashes must match those generated in wdesignkit_mcp_generate_section_data()
1456 $h_title = substr(md5($widget_id . ':title'), 0, 8);
1457 $h_desc = substr(md5($widget_id . ':desc'), 0, 8);
1458
1459 return '<div class="' . $widget_css_class . ' wkit-wb-' . $file_name . '" data-wdkitunique="' . $widget_id . '">' . "\n"
1460 . ' <h3 class="' . $widget_css_class . '__title">{{text_' . $h_title . '}}</h3>' . "\n"
1461 . ' <p class="' . $widget_css_class . '__desc">{{textarea_' . $h_desc . '}}</p>' . "\n"
1462 . '</div>' . "\n";
1463 }
1464
1465 /**
1466 * Generate the default section_data for a new widget.
1467 *
1468 * This is the FALLBACK used when the caller does not provide custom section_data.
1469 * For custom PHP controls (register_controls with non-default fields), always pass
1470 * matching section_data directly to wdesignkit_mcp_generate_json_config() instead.
1471 *
1472 * Produces 1 layout section + 1 style section — a complete 1:1 mapping of the
1473 * default PHP boilerplate's register_controls():
1474 *
1475 * Layout (maps to PHP TAB_CONTENT "Content" section):
1476 * - title (text) ← PHP TEXT 'title', default = $name
1477 * - description (textarea) ← PHP TEXTAREA 'description', default = $description
1478 *
1479 * Style (maps to PHP TAB_STYLE "Widget Style" section):
1480 * - widget_padding (dimension) ← PHP DIMENSIONS 'widget_padding' → .{class}
1481 * - title_typography (typography) ← PHP Typography 'title_typography' → .{class}__title
1482 * - title_color (color) ← PHP COLOR 'title_color' → .{class}__title #374151
1483 * - description_typography ← PHP Typography 'description_typography' → .{class}__desc
1484 * - description_color (color) ← PHP COLOR 'description_color' → .{class}__desc #6b7280
1485 *
1486 * NOTE: "Need Help?" is intentionally NOT generated — it is a UI panel for end-users
1487 * of marketplace widgets, not part of the widget's control schema.
1488 *
1489 * Control name format: {type}_{8charHash} e.g. "text_kmrd6l24"
1490 * Hashes are deterministic (derived from widget_id) so they always match the
1491 * {{control_name}} placeholders in Editor_data.html.
1492 *
1493 * Control object fields differ slightly between builders:
1494 * - Gutenberg adds unique_id, ai_support; changes condition_value key order
1495 * - Elementor/Bricks use the standard field set
1496 */
1497 function wdesignkit_mcp_generate_section_data(
1498 string $builder,
1499 string $name,
1500 string $description, // widget description — used as textarea defaultValue to match PHP
1501 string $widget_id,
1502 string $widget_css_class
1503 ): array {
1504 $is_gutenberg = ($builder === 'gutenberg' || $builder === 'gutenberg_core');
1505
1506 // Deterministic 8-char hashes — same seed → same name every time.
1507 // These MUST stay in sync with wdesignkit_mcp_generate_editor_html().
1508 $h_title = substr(md5($widget_id . ':title'), 0, 8);
1509 $h_desc = substr(md5($widget_id . ':desc'), 0, 8);
1510 $h_pad = substr(md5($widget_id . ':pad'), 0, 8);
1511 $h_ttyt = substr(md5($widget_id . ':ttyt'), 0, 8);
1512 $h_tcol = substr(md5($widget_id . ':tcol'), 0, 8);
1513 $h_dtyt = substr(md5($widget_id . ':dtyt'), 0, 8);
1514 $h_dcol = substr(md5($widget_id . ':dcol'), 0, 8);
1515
1516 // condition_value key order differs between builders (verified from real widget files)
1517 $cv = $is_gutenberg
1518 ? ['values' => [['name' => '', 'value' => '', 'operator' => '==']], 'relation' => 'or']
1519 : ['relation' => 'or', 'values' => [['name' => '', 'operator' => '==', 'value' => '']]];
1520
1521 // ── Layout: "Content" section ─────────────────────────────────────────────
1522 // Mirrors PHP register_controls() "content_section" exactly:
1523 // title (TEXT) + description (TEXTAREA) — same two controls, same defaults.
1524 if ($is_gutenberg) {
1525 // Gutenberg controls: name first, unique_id + ai_support required, sanitizer_value=""
1526 $ctrl_title = [
1527 'name' => 'text_' . $h_title,
1528 'type' => 'text',
1529 'class' => '',
1530 'lable' => 'Title',
1531 'dynamic' => false,
1532 'showLable' => true,
1533 'unique_id' => $h_title,
1534 'ai_support' => false,
1535 'conditions' => false,
1536 'input_type' => 'text',
1537 'lableBlock' => true,
1538 'description' => '',
1539 'placeHolder' => '',
1540 'defaultValue' => $name,
1541 'condition_value' => $cv,
1542 'sanitizer_value' => '',
1543 'sanitizer_dropdown_value' => 'wdk_senitize_js',
1544 ];
1545 $ctrl_desc = [
1546 'name' => 'textarea_' . $h_desc,
1547 'type' => 'textarea',
1548 'class' => '',
1549 'lable' => 'Description',
1550 'dynamic' => false,
1551 'showLable' => true,
1552 'unique_id' => $h_desc,
1553 'ai_support' => false,
1554 'conditions' => false,
1555 'lableBlock' => true,
1556 'description' => '',
1557 'placeHolder' => '',
1558 'defaultValue' => $description,
1559 'condition_value' => $cv,
1560 'sanitizer_value' => '',
1561 'sanitizer_dropdown_value' => 'wdk_senitize_js',
1562 ];
1563 } else {
1564 // Elementor/Bricks controls: type first, dynamic=true for text, sanitizer_value set
1565 $ctrl_title = [
1566 'type' => 'text',
1567 'lable' => 'Title',
1568 'name' => 'text_' . $h_title,
1569 'description' => '',
1570 'placeHolder' => '',
1571 'defaultValue' => $name,
1572 'showLable' => true,
1573 'lableBlock' => false,
1574 'separator' => 'default',
1575 'controlClass' => '',
1576 'input_type' => 'text',
1577 'dynamic' => true,
1578 'conditions' => false,
1579 'condition_value' => $cv,
1580 'class' => '',
1581 'sanitizer_value' => 'wdk_senitize_js',
1582 'sanitizer_dropdown_value' => 'wdk_senitize_js',
1583 ];
1584 $ctrl_desc = [
1585 'type' => 'textarea',
1586 'lable' => 'Description',
1587 'name' => 'textarea_' . $h_desc,
1588 'description' => '',
1589 'placeHolder' => '',
1590 'defaultValue' => $description,
1591 'showLable' => true,
1592 'lableBlock' => true,
1593 'separator' => 'default',
1594 'controlClass' => '',
1595 'dynamic' => false,
1596 'conditions' => false,
1597 'condition_value' => $cv,
1598 'class' => '',
1599 'sanitizer_value' => 'wdk_senitize_js',
1600 'sanitizer_dropdown_value' => 'wdk_senitize_js',
1601 ];
1602 }
1603
1604 $layout_content = [
1605 'name' => 'builder-1',
1606 'section' => 'Content',
1607 'compo_index' => 1,
1608 'inner_sec' => [$ctrl_title, $ctrl_desc],
1609 ];
1610
1611 // ── Style: "Widget Style" section — mirrors PHP TAB_STYLE section ───────────
1612 $ctrl_padding = [
1613 'type' => 'dimension',
1614 'lable' => 'Widget Padding',
1615 'name' => 'dimension_' . $h_pad,
1616 'description' => '',
1617 'showLable' => true,
1618 'lableBlock' => true,
1619 'separator' => 'default',
1620 'dimension_units' => ['px', 'em', 'rem', '%'],
1621 'dimension_defaultValue' => ['top' => '', 'right' => '', 'bottom' => '', 'left' => '', 'unit' => 'px', 'isLinked' => true],
1622 'selectors' => '.' . $widget_css_class,
1623 'responsive' => false,
1624 'selector_value' => 'padding',
1625 'controlClass' => '',
1626 'conditions' => false,
1627 'condition_value' => $cv,
1628 ];
1629 if ($is_gutenberg) {
1630 $ctrl_padding['unique_id'] = $h_pad;
1631 $ctrl_padding['ai_support'] = false;
1632 }
1633
1634 $ctrl_title_typo = [
1635 'type' => 'typography',
1636 'lable' => 'Title Typography',
1637 'name' => 'typography_' . $h_ttyt,
1638 'separator' => 'default',
1639 'conditions' => false,
1640 'selector' => '.' . $widget_css_class . '__title',
1641 'controlClass' => '',
1642 'condition_value' => $cv,
1643 ];
1644 if ($is_gutenberg) {
1645 $ctrl_title_typo['showLable'] = true;
1646 $ctrl_title_typo['lableBlock'] = true;
1647 $ctrl_title_typo['unique_id'] = $h_ttyt;
1648 $ctrl_title_typo['ai_support'] = false;
1649 $ctrl_title_typo['fields'] = [];
1650 }
1651
1652 $ctrl_title_color = [
1653 'type' => 'color',
1654 'lable' => 'Title Color',
1655 'name' => 'color_' . $h_tcol,
1656 'description' => '',
1657 'defaultValue' => '#374151',
1658 'showLable' => true,
1659 'lableBlock' => true,
1660 'separator' => 'default',
1661 'alpha' => true,
1662 'global' => true,
1663 'selectors' => '.' . $widget_css_class . '__title',
1664 'selector_value' => 'color',
1665 'controlClass' => '',
1666 'conditions' => false,
1667 'condition_value' => $cv,
1668 ];
1669 if ($is_gutenberg) {
1670 $ctrl_title_color['unique_id'] = $h_tcol;
1671 $ctrl_title_color['ai_support'] = false;
1672 }
1673
1674 $ctrl_desc_typo = [
1675 'type' => 'typography',
1676 'lable' => 'Description Typography',
1677 'name' => 'typography_' . $h_dtyt,
1678 'separator' => 'default',
1679 'conditions' => false,
1680 'selector' => '.' . $widget_css_class . '__desc',
1681 'controlClass' => '',
1682 'condition_value' => $cv,
1683 ];
1684 if ($is_gutenberg) {
1685 $ctrl_desc_typo['showLable'] = true;
1686 $ctrl_desc_typo['lableBlock'] = true;
1687 $ctrl_desc_typo['unique_id'] = $h_dtyt;
1688 $ctrl_desc_typo['ai_support'] = false;
1689 $ctrl_desc_typo['fields'] = [];
1690 }
1691
1692 $ctrl_desc_color = [
1693 'type' => 'color',
1694 'lable' => 'Description Color',
1695 'name' => 'color_' . $h_dcol,
1696 'description' => '',
1697 'defaultValue' => '#6b7280',
1698 'showLable' => true,
1699 'lableBlock' => true,
1700 'separator' => 'default',
1701 'alpha' => true,
1702 'global' => true,
1703 'selectors' => '.' . $widget_css_class . '__desc',
1704 'selector_value' => 'color',
1705 'controlClass' => '',
1706 'conditions' => false,
1707 'condition_value' => $cv,
1708 ];
1709 if ($is_gutenberg) {
1710 $ctrl_desc_color['unique_id'] = $h_dcol;
1711 $ctrl_desc_color['ai_support'] = false;
1712 }
1713
1714 $style_widget = [
1715 'name' => 'builder-1',
1716 'section' => 'Widget Style', // matches PHP style section label 'Widget Style'
1717 'compo_index' => 1,
1718 'inner_sec' => [
1719 $ctrl_padding,
1720 $ctrl_title_typo,
1721 $ctrl_title_color,
1722 $ctrl_desc_typo,
1723 $ctrl_desc_color,
1724 ],
1725 ];
1726
1727 return [
1728 [
1729 'layout' => [$layout_content],
1730 'style' => [$style_widget],
1731 ],
1732 ];
1733 }
1734
1735 // ═══════════════════════════════════════════════════════════════════════════════
1736 // PHP register_controls() AUTO-PARSER
1737 // Reads custom php_code and builds section_data automatically so the JSON always
1738 // mirrors the PHP. No manual section_data JSON is needed when providing php_code.
1739 // ═══════════════════════════════════════════════════════════════════════════════
1740
1741 /**
1742 * Auto-generate section_data by parsing register_controls() from custom php_code.
1743 *
1744 * Handles: add_control, add_responsive_control, add_group_control, section start/end,
1745 * and Normal/Hover tab groups (mapped to a normalhover container).
1746 *
1747 * Returns null on complete parsing failure; caller falls back to generate_section_data().
1748 */
1749 function wdesignkit_mcp_parse_php_section_data(string $php_code, string $widget_css_class): ?array {
1750 $body = wdesignkit_mcp_extract_method_body($php_code, 'register_controls');
1751 if (empty($body)) {
1752 return null;
1753 }
1754
1755 $calls = wdesignkit_mcp_tokenize_this_calls($body);
1756 if (empty($calls)) {
1757 return null;
1758 }
1759
1760 $layout_sections = [];
1761 $style_sections = [];
1762 $current_section = null;
1763 $in_tabs = false;
1764 $current_tab_key = null;
1765 $tab_buffer = [];
1766 $php_to_json_map = []; // PHP control name → JSON control name (for condition mapping)
1767 $layout_idx = 0;
1768 $style_idx = 0;
1769
1770 foreach ($calls as $call) {
1771 $method = $call['method'];
1772 $args = $call['args'];
1773
1774 switch ($method) {
1775
1776 case 'start_controls_section': {
1777 $label = wdesignkit_mcp_php_get_string($args, 'label');
1778 $is_style = (strpos($args, 'TAB_STYLE') !== false);
1779 $idx = $is_style ? $style_idx++ : $layout_idx++;
1780 // Extract section-level condition so controls inside it can inherit it.
1781 // References PHP names defined in earlier sections — already in php_to_json_map.
1782 $sec_cond = wdesignkit_mcp_php_extract_condition($args, $php_to_json_map);
1783 $current_section = [
1784 'name' => 'builder-' . $idx,
1785 'section' => $label ?: ($is_style ? 'Style' : 'Content'),
1786 'compo_index' => $idx,
1787 'is_style' => $is_style,
1788 'controls' => [],
1789 'section_condition' => $sec_cond, // null = no section condition
1790 ];
1791 // Reset tab state when entering a new section
1792 $in_tabs = false; $current_tab_key = null; $tab_buffer = [];
1793 break;
1794 }
1795
1796 case 'end_controls_section': {
1797 if ($current_section !== null) {
1798 $entry = [
1799 'name' => $current_section['name'],
1800 'section' => $current_section['section'],
1801 'compo_index' => $current_section['compo_index'],
1802 'inner_sec' => $current_section['controls'],
1803 ];
1804 if ($current_section['is_style']) {
1805 $style_sections[] = $entry;
1806 } else {
1807 $layout_sections[] = $entry;
1808 }
1809 $current_section = null;
1810 }
1811 break;
1812 }
1813
1814 case 'start_controls_tabs': {
1815 $in_tabs = true; $current_tab_key = null; $tab_buffer = [];
1816 break;
1817 }
1818
1819 case 'start_controls_tab': {
1820 // Detect Normal vs Hover from the tab label
1821 $label = strtolower(wdesignkit_mcp_php_get_string($args, 'label'));
1822 $current_tab_key = (strpos($label, 'hover') !== false) ? 'hover' : 'normal';
1823 break;
1824 }
1825
1826 case 'end_controls_tab': {
1827 $current_tab_key = null;
1828 break;
1829 }
1830
1831 case 'end_controls_tabs': {
1832 // Wrap all buffered tab controls in a normalhover container
1833 if (!empty($tab_buffer) && $current_section !== null) {
1834 $nh_name = 'normalhover_' . substr(md5($current_section['section']), 0, 8);
1835 // Inherit section-level condition onto the normalhover wrapper
1836 $nh_cond = false;
1837 $nh_cv = wdesignkit_mcp_empty_cv();
1838 if (isset($current_section['section_condition']) && $current_section['section_condition'] !== null) {
1839 $nh_cond = true;
1840 $nh_cv = $current_section['section_condition'];
1841 }
1842 $current_section['controls'][] = [
1843 'type' => 'normalhover',
1844 'lable' => 'normalhover',
1845 'name' => $nh_name,
1846 'defaultValue' => 'normal',
1847 'nha_type' => 'hover',
1848 'tabTitle' => 'normal',
1849 'description' => '',
1850 'nha_array' => ['normal', 'hover'],
1851 'fields' => $tab_buffer,
1852 'showLable' => true,
1853 'controlClass' => '',
1854 'dynamic' => false,
1855 'conditions' => $nh_cond,
1856 'condition_value' => $nh_cv,
1857 'class' => '',
1858 'nha_array_lable' => ['Normal ', 'Hover '],
1859 ];
1860 }
1861 $in_tabs = false; $current_tab_key = null; $tab_buffer = [];
1862 break;
1863 }
1864
1865 case 'add_control':
1866 case 'add_responsive_control': {
1867 $ctrl = wdesignkit_mcp_php_build_control(
1868 $args,
1869 $method === 'add_responsive_control',
1870 $widget_css_class,
1871 $php_to_json_map
1872 );
1873 if ($ctrl === null) {
1874 break;
1875 }
1876 // Record PHP→JSON name mapping for resolving conditions in later controls
1877 $php_name = $ctrl['_php_name'] ?? null;
1878 unset($ctrl['_php_name']);
1879 if ($php_name) {
1880 $php_to_json_map[$php_name] = $ctrl['name'];
1881 }
1882 // Inherit section-level condition when control has no condition of its own
1883 if ($ctrl['conditions'] === false
1884 && isset($current_section['section_condition'])
1885 && $current_section['section_condition'] !== null) {
1886 $ctrl['conditions'] = true;
1887 $ctrl['condition_value'] = $current_section['section_condition'];
1888 }
1889 // Route: inside tab buffer or directly into the current section
1890 if ($in_tabs && $current_tab_key !== null) {
1891 $ctrl['key'] = $current_tab_key;
1892 $tab_buffer[] = $ctrl;
1893 } elseif ($current_section !== null) {
1894 $current_section['controls'][] = $ctrl;
1895 }
1896 break;
1897 }
1898
1899 case 'add_group_control': {
1900 $ctrl = wdesignkit_mcp_php_build_group_control($args, $widget_css_class);
1901 if ($ctrl !== null && $current_section !== null) {
1902 // Inherit section-level condition when group control has none of its own
1903 if ($ctrl['conditions'] === false
1904 && isset($current_section['section_condition'])
1905 && $current_section['section_condition'] !== null) {
1906 $ctrl['conditions'] = true;
1907 $ctrl['condition_value'] = $current_section['section_condition'];
1908 }
1909 $current_section['controls'][] = $ctrl;
1910 }
1911 break;
1912 }
1913 }
1914 }
1915
1916 if (empty($layout_sections) && empty($style_sections)) {
1917 return null;
1918 }
1919
1920 return [['layout' => $layout_sections, 'style' => $style_sections]];
1921 }
1922
1923 // ── Parser helpers ────────────────────────────────────────────────────────────
1924
1925 /**
1926 * Extract a named method body from PHP source by counting braces.
1927 * Handles single-quoted and double-quoted strings to avoid false brace matches.
1928 */
1929 function wdesignkit_mcp_extract_method_body(string $php_code, string $method_name): string {
1930 $pat = '/(?:protected|public|private)\s+function\s+' . preg_quote($method_name, '/') . '\s*\([^)]*\)\s*\{/';
1931 if (!preg_match($pat, $php_code, $m, PREG_OFFSET_CAPTURE)) {
1932 return '';
1933 }
1934 $start = $m[0][1] + strlen($m[0][0]);
1935 $depth = 1;
1936 $len = strlen($php_code);
1937 $end = $start;
1938 $in_str = false;
1939 $s_char = '';
1940 $escaped = false;
1941
1942 for ($i = $start; $i < $len; $i++) {
1943 $c = $php_code[$i];
1944 if ($escaped) { $escaped = false; continue; }
1945 if ($c === '\\' && $in_str){ $escaped = true; continue; }
1946 if (!$in_str) {
1947 if ($c === '"' || $c === "'") { $in_str = true; $s_char = $c; }
1948 elseif ($c === '{') { $depth++; }
1949 elseif ($c === '}') { $depth--; if ($depth === 0) { $end = $i; break; } }
1950 } else {
1951 if ($c === $s_char) { $in_str = false; }
1952 }
1953 }
1954 return substr($php_code, $start, $end - $start);
1955 }
1956
1957 /**
1958 * Tokenize all `$this->method(…)` calls found in a PHP string.
1959 * Returns array of ['method' => string, 'args' => string (inner content, no outer parens)].
1960 */
1961 function wdesignkit_mcp_tokenize_this_calls(string $body): array {
1962 $calls = [];
1963 $offset = 0;
1964 $pat = '/\$this->([a-zA-Z_]+)\s*\(/';
1965
1966 while (preg_match($pat, $body, $m, PREG_OFFSET_CAPTURE, $offset)) {
1967 $method = $m[1][0];
1968 $paren_pos = $m[0][1] + strlen($m[0][0]) - 1; // position of the opening '('
1969 $balanced = wdesignkit_mcp_extract_balanced($body, $paren_pos);
1970 $calls[] = ['method' => $method, 'args' => substr($balanced, 1, -1)];
1971 $offset = $paren_pos + strlen($balanced);
1972 }
1973 return $calls;
1974 }
1975
1976 /**
1977 * Extract balanced parentheses (or any open/close pair) from $code starting at $offset.
1978 * Handles single/double-quoted strings so parens inside strings are ignored.
1979 */
1980 function wdesignkit_mcp_extract_balanced(string $code, int $offset, string $open = '(', string $close = ')'): string {
1981 $depth = 0;
1982 $result = '';
1983 $len = strlen($code);
1984 $in_str = false;
1985 $s_char = '';
1986 $escaped = false;
1987
1988 for ($i = $offset; $i < $len; $i++) {
1989 $c = $code[$i];
1990 $result .= $c;
1991 if ($escaped) { $escaped = false; continue; }
1992 if ($c === '\\' && $in_str){ $escaped = true; continue; }
1993 if (!$in_str) {
1994 if ($c === '"' || $c === "'") { $in_str = true; $s_char = $c; }
1995 elseif ($c === $open) { $depth++; }
1996 elseif ($c === $close) { $depth--; if ($depth === 0) break; }
1997 } else {
1998 if ($c === $s_char) { $in_str = false; }
1999 }
2000 }
2001 return $result;
2002 }
2003
2004 /**
2005 * Extract a string value for a given key from a PHP array definition string.
2006 * Handles esc_html__(), __(), esc_html(), single-quotes, and double-quotes.
2007 */
2008 function wdesignkit_mcp_php_get_string(string $content, string $key): string {
2009 $k = preg_quote($key, '/');
2010 foreach ([
2011 "/'$k'\s*=>\s*esc_html__\s*\(\s*'([^']+)'/",
2012 "/'$k'\s*=>\s*esc_html__\s*\(\s*\"([^\"]+)\"/",
2013 "/'$k'\s*=>\s*__\s*\(\s*'([^']+)'/",
2014 "/'$k'\s*=>\s*__\s*\(\s*\"([^\"]+)\"/",
2015 "/'$k'\s*=>\s*esc_html\s*\(\s*'([^']+)'/",
2016 "/'$k'\s*=>\s*'([^']+)'/",
2017 "/'$k'\s*=>\s*\"([^\"]+)\"/",
2018 ] as $p) {
2019 if (preg_match($p, $content, $m)) return $m[1];
2020 }
2021 return '';
2022 }
2023
2024 /**
2025 * Extract a numeric (integer or float) default value for a given key.
2026 * Returns null when no numeric default is found.
2027 */
2028 function wdesignkit_mcp_php_get_number(string $content, string $key): ?string {
2029 $k = preg_quote($key, '/');
2030 if (preg_match("/'$k'\s*=>\s*(-?[\d.]+)/", $content, $m)) {
2031 return $m[1];
2032 }
2033 return null;
2034 }
2035
2036 /**
2037 * Map an Elementor Controls_Manager constant name to the WDesignKit JSON control type string.
2038 * Returns '' for unsupported/internal types (skip those controls).
2039 */
2040 function wdesignkit_mcp_php_map_type(string $php_type): string {
2041 static $map = [
2042 'TEXT' => 'text',
2043 'TEXTAREA' => 'textarea',
2044 'URL' => 'url',
2045 'ICONS' => 'iconscontrol',
2046 'SELECT' => 'select',
2047 'CHOOSE' => 'choose',
2048 'SWITCHER' => 'switcher',
2049 'COLOR' => 'color',
2050 'SLIDER' => 'slider',
2051 'DIMENSIONS' => 'dimension',
2052 'NUMBER' => 'text',
2053 'RAW_HTML' => 'rawhtml',
2054 // Intentionally skipped: HIDDEN, MEDIA, REPEATER (too complex / internal)
2055 ];
2056 return $map[$php_type] ?? '';
2057 }
2058
2059 /** Return a blank condition_value object (conditions: false). */
2060 function wdesignkit_mcp_empty_cv(): array {
2061 return ['relation' => 'or', 'values' => [['name' => '', 'operator' => '==', 'value' => '']]];
2062 }
2063
2064 /**
2065 * Extract ALL CSS selectors from a PHP `selectors` (array) or `selector` (string) key.
2066 * Keeps the full {{WRAPPER}} … path so the JSON contains the proper Elementor selector.
2067 * Returns a comma-joined string when multiple selectors exist, empty string when none found.
2068 */
2069 function wdesignkit_mcp_php_extract_selector(string $args, string $key = 'selectors'): string {
2070 $k = preg_quote($key, '/');
2071
2072 // Array format with ( or [ : 'selectors' => [ '{{WRAPPER}} .class' => '...' ]
2073 if (preg_match("/'$k'\s*=>\s*(?:array\s*)?([(\[])/", $args, $m, PREG_OFFSET_CAPTURE)) {
2074 $open = $m[1][0];
2075 $close = $open === '(' ? ')' : ']';
2076 $paren_pos = $m[0][1] + strlen($m[0][0]) - 1;
2077 $balanced = wdesignkit_mcp_extract_balanced($args, $paren_pos, $open, $close);
2078 $inner = substr($balanced, 1, -1);
2079
2080 // Collect every quoted key (the CSS selectors)
2081 $selectors = [];
2082 if (preg_match_all("/'([^']+)'\s*=>/", $inner, $matches)) {
2083 foreach ($matches[1] as $sel) {
2084 $sel = trim($sel);
2085 if ($sel !== '') {
2086 $selectors[] = $sel;
2087 }
2088 }
2089 }
2090 if (!empty($selectors)) {
2091 return implode(', ', $selectors);
2092 }
2093 }
2094
2095 // String format: 'selector' => '{{WRAPPER}} .class'
2096 if (preg_match("/'$k'\s*=>\s*'([^']+)'/", $args, $m)) {
2097 return trim($m[1]);
2098 }
2099
2100 return '';
2101 }
2102
2103 /**
2104 * Extract the CSS property name from the first selector value in a PHP selectors array.
2105 * e.g. '{{WRAPPER}} .btn' => 'color: {{VALUE}};' → returns 'color'
2106 * For dimension shorthand ('padding: {{TOP}}…') returns the property name only.
2107 */
2108 function wdesignkit_mcp_php_extract_property_from_selector(string $args): string {
2109 // Find value side of first selector entry: => 'color: {{VALUE}};'
2110 if (preg_match("/=>\s*'([a-z-]+)\s*:/", $args, $m)) {
2111 return $m[1];
2112 }
2113 if (preg_match('/=>\s*"([a-z-]+)\s*:/', $args, $m)) {
2114 return $m[1];
2115 }
2116 return '';
2117 }
2118
2119 /**
2120 * @deprecated Use wdesignkit_mcp_php_extract_property_from_selector() instead.
2121 * Kept for compatibility — delegates to the new function.
2122 */
2123 function wdesignkit_mcp_php_extract_property(string $args): string {
2124 return wdesignkit_mcp_php_extract_property_from_selector($args);
2125 }
2126
2127 /**
2128 * Extract select/choose options from a PHP `options` array.
2129 * Handles both array() and [] syntax, and both __() and esc_html__() wrappers.
2130 * Returns [['key' => ..., 'value' => ...], ...] (WDesignKit select format).
2131 */
2132 function wdesignkit_mcp_php_extract_options(string $args): array {
2133 $options = [];
2134
2135 // Support both array( and [ syntax
2136 if (!preg_match("/'options'\s*=>\s*(?:array\s*)?([(\[])/", $args, $m, PREG_OFFSET_CAPTURE)) {
2137 return $options;
2138 }
2139 $open = $m[1][0];
2140 $close = $open === '(' ? ')' : ']';
2141 $paren_pos = $m[0][1] + strlen($m[0][0]) - 1;
2142 $balanced = wdesignkit_mcp_extract_balanced($args, $paren_pos, $open, $close);
2143 $opts_str = substr($balanced, 1, -1);
2144
2145 // Match: 'key' => __( 'Label', ... ) OR 'key' => esc_html__( 'Label', ... ) OR 'key' => 'Label'
2146 // Also handles nested arrays for CHOOSE (skip those — they have 'title' sub-key)
2147 if (preg_match_all(
2148 "/'([^']+)'\s*=>\s*(?:(?:esc_html__|__)\s*\(\s*)?'([^']+)'/",
2149 $opts_str, $matches, PREG_SET_ORDER
2150 )) {
2151 foreach ($matches as $match) {
2152 $key = $match[1];
2153 $label = $match[2];
2154 // Skip sub-keys from nested arrays (e.g. 'title', 'icon' from CHOOSE options)
2155 if (in_array($key, ['title', 'icon', 'wdesignkit', 'textdomain'], true)) {
2156 continue;
2157 }
2158 $options[] = ['key' => $key, 'value' => $label];
2159 }
2160 }
2161 return $options;
2162 }
2163
2164 /**
2165 * Extract a PHP `condition` array and translate PHP control names to JSON control names.
2166 * Handles both array() and [] syntax.
2167 * Returns null when no condition is present or mapping fails entirely.
2168 */
2169 function wdesignkit_mcp_php_extract_condition(string $args, array $php_to_json_map): ?array {
2170 if (strpos($args, "'condition'") === false) {
2171 return null;
2172 }
2173 if (!preg_match("/'condition'\s*=>\s*(?:array\s*)?([(\[])/", $args, $m, PREG_OFFSET_CAPTURE)) {
2174 return null;
2175 }
2176 $open = $m[1][0];
2177 $close = $open === '(' ? ')' : ']';
2178 $paren_pos = $m[0][1] + strlen($m[0][0]) - 1;
2179 $balanced = wdesignkit_mcp_extract_balanced($args, $paren_pos, $open, $close);
2180 $cond_str = substr($balanced, 1, -1);
2181
2182 if (!preg_match_all("/'([^']+)'\s*=>\s*'([^']*)'/", $cond_str, $matches, PREG_SET_ORDER)) {
2183 return null;
2184 }
2185
2186 $values = [];
2187 foreach ($matches as $match) {
2188 $php_key = $match[1]; // e.g. 'button_icon[value]!'
2189 $cond_val = $match[2]; // e.g. ''
2190 $negated = (substr($php_key, -1) === '!');
2191 if ($negated) {
2192 $php_key = rtrim($php_key, '!');
2193 }
2194 // Strip Elementor sub-key suffixes: [value], [url], etc.
2195 $php_name = preg_replace('/\[.*?\]/', '', $php_key);
2196 $json_name = $php_to_json_map[$php_name] ?? null;
2197 if (!$json_name) {
2198 continue;
2199 }
2200 $values[] = [
2201 'name' => $json_name,
2202 'operator' => $negated ? '!=' : '==',
2203 'value' => $cond_val,
2204 ];
2205 }
2206
2207 if (empty($values)) {
2208 return null;
2209 }
2210 return ['relation' => 'and', 'values' => $values];
2211 }
2212
2213 /**
2214 * Build a single WDesignKit control array from PHP add_control() argument string.
2215 *
2216 * Temporarily stores '_php_name' so the caller can update the php_to_json_map
2217 * before moving on to the next control (needed for condition resolution).
2218 *
2219 * Returns null to skip unsupported or unrecognised control types.
2220 */
2221 function wdesignkit_mcp_php_build_control(
2222 string $args,
2223 bool $responsive,
2224 string $css_class,
2225 array $php_to_json_map
2226 ): ?array {
2227 // First argument is the PHP control name (single-quoted string)
2228 if (!preg_match("/^\s*'([^']+)'/", $args, $m)) {
2229 return null;
2230 }
2231 $php_name = $m[1];
2232
2233 // Elementor control type constant
2234 if (!preg_match("/'type'\s*=>\s*Controls_Manager::(\w+)/", $args, $m)) {
2235 return null;
2236 }
2237 $wdk_type = wdesignkit_mcp_php_map_type($m[1]);
2238 if ($wdk_type === '') {
2239 return null; // Unsupported type — skip
2240 }
2241
2242 $json_name = $wdk_type . '_' . $php_name;
2243 $label = wdesignkit_mcp_php_get_string($args, 'label');
2244
2245 $ctrl = [
2246 '_php_name' => $php_name, // temp — removed after map update
2247 'type' => $wdk_type,
2248 'lable' => $label,
2249 'name' => $json_name,
2250 'description' => '',
2251 'showLable' => true,
2252 'lableBlock' => false,
2253 'separator' => 'default',
2254 'controlClass' => '',
2255 'conditions' => false,
2256 'condition_value' => wdesignkit_mcp_empty_cv(),
2257 ];
2258
2259 // ── Type-specific fields ───────────────────────────────────────────────────
2260 switch ($wdk_type) {
2261
2262 case 'text': {
2263 $ctrl['placeHolder'] = '';
2264 // For NUMBER controls prefer numeric default; for TEXT prefer string default
2265 $str_default = wdesignkit_mcp_php_get_string($args, 'default');
2266 $num_default = wdesignkit_mcp_php_get_number($args, 'default');
2267 $ctrl['defaultValue'] = ($str_default !== '') ? $str_default
2268 : (($num_default !== null) ? $num_default : '');
2269 // Set input_type = 'number' when original PHP control was Controls_Manager::NUMBER
2270 $ctrl['input_type'] = ($m[1] === 'NUMBER') ? 'number' : 'text';
2271 $ctrl['dynamic'] = false;
2272 $ctrl['responsive'] = $responsive;
2273 $ctrl['sanitizer_value'] = 'wdk_senitize_js';
2274 $ctrl['sanitizer_dropdown_value'] = 'wdk_senitize_js';
2275 break;
2276 }
2277
2278 case 'textarea':
2279 $ctrl['placeHolder'] = '';
2280 $ctrl['defaultValue'] = wdesignkit_mcp_php_get_string($args, 'default');
2281 $ctrl['lableBlock'] = true;
2282 $ctrl['dynamic'] = false;
2283 $ctrl['sanitizer_value'] = 'wdk_senitize_js';
2284 $ctrl['sanitizer_dropdown_value'] = 'wdk_senitize_js';
2285 break;
2286
2287 case 'url':
2288 $ctrl['placeHolder'] = '';
2289 $ctrl['defaultValue'] = '#';
2290 $ctrl['url_options'] = true;
2291 $ctrl['url_options_array'] = ['url', 'is_external', 'nofollow'];
2292 $ctrl['is_external'] = true;
2293 $ctrl['nofollow'] = true;
2294 $ctrl['custom_attributes'] = '';
2295 $ctrl['lableBlock'] = true;
2296 $ctrl['dynamic'] = false;
2297 $ctrl['sanitizer_value'] = 'wdk_senitize_js';
2298 $ctrl['sanitizer_dropdown_value'] = 'wdk_senitize_js';
2299 break;
2300
2301 case 'iconscontrol':
2302 $ctrl['defaultValue'] = '';
2303 $ctrl['lableBlock'] = true;
2304 $ctrl['skin'] = 'inline';
2305 $ctrl['exclude_inline_options'] = 'none';
2306 $ctrl['controlClass'] = '.' . $css_class . '__icon';
2307 break;
2308
2309 case 'select': {
2310 $options = wdesignkit_mcp_php_extract_options($args);
2311 // Also try to override default from PHP 'default' key
2312 $php_default = wdesignkit_mcp_php_get_string($args, 'default');
2313 $first_opt = !empty($options) ? $options[0] : null;
2314 if ($php_default !== '' && $first_opt !== null) {
2315 // Find matching option label
2316 foreach ($options as $opt) {
2317 if ($opt['key'] === $php_default) {
2318 $first_opt = $opt;
2319 break;
2320 }
2321 }
2322 }
2323 $ctrl['select_defaultValue'] = $first_opt
2324 ? [$first_opt['key'], $first_opt['value']]
2325 : ['', ''];
2326 $ctrl['options'] = $options;
2327 $ctrl['sanitizer_value'] = 'wdk_senitize_js';
2328 $ctrl['sanitizer_dropdown_value'] = 'wdk_senitize_js';
2329 break;
2330 }
2331
2332 case 'choose': {
2333 $ctrl['align_defaultValue'] = wdesignkit_mcp_php_get_string($args, 'default');
2334 $ctrl['toggle'] = false;
2335 $ctrl['responsive'] = $responsive;
2336
2337 // Extract selector and property from PHP
2338 $ch_sel = wdesignkit_mcp_php_extract_selector($args);
2339 $ch_prop = wdesignkit_mcp_php_extract_property_from_selector($args);
2340 // If selector uses flex values (selectors_dictionary), infer justify-content
2341 $uses_flex = (strpos($args, 'flex-start') !== false || strpos($args, 'flex-end') !== false);
2342 $ctrl['selectors'] = $ch_sel ?: ('{{WRAPPER}} .' . $css_class);
2343 $ctrl['selector_value'] = $ch_prop ?: ($uses_flex ? 'justify-content' : 'text-align');
2344 $ctrl['alignmentType'] = $uses_flex ? 'content' : 'text';
2345
2346 // Build options from PHP or fall back to standard 4-option set
2347 $choose_options = [];
2348 if (preg_match("/'options'\s*=>\s*(?:array\s*)?([(\[])/", $args, $com, PREG_OFFSET_CAPTURE)) {
2349 $coo = $com[1][0]; $coc = $coo === '(' ? ')' : ']';
2350 $cob = wdesignkit_mcp_extract_balanced($args, $com[0][1] + strlen($com[0][0]) - 1, $coo, $coc);
2351 $coi = substr($cob, 1, -1);
2352 // Each entry: 'value' => [ 'title' => 'Label', 'icon' => 'eicon-...' ]
2353 if (preg_match_all("/'([^']+)'\s*=>\s*(?:array\s*)?[(\[]/", $coi, $cok, PREG_SET_ORDER | PREG_OFFSET_CAPTURE)) {
2354 $icon_map = [
2355 'left' => 'eicon-text-align-left',
2356 'center' => 'eicon-text-align-center',
2357 'right' => 'eicon-text-align-right',
2358 'justify' => 'eicon-text-align-justify',
2359 ];
2360 $value_map = [
2361 'left' => $uses_flex ? 'flex-start' : 'left',
2362 'center' => 'center',
2363 'right' => $uses_flex ? 'flex-end' : 'right',
2364 'justify' => $uses_flex ? 'flex-start' : 'justify',
2365 ];
2366 foreach ($cok as $co) {
2367 $opt_key = $co[1][0];
2368 if (in_array($opt_key, ['title', 'icon', 'wdesignkit'], true)) continue;
2369 // Extract label from 'title' sub-key
2370 $opt_label = $opt_key;
2371 $opt_offset = $co[0][1] + strlen($co[0][0]) - 1;
2372 // Find the sub-array for this option
2373 $sub_open = $coi[$opt_offset] ?? '[';
2374 $sub_close = $sub_open === '(' ? ')' : ']';
2375 $sub_bal = wdesignkit_mcp_extract_balanced($coi, $opt_offset, $sub_open, $sub_close);
2376 $title_m = wdesignkit_mcp_php_get_string($sub_bal, 'title');
2377 if ($title_m !== '') $opt_label = $title_m;
2378 $choose_options[] = [
2379 'align_lable' => $opt_label,
2380 'align_value' => $value_map[$opt_key] ?? $opt_key,
2381 'align_icon' => $icon_map[$opt_key] ?? '',
2382 'align_title' => '',
2383 ];
2384 }
2385 }
2386 }
2387 if (empty($choose_options)) {
2388 $choose_options = [
2389 ['align_lable' => 'Left', 'align_value' => $uses_flex ? 'flex-start' : 'left', 'align_icon' => 'eicon-text-align-left', 'align_title' => ''],
2390 ['align_lable' => 'Center', 'align_value' => 'center', 'align_icon' => 'eicon-text-align-center', 'align_title' => ''],
2391 ['align_lable' => 'Right', 'align_value' => $uses_flex ? 'flex-end' : 'right', 'align_icon' => 'eicon-text-align-right', 'align_title' => ''],
2392 ['align_lable' => 'Justify', 'align_value' => $uses_flex ? 'flex-start' : 'justify','align_icon' => 'eicon-text-align-justify', 'align_title' => ''],
2393 ];
2394 }
2395 $ctrl['align_option'] = $choose_options;
2396 $ctrl['sanitizer_value'] = 'wdk_senitize_js';
2397 $ctrl['sanitizer_dropdown_value'] = 'wdk_senitize_js';
2398 break;
2399 }
2400
2401 case 'switcher': {
2402 // Default is 'yes' when PHP has: 'default' => 'yes' OR 'default' => true
2403 $sw_str = wdesignkit_mcp_php_get_string($args, 'default');
2404 $sw_def = ($sw_str === 'yes')
2405 || (strpos($args, "'default' => true") !== false)
2406 || (strpos($args, '"default" => true') !== false);
2407 $ctrl['defaultValue'] = $sw_def ? 'yes' : '';
2408 $ctrl['label_on'] = wdesignkit_mcp_php_get_string($args, 'label_on') ?: 'Yes';
2409 $ctrl['label_off'] = wdesignkit_mcp_php_get_string($args, 'label_off') ?: 'No';
2410 $ctrl['return_value'] = wdesignkit_mcp_php_get_string($args, 'return_value') ?: 'yes';
2411 $ctrl['responsive'] = false;
2412 $ctrl['sanitizer_value'] = 'wdk_senitize_js';
2413 $ctrl['sanitizer_dropdown_value'] = 'wdk_senitize_js';
2414 break;
2415 }
2416
2417 case 'color':
2418 $ctrl['defaultValue'] = wdesignkit_mcp_php_get_string($args, 'default');
2419 $ctrl['alpha'] = true;
2420 $ctrl['global'] = true;
2421 $ctrl['selectors'] = wdesignkit_mcp_php_extract_selector($args);
2422 $ctrl['selector_value'] = wdesignkit_mcp_php_extract_property($args);
2423 break;
2424
2425 case 'slider': {
2426 $ctrl['placeHolder'] = '';
2427 $ctrl['lableBlock'] = true;
2428 $ctrl['show_unit'] = true;
2429 $ctrl['dynamic'] = false;
2430 $ctrl['responsive'] = $responsive;
2431 $ctrl['selectors'] = wdesignkit_mcp_php_extract_selector($args);
2432 $ctrl['selector_value'] = wdesignkit_mcp_php_extract_property_from_selector($args);
2433
2434 // ── Parse size_units and range from PHP ───────────────────────────
2435 // 'size_units' => [ 'px', 'em', 'rem' ]
2436 $parsed_units = [];
2437 if (preg_match("/'size_units'\s*=>\s*(?:array\s*)?([(\[])/", $args, $um, PREG_OFFSET_CAPTURE)) {
2438 $uo = $um[1][0]; $uc = $uo === '(' ? ')' : ']';
2439 $ub = wdesignkit_mcp_extract_balanced($args, $um[0][1] + strlen($um[0][0]) - 1, $uo, $uc);
2440 preg_match_all("/'([a-z%]+)'/", substr($ub, 1, -1), $um2);
2441 $parsed_units = $um2[1] ?? [];
2442 }
2443 if (empty($parsed_units)) {
2444 $parsed_units = ['px', 'em', 'rem'];
2445 }
2446
2447 // 'range' => [ 'px' => [ 'min' => X, 'max' => Y ], 'vh' => [...] ]
2448 $range_data = [];
2449 if (preg_match("/'range'\s*=>\s*(?:array\s*)?([(\[])/", $args, $rm, PREG_OFFSET_CAPTURE)) {
2450 $ro = $rm[1][0]; $rc = $ro === '(' ? ')' : ']';
2451 $rb = wdesignkit_mcp_extract_balanced($args, $rm[0][1] + strlen($rm[0][0]) - 1, $ro, $rc);
2452 // Each unit entry: 'px' => [ 'min' => N, 'max' => N ]
2453 if (preg_match_all("/'([a-z%]+)'\s*=>\s*(?:array\s*)?[(\[]/", substr($rb, 1, -1), $rm2, PREG_SET_ORDER | PREG_OFFSET_CAPTURE)) {
2454 foreach ($rm2 as $ru) {
2455 $unit_key = $ru[1][0];
2456 // find the sub-array for this unit
2457 $sub_offset = $rm[0][1] + strlen($rm[0][0]) - 1 + $ru[0][1] + strlen($ru[1][0]) + 5; // approx
2458 $min_match = null; $max_match = null;
2459 if (preg_match("/'$unit_key'\s*=>\s*(?:array\s*)?[(\[]\s*'min'\s*=>\s*(-?[\d.]+)\s*,\s*'max'\s*=>\s*(-?[\d.]+)/", substr($rb, 1, -1), $rr)) {
2460 $min_match = $rr[1];
2461 $max_match = $rr[2];
2462 } elseif (preg_match("/'$unit_key'\s*=>\s*(?:array\s*)?[(\[]\s*'max'\s*=>\s*(-?[\d.]+)/", substr($rb, 1, -1), $rr)) {
2463 $max_match = $rr[1];
2464 }
2465 $range_data[$unit_key] = [
2466 'min' => $min_match !== null ? (float)$min_match : 0,
2467 'max' => $max_match !== null ? (float)$max_match : 100,
2468 ];
2469 }
2470 }
2471 }
2472
2473 // 'default' => [ 'unit' => 'px', 'size' => 450 ]
2474 $def_unit = 'px';
2475 $def_size = '';
2476 if (preg_match("/'default'\s*=>\s*(?:array\s*)?[(\[]/", $args, $dm, PREG_OFFSET_CAPTURE)) {
2477 $du = wdesignkit_mcp_php_get_string($args, 'unit');
2478 if ($du !== '') {
2479 $def_unit = $du;
2480 }
2481 // Extract 'size' key from the default array
2482 if (preg_match("/'default'\s*=>\s*(?:array\s*)?[(\[][^)\]]*'size'\s*=>\s*(-?[\d.]+)/", $args, $ds)) {
2483 $def_size = $ds[1];
2484 }
2485 }
2486 $ctrl['slider_defaultValue'] = [$def_unit, $def_size];
2487
2488 // Build size_units array with proper min/max
2489 $default_ranges = [
2490 'px' => ['min' => 0, 'max' => 500, 'step' => 1 ],
2491 'em' => ['min' => 0, 'max' => 10, 'step' => 0.1],
2492 'rem' => ['min' => 0, 'max' => 10, 'step' => 0.1],
2493 'vh' => ['min' => 0, 'max' => 100, 'step' => 1 ],
2494 'vw' => ['min' => 0, 'max' => 100, 'step' => 1 ],
2495 '%' => ['min' => 0, 'max' => 100, 'step' => 1 ],
2496 ];
2497 $size_units = [];
2498 foreach ($parsed_units as $i => $unit) {
2499 $dr = $default_ranges[$unit] ?? ['min' => 0, 'max' => 100, 'step' => 1];
2500 $rng = $range_data[$unit] ?? [];
2501 $size_units[] = [
2502 'type' => $unit,
2503 'checked' => ($i === 0),
2504 'min' => $rng['min'] ?? $dr['min'],
2505 'max' => $rng['max'] ?? $dr['max'],
2506 'step' => $dr['step'],
2507 ];
2508 }
2509 $ctrl['size_units'] = $size_units;
2510 break;
2511 }
2512
2513 case 'rawhtml':
2514 // RAW_HTML controls (e.g. "Need Help?" panels) are UI elements whose HTML
2515 // content cannot be auto-extracted from PHP. Include the control with an
2516 // empty rawhtml field so callers can fill it in via update-widget if needed.
2517 $ctrl['showLable'] = false;
2518 $ctrl['rawhtml'] = '';
2519 break;
2520
2521 case 'dimension': {
2522 $ctrl['lableBlock'] = true;
2523 $ctrl['responsive'] = $responsive;
2524 $ctrl['selectors'] = wdesignkit_mcp_php_extract_selector($args);
2525 $ctrl['selector_value'] = wdesignkit_mcp_php_extract_property_from_selector($args);
2526
2527 // Parse size_units
2528 $dim_units = ['px', 'em', 'rem', '%'];
2529 if (preg_match("/'size_units'\s*=>\s*(?:array\s*)?([(\[])/", $args, $dum, PREG_OFFSET_CAPTURE)) {
2530 $duo = $dum[1][0]; $duc = $duo === '(' ? ')' : ']';
2531 $dub = wdesignkit_mcp_extract_balanced($args, $dum[0][1] + strlen($dum[0][0]) - 1, $duo, $duc);
2532 preg_match_all("/'([a-z%]+)'/", substr($dub, 1, -1), $dum2);
2533 if (!empty($dum2[1])) {
2534 $dim_units = $dum2[1];
2535 }
2536 }
2537 $ctrl['dimension_units'] = $dim_units;
2538
2539 // Parse default values: 'default' => [ 'top' => 10, 'right' => 24, ... ]
2540 $dv = ['top' => '', 'right' => '', 'bottom' => '', 'left' => '', 'unit' => 'px', 'isLinked' => true];
2541 if (preg_match("/'default'\s*=>\s*(?:array\s*)?([(\[])/", $args, $ddm, PREG_OFFSET_CAPTURE)) {
2542 $ddo = $ddm[1][0]; $ddc = $ddo === '(' ? ')' : ']';
2543 $pos = $ddm[0][1] + strlen($ddm[0][0]) - 1;
2544 $ddb = wdesignkit_mcp_extract_balanced($args, $pos, $ddo, $ddc);
2545 $ddi = substr($ddb, 1, -1);
2546 foreach (['top', 'right', 'bottom', 'left'] as $side) {
2547 $n = wdesignkit_mcp_php_get_number($ddi, $side);
2548 if ($n !== null) {
2549 $dv[$side] = $n;
2550 }
2551 }
2552 $u = wdesignkit_mcp_php_get_string($ddi, 'unit');
2553 if ($u !== '') {
2554 $dv['unit'] = $u;
2555 }
2556 // isLinked: false when PHP has 'isLinked' => false
2557 if (strpos($ddi, "'isLinked' => false") !== false || strpos($ddi, '"isLinked" => false') !== false) {
2558 $dv['isLinked'] = false;
2559 }
2560 }
2561 $ctrl['dimension_defaultValue'] = $dv;
2562 break;
2563 }
2564 }
2565
2566 // Resolve condition: translate PHP control names → JSON control names
2567 $cond = wdesignkit_mcp_php_extract_condition($args, $php_to_json_map);
2568 if ($cond !== null) {
2569 $ctrl['conditions'] = true;
2570 $ctrl['condition_value'] = $cond;
2571 }
2572
2573 return $ctrl;
2574 }
2575
2576 /**
2577 * Build a WDesignKit group control (Typography, Border, Box Shadow, Background)
2578 * from a PHP add_group_control() argument string.
2579 */
2580 function wdesignkit_mcp_php_build_group_control(string $args, string $css_class): ?array {
2581 static $type_map = [
2582 'Group_Control_Typography' => 'typography',
2583 'Group_Control_Border' => 'border',
2584 'Group_Control_Box_Shadow' => 'boxshadow',
2585 'Group_Control_Background' => 'background',
2586 ];
2587
2588 $wdk_type = null;
2589 foreach ($type_map as $php_class => $json_type) {
2590 if (strpos($args, $php_class) !== false) {
2591 $wdk_type = $json_type;
2592 break;
2593 }
2594 }
2595 if ($wdk_type === null) {
2596 return null; // Group_Control_Text_Shadow, Image_Size, etc. — skip
2597 }
2598
2599 $php_name = wdesignkit_mcp_php_get_string($args, 'name');
2600 $label = wdesignkit_mcp_php_get_string($args, 'label') ?: ucfirst($wdk_type);
2601 $json_name = $wdk_type . '_' . ($php_name ?: $wdk_type);
2602
2603 // Group controls use 'selector' (singular); fall back to 'selectors'
2604 $selector = wdesignkit_mcp_php_extract_selector($args, 'selector');
2605 if (empty($selector)) {
2606 $selector = wdesignkit_mcp_php_extract_selector($args, 'selectors');
2607 }
2608
2609 return [
2610 'type' => $wdk_type,
2611 'lable' => $label,
2612 'name' => $json_name,
2613 'selector' => $selector,
2614 'separator' => 'default',
2615 'conditions' => false,
2616 'controlClass' => '',
2617 'condition_value' => wdesignkit_mcp_empty_cv(),
2618 ];
2619 }
2620
2621 /**
2622 * Generate a smart Editor_data.html template from the final section_data.
2623 *
2624 * - If a URL control is present → button-like template (<a> with icon + text)
2625 * - Otherwise → card-like template (<h3> title + optional <p> description)
2626 * - Falls back to the legacy hash-based template only when no text control found.
2627 */
2628 function wdesignkit_mcp_generate_editor_html_from_section_data(
2629 string $widget_id,
2630 string $widget_css_class,
2631 string $file_name,
2632 array $section_data
2633 ): string {
2634 // Flatten all layout-section controls into one list
2635 $layout_controls = [];
2636 foreach ($section_data as $group) {
2637 foreach ($group['layout'] ?? [] as $section) {
2638 foreach ($section['inner_sec'] ?? [] as $ctrl) {
2639 $layout_controls[] = $ctrl;
2640 }
2641 }
2642 }
2643
2644 // Identify key control types
2645 $text_ctrl = null;
2646 $url_ctrl = null;
2647 $icon_ctrl = null;
2648 $align_ctrl = null;
2649 $icon_pos_ctrl = null;
2650 $desc_ctrl = null;
2651
2652 foreach ($layout_controls as $ctrl) {
2653 $t = $ctrl['type'] ?? '';
2654 if ($t === 'text' && $text_ctrl === null) $text_ctrl = $ctrl;
2655 if ($t === 'textarea' && $desc_ctrl === null) $desc_ctrl = $ctrl;
2656 if ($t === 'url' && $url_ctrl === null) $url_ctrl = $ctrl;
2657 if ($t === 'iconscontrol' && $icon_ctrl === null) $icon_ctrl = $ctrl;
2658 if ($t === 'choose' && $align_ctrl === null) $align_ctrl = $ctrl;
2659 if ($t === 'select' && $icon_pos_ctrl === null) $icon_pos_ctrl = $ctrl;
2660 }
2661
2662 $align_class = $align_ctrl ? ' align-{{' . $align_ctrl['name'] . '}}' : '';
2663
2664 // ── Button-like widget ─────────────────────────────────────────────────────
2665 if ($url_ctrl !== null) {
2666 $icon_ph = $icon_ctrl ? "\n {{" . $icon_ctrl['name'] . "}}" : '';
2667 $pos_class = $icon_pos_ctrl ? ' {{' . $icon_pos_ctrl['name'] . '}}' : '';
2668 $text_ph = $text_ctrl
2669 ? "\n <span class=\"{$widget_css_class}__text\">{{" . $text_ctrl['name'] . "}}</span>"
2670 : '';
2671
2672 return '<div class="' . $widget_css_class . ' wkit-wb-' . $file_name . $align_class . '" data-wdkitunique="' . $widget_id . '">' . "\n"
2673 . ' <div class="' . $widget_css_class . '__wrap">' . "\n"
2674 . ' <a class="' . $widget_css_class . '__btn' . $pos_class . '"'
2675 . ' href="{{' . $url_ctrl['name'] . '-url}}"'
2676 . ' target="{{' . $url_ctrl['name'] . '-is_external}}"'
2677 . ' rel="{{' . $url_ctrl['name'] . '-nofollow}}">'
2678 . $icon_ph . $text_ph . "\n"
2679 . ' </a>' . "\n"
2680 . ' </div>' . "\n"
2681 . '</div>' . "\n";
2682 }
2683
2684 // ── Card / content-block widget ────────────────────────────────────────────
2685 if ($text_ctrl !== null) {
2686 $html = '<div class="' . $widget_css_class . ' wkit-wb-' . $file_name . $align_class . '" data-wdkitunique="' . $widget_id . '">' . "\n"
2687 . ' <h3 class="' . $widget_css_class . '__title">{{' . $text_ctrl['name'] . '}}</h3>' . "\n";
2688 if ($desc_ctrl !== null) {
2689 $html .= ' <p class="' . $widget_css_class . '__desc">{{' . $desc_ctrl['name'] . '}}</p>' . "\n";
2690 }
2691 $html .= '</div>' . "\n";
2692 return $html;
2693 }
2694
2695 // ── Fallback: legacy hash-based template ───────────────────────────────────
2696 return wdesignkit_mcp_generate_editor_html($widget_id, $widget_css_class, $file_name);
2697 }
2698