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 / BricksSettingsReader.php

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

680 lines 22.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\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 * Bricks' colours, read from the theme style Bricks applies to the page.
11 *
12 * Bricks publishes neither its palette nor its theme styles to theme.json, so
13 * without a reader inheritance has nothing to follow. Its colours live in theme
14 * styles (`bricks_theme_styles`): several can exist, each applied by its own
15 * conditions, and Bricks itself picks the ones for the page on `wp` into
16 * `Theme_Styles::$settings_by_id` — the same array its stylesheet is printed
17 * from (Assets::generate_inline_css()). That choice is what is read here (see
18 * activeStyles()); a page no style applies to states nothing, and so does a
19 * fresh install, which has no theme style at all.
20 *
21 * The keys, from the theme-style controls:
22 * - Primary color (`colors.colorPrimary`) is the accent — the Link colour
23 * (`links.typography.color`) when it is unset — and also what Bricks fills
24 * its default button with (`.bricks-background-primary`; a new Button
25 * element's style is `primary`).
26 * - Body typography colour (`typography.typographyBody.color`) is the text.
27 * - Site background (`general.siteBackground`) paints `html`; on a boxed
28 * layout the content background (`general.contentBackground`) paints the
29 * `.brx-boxed` body over it. Unset, Bricks' stylesheet paints the body
30 * white.
31 * - The primary button (`button.primaryBackground`,
32 * `button.primaryTypography.color`, and the every-button
33 * `button.typography.color`), with its `:hover` keys — Bricks stores a
34 * pseudo-class state as the setting key suffixed with it.
35 * Bricks has no border colour setting (`--bricks-border-color` is static), so
36 * no border is stated.
37 *
38 * A colour is an object `{hex, rgb, raw, id}` printed the way
39 * Assets::generate_css_color() prints it: a palette colour as its palette
40 * property (`var(--bricks-color-{id})`, or the property its raw value names),
41 * then raw, then rgb, then hex. A palette reference stays live, with the
42 * palette colour as its fallback (see paletteValues()). A value that cannot be
43 * measured — a translucent rgba(), dynamic data, a gradient, a var() nothing
44 * resolves — is not stated.
45 *
46 * Radii (see radii()) come from the same theme style: the button's border
47 * radius (`button.primaryBorder`, else `button.border`) and the Form element's
48 * field border radius (`form.fieldBorder`). Bricks has no product-card style.
49 *
50 * Verified against Bricks 2.1.3.
51 */
52 class BricksSettingsReader implements ThemeSettingsReader, ThemeRadiusReader
53 {
54 /**
55 * Bricks' stylesheet: `body{background-color:#fff}` (frontend.min.css).
56 *
57 * @var string
58 */
59 protected static $defaultSurface = '#ffffff';
60
61 /**
62 * Detected by the running Bricks theme — its `Theme` singleton holding the
63 * Theme_Styles object its constructor creates — not by theme name, so a
64 * child theme or renamed folder keeps working.
65 *
66 * @return bool
67 */
68 public static function applies(): bool
69 {
70 if (!class_exists('Bricks\\Theme', false) || !class_exists('Bricks\\Theme_Styles', false)) {
71 return false;
72 }
73
74 $bricks = \Bricks\Theme::$instance;
75
76 return is_object($bricks)
77 && isset($bricks->theme_styles)
78 && $bricks->theme_styles instanceof \Bricks\Theme_Styles;
79 }
80
81 /**
82 * @return array Role => hex or `var(--bricks-color-{id}, #hex)`.
83 */
84 public static function roles(): array
85 {
86 if (!self::applies()) {
87 return [];
88 }
89
90 $styles = self::activeStyles();
91
92 if (!$styles) {
93 return [];
94 }
95
96 $text = self::color($styles, 'typography', 'typographyBody', 'color');
97
98 $roles = [
99 'accent' => self::accent($styles),
100 'text' => $text,
101 ];
102
103 $button = self::button($styles, $text);
104
105 if ($button) {
106 $roles = array_merge($roles, $button);
107 }
108
109 $roles = array_filter($roles, function ($value) {
110 return $value !== '';
111 });
112
113 // A style that states no colour says nothing about colour: the white
114 // body below is only Bricks' stylesheet, not a choice to wear.
115 if (!$roles && !self::states($styles, 'general', 'siteBackground') && !self::states($styles, 'general', 'contentBackground')) {
116 return [];
117 }
118
119 $surface = self::surface($styles);
120
121 if ($surface !== '') {
122 $roles['surface'] = $surface;
123 }
124
125 return $roles;
126 }
127
128 /**
129 * Each palette colour's property and current colour, as Bricks prints them
130 * (Assets::generate_inline_css_color_vars()): `--bricks-color-{id}`, or
131 * the property a raw `var(--x)` names, => the colour — rgb before hex, a
132 * raw non-var value over both. Only an opaque colour is kept: a
133 * translucent one cannot be measured, and ColorMath would drop its alpha.
134 *
135 * @return array Property => hex.
136 */
137 public static function paletteValues(): array
138 {
139 if (!self::applies()) {
140 return [];
141 }
142
143 $values = [];
144
145 foreach (self::palette() as $entry) {
146 $hex = self::opaque($entry['value']);
147
148 if ($hex !== '') {
149 $values[$entry['property']] = $hex;
150 }
151 }
152
153 return $values;
154 }
155
156 /**
157 * The settings of the theme style(s) Bricks applies to this page, least
158 * specific first — the order its stylesheet prints them in.
159 *
160 * On a front-end page Bricks chose them on `wp` for the queried post (the
161 * id it scored every style's conditions against), and that choice is
162 * read as is; if nothing applied, nothing did. Before `wp` — the admin
163 * settings preview, a REST request — no page is being shown and Bricks
164 * has chosen nothing, so it is asked the way its own block-editor
165 * integration asks (`set_active_style()` with no post: the site-wide
166 * conditions), and its state is put back afterwards: `set_active_style()`
167 * only ever adds, and a leftover style would be painted on the page.
168 *
169 * ThemePalette caches the result per request, which is right here: one
170 * request shows one page, and the choice is made before FluentCart prints.
171 *
172 * @return array Style id => settings.
173 */
174 protected static function activeStyles(): array
175 {
176 $chosen = \Bricks\Theme_Styles::$settings_by_id;
177
178 if (!empty($chosen) || did_action('wp')) {
179 return is_array($chosen) ? $chosen : [];
180 }
181
182 $before = $chosen;
183
184 try {
185 \Bricks\Theme_Styles::set_active_style(0);
186 $chosen = \Bricks\Theme_Styles::$settings_by_id;
187 } catch (\Throwable $e) {
188 $chosen = [];
189 }
190
191 \Bricks\Theme_Styles::$settings_by_id = $before;
192
193 return is_array($chosen) ? $chosen : [];
194 }
195
196 /**
197 * Primary color, or the Link colour when Primary is unset. A Primary
198 * color that is set but unwritable is not replaced: Bricks is painting it.
199 *
200 * @param array $styles
201 * @return string
202 */
203 protected static function accent(array $styles): string
204 {
205 if (self::states($styles, 'colors', 'colorPrimary')) {
206 return self::color($styles, 'colors', 'colorPrimary');
207 }
208
209 return self::color($styles, 'links', 'typography', 'color');
210 }
211
212 /**
213 * The filled button Bricks paints by default, and its hover.
214 *
215 * A Button element's default style is `primary`, which carries the class
216 * `bricks-background-primary`: Primary background
217 * (`:root .bricks-button[class*="primary"]:not(.outline)`) when set, else
218 * the Primary color. A background set but unwritable states no button —
219 * Bricks paints it, just not in a form FluentCart can write — and neither
220 * does no background at all.
221 *
222 * The text is the primary text, else the every-button text; unset, the
223 * button inherits the body text, which is kept while it reads (WCAG 4.5:1),
224 * otherwise the colour that does. The hover background is stated only
225 * when written; an unset hover text is not stated, so the resting text
226 * carries over as resolve() already does.
227 *
228 * @param array $styles
229 * @param string $bodyText The stated body text, or ''.
230 * @return array
231 */
232 protected static function button(array $styles, string $bodyText): array
233 {
234 $fromPrimaryColor = !self::states($styles, 'button', 'primaryBackground');
235 $background = $fromPrimaryColor
236 ? self::color($styles, 'colors', 'colorPrimary')
237 : self::color($styles, 'button', 'primaryBackground');
238
239 $backgroundHex = ThemePalette::measurable($background);
240
241 if ($backgroundHex === '') {
242 return [];
243 }
244
245 $text = self::firstColor($styles, [
246 ['button', 'primaryTypography', 'color'],
247 ['button', 'typography', 'color'],
248 ]);
249
250 if ($text === '') {
251 $bodyHex = ThemePalette::measurable($bodyText);
252
253 $text = $bodyHex !== '' && ColorMath::contrast($backgroundHex, $bodyHex) >= 4.5
254 ? $bodyText
255 : ColorMath::readableText($backgroundHex);
256 }
257
258 $roles = [
259 'button_bg' => $background,
260 'button_text' => $text,
261 ];
262
263 // The hover of the rule that paints the resting button: a Primary
264 // color hover loses to a Primary background's resting rule.
265 $hoverBg = self::color($styles, 'button', 'primaryBackground:hover');
266
267 if ($hoverBg === '' && $fromPrimaryColor && !self::states($styles, 'button', 'primaryBackground:hover')) {
268 $hoverBg = self::color($styles, 'colors', 'colorPrimary:hover');
269 }
270
271 if ($hoverBg !== '') {
272 $roles['button_hover_bg'] = $hoverBg;
273 $roles['button_hover_text'] = self::firstColor($styles, [
274 ['button', 'primaryTypography:hover', 'color'],
275 ['button', 'typography:hover', 'color'],
276 ]);
277 }
278
279 return array_filter($roles, function ($value) {
280 return $value !== '';
281 });
282 }
283
284 /**
285 * The surface Bricks paints under the content.
286 *
287 * `siteBackground` paints `html` (and clears the body); on a boxed layout
288 * the body is `.brx-boxed`, painted with `contentBackground`, and an unset
289 * one lets the site background through. `.brx-boxed` never exists on a
290 * wide layout, so a content background there paints nothing. An image
291 * paints no colour, so it states no surface and nothing is guessed under
292 * it. With no background set, Bricks' stylesheet paints the body white.
293 *
294 * @param array $styles
295 * @return string
296 */
297 protected static function surface(array $styles): string
298 {
299 $settings = ['siteBackground'];
300
301 if (self::layout($styles) === 'boxed') {
302 array_unshift($settings, 'contentBackground');
303 }
304
305 foreach ($settings as $setting) {
306 if (self::hasImage(self::setting($styles, 'general', $setting, 'image'))) {
307 return '';
308 }
309
310 if (self::states($styles, 'general', $setting, 'color')) {
311 return self::color($styles, 'general', $setting, 'color');
312 }
313 }
314
315 return self::$defaultSurface;
316 }
317
318 /**
319 * The site layout, as Bricks decides the body class: the page's own
320 * setting over the theme style's (Setup::body_class()).
321 *
322 * @param array $styles
323 * @return string
324 */
325 protected static function layout(array $styles): string
326 {
327 if (class_exists('Bricks\\Database', false) && isset(\Bricks\Database::$page_settings)) {
328 $page = (array)\Bricks\Database::$page_settings;
329
330 if (!empty($page['siteLayout'])) {
331 return (string)$page['siteLayout'];
332 }
333 }
334
335 return (string)self::setting($styles, 'general', 'siteLayout');
336 }
337
338 /**
339 * @param mixed $image
340 * @return bool
341 */
342 protected static function hasImage($image): bool
343 {
344 return is_array($image) && (!empty($image['url']) || !empty($image['useDynamicData']));
345 }
346
347 /**
348 * The first colour of several settings that is set, normalised. A set but
349 * unwritable one ends the search rather than falling through.
350 *
351 * @param array $styles
352 * @param array $paths List of [group, key, sub-key].
353 * @return string
354 */
355 protected static function firstColor(array $styles, array $paths): string
356 {
357 foreach ($paths as $path) {
358 if (self::states($styles, $path[0], $path[1], $path[2])) {
359 return self::color($styles, $path[0], $path[1], $path[2]);
360 }
361 }
362
363 return '';
364 }
365
366 /**
367 * Whether any applied style sets this value.
368 *
369 * @param array $styles
370 * @param string $group
371 * @param string $key
372 * @param string|null $subKey
373 * @return bool
374 */
375 protected static function states(array $styles, string $group, string $key, $subKey = null): bool
376 {
377 $value = self::setting($styles, $group, $key, $subKey);
378
379 return $value !== null && $value !== '' && $value !== [];
380 }
381
382 /**
383 * One value from the applied styles, the most specific first — the last
384 * printed rule wins in Bricks' stylesheet, and a style that does not set
385 * a value prints nothing for it. Read per leaf, so a later style's body
386 * font size does not hide an earlier style's body colour.
387 *
388 * @param array $styles
389 * @param string $group
390 * @param string $key
391 * @param string|null $subKey
392 * @return mixed|null
393 */
394 protected static function setting(array $styles, string $group, string $key, $subKey = null)
395 {
396 foreach (array_reverse($styles, true) as $settings) {
397 if (!is_array($settings) || !isset($settings[$group]) || !is_array($settings[$group])) {
398 continue;
399 }
400
401 if (!array_key_exists($key, $settings[$group])) {
402 continue;
403 }
404
405 $value = $settings[$group][$key];
406
407 if ($subKey !== null) {
408 if (!is_array($value) || !array_key_exists($subKey, $value)) {
409 continue;
410 }
411
412 $value = $value[$subKey];
413 }
414
415 if ($value === null || $value === '' || $value === []) {
416 continue;
417 }
418
419 return $value;
420 }
421
422 return null;
423 }
424
425 /**
426 * One colour setting, written as FluentCart can write it.
427 *
428 * @param array $styles
429 * @param string $group
430 * @param string $key
431 * @param string|null $subKey
432 * @return string
433 */
434 protected static function color(array $styles, string $group, string $key, $subKey = null): string
435 {
436 return self::normalise(self::setting($styles, $group, $key, $subKey));
437 }
438
439 /**
440 * A Bricks colour object as a value FluentCart can write, in the order
441 * Assets::generate_css_color() prints it: a palette colour as its live
442 * property (measured through ThemePalette::settingValue(), which attaches
443 * the palette colour as the fallback), then raw, then rgb, then hex.
444 *
445 * @param mixed $color
446 * @return string Hex, `var(--x, #hex)`, or '' when it cannot be measured.
447 */
448 protected static function normalise($color): string
449 {
450 if (!is_array($color)) {
451 return '';
452 }
453
454 $id = isset($color['id']) && is_string($color['id']) ? $color['id'] : '';
455
456 if ($id !== '') {
457 foreach (self::palette() as $entry) {
458 if ($entry['id'] === $id) {
459 return self::measured('var(' . $entry['property'] . ')');
460 }
461 }
462 }
463
464 foreach (['raw', 'rgb', 'hex'] as $field) {
465 $value = isset($color[$field]) && is_string($color[$field]) ? trim($color[$field]) : '';
466
467 if ($value === '') {
468 continue;
469 }
470
471 if (strpos($value, 'var(') === 0) {
472 return self::measured($value);
473 }
474
475 return self::opaque($value);
476 }
477
478 return '';
479 }
480
481 /**
482 * A reference, kept only when it can be measured.
483 *
484 * @param string $reference
485 * @return string
486 */
487 protected static function measured(string $reference): string
488 {
489 $value = ThemePalette::settingValue($reference);
490
491 return ThemePalette::measurable($value) !== '' ? $value : '';
492 }
493
494 /**
495 * An opaque hex, #rgb or rgb()/rgba() as a lowercase hex; '' for a
496 * translucent colour or anything else. ColorMath drops alpha, so it is
497 * checked here first.
498 *
499 * @param mixed $value
500 * @return string
501 */
502 protected static function opaque($value): string
503 {
504 $value = trim((string)$value);
505
506 if (preg_match('/^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})(?:[fF]{2})?$/', $value)) {
507 return strtolower(ColorMath::hex($value));
508 }
509
510 if (preg_match('/^rgba?\(\s*[0-9.]+[\s,]+[0-9.]+[\s,]+[0-9.]+\s*(?:[,\/]\s*([0-9.]+%?)\s*)?\)$/i', $value, $matches)) {
511 $alpha = isset($matches[1]) ? $matches[1] : '1';
512 $opaque = substr($alpha, -1) === '%' ? (float)$alpha >= 100 : (float)$alpha >= 1;
513
514 return $opaque ? strtolower(ColorMath::hex($value)) : '';
515 }
516
517 return '';
518 }
519
520 /**
521 * The palette colours Bricks prints as custom properties, in its own
522 * precedence: rgb, then hex, then a raw value that is not a var(); a raw
523 * `var(--x)` with a colour renames the property to `--x`. A colour with
524 * neither is not printed, and a reference to it falls through to the
525 * colour object's own values, as in Bricks.
526 *
527 * The palette is the one Bricks prints on the page
528 * (`Database::$global_data['colorPalette']`, multisite-aware), else the
529 * `bricks_color_palette` option.
530 *
531 * @return array List of ['id', 'property', 'value'].
532 */
533 protected static function palette(): array
534 {
535 $palettes = null;
536
537 if (class_exists('Bricks\\Database', false) && isset(\Bricks\Database::$global_data['colorPalette'])) {
538 $palettes = \Bricks\Database::$global_data['colorPalette'];
539 }
540
541 if (!is_array($palettes)) {
542 $palettes = get_option(defined('BRICKS_DB_COLOR_PALETTE') ? BRICKS_DB_COLOR_PALETTE : 'bricks_color_palette', []);
543 }
544
545 $entries = [];
546
547 foreach ((array)$palettes as $palette) {
548 if (!is_array($palette) || empty($palette['id']) || empty($palette['colors']) || !is_array($palette['colors'])) {
549 continue;
550 }
551
552 foreach ($palette['colors'] as $color) {
553 $id = is_array($color) ? (string)Arr::get($color, 'id', '') : '';
554
555 if ($id === '' || !preg_match('/^[A-Za-z0-9_-]+$/', $id)) {
556 continue;
557 }
558
559 $value = (string)Arr::get($color, 'rgb', '');
560
561 if ($value === '') {
562 $value = (string)Arr::get($color, 'hex', '');
563 }
564
565 $property = '--bricks-color-' . $id;
566 $raw = trim((string)Arr::get($color, 'raw', ''));
567
568 if ($raw !== '') {
569 if (strpos($raw, 'var(') === false) {
570 $value = $raw;
571 } elseif ($value !== '') {
572 $property = trim(str_replace(['var(', ')'], '', $raw));
573 } else {
574 continue;
575 }
576 }
577
578 if ($value === '' || !preg_match('/^--[A-Za-z0-9_-]+$/', $property)) {
579 continue;
580 }
581
582 $entries[] = [
583 'id' => $id,
584 'property' => $property,
585 'value' => $value,
586 ];
587 }
588 }
589
590 return $entries;
591 }
592
593 /**
594 * The button and form-field radii of the theme style Bricks applies.
595 *
596 * Button: the primary button's border (`:root .bricks-button[class*="primary"]`,
597 * the style a new Button element gets) over every button's
598 * (`.bricks-button`). Input: the Form element's field border
599 * (`.brxe-form` inputs, selects and textareas) — the only field radius a
600 * theme style holds. A style that sets neither states none; Bricks'
601 * stylesheet is not read for one.
602 *
603 * @return array Role => length, as Bricks prints it.
604 */
605 public static function radii(): array
606 {
607 if (!self::applies()) {
608 return [];
609 }
610
611 $styles = self::activeStyles();
612
613 if (!$styles) {
614 return [];
615 }
616
617 $radii = [];
618
619 foreach (['primaryBorder', 'border'] as $key) {
620 $radius = self::setting($styles, 'button', $key, 'radius');
621
622 if ($radius !== null) {
623 $radii['btn'] = self::radiusLength($radius);
624 break;
625 }
626 }
627
628 $field = self::setting($styles, 'form', 'fieldBorder', 'radius');
629
630 if ($field !== null) {
631 $radii['input'] = self::radiusLength($field);
632 }
633
634 return array_filter($radii, function ($value) {
635 return $value !== '';
636 });
637 }
638
639 /**
640 * A border control's radius as one length, built the way
641 * Assets::generate_css_rules_from_setting() builds it: per corner, a
642 * non-zero bare number takes px, otherwise the corner's unit is appended
643 * unless the value already carries it. Only four set, equal corners are
644 * one length.
645 *
646 * @param mixed $radius
647 * @return string
648 */
649 protected static function radiusLength($radius): string
650 {
651 if (!is_array($radius)) {
652 return '';
653 }
654
655 $units = isset($radius['unit']) && is_array($radius['unit']) ? $radius['unit'] : [];
656 $corners = [];
657
658 foreach (['top', 'right', 'bottom', 'left'] as $direction) {
659 $number = isset($radius[$direction]) && is_scalar($radius[$direction]) ? (string)$radius[$direction] : '';
660 $unit = !empty($units[$direction]) && is_string($units[$direction]) ? $units[$direction] : '';
661
662 if ($number === '') {
663 return '';
664 }
665
666 if (is_numeric($number) && (float)$number != 0) {
667 $unit = 'px';
668 }
669
670 if ($unit === '-' || $unit === 'none') {
671 $unit = '';
672 }
673
674 $corners[] = $unit !== '' && strpos($number, $unit) === false ? $number . $unit : $number;
675 }
676
677 return count(array_unique($corners)) === 1 ? $corners[0] : '';
678 }
679 }
680