1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
/* |
4
|
|
|
* This file is part of ocubom/twig-svg-extension |
5
|
|
|
* |
6
|
|
|
* © Oscar Cubo Medina <https://ocubom.github.io> |
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 Ocubom\Twig\Extension\Svg\Provider\FontAwesome; |
13
|
|
|
|
14
|
|
|
use Ocubom\Twig\Extension\Svg\Exception\ParseException; |
15
|
|
|
use Ocubom\Twig\Extension\Svg\Processor\ClassProcessor; |
16
|
|
|
use Ocubom\Twig\Extension\Svg\Processor\RemoveAttributesProcessor; |
17
|
|
|
use Ocubom\Twig\Extension\Svg\Svg; |
18
|
|
|
use Ocubom\Twig\Extension\Svg\Util\DomUtil; |
19
|
|
|
use Symfony\Component\OptionsResolver\Options; |
20
|
|
|
use Symfony\Component\OptionsResolver\OptionsResolver; |
21
|
|
|
|
22
|
|
|
use function BenTools\IterableFunctions\iterable_to_array; |
23
|
|
|
|
24
|
|
|
class FontAwesomeSvg extends Svg |
25
|
|
|
{ |
26
|
|
|
/** |
27
|
|
|
* Full path to the icon. |
28
|
|
|
*/ |
29
|
|
|
protected \SplFileInfo $path; |
30
|
|
|
|
31
|
|
|
/** |
32
|
|
|
* @param mixed $data The FontAwesome Icon data |
33
|
|
|
*/ |
34
|
3 |
|
public function __construct($data, iterable $options = null) |
35
|
|
|
{ |
36
|
|
|
try { |
37
|
|
|
switch (true) { |
38
|
3 |
|
case $data instanceof FontAwesomeSvg: // "Copy" constructor |
39
|
|
|
$this->path = $data->path; |
40
|
|
|
break; |
41
|
|
|
|
42
|
3 |
|
case $data instanceof \SplFileInfo: |
43
|
3 |
|
$this->path = $data; |
44
|
3 |
|
break; |
45
|
|
|
|
46
|
|
|
default: |
47
|
|
|
throw new ParseException(sprintf('Unable to create "%s" from "%s"', __CLASS__, get_debug_type($data))); |
48
|
|
|
} |
49
|
|
|
|
50
|
|
|
/** @var array $options */ |
51
|
3 |
|
$options = iterable_to_array($options ?? []); |
52
|
|
|
|
53
|
|
|
// Construct from path |
54
|
3 |
|
parent::__construct($this->path, array_merge($options, [ |
55
|
3 |
|
'class_default' => array_merge($options['class_default'] ?? [], [ |
56
|
3 |
|
FontAwesome::INLINE_CLASS, // Add inlined class |
57
|
3 |
|
'fa-'.$this->getName(), // Add icon name class |
58
|
3 |
|
]), |
59
|
3 |
|
'class_block' => array_merge($options['class_block'] ?? [], [ |
60
|
3 |
|
$this->getStyle(), // Block style |
61
|
3 |
|
$this->getStyleClass(), // Block current classes |
62
|
3 |
|
$this->getStyleClass('5.0'), // Block pre-6.0 classes |
63
|
3 |
|
]), |
64
|
|
|
// Add Font Awesome data-* |
65
|
3 |
|
'data-prefix' => $this->getStyleClass('5.0'), |
66
|
3 |
|
'data-icon' => $this->getName(), |
67
|
3 |
|
])); |
68
|
|
|
} catch (ParseException $exc) { |
69
|
|
|
throw new ParseException(sprintf('Unable to create a FontAwesome Icon from "%s"', get_debug_type($data)), 0, $exc); |
70
|
|
|
} |
71
|
|
|
} |
72
|
|
|
|
73
|
|
|
/** |
74
|
|
|
* @codeCoverageIgnore |
75
|
|
|
*/ |
76
|
|
|
public function getFaId(): string |
77
|
|
|
{ |
78
|
|
|
return sprintf( |
79
|
|
|
'fa-%s-%s', |
80
|
|
|
$this->getStyle(), |
81
|
|
|
$this->getName(), |
82
|
|
|
); |
83
|
|
|
} |
84
|
|
|
|
85
|
3 |
|
public function getName(): string |
86
|
|
|
{ |
87
|
3 |
|
return $this->path->getBasename('.svg'); |
88
|
|
|
} |
89
|
|
|
|
90
|
3 |
|
public function getStyle(): string |
91
|
|
|
{ |
92
|
3 |
|
$path = $this->path->getPathInfo(); |
93
|
|
|
assert($path instanceof \SplFileInfo); |
94
|
|
|
|
95
|
3 |
|
return $path->getBasename(); |
96
|
|
|
} |
97
|
|
|
|
98
|
3 |
|
public function getStyleClass(string $version = '6.0'): string |
99
|
|
|
{ |
100
|
3 |
|
return version_compare($version, '6.0', '<') |
101
|
3 |
|
? 'fa'.$this->getStyle()[0] |
102
|
3 |
|
: 'fa-'.$this->getStyle(); |
103
|
|
|
} |
104
|
|
|
|
105
|
1 |
|
public function getHtmlTag(iterable $options = null): \DOMElement |
106
|
|
|
{ |
107
|
|
|
// Create the HTML Tag node |
108
|
1 |
|
$node = DomUtil::createElement(FontAwesome::HTML_TAG); |
109
|
|
|
// Copy options as attributes |
110
|
1 |
|
foreach ($options ?? [] as $key => $val) { |
111
|
1 |
|
if (!empty($val)) { |
112
|
1 |
|
$val = is_iterable($val) ? implode(' ', iterable_to_array($val)) : (string) $val; |
113
|
|
|
|
114
|
1 |
|
$node->setAttribute($key, $val); |
115
|
|
|
} |
116
|
|
|
} |
117
|
|
|
|
118
|
|
|
// Process classes |
119
|
1 |
|
$processor = new ClassProcessor(); |
120
|
1 |
|
$processor($node, [ |
121
|
1 |
|
'class' => [ |
122
|
1 |
|
$this->getStyleClass(), |
123
|
1 |
|
'fa-'.$this->getName(), |
124
|
1 |
|
], |
125
|
1 |
|
'class_block' => [ |
126
|
1 |
|
FontAwesome::INLINE_CLASS, |
127
|
1 |
|
], |
128
|
1 |
|
]); |
129
|
|
|
|
130
|
1 |
|
return $node; |
131
|
|
|
} |
132
|
|
|
|
133
|
3 |
|
protected static function getProcessors(): array |
134
|
|
|
{ |
135
|
1 |
|
return array_merge(parent::getProcessors(), [ |
136
|
|
|
// Remove non-attributes options |
137
|
1 |
|
[new RemoveAttributesProcessor( |
138
|
|
|
// Options |
139
|
1 |
|
'class_default', |
140
|
1 |
|
'class_block', |
141
|
1 |
|
'fill', |
142
|
1 |
|
'opacity', |
143
|
1 |
|
'primary_fill', |
144
|
1 |
|
'primary_opacity', |
145
|
1 |
|
'secondary_fill', |
146
|
1 |
|
'secondary_opacity', |
147
|
|
|
// Remove special attributes |
148
|
1 |
|
'data-fa-title-id', |
149
|
1 |
|
), 1000], |
150
|
|
|
|
151
|
|
|
// Global changes |
152
|
1 |
|
function (\DOMElement $svg, array $options = []): \DOMElement { |
153
|
|
|
// Add FontAwesome fill and opacity values to each path |
154
|
|
|
/** @var \DOMElement $path */ |
155
|
3 |
|
foreach ($svg->getElementsByTagName('path') as $path) { |
156
|
3 |
|
$class = array_intersect( |
157
|
3 |
|
['fa-primary', 'fa-secondary'], |
158
|
3 |
|
preg_split('@\s+@Uis', $path->getAttribute('class')) |
159
|
3 |
|
); |
160
|
3 |
|
$class = count($class) > 0 |
161
|
|
|
? substr($class[0], 3) |
162
|
3 |
|
: ''; |
163
|
|
|
|
164
|
3 |
|
foreach (['fill', 'opacity'] as $name) { |
165
|
3 |
|
$key = $class.'_'.$name; |
166
|
3 |
|
if (!empty($options[$key])) { |
167
|
|
|
$path->setAttribute($name, $options[$key]); |
168
|
3 |
|
} elseif (!empty($options[$name])) { |
169
|
3 |
|
$path->setAttribute($name, $options[$name]); |
170
|
|
|
} |
171
|
|
|
} |
172
|
|
|
} |
173
|
|
|
|
174
|
3 |
|
return $svg; |
175
|
1 |
|
}, |
176
|
1 |
|
]); |
177
|
|
|
} |
178
|
|
|
|
179
|
3 |
|
public static function configureOptions(OptionsResolver $resolver = null): OptionsResolver |
180
|
|
|
{ |
181
|
3 |
|
$resolver = parent::configureOptions($resolver); |
182
|
|
|
|
183
|
|
|
/** @psalm-suppress MissingClosureParamType */ |
184
|
3 |
|
$normalizeFloat = function (Options $options, $value): ?float { |
185
|
3 |
|
return is_numeric((string) $value) ? floatval((string) $value) : null; |
186
|
3 |
|
}; |
187
|
|
|
|
188
|
3 |
|
$resolver->define('class_default') |
189
|
3 |
|
->default([]) |
190
|
3 |
|
->allowedTypes('string[]') |
191
|
3 |
|
->info('Default classes to add unless null'); |
192
|
|
|
|
193
|
3 |
|
$resolver->define('fill') |
194
|
3 |
|
->default('currentColor') |
195
|
3 |
|
->allowedTypes('null', 'string') |
196
|
3 |
|
->info('Default fill color for paths'); |
197
|
|
|
|
198
|
3 |
|
$resolver->define('opacity') |
199
|
3 |
|
->default(null) |
200
|
3 |
|
->allowedTypes('null', 'string', 'float') |
201
|
3 |
|
->normalize($normalizeFloat) |
202
|
3 |
|
->info('Default opacity color for paths'); |
203
|
|
|
|
204
|
3 |
|
$resolver->define('primary_fill') |
205
|
3 |
|
->default(null) |
206
|
3 |
|
->allowedTypes('null', 'string') |
207
|
3 |
|
->info('Default fill color for primary paths (duotone)'); |
208
|
|
|
|
209
|
3 |
|
$resolver->define('primary_opacity') |
210
|
3 |
|
->default(null) |
211
|
3 |
|
->allowedTypes('null', 'string', 'float') |
212
|
3 |
|
->normalize($normalizeFloat) |
213
|
3 |
|
->info('Default opacity color for primary paths (duotone)'); |
214
|
|
|
|
215
|
3 |
|
$resolver->define('secondary_fill') |
216
|
3 |
|
->default(null) |
217
|
3 |
|
->allowedTypes('null', 'string') |
218
|
3 |
|
->info('Default fill color for secondary paths (duotone)'); |
219
|
|
|
|
220
|
3 |
|
$resolver->define('secondary_opacity') |
221
|
3 |
|
->default(null) |
222
|
3 |
|
->allowedTypes('null', 'string', 'float') |
223
|
3 |
|
->normalize($normalizeFloat) |
224
|
3 |
|
->info('Default opacity color for secondary paths (duotone)'); |
225
|
|
|
|
226
|
3 |
|
$resolver->define('data-fa-title-id') |
227
|
3 |
|
->default(null) |
228
|
3 |
|
->allowedTypes('null', 'string') |
229
|
3 |
|
->info('Set the icon title id instead of generate a new one'); |
230
|
|
|
|
231
|
|
|
// Uses data-fa-title-id as aria-labelledby if not defined |
232
|
3 |
|
$resolver->addNormalizer( |
233
|
3 |
|
'aria-labelledby', |
234
|
3 |
|
function (Options $options, ?string $value): ?string { |
235
|
3 |
|
if (empty($value) && !empty($options['data-fa-title-id'])) { |
236
|
1 |
|
return $options['data-fa-title-id']; |
237
|
|
|
} |
238
|
|
|
|
239
|
3 |
|
return $value; |
240
|
3 |
|
}, |
241
|
3 |
|
true |
242
|
3 |
|
); |
243
|
|
|
|
244
|
3 |
|
return $resolver; |
245
|
|
|
} |
246
|
|
|
} |
247
|
|
|
|