Skip to content

Commit

Permalink
[BUGFIX] Preserve case of CSS variable names
Browse files Browse the repository at this point in the history
Unlike standard CSS properties, custom properties have case-sensitive names.

Relates to #1276.

Also ensure with type hints that property names (keys) in parsed declaration
blocks cannot be empty strings.
  • Loading branch information
JakeQZ committed Sep 22, 2024
1 parent f8504aa commit f16f59c
Show file tree
Hide file tree
Showing 5 changed files with 139 additions and 9 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ Please also have a look at our

### Fixed

- Preserve case of CSS custom property (varible) names (#1332)

### Documentation

- Add an API and deprecation policy (#1323)
Expand Down
21 changes: 16 additions & 5 deletions src/CssInliner.php
Original file line number Diff line number Diff line change
Expand Up @@ -446,19 +446,21 @@ private function getAllNodesWithStyleAttribute(): \DOMNodeList
*/
private function normalizeStyleAttributes(\DOMElement $node): void
{
$declarationBlockParser = new DeclarationBlockParser();

$normalizedOriginalStyle = (new Preg())->throwExceptions($this->debug)->replaceCallback(
'/-?+[_a-zA-Z][\\w\\-]*+(?=:)/S',
'/-{0,2}+[_a-zA-Z][\\w\\-]*+(?=:)/S',
/** @param array<array-key, string> $propertyNameMatches */
static function (array $propertyNameMatches): string {
return \strtolower($propertyNameMatches[0]);
static function (array $propertyNameMatches) use ($declarationBlockParser): string {
return $declarationBlockParser->normalizePropertyName($propertyNameMatches[0]);
},
$node->getAttribute('style')
);

// In order to not overwrite existing style attributes in the HTML, we have to save the original HTML styles.
$nodePath = $node->getNodePath();
if (\is_string($nodePath) && !isset($this->styleAttributesForNodes[$nodePath])) {
$this->styleAttributesForNodes[$nodePath] = (new DeclarationBlockParser())->parse($normalizedOriginalStyle);
$this->styleAttributesForNodes[$nodePath] = $declarationBlockParser->parse($normalizedOriginalStyle);
$this->visitedNodes[$nodePath] = $node;
}

Expand Down Expand Up @@ -805,6 +807,8 @@ private function copyInlinableCssToStyleAttribute(\DOMElement $node, array $cssR
* @param array<string, string> $newStyles
*
* @return string
*
* @throws \UnexpectedValueException if an empty property name is encountered (which should not happen)
*/
private function generateStyleStringFromDeclarationsArrays(array $oldStyles, array $newStyles): string
{
Expand Down Expand Up @@ -832,9 +836,16 @@ private function generateStyleStringFromDeclarationsArrays(array $oldStyles, arr

$combinedStyles = \array_merge($oldStyles, $newStyles);

$declarationBlockParser = new DeclarationBlockParser();
$style = '';
foreach ($combinedStyles as $attributeName => $attributeValue) {
$style .= \strtolower(\trim($attributeName)) . ': ' . \trim($attributeValue) . '; ';
$trimmedAttributeName = \trim($attributeName);
if ($trimmedAttributeName === '') {
throw new \UnexpectedValueException('An empty property name was encountered.', 1727046078);
}
$propertyName = $declarationBlockParser->normalizePropertyName($trimmedAttributeName);
$propertyValue = \trim($attributeValue);
$style .= $propertyName . ': ' . $propertyValue . '; ';
}
$trimmedStyle = \rtrim($style);

Expand Down
31 changes: 27 additions & 4 deletions src/Utilities/DeclarationBlockParser.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,27 @@
class DeclarationBlockParser
{
/**
* @var array<string, array<string, string>>
* @var array<string, array<non-empty-string, string>>
*/
private static $cache = [];

/**
* CSS custom properties (variables) have case-sensitive names, so their case must be preserved.
* Standard CSS properties have case-insensitive names, which are converted to lowercase.
*
* @param non-empty-string $name
*
* @return non-empty-string
*/
public function normalizePropertyName(string $name): string
{
if (\substr($name, 0, 2) === '--') {
return $name;
} else {
return \strtolower($name);
}
}

/**
* Parses a CSS declaration block into property name/value pairs.
*
Expand All @@ -35,8 +52,10 @@ class DeclarationBlockParser
*
* @param string $declarationBlock the CSS declarations block without the curly braces, may be empty
*
* @return array<string, string>
* @return array<non-empty-string, string>
* the CSS declarations with the property names as array keys and the property values as array values
*
* @throws \UnexpectedValueException if an empty property name is encountered (which cannot happen)
*/
public function parse(string $declarationBlock): array
{
Expand All @@ -62,9 +81,13 @@ public function parse(string $declarationBlock): array
continue;
}

$propertyName = \strtolower($matches[1]);
$propertyName = $matches[1];
if ($propertyName === '') {
// This cannot happen since the regular epression matches one or more characters.
throw new \UnexpectedValueException('An empty property name was encountered.', 1727046409);
}
$propertyValue = $matches[2];
$properties[$propertyName] = $propertyValue;
$properties[$this->normalizePropertyName($propertyName)] = $propertyValue;
}
self::$cache[$declarationBlock] = $properties;

Expand Down
24 changes: 24 additions & 0 deletions tests/Unit/CssInlinerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -1546,6 +1546,30 @@ public function inlineCssLowercasesAttributeNamesFromPassedInCss(): void
);
}

/**
* @test
*/
public function inlineCssPreservesCaseForVariableNamesFromStyleAttributes(): void
{
$subject = $this->buildDebugSubject('<html style="--Text-Color:#ccc;"></html>');

$subject->inlineCss();

self::assertStringContainsString('style="--Text-Color: #ccc;"', $subject->render());
}

/**
* @test
*/
public function inlineCssPreservesCaseForVariableNamesFromPassedInCss(): void
{
$subject = $this->buildDebugSubject('<html></html>');

$subject->inlineCss('html {--Text-Color: #ccc;}');

self::assertStringContainsString('style="--Text-Color: #ccc;"', $subject->render());
}

/**
* @test
*/
Expand Down
70 changes: 70 additions & 0 deletions tests/Unit/Utilities/DeclarationBlockParserTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,56 @@
*/
final class DeclarationBlockParserTest extends TestCase
{
/**
* @return array<non-empty-string, array{name: non-empty-string, expect: non-empty-string}>
*/
public function providePropertyNameAndExpectedNormalization(): array
{
return [
'standard property' => [
'name' => 'color',
'expect' => 'color',
],
'vendor property' => [
'name' => '-moz-box-sizing',
'expect' => '-moz-box-sizing',
],
'custom property' => [
'name' => '--text-color',
'expect' => '--text-color',
],
'standard property with some uppercase' => [
'name' => 'cOlOr',
'expect' => 'color',
],
'vendor property with some uppercase' => [
'name' => '-MOZ-box-Sizing',
'expect' => '-moz-box-sizing',
],
'custom property with some uppercase' => [
'name' => '--TEXT-Color',
'expect' => '--TEXT-Color',
],
];
}

/**
* @test
*
* @param non-empty-string $name
* @param non-empty-string $expectedNormalization
*
* @dataProvider providePropertyNameAndExpectedNormalization
*/
public function normalizesPropertyName(string $name, string $expectedNormalization): void
{
$subject = new DeclarationBlockParser();

$result = $subject->normalizePropertyName($name);

self::assertSame($expectedNormalization, $result);
}

/**
* @return array<non-empty-string, array{string: string, array: array<non-empty-string, non-empty-string>}>
*/
Expand Down Expand Up @@ -94,6 +144,26 @@ public function provideDeclratationBlockAsStringAndArray(): array
'string' => 'color: var(--text-color);',
'array' => ['color' => 'var(--text-color)'],
],
'declaration with uppercase in property name' => [
'string' => 'CoLoR: green;',
'array' => ['color' => 'green'],
],
'vendor property declaration with uppercase in property name' => [
'string' => '-Moz-Box-Sizing: border-box;',
'array' => ['-moz-box-sizing' => 'border-box'],
],
'custom property definition with uppercase in property name' => [
'string' => '--Text-Color: green;',
'array' => ['--Text-Color' => 'green'],
],
'declaration using custom property with uppercase in its name' => [
'string' => 'color: var(--Text-Color);',
'array' => ['color' => 'var(--Text-Color)'],
],
'declaration with uppercase in property value' => [
'string' => 'font-family: Courier;',
'array' => ['font-family' => 'Courier'],
],
'declaration using CSS function' => [
'string' => 'width: calc(50% + 10vw + 10rem);',
'array' => ['width' => 'calc(50% + 10vw + 10rem)'],
Expand Down

0 comments on commit f16f59c

Please sign in to comment.