PluginProbe
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler / 1.7.1
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler v1.7.1
1.7.1 1.7.0 1.6.6 1.6.5 1.6.4 1.6.3 1.6.2 1.6.1 1.6.0 1.5.4 1.5.5 1.5.3 1.5.2 1.5.1 1.5.0 1.4.2 1.4.1 1.4.0 1.3.28 1.3.27 1.3.26 1.3.25 1.3.23 1.3.22 1.3.21 All 51 releases
← All changes | app/Services/Theme/ThemePalette.php +338 -0 1.7.0 → 1.7.1 View file →
@@ -7,8 +7,9 @@
7 7 use FluentCart\App\Services\Theme\Readers\BricksSettingsReader;
8 8 use FluentCart\App\Services\Theme\Readers\DiviSettingsReader;
9 9 use FluentCart\App\Services\Theme\Readers\GeneratePressSettingsReader;
10 10 use FluentCart\App\Services\Theme\Readers\KadenceSettingsReader;
11 +use FluentCart\App\Services\Theme\Readers\ThemeRadiusReader;
11 12 use FluentCart\Framework\Support\Arr;
12 13
13 14 /**
14 15 * Reads the active theme's colour palette and resolves it into the semantic
@@ -95,8 +96,22 @@
95 96 */
96 97 protected static $cachedSettingsRoles = null;
97 98
98 99 /**
100 + * @var array|null
101 + */
102 + protected static $cachedRadii = null;
103 +
104 + /**
105 + * Whether the site switched the theme's settings reader off: a reader
106 + * stated colours and `fluent_cart/theme/settings_roles` returned none.
107 + * Set by settingsRoles().
108 + *
109 + * @var bool
110 + */
111 + protected static $settingsReaderOff = false;
112 +
113 + /**
99 114 * Theme settings readers, keyed by the theme name passed to the
100 115 * `fluent_cart/theme/settings_roles` filter. The first that applies wins.
101 116 *
102 117 * Only one theme runs on a site, so the order only matters when several
@@ -145,8 +160,10 @@
145 160 self::$cachedPalette = null;
146 161 self::$cachedRoles = null;
147 162 self::$cachedVendorValues = null;
148 163 self::$cachedSettingsRoles = null;
164 + self::$cachedRadii = null;
165 + self::$settingsReaderOff = false;
149 166 }
150 167
151 168 /**
152 169 * The active theme's colour palette, normalised to hex.
@@ -525,8 +542,10 @@
525 542 break;
526 543 }
527 544 }
528 545
546 + $stated = $roles;
547 +
529 548 /**
530 549 * Filter the colours the active theme's settings state, by role.
531 550 *
532 551 * Runs whether or not a built-in reader applied, so a theme FluentCart
@@ -558,9 +577,328 @@
558 577 }
559 578 }
560 579 }
561 580
581 + // Returning [] for a reader that stated something is the documented
582 + // way to switch it off; radii() honours that for the same reader.
583 + self::$settingsReaderOff = !empty($stated) && is_array($roles) && $roles === [];
584 +
562 585 return self::$cachedSettingsRoles = $clean;
586 + }
587 +
588 + /**
589 + * The border radii the active theme states, by role.
590 + *
591 + * theme.json speaks first, as it does for the button colours
592 + * (buttonGlobals()): the button element, else the Button block, and the
593 + * text-input element, else select, else the Search block — the owner's
594 + * site editor and picked style variation over the theme's own, see
595 + * radiusGlobals(). Then the first applying settings reader that
596 + * implements Readers\ThemeRadiusReader fills the roles the site editor
597 + * left unstated — unless the site switched that reader off by returning
598 + * [] from `fluent_cart/theme/settings_roles`, which turns off its radii
599 + * too. A role nothing states is absent, and FluentCart prints nothing
600 + * for it.
601 + *
602 + * Roles: card, btn, input (RadiusPalette::roles()). Every value is one
603 + * length in RadiusPalette::sanitizeLength()'s grammar; anything else is
604 + * dropped rather than guessed.
605 + *
606 + * @return array Role => length (`0`, `8px`, `0.5rem`, `1em`).
607 + */
608 + public static function radii(): array
609 + {
610 + if (self::$cachedRadii !== null) {
611 + return self::$cachedRadii;
612 + }
613 +
614 + $radii = self::radiusGlobals();
615 + $readerName = '';
616 +
617 + // A reader the site switched off through `fluent_cart/theme/settings_roles`
618 + // states no radius either. The site editor is not a reader and still counts.
619 + self::settingsRoles();
620 + $readers = self::$settingsReaderOff ? [] : self::$settingsReaders;
621 +
622 + foreach ($readers as $name => $reader) {
623 + if (!$reader::applies() || !is_subclass_of($reader, ThemeRadiusReader::class)) {
624 + continue;
625 + }
626 +
627 + $readerName = $name;
628 +
629 + foreach ((array)$reader::radii() as $role => $value) {
630 + if (!isset($radii[$role])) {
631 + $radii[$role] = $value;
632 + }
633 + }
634 +
635 + break;
636 + }
637 +
638 + /**
639 + * Filter the border radii the active theme states, by role.
640 + *
641 + * Runs whether or not a built-in reader applied, so a theme FluentCart
642 + * does not read can be supplied here; return [] to state none. Every
643 + * value is normalised again: only `card`, `btn` and `input` survive,
644 + * each as one length (`0`, or a number in px, rem or em).
645 + *
646 + * @param array $radii Role => length. Keys: card, btn, input.
647 + * @param array $context ['theme' => the active template's slug,
648 + * 'reader' => the reader that applied ('blocksy', 'kadence',
649 + * 'divi', 'bricks', 'astra'), or '' when none did].
650 + */
651 + $radii = apply_filters('fluent_cart/theme/radius_roles', $radii, [
652 + 'theme' => get_template(),
653 + 'reader' => $readerName,
654 + ]);
655 +
656 + $clean = [];
657 +
658 + if (is_array($radii)) {
659 + foreach (array_keys(RadiusPalette::roles()) as $role) {
660 + $value = RadiusPalette::sanitizeLength(Arr::get($radii, $role, ''));
661 +
662 + if ($value !== '') {
663 + $clean[$role] = $value;
664 + }
665 + }
666 + }
667 +
668 + return self::$cachedRadii = $clean;
669 + }
670 +
671 + /**
672 + * The radii theme.json states, from the `theme` and `user` origins — the
673 + * owner's (the site editor, or a style variation they picked, both saved
674 + * in the user origin) over the theme's. Core's origin is left out, as in
675 + * buttonGlobals().
676 + *
677 + * Within one origin each role reads a chain, the closest match first:
678 + * - btn: the button element (`styles.elements.button`, what a plain
679 + * `<button class="wp-element-button">` wears — FluentCart's
680 + * buttons are plain buttons), then the Button block
681 + * (`styles.blocks.core/button`);
682 + * - input: the text-input element, the select element, then the Search
683 + * block (`styles.blocks.core/search`, whose border styles its
684 + * input).
685 + * No path is read as the product-card radius: no theme.json path names a
686 + * product card, and a featured-image or group radius is not one.
687 + *
688 + * @return array Role => length, normalised; only stated roles.
689 + */
690 + protected static function radiusGlobals(): array
691 + {
692 + $radii = [];
693 +
694 + if (!class_exists('WP_Theme_JSON_Resolver')) {
695 + return $radii;
696 + }
697 +
698 + $paths = [
699 + 'btn' => [
700 + 'styles.elements.button.border.radius',
701 + 'styles.blocks.core/button.border.radius',
702 + ],
703 + 'input' => [
704 + 'styles.elements.textInput.border.radius',
705 + 'styles.elements.select.border.radius',
706 + 'styles.blocks.core/search.border.radius',
707 + ],
708 + ];
709 +
710 + foreach (['get_theme_data', 'get_user_data'] as $origin) {
711 + if (!method_exists('WP_Theme_JSON_Resolver', $origin)) {
712 + continue;
713 + }
714 +
715 + $data = \WP_Theme_JSON_Resolver::$origin();
716 +
717 + if (!is_object($data) || !method_exists($data, 'get_raw_data')) {
718 + continue;
719 + }
720 +
721 + $raw = (array)$data->get_raw_data();
722 +
723 + foreach ($paths as $role => $candidates) {
724 + foreach ($candidates as $path) {
725 + $stated = Arr::get($raw, $path);
726 +
727 + if ($stated === null || $stated === '' || $stated === []) {
728 + continue;
729 + }
730 +
731 + // The first stated path ends the chain, writable or not:
732 + // a later path only stands in when the earlier is unset.
733 + // An unwritable one leaves the role to the theme's reader.
734 + $value = self::radiusValue($stated);
735 +
736 + if ($value !== '') {
737 + $radii[$role] = $value;
738 + } else {
739 + unset($radii[$role]);
740 + }
741 +
742 + break;
743 + }
744 + }
745 + }
746 +
747 + return $radii;
748 + }
749 +
750 + /**
751 + * One theme.json radius as a single length.
752 + *
753 + * - `{"ref": "styles.…"}` points at another value in the merged
754 + * theme.json data and is followed (at most three hops);
755 + * - a four-corner object is one length only when every corner is;
756 + * - preset references resolve to the preset's size:
757 + * `var:preset|border-radius|slug` / `var(--wp--preset--border-radius--slug)`
758 + * through `settings.border.radiusSizes`, and
759 + * `var:preset|spacing|slug` / `var(--wp--preset--spacing--slug)`
760 + * through `settings.spacing.spacingSizes` (a fluid `min()`/`clamp()`
761 + * size fails the grammar and is dropped);
762 + * - `var(--wp--custom--…)` resolves to the custom value it names.
763 + *
764 + * @param mixed $value
765 + * @param int $hops Ref hops left.
766 + * @return string Normalised length, or ''.
767 + */
768 + protected static function radiusValue($value, int $hops = 3): string
769 + {
770 + if (is_array($value)) {
771 + if (isset($value['ref'])) {
772 + if ($hops <= 0 || !is_string($value['ref']) || $value['ref'] === '') {
773 + return '';
774 + }
775 +
776 + return self::radiusValue(Arr::get(self::mergedThemeJson(), $value['ref']), $hops - 1);
777 + }
778 +
779 + $corners = [];
780 +
781 + foreach (['topLeft', 'topRight', 'bottomRight', 'bottomLeft'] as $corner) {
782 + $corners[] = self::radiusValue(Arr::get($value, $corner, ''), $hops);
783 + }
784 +
785 + $corners = array_unique($corners);
786 +
787 + return count($corners) === 1 ? (string)reset($corners) : '';
788 + }
789 +
790 + if (!is_string($value)) {
791 + return RadiusPalette::sanitizeLength($value);
792 + }
793 +
794 + $value = trim($value);
795 +
796 + if (preg_match('/^var:preset\|(border-radius|spacing)\|([\w-]+)$/', $value, $matches)
797 + || preg_match('/^var\(\s*--wp--preset--(border-radius|spacing)--([\w-]+)\s*\)$/', $value, $matches)) {
798 + $setting = $matches[1] === 'spacing' ? ['spacing', 'spacingSizes'] : ['border', 'radiusSizes'];
799 +
800 + return RadiusPalette::sanitizeLength(self::presetSize($setting, $matches[2]));
801 + }
802 +
803 + if (preg_match('/^var\(\s*--wp--custom--([\w-]+)\s*\)$/', $value, $matches)) {
804 + return RadiusPalette::sanitizeLength(self::customSetting($matches[1]));
805 + }
806 +
807 + return RadiusPalette::sanitizeLength($value);
808 + }
809 +
810 + /**
811 + * The merged theme.json data (core, theme, user) a `ref` resolves
812 + * against — the same data WordPress resolves refs against when it prints.
813 + *
814 + * @return array
815 + */
816 + protected static function mergedThemeJson(): array
817 + {
818 + if (!class_exists('WP_Theme_JSON_Resolver') || !method_exists('WP_Theme_JSON_Resolver', 'get_merged_data')) {
819 + return [];
820 + }
821 +
822 + $data = \WP_Theme_JSON_Resolver::get_merged_data();
823 +
824 + return is_object($data) && method_exists($data, 'get_raw_data') ? (array)$data->get_raw_data() : [];
825 + }
826 +
827 + /**
828 + * The size of a preset (`settings.border.radiusSizes` or
829 + * `settings.spacing.spacingSizes`), the owner's over the theme's over
830 + * core's.
831 + *
832 + * @param array $setting Settings path, e.g. ['spacing', 'spacingSizes'].
833 + * @param string $slug
834 + * @return string
835 + */
836 + protected static function presetSize(array $setting, string $slug): string
837 + {
838 + if (!function_exists('wp_get_global_settings')) {
839 + return '';
840 + }
841 +
842 + $sizes = wp_get_global_settings($setting);
843 +
844 + if (!is_array($sizes)) {
845 + return '';
846 + }
847 +
848 + // Merged settings are keyed by origin; a single origin is a list.
849 + $lists = isset($sizes[0]) ? [$sizes] : array_values(array_intersect_key(
850 + $sizes,
851 + array_flip(['default', 'theme', 'custom'])
852 + ));
853 +
854 + $size = '';
855 +
856 + foreach ($lists as $list) {
857 + foreach ((array)$list as $preset) {
858 + if (is_array($preset) && (string)Arr::get($preset, 'slug') === $slug && is_string(Arr::get($preset, 'size'))) {
859 + $size = $preset['size'];
860 + }
861 + }
862 + }
863 +
864 + return $size;
865 + }
866 +
867 + /**
868 + * The `settings.custom` value a `--wp--custom--a--b` property is printed
869 + * from. WordPress names each level by its key in kebab case, joined by
870 + * `--`, so the property is walked one level at a time.
871 + *
872 + * @param string $property The part after `--wp--custom--`.
873 + * @return string
874 + */
875 + protected static function customSetting(string $property): string
876 + {
877 + if (!function_exists('wp_get_global_settings')) {
878 + return '';
879 + }
880 +
881 + $node = wp_get_global_settings(['custom']);
882 +
883 + foreach (explode('--', $property) as $segment) {
884 + if (!is_array($node)) {
885 + return '';
886 + }
887 +
888 + $next = null;
889 +
890 + foreach ($node as $key => $child) {
891 + if (_wp_to_kebab_case((string)$key) === $segment) {
892 + $next = $child;
893 + break;
894 + }
895 + }
896 +
897 + $node = $next;
898 + }
899 +
900 + return is_string($node) || is_int($node) || is_float($node) ? (string)$node : '';
563 901 }
564 902
565 903 /**
566 904 * Whether the active theme gave us anything usable.