PluginProbe
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler / 1.6.6
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler v1.6.6
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 1.3.20 1.3.19 All 49 releases
fluent-cart / app / Services / Theme / Readers / BricksSettingsReader.php

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

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