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

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