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