1212namespace Twig \Extra \Html ;
1313
1414use Symfony \Component \Mime \MimeTypes ;
15+ use Twig \Environment ;
1516use Twig \Error \RuntimeError ;
1617use Twig \Extension \AbstractExtension ;
18+ use Twig \Extension \CoreExtension ;
19+ use Twig \Extension \EscaperExtension ;
20+ use Twig \Runtime \EscaperRuntime ;
1721use Twig \TwigFilter ;
1822use Twig \TwigFunction ;
1923
@@ -30,6 +34,7 @@ public function getFilters(): array
3034 {
3135 return [
3236 new TwigFilter ('data_uri ' , [$ this , 'dataUri ' ]),
37+ new TwigFilter ('html_attr_merge ' , [self ::class, 'htmlAttrMerge ' ]),
3338 ];
3439 }
3540
@@ -38,6 +43,7 @@ public function getFunctions(): array
3843 return [
3944 new TwigFunction ('html_classes ' , [self ::class, 'htmlClasses ' ]),
4045 new TwigFunction ('html_cva ' , [self ::class, 'htmlCva ' ]),
46+ new TwigFunction ('html_attr ' , [self ::class, 'htmlAttr ' ], ['needs_environment ' => true , 'is_safe ' => ['html ' ]]),
4147 ];
4248 }
4349
@@ -124,4 +130,77 @@ public static function htmlCva(array|string $base = [], array $variants = [], ar
124130 {
125131 return new Cva ($ base , $ variants , $ compoundVariants , $ defaultVariant );
126132 }
133+
134+ public static function htmlAttrMerge (...$ arrays ): array
135+ {
136+ $ result = [];
137+
138+ foreach ($ arrays as $ argNumber => $ array ) {
139+ if (!$ array ) {
140+ continue ;
141+ }
142+
143+ if (!is_iterable ($ array )) {
144+ throw new RuntimeError (sprintf ('The "attr_merge" filter only works with arrays or "Traversable", got "%s" for argument %d. ' , \gettype ($ array ), $ argNumber + 1 ));
145+ }
146+
147+ $ array = CoreExtension::toArray ($ array );
148+
149+ foreach (['class ' , 'style ' , 'data ' , 'aria ' ] as $ deepMergeKey ) {
150+ if (isset ($ array [$ deepMergeKey ])) {
151+ $ value = $ array [$ deepMergeKey ];
152+ unset($ array [$ deepMergeKey ]);
153+
154+ if (!is_iterable ($ value )) {
155+ $ value = (array ) $ value ;
156+ }
157+
158+ $ value = CoreExtension::toArray ($ value );
159+
160+ $ result [$ deepMergeKey ] = array_merge ($ result [$ deepMergeKey ] ?? [], $ value );
161+ }
162+ }
163+
164+ $ result = array_merge ($ result , $ array );
165+ }
166+
167+ return $ result ;
168+ }
169+
170+ public static function htmlAttr (Environment $ env , ...$ args ): string
171+ {
172+ $ attr = self ::htmlAttrMerge (...$ args );
173+
174+ if (isset ($ attr ['class ' ])) {
175+ $ attr ['class ' ] = trim (implode (' ' , $ attr ['class ' ]));
176+ }
177+
178+ if (isset ($ attr ['style ' ])) {
179+ $ style = '' ;
180+ foreach ($ attr ['style ' ] as $ name => $ value ) {
181+ if (is_numeric ($ name )) {
182+ $ style .= $ value .'; ' ;
183+ } else {
184+ $ style .= $ name .': ' .$ value .'; ' ;
185+ }
186+ }
187+ $ attr ['style ' ] = trim ($ style );
188+ }
189+
190+ if (isset ($ attr ['data ' ])) {
191+ foreach ($ attr ['data ' ] as $ name => $ value ) {
192+ $ attr ['data- ' .$ name ] = $ value ;
193+ }
194+ unset($ attr ['data ' ]);
195+ }
196+
197+ $ result = '' ;
198+ $ runtime = $ env ->getRuntime (EscaperRuntime::class);
199+
200+ foreach ($ attr as $ name => $ value ) {
201+ $ result .= $ runtime ->escape ($ name , 'html_attr ' ).'=" ' .$ runtime ->escape ($ value ).'" ' ;
202+ }
203+
204+ return trim ($ result );
205+ }
127206}
0 commit comments