PluginProbe
Extendify / 3.2.1
Extendify v3.2.1
3.2.1 3.2.0 3.1.6 3.1.5 3.1.4 3.1.3 3.1.2 3.1.1 3.1.0 3.0.6 3.0.5 3.0.4 trunk 0.1.0 0.10.0 0.10.1 0.10.2 0.11.0 0.11.1 0.2.0 0.3.0 0.3.1 0.4.0 0.5.0 0.6.0 All 127 releases
← All changes | app/Agent/Controllers/WPController.php +381 -124 3.0.53.2.1 View file →
@@ -7,9 +7,14 @@
7 7 namespace Extendify\Agent\Controllers;
8 8
9 9 defined('ABSPATH') || die('No direct access.');
10 10
11 +use Extendify\Agent\PostBlockFinder;
12 +use Extendify\Agent\TemplatePartBlockFinder;
13 +use Extendify\Constants;
14 +use Extendify\Shared\Services\HeroDescription;
11 15 use Extendify\Shared\Services\Sanitizer;
16 +use Extendify\Shared\Services\SiteImages;
12 17
13 18 /**
14 19 * The controller for interacting with WordPress.
15 20 */
@@ -49,8 +54,12 @@
49 54 'onyx',
50 55 'glasgow',
51 56 'royal',
52 57 'obsidian',
58 + 'heath',
59 + 'signal',
60 + 'marzipan',
61 + 'butterscotch',
53 62 ];
54 63
55 64 /**
56 65 * Recursively filter an array to include only specified properties.
@@ -282,9 +291,10 @@
282 291 'lineHeight' => $typography['lineHeight'] ?? null,
283 292 'letterSpacing' => $typography['letterSpacing'] ?? null,
284 293 'fontStyle' => $typography['fontStyle'] ?? null,
285 294 'fontWeight' => $typography['fontWeight'] ?? null,
286 - 'textTransform' => $typography['textTransform'] ?? 'none',
295 + // Defaulting to 'none' would cancel the applied vibe's uppercase.
296 + 'textTransform' => $typography['textTransform'] ?? null,
287 297 ], function ($v) {
288 298 return $v !== null;
289 299 })
290 300 ];
@@ -291,8 +301,75 @@
291 301 }
292 302
293 303
294 304 /**
305 + * Where else the synced pattern or menu a composite blockId names is used
306 + *
307 + * @param \WP_REST_Request $request The REST API request object.
308 + * @return \WP_REST_Response
309 + */
310 + public static function getSharedBlockUsage(\WP_REST_Request $request)
311 + {
312 + $composite = TemplatePartBlockFinder::parseCompositeId(
313 + $request->get_param('blockId')
314 + );
315 + if (!$composite) {
316 + return new \WP_REST_Response(['scope' => 'block', 'pages' => []], 200);
317 + }
318 +
319 + $seen = [];
320 + $pages = [];
321 + $sitewide = self::collectRefUsage($composite['ref'], $seen, $pages);
322 +
323 + return new \WP_REST_Response([
324 + 'scope' => $sitewide ? 'site' : ($pages ? 'pages' : 'block'),
325 + 'pages' => array_values($pages),
326 + ], 200);
327 + }
328 +
329 + // A template or part renders it everywhere, so a page list only matters otherwise.
330 + private static function collectRefUsage(int $ref, array &$seen, array &$pages): bool
331 + {
332 + if (isset($seen[$ref])) {
333 + return false;
334 + }
335 + $seen[$ref] = true;
336 +
337 + $sitewide = false;
338 + foreach (self::postsReferencing($ref) as $row) {
339 + $id = (int) $row->ID;
340 + if (in_array($row->post_type, ['wp_template', 'wp_template_part'], true)) {
341 + $sitewide = true;
342 + continue;
343 + }
344 + // A pattern nested in a pattern inherits wherever the outer one lands.
345 + if ($row->post_type === 'wp_block') {
346 + $sitewide = self::collectRefUsage($id, $seen, $pages) || $sitewide;
347 + continue;
348 + }
349 + $pages[$id] = ['id' => $id, 'title' => \get_the_title($id)];
350 + }
351 +
352 + return $sitewide;
353 + }
354 +
355 + // The ref is literal text inside a block comment, which WP indexes nowhere.
356 + private static function postsReferencing(int $ref)
357 + {
358 + global $wpdb;
359 +
360 + $like = $wpdb->esc_like('"ref":' . $ref);
361 + return $wpdb->get_results($wpdb->prepare(
362 + "SELECT ID, post_type FROM {$wpdb->posts}
363 + WHERE post_status IN ('publish', 'private', 'draft')
364 + AND post_type IN ('post', 'page', 'wp_block', 'wp_template', 'wp_template_part')
365 + AND (post_content LIKE %s OR post_content LIKE %s)",
366 + '%' . $like . '}%',
367 + '%' . $like . ',%'
368 + ));
369 + }
370 +
371 + /**
295 372 * Get the HTML of a specific tagged block code
296 373 *
297 374 * @param \WP_REST_Request $request The REST API request object.
298 375 * @return \WP_REST_Response
@@ -298,69 +375,127 @@
298 375 * @return \WP_REST_Response
299 376 */
300 377 public static function getBlockCode(\WP_REST_Request $request)
301 378 {
302 - $blockId = (int) $request->get_param('blockId');
303 - $postId = (int) $request->get_param('postId');
379 + $rawId = $request->get_param('blockId');
380 + $partSlug = (string) $request->get_param('partSlug');
304 381
382 + $composite = TemplatePartBlockFinder::parseCompositeId($rawId);
383 + if ($composite) {
384 + return self::getRefPostBlockCode(
385 + $composite,
386 + (string) $rawId,
387 + $partSlug,
388 + (int) $request->get_param('postId')
389 + );
390 + }
391 +
392 + $blockId = (int) $rawId;
305 393 if ($blockId < 1) {
306 394 return new \WP_REST_Response(['error' => 'Invalid blockId'], 400);
307 395 }
308 396
397 + // A template-part source resolves the part and walks it with the shared
398 + // preorder finder, instead of the post path below.
399 + if ($partSlug !== '') {
400 + return self::getTemplatePartBlockCode($partSlug, $blockId);
401 + }
402 +
403 + $postId = (int) $request->get_param('postId');
309 404 $post = \get_post($postId);
310 405 if (!$post) {
311 406 return new \WP_REST_Response(['error' => 'Post not found'], 404);
312 407 }
313 408
314 - $ignored = ['core/query', 'core/post-template', 'core/post-content'];
409 + $found = PostBlockFinder::find(parse_blocks($post->post_content), $blockId);
315 410
316 - $ast = array_values(array_filter(
317 - parse_blocks($post->post_content),
318 - static function ($b) {
319 - return is_array($b) && !empty($b['blockName']);
320 - }
321 - ));
411 + if (!$found || empty($found['block']['blockName'])) {
412 + return new \WP_REST_Response(['error' => 'Block id not found in this post'], 404);
413 + }
322 414
323 - $seq = 0;
324 - $found = null;
415 + return self::blockCodeResponse($found['block'], ['postId' => $postId], $blockId);
416 + }
325 417
326 - $walk = function (array $list) use (&$walk, &$seq, $blockId, &$found, $ignored) {
327 - foreach ($list as $b) {
328 - $name = $b['blockName'] ?? null;
329 - if (!$name) {
330 - continue;
331 - }
418 + // One response shape for every source, so the client reads the same fields
419 + // whichever post the block turned out to live in.
420 + private static function blockCodeResponse(array $block, array $scope, $blockId)
421 + {
422 + return new \WP_REST_Response(array_merge($scope, [
423 + 'blockId' => $blockId,
424 + 'name' => $block['blockName'],
425 + 'attrs' => $block['attrs'] ?? (object)[],
426 + 'block' => serialize_blocks([$block]),
427 + ]), 200);
428 + }
332 429
333 - // Ignore this block and its subtree (matches tagger behavior)
334 - if (in_array($name, $ignored, true)) {
335 - continue; // do NOT increment seq, do NOT traverse children
336 - }
430 + // Resolves the part the same way SaveController does — active-theme-scoped
431 + // get_block_template, not a raw get_posts by name — then walks it with the
432 + // shared preorder finder so the blockId lands on the block save will write.
433 + private static function getTemplatePartBlockCode(string $slug, int $blockId)
434 + {
435 + $content = self::templatePartContent($slug);
436 + if ($content === null) {
437 + return new \WP_REST_Response(['error' => 'Template part not found'], 404);
438 + }
337 439
338 - $seq++;
339 - if ($seq === $blockId) {
340 - $found = $b;
341 - return true;
342 - }
440 + $found = TemplatePartBlockFinder::find(
441 + parse_blocks($content),
442 + $blockId
443 + );
444 + if (!is_array($found) || empty($found['block']['blockName'])) {
445 + return new \WP_REST_Response(['error' => 'Block id not found in this template part'], 404);
446 + }
343 447
344 - if (!empty($b['innerBlocks']) && $walk($b['innerBlocks'])) {
345 - return true;
346 - }
347 - }
348 - return false;
349 - };
350 - $walk($ast);
448 + return self::blockCodeResponse($found['block'], ['partSlug' => $slug], $blockId);
449 + }
351 450
352 - if (!is_array($found) || empty($found['blockName'])) {
451 + // Post and theme file number identically, so an id survives the first edit.
452 + private static function templatePartContent(string $slug)
453 + {
454 + $stylesheet = \wp_get_theme()->get_stylesheet();
455 + $template = \get_block_template("{$stylesheet}//{$slug}", 'wp_template_part');
456 + if (!$template) {
457 + return null;
458 + }
459 + $post = empty($template->wp_id) ? null : \get_post($template->wp_id);
460 + return $post ? $post->post_content : $template->content;
461 + }
462 +
463 + // A synced pattern's or menu's blocks live in the post its id names, walked
464 + // with the same finder — the part they render inside owns none of them.
465 + private static function getRefPostBlockCode(
466 + array $composite,
467 + string $blockId,
468 + string $partSlug,
469 + int $postId
470 + ) {
471 + $post = \get_post($composite['ref']);
472 + if (!$post || $post->post_type !== $composite['postType']) {
473 + return new \WP_REST_Response(['error' => 'Referenced post not found'], 404);
474 + }
475 +
476 + $containerPost = $partSlug === '' ? \get_post($postId) : null;
477 + $container = $partSlug !== ''
478 + ? self::templatePartContent($partSlug)
479 + : ($containerPost ? $containerPost->post_content : null);
480 + if ($container === null) {
353 481 return new \WP_REST_Response(['error' => 'Block id not found in this post'], 404);
354 482 }
355 483
356 - return new \WP_REST_Response([
357 - 'postId' => $postId,
358 - 'blockId' => $blockId,
359 - 'name' => $found['blockName'],
360 - 'attrs' => $found['attrs'] ?? (object)[],
361 - 'block' => serialize_blocks([$found]),
362 - ], 200);
484 + // Unchecked, every pattern and menu on the site reads back from any page.
485 + if (!TemplatePartBlockFinder::contentRendersRef($container, $composite['ref'])) {
486 + return new \WP_REST_Response(['error' => 'Block id not found in this post'], 404);
487 + }
488 +
489 + $found = TemplatePartBlockFinder::find(
490 + parse_blocks($post->post_content),
491 + $composite['seq']
492 + );
493 + if (!is_array($found) || empty($found['block']['blockName'])) {
494 + return new \WP_REST_Response(['error' => 'Block id not found in this referenced post'], 404);
495 + }
496 +
497 + return self::blockCodeResponse($found['block'], ['partSlug' => $partSlug], $blockId);
363 498 }
364 499
365 500 /**
366 501 * Get the rendered HTML of some block code
@@ -372,9 +507,15 @@
372 507 {
373 508 $blockCode = $request->get_param('blockCode');
374 509 $content = \do_blocks($blockCode);
375 510
376 - return new \WP_REST_Response(['content' => trim($content)]);
511 + // Layout supports register per-container CSS for a page-side enqueue
512 + // that never happens on a REST fragment — ship it with the markup.
513 + $styles = function_exists('wp_style_engine_get_stylesheet_from_context')
514 + ? \wp_style_engine_get_stylesheet_from_context('block-supports')
515 + : '';
516 +
517 + return new \WP_REST_Response(['content' => trim($content), 'styles' => $styles]);
377 518 }
378 519
379 520 /**
380 521 * Get the Hero Patterns from the API
@@ -382,15 +523,22 @@
382 523 * @param string $title The title to replace in the pattern code
383 524 * @param string $description The description to replace in the pattern code
384 525 * @param array $images The images to replace in the pattern code, as an array of urls
385 526 * @param array $cta The cta to replace in the pattern code, as an array with 'label' and 'link' keys
386 - * @param bool $featured Whether to limit to featured patterns
387 - * @return array|WP_Error|array<string|int, mixed> The hero patterns data or a WP_Error on failure
527 + * @param bool $featuredOnly Whether to limit to featured patterns
528 + * @param string|null $source What triggered the request, e.g. 'change-site-design-workflow'
529 + * @return array|\WP_Error|array<string|int, mixed> The hero patterns data or a WP_Error on failure
388 530 */
389 - protected static function getHeroPatternsData($title, $description, $images, $cta, $featuredOnly = false)
390 - {
531 + protected static function getHeroPatternsData(
532 + $title,
533 + $description,
534 + $images,
535 + $cta,
536 + $featuredOnly = false,
537 + $source = null
538 + ) {
391 539 $response = \wp_remote_post(
392 - 'https://patterns.extendify.com/api/heros',
540 + Constants::PATTERNS_HOST . '/api/heros',
393 541 [
394 542 'headers' => [
395 543 'Content-Type' => 'application/json',
396 544 'Accept' => 'application/json',
@@ -398,8 +546,9 @@
398 546 'body' => wp_json_encode([
399 547 "wpVersion" => \get_bloginfo('version'),
400 548 "wpLanguage" => \get_locale(),
401 549 "featured" => $featuredOnly,
550 + "source" => $source,
402 551 ])
403 552 ]
404 553 );
405 554
@@ -408,94 +557,104 @@
408 557 }
409 558
410 559 $body = json_decode(\wp_remote_retrieve_body($response), true);
411 560
412 - $heroPatterns = array_map(
413 - function ($heroPattern) use ($description, $title, $images, $cta) {
414 - $code = $heroPattern['code'] ?? '';
561 + $cursor = 0;
562 + $imageCount = count($images);
563 + $heroPatterns = [];
415 564
416 - if ($title) {
417 - $code = preg_replace(
418 - '/(<!-- wp:heading[^>]*-->[\s\S]*?<h1[^>]*>)[\s\S]*?(<\/h1>[\s\S]*?<!-- \/wp:heading -->)/m',
419 - '${1}' . esc_html($title) . '${2}',
420 - $code,
421 - 1
422 - );
423 - }
565 + foreach ($body as $heroPattern) {
566 + $code = $heroPattern['code'] ?? '';
424 567
425 - if ($description) {
426 - $code = preg_replace(
427 - '/(<!-- wp:paragraph[^>]*-->[\s\S]*?<p[^>]*>)[\s\S]*?(<\/p>[\s\S]*?<!-- \/wp:paragraph -->)/m',
428 - '${1}' . esc_html($description) . '${2}',
429 - $code,
430 - 1
431 - );
432 - }
568 + if ($title) {
569 + $code = preg_replace(
570 + '/(<!-- wp:heading[^>]*-->[\s\S]*?<h1[^>]*>)[\s\S]*?(<\/h1>[\s\S]*?<!-- \/wp:heading -->)/m',
571 + '${1}' . esc_html($title) . '${2}',
572 + $code,
573 + 1
574 + );
575 + }
433 576
434 - if ($cta['label'] ?? null) {
435 - $code = preg_replace(
436 - '/(<!-- wp:button[^>]*-->[\s\S]*?<a[^>]*>)[\s\S]*?(<\/a>[\s\S]*?<!-- \/wp:button -->)/m',
437 - '${1}' . esc_html($cta['label']) . '${2}',
438 - $code,
439 - 1
440 - );
441 - }
577 + if (trim((string) $description) === '') {
578 + // Catalog paragraphs describe the slot, not the site.
579 + $code = preg_replace(
580 + '/<!-- wp:paragraph[\s\S]*?<!-- \/wp:paragraph -->/m',
581 + '',
582 + $code,
583 + 1
584 + );
585 + } else {
586 + $code = preg_replace(
587 + '/(<!-- wp:paragraph[^>]*-->[\s\S]*?<p[^>]*>)[\s\S]*?(<\/p>[\s\S]*?<!-- \/wp:paragraph -->)/m',
588 + '${1}' . esc_html($description) . '${2}',
589 + $code,
590 + 1
591 + );
592 + }
442 593
443 - if ($cta['link'] ?? null) {
444 - $code = preg_replace(
445 - '/(<!-- wp:button[\s\S]*?<a[^>]*\shref=")[^"]*(")/m',
446 - '${1}' . esc_url($cta['link']) . '${2}',
447 - $code,
448 - 1
449 - );
450 - }
594 + if ($cta['label'] ?? null) {
595 + $code = preg_replace(
596 + '/(<!-- wp:button[^>]*-->[\s\S]*?<a[^>]*>)[\s\S]*?(<\/a>[\s\S]*?<!-- \/wp:button -->)/m',
597 + '${1}' . esc_html($cta['label']) . '${2}',
598 + $code,
599 + 1
600 + );
601 + }
451 602
452 - foreach ($heroPattern['urls'] ?? [] as $key => $url) {
453 - if (!($images[$key] ?? null)) {
454 - break;
455 - }
603 + if ($cta['link'] ?? null) {
604 + $code = preg_replace(
605 + '/(<!-- wp:button[\s\S]*?<a[^>]*\shref=")[^"]*(")/m',
606 + '${1}' . esc_url($cta['link']) . '${2}',
607 + $code,
608 + 1
609 + );
610 + }
456 611
457 - $code = str_replace($url, $images[$key], $code);
612 + $patternUrls = $heroPattern['urls'] ?? [];
613 + foreach ($patternUrls as $key => $url) {
614 + if (!$imageCount) {
615 + break;
458 616 }
617 + $code = str_replace($url, $images[($cursor + $key) % $imageCount], $code);
618 + }
619 + $cursor = $imageCount ? ($cursor + count($patternUrls)) % $imageCount : 0;
459 620
460 - $renderedHtml = do_blocks(str_replace('ext-animate--on', '', $code));
621 + $renderedHtml = do_blocks(str_replace('ext-animate--on', '', $code));
461 622
462 - $blockSupportsCss = function_exists('wp_style_engine_get_stylesheet_from_context')
463 - ? wp_style_engine_get_stylesheet_from_context('block-supports')
464 - : '';
623 + $blockSupportsCss = function_exists('wp_style_engine_get_stylesheet_from_context')
624 + ? wp_style_engine_get_stylesheet_from_context('block-supports')
625 + : '';
465 626
466 - // Clear block-supports store so CSS doesn't accumulate across patterns.
467 - \WP_Style_Engine_CSS_Rules_Store::remove_all_stores();
627 + // Clear block-supports store so CSS doesn't accumulate across patterns.
628 + \WP_Style_Engine_CSS_Rules_Store::remove_all_stores();
468 629
469 - $linkStyles = array_values(
470 - array_filter(
471 - array_map(
472 - function ($style) {
473 - return wp_styles()->registered[$style]->src ?? null;
474 - },
475 - wp_styles()->queue ?? []
476 - )
630 + $linkStyles = array_values(
631 + array_filter(
632 + array_map(
633 + function ($style) {
634 + return wp_styles()->registered[$style]->src ?? null;
635 + },
636 + wp_styles()->queue ?? []
477 637 )
478 - );
638 + )
639 + );
479 640
480 - /**
481 - * Clear queue for the next pattern.
482 - *
483 - * `do_blocks` appends to the global queue the styles needed for the blocks.
484 - */
485 - wp_styles()->queue = [];
641 + /**
642 + * Clear queue for the next pattern.
643 + *
644 + * `do_blocks` appends to the global queue the styles needed for the blocks.
645 + */
646 + wp_styles()->queue = [];
486 647
487 - return [
488 - 'id' => $heroPattern['id'],
489 - 'name' => $heroPattern['name'],
490 - 'code' => $code,
491 - 'renderedHtml' => $renderedHtml,
492 - 'blockSupportsCss' => $blockSupportsCss,
493 - 'linkStyles' => $linkStyles,
494 - ];
495 - },
496 - $body
497 - );
648 + $heroPatterns[] = [
649 + 'id' => $heroPattern['id'],
650 + 'name' => $heroPattern['name'],
651 + 'code' => $code,
652 + 'renderedHtml' => $renderedHtml,
653 + 'blockSupportsCss' => $blockSupportsCss,
654 + 'linkStyles' => $linkStyles,
655 + ];
656 + }
498 657
499 658 return $heroPatterns;
500 659 }
501 660
@@ -523,19 +682,110 @@
523 682
524 683 return new \WP_REST_Response(['patterns' => $heroPatterns, 'blockEditorSettings' => $editorSettings ?? null]);
525 684 }
526 685
686 + /**
687 + * Build the image list for the hero section.
688 + *
689 + * Returns up to 6 images: unused site images first, already-used images last.
690 + * When already at capacity, reverses the array so the last-used image leads next time.
691 + */
692 + protected static function getHeroSectionImages(array $images, array $siteImages, int $postId): array
693 + {
694 + $maxSlots = 6;
695 +
696 + if (count($images) >= $maxSlots) {
697 + return array_reverse($images);
698 + }
699 +
700 + $candidates = SiteImages::urls($siteImages);
701 +
702 + if (!$postId || empty($candidates)) {
703 + return $images;
704 + }
705 +
706 + $usedImages = self::resolveUsedImages($postId);
707 + $candidates = array_map(function ($url) {
708 + return self::stripQueryString($url);
709 + }, $candidates);
710 +
711 + $unusedImages = array_values(array_filter(
712 + $candidates,
713 + function ($url) use ($usedImages, $images) {
714 + return !in_array($url, $usedImages, true) && !in_array($url, $images, true);
715 + }
716 + ));
717 +
718 + $unusedSlots = $maxSlots - count($images);
719 + return array_merge(array_slice($unusedImages, 0, $unusedSlots), $images);
720 + }
721 +
722 + protected static function stripQueryString(string $url): string
723 + {
724 + $parsed = wp_parse_url($url);
725 + return ($parsed['scheme'] ?? 'https') . '://' . ($parsed['host'] ?? '') . ($parsed['path'] ?? '');
726 + }
727 +
728 + /**
729 + * Get all Unsplash URLs used in a post.
730 + */
731 + protected static function resolveUsedImages(int $postId): array
732 + {
733 + $post = get_post($postId);
734 + if (!$post) {
735 + return [];
736 + }
737 +
738 + $processor = new \WP_HTML_Tag_Processor($post->post_content);
739 + $usedImages = [];
740 +
741 + while ($processor->next_tag('img')) {
742 + $src = $processor->get_attribute('src');
743 + if (!$src) {
744 + continue;
745 + }
746 +
747 + $baseUrl = self::stripQueryString($src);
748 +
749 + if (str_contains($src, 'unsplash.com')) {
750 + $usedImages[] = $baseUrl;
751 + continue;
752 + }
753 +
754 + $attachmentId = attachment_url_to_postid(esc_url($baseUrl));
755 +
756 + if (!$attachmentId) {
757 + continue;
758 + }
759 +
760 + $sourceUrl = get_post_meta($attachmentId, '_extendify_source_url', true);
761 +
762 + if (!$sourceUrl) {
763 + continue;
764 + }
765 +
766 + $usedImages[] = self::stripQueryString($sourceUrl);
767 + }
768 +
769 + return array_values(array_unique($usedImages));
770 + }
771 +
527 772 public static function getSiteDesignVariations(\WP_REST_Request $request)
528 773 {
529 774 $title = $request->get_param('title');
530 - $description = $request->get_param('description');
531 - $images = $request->get_param('images');
775 + $description = HeroDescription::resolve($request->get_param('description'));
776 + $images = $request->get_param('images') ?? [];
532 777 $cta = $request->get_param('cta');
533 778 $featuredOnly = true; // Only show featured patterns
534 779 $currentHeroPattern = $request->get_param('currentHeroPattern');
780 + $postId = (int) $request->get_param('postId');
781 + $siteImages = $request->get_param('siteImages') ?? [];
782 + $source = $request->get_param('source');
535 783
536 - $heroPatterns = self::getHeroPatternsData($title, $description, $images, $cta, $featuredOnly);
784 + $images = self::getHeroSectionImages($images, $siteImages, $postId);
537 785
786 + $heroPatterns = self::getHeroPatternsData($title, $description, $images, $cta, $featuredOnly, $source);
787 +
538 788 if (\is_wp_error($heroPatterns)) {
539 789 return new \WP_REST_Response([], 500);
540 790 }
541 791
@@ -555,10 +805,17 @@
555 805 $current = \WP_Theme_JSON_Resolver::get_merged_data('theme');
556 806
557 807 $unfiltered = \WP_Theme_JSON_Resolver::get_style_variations();
558 808
809 + // Keep only full style variations — exclude color-only and font-only
810 + // presets that get_style_variations() returns from styles/colors/* and
811 + // styles/typography/*.
559 812 $colorAndFontsVariations = array_filter($unfiltered, function ($variation) {
560 - return self::variationHasProperties($variation, ['color', 'elements', 'typography']);
813 + $hasPalette = ($variation['settings']['color']['palette'] ?? []) !== [];
814 + $hasTypography = ($variation['styles']['typography'] ?? []) !== []
815 + || ($variation['settings']['typography'] ?? []) !== [];
816 + $hasElements = ($variation['styles']['elements'] ?? []) !== [];
817 + return $hasPalette && $hasTypography && $hasElements;
561 818 });
562 819
563 820 $buildSlugMap = function ($unfiltered) {
564 821 $slugMap = [];