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 / Readers / BlocksySettingsReader.php

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

395 lines 12.8 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\Readers;
4
5 use FluentCart\App\Services\Theme\ColorMath;
6 use FluentCart\App\Services\Theme\ThemePalette;
7 use FluentCart\Framework\Support\Arr;
8
9 /**
10 * Blocksy's Customizer colours, read from its theme mods rather than its slots.
11 *
12 * Blocksy keeps each semantic colour as a theme mod shaped
13 * `['default' => ['color' => …], 'hover' => ['color' => …]]`, whose colour
14 * points at a palette slot (`var(--theme-palette-color-N)`) or holds a custom
15 * colour. Links, Base Text, Borders, Site Background and Buttons are separate
16 * settings — the button is not tied to the link colour — so each role is read
17 * from its own mod.
18 *
19 * A mod the owner never saved, or a sub-key missing from one, takes Blocksy's
20 * own default for it, the same fill `blocksy_get_colors()` applies before
21 * printing (inc/dynamic-styles/global/all.php). A colour saved as Blocksy's
22 * "inherit" sentinel (`CT_CSS_SKIP_RULE…`) counts as unset the same way.
23 *
24 * Palette references are resolved through
25 * `blocksy_manager()->colors->get_color_palette()` (see paletteValues()), and
26 * always kept live: Blocksy Companion's dark mode redefines the palette
27 * properties under `:root[data-color-mode*="dark"]`, which a bare hex would
28 * never follow.
29 *
30 * Radii (see radii()): Buttons → Border Radius (`buttonRadius`) and Form
31 * Elements → Border Radius (`formFieldBorderRadius`). The product card radius
32 * (`cardProductRadius`) is a WooCommerce setting Blocksy only prints with
33 * WooCommerce, so it is not read.
34 *
35 * Verified against Blocksy 2.1.57.
36 */
37 class BlocksySettingsReader implements ThemeSettingsReader, ThemeRadiusReader
38 {
39 /**
40 * Blocksy's defaults per mod and sub-key (inc/dynamic-styles/global/all.php
41 * and background.php).
42 *
43 * @var array
44 */
45 protected static $defaults = [
46 'linkColor' => ['default' => 'var(--theme-palette-color-1)', 'hover' => 'var(--theme-palette-color-2)'],
47 'fontColor' => ['default' => 'var(--theme-palette-color-3)'],
48 'border_color' => ['default' => 'var(--theme-palette-color-5)'],
49 'buttonColor' => ['default' => 'var(--theme-palette-color-1)', 'hover' => 'var(--theme-palette-color-2)'],
50 'buttonTextColor' => ['default' => '#ffffff', 'hover' => '#ffffff'],
51 'site_background' => ['default' => 'var(--theme-palette-color-7)'],
52 ];
53
54 /**
55 * Detected by Blocksy's own manager, not by theme name, so a child theme
56 * or renamed folder keeps working.
57 *
58 * @return bool
59 */
60 public static function applies(): bool
61 {
62 return function_exists('blocksy_get_theme_mod')
63 && function_exists('blocksy_manager')
64 && is_object(blocksy_manager());
65 }
66
67 /**
68 * @return array Role => hex or `var(--theme-palette-color-N, #hex)`.
69 */
70 public static function roles(): array
71 {
72 if (!self::applies()) {
73 return [];
74 }
75
76 $roles = [
77 'accent' => self::read('linkColor'),
78 'text' => self::read('fontColor'),
79 'border' => self::read('border_color'),
80 'surface' => self::surface(),
81 ];
82
83 // Blocksy states both halves of its button, resting and hover. The
84 // hover belongs to the resting button: without one there is no hover.
85 $button = self::buttonPair('default');
86
87 if ($button) {
88 $roles = array_merge($roles, $button, self::buttonPair('hover', 'button_hover_'));
89 }
90
91 return array_filter($roles, function ($value) {
92 return $value !== '';
93 });
94 }
95
96 /**
97 * Each palette slot's property and current colour: `--theme-palette-color-N`
98 * (or the slot's own `variable`) => hex. The same list Blocksy prints the
99 * properties from and publishes to the editor palette.
100 *
101 * @return array Property => colour, as Blocksy saved it.
102 */
103 public static function paletteValues(): array
104 {
105 if (!self::applies()) {
106 return [];
107 }
108
109 $manager = blocksy_manager();
110 $colors = isset($manager->colors) ? $manager->colors : null;
111
112 if (!is_object($colors) || !method_exists($colors, 'get_color_palette')) {
113 return [];
114 }
115
116 $values = [];
117
118 try {
119 foreach ((array)$colors->get_color_palette() as $slot) {
120 $variable = (string)Arr::get((array)$slot, 'variable', '');
121
122 if ($variable !== '') {
123 $values['--' . ltrim($variable, '-')] = (string)Arr::get((array)$slot, 'color', '');
124 }
125 }
126 } catch (\Throwable $e) {
127 // Blocksy's API misbehaving means no palette values: references
128 // stay bare and unmeasurable, which resolve() already refuses.
129 return [];
130 }
131
132 return $values;
133 }
134
135 /**
136 * One button half-pair: background from `buttonColor`, text from
137 * `buttonTextColor`, both at the same state.
138 *
139 * A background that is set but unwritable (an rgba(), say) is not
140 * replaced by a default — Blocksy is painting it, just not in a form
141 * FluentCart can write — so the pair is omitted. A text that cannot be
142 * written gets the colour that reads on the background.
143 *
144 * @param string $state 'default' or 'hover'.
145 * @param string $prefix Role prefix.
146 * @return array
147 */
148 protected static function buttonPair(string $state, string $prefix = 'button_'): array
149 {
150 $background = self::read('buttonColor', $state);
151
152 if ($background === '') {
153 return [];
154 }
155
156 $text = self::read('buttonTextColor', $state);
157
158 if ($text === '') {
159 $bgHex = ThemePalette::measurable($background);
160 $text = $bgHex !== '' ? ColorMath::readableText($bgHex) : '';
161 }
162
163 return [
164 $prefix . 'bg' => $background,
165 $prefix . 'text' => $text,
166 ];
167 }
168
169 /**
170 * The body background (Site Background). Blocksy saves it flat or per
171 * device; desktop speaks for all, as blocksy_expand_responsive_value()
172 * reads it. A gradient paints no colour (Blocksy writes
173 * `background-color: initial`), so it states no surface.
174 *
175 * @return string
176 */
177 protected static function surface(): string
178 {
179 $value = blocksy_get_theme_mod('site_background', []);
180
181 if (is_array($value) && isset($value['desktop'])) {
182 $value = $value['desktop'];
183 }
184
185 $value = is_array($value) ? $value : [];
186
187 if (Arr::get($value, 'background_type', 'color') === 'gradient') {
188 return '';
189 }
190
191 return self::normalise(
192 Arr::get($value, 'backgroundColor.default.color'),
193 self::$defaults['site_background']['default']
194 );
195 }
196
197 /**
198 * One colour mod at one state, over Blocksy's default for it.
199 *
200 * @param string $mod
201 * @param string $state
202 * @return string
203 */
204 protected static function read(string $mod, string $state = 'default'): string
205 {
206 $value = blocksy_get_theme_mod($mod, []);
207 $raw = is_array($value) ? Arr::get($value, $state . '.color') : null;
208
209 return self::normalise($raw, (string)Arr::get(self::$defaults, $mod . '.' . $state, ''));
210 }
211
212 /**
213 * Unset — missing, empty, or Blocksy's inherit sentinel — takes the
214 * default; anything else is normalised as saved.
215 *
216 * @param mixed $raw
217 * @param string $default
218 * @return string
219 */
220 protected static function normalise($raw, string $default): string
221 {
222 if (!is_string($raw) || trim($raw) === '' || strpos($raw, 'CT_CSS_SKIP_RULE') === 0) {
223 $raw = $default;
224 }
225
226 return ThemePalette::settingValue($raw);
227 }
228
229 /**
230 * Blocksy's stylesheet fallback for both radii
231 * (`var(--theme-button-border-radius, 3px)`,
232 * `var(--theme-form-field-border-radius, 3px)`, static/bundle/main.min.css),
233 * and the `empty_value` its dynamic CSS skips printing at.
234 *
235 * @var int
236 */
237 protected static $defaultRadius = 3;
238
239 /**
240 * The button and form-field radii.
241 *
242 * Button: `buttonRadius`, a ct-spacing value printed as
243 * `--theme-button-border-radius` (inc/dynamic-styles/global/all.php). Its
244 * desktop value speaks for all; an empty one is Blocksy's 3px.
245 *
246 * Form field: `formFieldBorderRadius`, a number of px (default 3) printed
247 * as `--theme-form-field-border-radius` (global/forms.php). It is only
248 * worn by classic forms: modern forms paint
249 * `var(--has-classic-forms, …)` with `--false`, which leaves the field
250 * square, so they state 0.
251 *
252 * @return array Role => length, as Blocksy prints it.
253 */
254 public static function radii(): array
255 {
256 if (!self::applies()) {
257 return [];
258 }
259
260 $radii = [];
261
262 $button = blocksy_get_theme_mod('buttonRadius', null);
263
264 if (is_array($button) && isset($button['desktop'])) {
265 $button = $button['desktop'];
266 }
267
268 $buttonRadius = self::spacingLength($button);
269
270 if ($buttonRadius !== '') {
271 $radii['btn'] = $buttonRadius;
272 }
273
274 if (blocksy_get_theme_mod('forms_type', 'classic-forms') !== 'classic-forms') {
275 $radii['input'] = '0';
276 } else {
277 $field = blocksy_get_theme_mod('formFieldBorderRadius', self::$defaultRadius);
278
279 if (is_numeric($field)) {
280 $radii['input'] = $field . 'px';
281 }
282 }
283
284 return $radii;
285 }
286
287 /**
288 * A ct-spacing value as the one length Blocksy prints, the way
289 * blocksy_spacing_prepare_for_device() writes it (inc/css/spacing.php):
290 *
291 * - unset, or every side empty: Blocksy prints nothing and its
292 * stylesheet's 3px applies;
293 * - custom (state 3): the custom string as typed;
294 * - otherwise each side's value and unit, an empty side taking 3 when
295 * the sides are linked (0 when not), a side without a unit taking the
296 * others'. Only four equal sides are one length.
297 *
298 * The pre-`values` format (`top`/`right`/`bottom`/`left` strings) is read
299 * the same way.
300 *
301 * @param mixed $value
302 * @return string
303 */
304 protected static function spacingLength($value): string
305 {
306 $fallback = self::$defaultRadius . 'px';
307
308 if (!is_array($value)) {
309 return $fallback;
310 }
311
312 if (!isset($value['values'])) {
313 return self::legacySpacingLength($value);
314 }
315
316 $state = (int)Arr::get($value, 'state', 1);
317
318 if ($state === 3) {
319 $custom = trim((string)Arr::get($value, 'custom', ''));
320
321 return $custom === '' ? $fallback : $custom;
322 }
323
324 $sides = [];
325 $unit = '';
326 $allEmpty = true;
327
328 foreach (array_slice(array_values((array)$value['values']), 0, 4) as $side) {
329 $number = is_array($side) ? Arr::get($side, 'value', '') : '';
330 $sideUnit = is_array($side) ? (string)Arr::get($side, 'unit', '') : '';
331
332 if ($number === '' || $number === 'auto' || $number === null) {
333 $number = $state === 1 ? self::$defaultRadius : 0;
334 } else {
335 $allEmpty = false;
336 }
337
338 if ($sideUnit !== '') {
339 $unit = $sideUnit;
340 }
341
342 $sides[] = ['value' => (string)$number, 'unit' => $sideUnit];
343 }
344
345 if (count($sides) !== 4 || $allEmpty) {
346 return $fallback;
347 }
348
349 $lengths = [];
350
351 foreach ($sides as $side) {
352 $lengths[] = $side['value'] . ($side['unit'] !== '' ? $side['unit'] : $unit);
353 }
354
355 return count(array_unique($lengths)) === 1 ? $lengths[0] : '';
356 }
357
358 /**
359 * The pre-`values` spacing format: four strings carrying their unit.
360 * Blocksy writes an empty, `auto` or `0` side as its empty value.
361 *
362 * @param array $value
363 * @return string
364 */
365 protected static function legacySpacingLength(array $value): string
366 {
367 $sides = [];
368
369 foreach (['top', 'right', 'bottom', 'left'] as $key) {
370 $side = trim((string)Arr::get($value, $key, ''));
371 $sides[] = ($side === '' || $side === 'auto' || $side === '0') ? '' : $side;
372 }
373
374 if (implode('', $sides) === '') {
375 return self::$defaultRadius . 'px';
376 }
377
378 $unit = 'px';
379
380 foreach ($sides as $side) {
381 if ($side !== '' && preg_match('/^[\d.]+([a-z%]+)$/i', $side, $matches)) {
382 $unit = $matches[1];
383 }
384 }
385
386 foreach ($sides as $index => $side) {
387 if ($side === '') {
388 $sides[$index] = self::$defaultRadius . $unit;
389 }
390 }
391
392 return count(array_unique($sides)) === 1 ? $sides[0] : '';
393 }
394 }
395