Skip to content

Commit 32d2ee9

Browse files
WebMambafabpot
authored andcommitted
Introduce CVA to html-extra
1 parent d8cad8f commit 32d2ee9

6 files changed

Lines changed: 870 additions & 0 deletions

File tree

Cva.php

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
<?php
2+
3+
/*
4+
* This file is part of Twig.
5+
*
6+
* (c) Fabien Potencier
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Twig\Extra\Html;
13+
14+
/**
15+
* Class Variant Authority (CVA) resolver.
16+
*
17+
* @author Mathéo Daninos <matheo.daninos@gmail.com>
18+
*/
19+
final class Cva
20+
{
21+
/**
22+
* @var list<string|null>
23+
*/
24+
private array $base;
25+
26+
/**
27+
* @param string|list<string|null> $base The base classes to apply to the component
28+
*/
29+
public function __construct(
30+
string|array $base = [],
31+
/**
32+
* The variants to apply based on recipes.
33+
*
34+
* Format: [variantCategory => [variantName => classes]]
35+
*
36+
* Example:
37+
* 'colors' => [
38+
* 'primary' => 'bleu-8000',
39+
* 'danger' => 'red-800 text-bold',
40+
* ],
41+
* 'size' => [...],
42+
*
43+
* @var array<string, array<string, string|list<string>>>
44+
*/
45+
private array $variants = [],
46+
47+
/**
48+
* The compound variants to apply based on recipes.
49+
*
50+
* Format: [variantsCategory => ['variantName', 'variantName'], class: classes]
51+
*
52+
* Example:
53+
* [
54+
* 'colors' => ['primary'],
55+
* 'size' => ['small'],
56+
* 'class' => 'text-red-500',
57+
* ],
58+
* [
59+
* 'size' => ['large'],
60+
* 'class' => 'font-weight-500',
61+
* ]
62+
*
63+
* @var array<array<string, string|array<string>>>
64+
*/
65+
private array $compoundVariants = [],
66+
67+
/**
68+
* The default variants to apply if specific recipes aren't provided.
69+
*
70+
* Format: [variantCategory => variantName]
71+
*
72+
* Example:
73+
* 'colors' => 'primary',
74+
*
75+
* @var array<string, string>
76+
*/
77+
private array $defaultVariants = [],
78+
) {
79+
$this->base = (array) $base;
80+
}
81+
82+
public function apply(array $recipes, ?string ...$additionalClasses): string
83+
{
84+
$classes = $this->base;
85+
86+
// Resolve recipes against variants
87+
foreach ($recipes as $recipeName => $recipeValue) {
88+
if (\is_bool($recipeValue)) {
89+
$recipeValue = $recipeValue ? 'true' : 'false';
90+
}
91+
$recipeClasses = $this->variants[$recipeName][$recipeValue] ?? [];
92+
$classes = [...$classes, ...(array) $recipeClasses];
93+
}
94+
95+
// Resolve compound variants
96+
foreach ($this->compoundVariants as $compound) {
97+
$compoundClasses = $this->resolveCompoundVariant($compound, $recipes) ?? [];
98+
$classes = [...$classes, ...$compoundClasses];
99+
}
100+
101+
// Apply default variants if specific recipes aren't provided
102+
foreach ($this->defaultVariants as $defaultVariantName => $defaultVariantValue) {
103+
if (!isset($recipes[$defaultVariantName])) {
104+
$variantClasses = $this->variants[$defaultVariantName][$defaultVariantValue] ?? [];
105+
$classes = [...$classes, ...(array) $variantClasses];
106+
}
107+
}
108+
$classes = [...$classes, ...array_values($additionalClasses)];
109+
110+
$classes = implode(' ', array_filter($classes, 'is_string'));
111+
$classes = preg_split('#\s+#', $classes, -1, \PREG_SPLIT_NO_EMPTY) ?: [];
112+
113+
return implode(' ', array_unique($classes));
114+
}
115+
116+
private function resolveCompoundVariant(array $compound, array $recipes): array
117+
{
118+
foreach ($compound as $compoundName => $compoundValues) {
119+
if ('class' === $compoundName) {
120+
continue;
121+
}
122+
if (!isset($recipes[$compoundName]) || !\in_array($recipes[$compoundName], (array) $compoundValues)) {
123+
return [];
124+
}
125+
}
126+
127+
return (array) ($compound['class'] ?? []);
128+
}
129+
}

HtmlExtension.php

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ public function getFunctions(): array
3737
{
3838
return [
3939
new TwigFunction('html_classes', [self::class, 'htmlClasses']),
40+
new TwigFunction('html_cva', [self::class, 'htmlCva']),
4041
];
4142
}
4243

@@ -110,4 +111,17 @@ public static function htmlClasses(...$args): string
110111

111112
return implode(' ', array_unique(array_filter($classes, static function ($v) { return '' !== $v; })));
112113
}
114+
115+
/**
116+
* @param string|list<string|null> $base
117+
* @param array<string, array<string, string|array<string>> $variants
118+
* @param array<array<string, string|array<string>>> $compoundVariants
119+
* @param array<string, string> $defaultVariant
120+
*
121+
* @internal
122+
*/
123+
public static function htmlCva(array|string $base = [], array $variants = [], array $compoundVariants = [], array $defaultVariant = []): Cva
124+
{
125+
return new Cva($base, $variants, $compoundVariants, $defaultVariant);
126+
}
113127
}

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,8 @@ This package is a Twig extension that provides the following:
99
* [`html_classes`][2] function: returns a string by conditionally joining class
1010
names together.
1111

12+
* [`html_cva`][3] function: returns a `Cva` object to handle class variants.
13+
1214
[1]: https://twig.symfony.com/data_uri
1315
[2]: https://twig.symfony.com/html_classes
16+
[3]: https://twig.symfony.com/html_cva

0 commit comments

Comments
 (0)