diff --git a/extra/html-extra/HtmlExtension.php b/extra/html-extra/HtmlExtension.php index 94efbe064f9..944f0c7ea50 100644 --- a/extra/html-extra/HtmlExtension.php +++ b/extra/html-extra/HtmlExtension.php @@ -15,8 +15,6 @@ use Twig\Environment; use Twig\Error\RuntimeError; use Twig\Extension\AbstractExtension; -use Twig\Extension\CoreExtension; -use Twig\Extension\EscaperExtension; use Twig\Runtime\EscaperRuntime; use Twig\TwigFilter; use Twig\TwigFunction; @@ -144,21 +142,32 @@ public static function htmlAttrMerge(...$arrays): array throw new RuntimeError(sprintf('The "attr_merge" filter only works with arrays or "Traversable", got "%s" for argument %d.', \gettype($array), $argNumber + 1)); } - $array = CoreExtension::toArray($array); + $array = (array) ($array); - foreach (['class', 'style', 'data', 'aria'] as $deepMergeKey) { - if (isset($array[$deepMergeKey])) { - $value = $array[$deepMergeKey]; - unset($array[$deepMergeKey]); + foreach (['data', 'aria'] as $flatOutKey) { + if (!isset($array[$flatOutKey])) { + continue; + } + $values = (array) $array[$flatOutKey]; + foreach ($values as $key => $value) { + $result[$flatOutKey.'-'.$key] = $value; + } + unset($array[$flatOutKey]); + } - if (!is_iterable($value)) { - $value = (array) $value; - } + foreach (['class', 'style'] as $deepMergeKey) { + if (!isset($array[$deepMergeKey])) { + continue; + } - $value = CoreExtension::toArray($value); + $value = $array[$deepMergeKey]; + unset($array[$deepMergeKey]); - $result[$deepMergeKey] = array_merge($result[$deepMergeKey] ?? [], $value); + if (!is_iterable($value)) { + $value = (array) $value; } + + $result[$deepMergeKey] = array_merge($result[$deepMergeKey] ?? [], $value); } $result = array_merge($result, $array); diff --git a/extra/html-extra/Tests/HtmlAttrMergeTest.php b/extra/html-extra/Tests/HtmlAttrMergeTest.php index e39c4b87b53..fb478649e68 100644 --- a/extra/html-extra/Tests/HtmlAttrMergeTest.php +++ b/extra/html-extra/Tests/HtmlAttrMergeTest.php @@ -19,11 +19,150 @@ public function testMerge(array $expected, array $inputs): void public function htmlAttrProvider(): \Generator { - yield 'simple test' => [ - ['id' => 'some-id', 'class' => ['some-class']], + yield 'merging different attributes from two arrays' => [ + ['id' => 'some-id', 'label' => 'some-label'], [ ['id' => 'some-id'], - ['class' => 'some-class'], + ['label' => 'some-label'], + ] + ]; + + yield 'merging different attributes from three arrays' => [ + ['id' => 'some-id', 'label' => 'some-label', 'role' => 'main'], + [ + ['id' => 'some-id'], + ['label' => 'some-label'], + ['role' => 'main'], + ] + ]; + + yield 'merging different attributes from Traversables' => [ + ['id' => 'some-id', 'label' => 'some-label', 'role' => 'main'], + [ + new \ArrayIterator(['id' => 'some-id']), + new \ArrayIterator(['label' => 'some-label']), + new \ArrayIterator(['role' => 'main']), + ] + ]; + + yield 'later keys override previous ones' => [ + ['id' => 'other'], + [ + ['id' => 'this'], + ['id' => 'that'], + ['id' => 'other'], + ] + ]; + + yield 'ignore empty strings or arrays passed as arguments' => [ + ['some' => 'attribute'], + [ + ['some' => 'attribute'], + [], // empty array + '', // empty string + ] + ]; + + yield 'keep "true" and "false" boolean values' => [ + ['disabled' => true, 'enabled' => false], + [ + ['disabled' => true], + ['enabled' => false], + ] + ]; + + yield 'consolidate values for the "class" key' => [ + ['class' => ['foo', 'bar', 'baz']], + [ + ['class' => ['foo']], + ['class' => 'bar'], // string, not array + ['class' => ['baz']], + ] + ]; + + yield 'class values can be overridden when they use names (array keys)' => [ + ['class' => ['foo', 'bar', 'importance' => 'high']], + [ + ['class' => 'foo'], + ['class' => ['bar', 'importance' => 'low']], + ['class' => ['importance' => 'high']], + ] + ]; + + yield 'inline style values with numerical keys are merely collected' => [ + ['style' => ['font-weight: light', 'color: green', 'font-weight: bold']], + [ + ['style' => ['font-weight: light']], + ['style' => ['color: green', 'font-weight: bold']], + ] + ]; + + yield 'inline style values can be overridden when they use names (array keys)' => [ + ['style' => ['font-weight' => 'bold', 'color' => 'red']], + [ + ['style' => ['font-weight' => 'light']], + ['style' => ['color' => 'green', 'font-weight' => 'bold']], + ['style' => ['color' => 'red']], + ] + ]; + + yield 'no merging happens when mixing numerically indexed inline styles with named ones' => [ + ['style' => ['color: green', 'color' => 'red']], + [ + ['style' => ['color: green']], + ['style' => ['color' => 'red']], + ] + ]; + + yield 'turning aria attributes from array to flat keys' => [ + ['aria-role' => 'banner'], + [ + ['aria' => ['role' => 'main']], + ['aria' => ['role' => 'banner']], + ] + ]; + + yield 'merging aria attributes, where the array values overrides the flat one' => [ + ['aria-role' => 'navigation'], + [ + ['aria-role' => 'main'], + ['aria' => ['role' => 'banner']], + ['aria' => ['role' => 'navigation']], + ] + ]; + + yield 'merging aria attributes, where the flat ones overrides the array' => [ + ['aria-role' => 'navigation'], + [ + ['aria' => ['role' => 'main']], + ['aria-role' => 'banner'], + ['aria-role' => 'navigation'], + ] + ]; + + yield 'turning data attributes from array to flat keys' => [ + ['data-test' => 'bar'], + [ + ['data' => ['test' => 'foo']], + ['data' => ['test' => 'bar']], + ] + ]; + + yield 'merging data attributes, where the array values overrides the flat one' => [ + ['data-test' => 'baz'], + [ + ['data-test' => 'foo'], + ['data' => ['test' => 'bar']], + ['data' => ['test' => 'baz']], + ] + ]; + + yield 'merging data attributes, where the flat ones overrides the array' => [ + ['data-test' => 'baz'], + [ + ['data' => ['test' => 'foo']], + ['data-test' => 'bar'], + ['data-test' => 'baz'], ] ]; }