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
fluent-cart / app / Services / Theme / RadiusPalette.php

RadiusPalette.php in FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler 1.7.1, at app/Services/Theme/RadiusPalette.php

186 lines 5.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace FluentCart\App\Services\Theme;
4
5 /**
6 * The storefront border-radius registry.
7 *
8 * FluentCart's stylesheets already read three radius tokens with their own
9 * fallbacks — `border-radius: var(--fct-btn-radius, <fallback>)` — and nothing
10 * declared them. This registry names those tokens once, for every consumer:
11 * the setting's sanitiser, the admin's controls, the theme readers and the CSS
12 * FrontendTheme prints.
13 */
14 class RadiusPalette
15 {
16 /**
17 * The radius roles, keyed by the settings key.
18 *
19 * Not filterable: each `var` is a token the shipped stylesheets read, and
20 * a role added from outside would print a property nothing consumes.
21 *
22 * @return array Role key => ['var' => custom property, 'label' => label, 'note' => hint].
23 */
24 public static function roles(): array
25 {
26 return [
27 'card' => [
28 'var' => '--fct-card-radius',
29 'label' => __('Card', 'fluent-cart'),
30 'note' => __('Product cards and their image corners.', 'fluent-cart'),
31 ],
32 'btn' => [
33 'var' => '--fct-btn-radius',
34 'label' => __('Button', 'fluent-cart'),
35 'note' => __('Add to cart, checkout and the other action buttons.', 'fluent-cart'),
36 ],
37 'input' => [
38 'var' => '--fct-input-radius',
39 'label' => __('Form input', 'fluent-cart'),
40 'note' => __('Text fields, selects, textareas and quantity boxes.', 'fluent-cart'),
41 ],
42 ];
43 }
44
45 /**
46 * Normalise one radius length, or refuse it.
47 *
48 * The grammar is a single length: `0`, or a non-negative number followed
49 * by `px`, `rem` or `em`. No percentages, no negatives, no var() or calc():
50 * the value is written into a style element on every storefront page, so
51 * anything wider would be a way to smuggle CSS in, and a value the grammar
52 * cannot read is better left unstated than guessed at.
53 *
54 * @param mixed $value
55 * @return string The normalised length (`0`, `8px`, `0.5rem`), or '' when refused.
56 */
57 public static function sanitizeLength($value): string
58 {
59 if (!is_string($value) && !is_int($value) && !is_float($value)) {
60 return '';
61 }
62
63 $value = strtolower(trim((string)$value));
64
65 if ($value === '0') {
66 return '0';
67 }
68
69 if (!preg_match('/^(\d+(?:\.\d+)?|\.\d+)(px|rem|em)$/', $value, $matches)) {
70 return '';
71 }
72
73 return self::formatNumber((float)$matches[1]) . $matches[2];
74 }
75
76 /**
77 * An owner-typed radius, normalised, or '' when it is not one.
78 *
79 * A bare number means pixels (`12` → `12px`), the way an owner reads a
80 * plain number; a number with a unit is kept with it (`1.5em`, `0.5rem`).
81 * Everything else goes through sanitizeLength()'s grammar, so the units
82 * are px, rem and em, and nothing negative survives.
83 *
84 * @param mixed $value
85 * @return string
86 */
87 public static function sanitizeOwnerLength($value): string
88 {
89 if (is_int($value) || is_float($value)) {
90 $value = (string)$value;
91 }
92
93 if (!is_string($value)) {
94 return '';
95 }
96
97 $value = strtolower(trim($value));
98
99 if (preg_match('/^(\d+(?:\.\d+)?|\.\d+)$/', $value)) {
100 $value .= 'px';
101 }
102
103 return self::sanitizeLength($value);
104 }
105
106 /**
107 * Keep only the known roles with a valid owner-typed radius.
108 *
109 * @param mixed $value Role key => typed radius.
110 * @return array Role key => normalised length.
111 */
112 public static function sanitizeOwnerMap($value): array
113 {
114 if (!is_array($value)) {
115 return [];
116 }
117
118 $radii = [];
119
120 foreach (array_keys(self::roles()) as $role) {
121 if (!array_key_exists($role, $value)) {
122 continue;
123 }
124
125 $length = self::sanitizeOwnerLength($value[$role]);
126
127 if ($length !== '') {
128 $radii[$role] = $length;
129 }
130 }
131
132 return $radii;
133 }
134
135 /**
136 * Save-time validation for the owner's typed radii, one rule per role.
137 *
138 * Each rule runs sanitizeOwnerLength() itself, so a value the sanitiser
139 * would drop is refused with a message naming the field, instead of being
140 * saved without it. Empty means "not set" and passes.
141 *
142 * @return array Field path => [rule].
143 */
144 public static function validationRules(): array
145 {
146 $rules = [];
147
148 foreach (self::roles() as $role => $definition) {
149 $label = $definition['label'];
150
151 $rules['appearance_radius.' . $role] = [
152 function ($attribute, $value) use ($label) {
153 if ($value === null || (is_string($value) && trim($value) === '')) {
154 return null;
155 }
156
157 if (self::sanitizeOwnerLength($value) !== '') {
158 return null;
159 }
160
161 return sprintf(
162 /* translators: %s: radius field label, e.g. "Button" */
163 __('%s radius: use a number, optionally with px, rem or em.', 'fluent-cart'),
164 $label
165 );
166 },
167 ];
168 }
169
170 return $rules;
171 }
172
173 /**
174 * A float without trailing zeros, never in exponent form.
175 *
176 * @param float $number
177 * @return string
178 */
179 protected static function formatNumber(float $number): string
180 {
181 $formatted = rtrim(rtrim(number_format($number, 4, '.', ''), '0'), '.');
182
183 return $formatted === '' ? '0' : $formatted;
184 }
185 }
186