From c7eb1c1d4ef3413e6df3a0d82f15e68c5fd654ce Mon Sep 17 00:00:00 2001 From: Oliver Klee Date: Sun, 12 Oct 2025 20:14:26 +0200 Subject: [PATCH 001/136] [TASK] Improve the Composer script naming (#1388) The scripts for checking things are not CI-specific. So rename their shared prefix from `ci:` to `check:`. --- .github/workflows/ci.yml | 4 +- .github/workflows/codecoverage.yml | 2 +- CONTRIBUTING.md | 4 +- composer.json | 66 +++++++++++++++--------------- 4 files changed, 38 insertions(+), 38 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 251e8fc4d..952c786ed 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -47,7 +47,7 @@ jobs: composer show; - name: PHP Lint - run: composer ci:php:lint + run: composer check:php:lint unit-tests: name: Unit tests @@ -145,4 +145,4 @@ jobs: phive --no-progress install --trust-gpg-keys 0FDE18AE1D09E19F60F6B1CBC00543248C87FB13,BBAB5DF0A0D6672989CF1869E82B2FB314E9906E - name: Run Command - run: composer ci:${{ matrix.command }} + run: composer check:${{ matrix.command }} diff --git a/.github/workflows/codecoverage.yml b/.github/workflows/codecoverage.yml index 29c37ba50..cefdd554a 100644 --- a/.github/workflows/codecoverage.yml +++ b/.github/workflows/codecoverage.yml @@ -52,7 +52,7 @@ jobs: composer show; - name: Run Tests - run: composer ci:tests:coverage + run: composer check:tests:coverage - name: Show generated coverage files run: ls -lah diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1d0085f3a..41f833679 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -80,7 +80,7 @@ code coverage of the fixed bugs and the new features. To run the existing PHPUnit tests, run this command: ```bash -composer ci:tests:unit +composer check:tests:unit ``` ## Coding Style @@ -94,7 +94,7 @@ We will only merge pull requests that follow the project's coding style. Please check your code with the provided static code analysis tools: ```bash -composer ci:static +composer check:static ``` Please make your code clean, well-readable and easy to understand. diff --git a/composer.json b/composer.json index bec30b48d..379b52206 100644 --- a/composer.json +++ b/composer.json @@ -67,31 +67,31 @@ } }, "scripts": { - "ci": [ - "@ci:static", - "@ci:dynamic" + "check": [ + "@check:static", + "@check:dynamic" ], - "ci:composer:normalize": "\"./.phive/composer-normalize\" --dry-run", - "ci:dynamic": [ - "@ci:tests" + "check:composer:normalize": "\"./.phive/composer-normalize\" --dry-run", + "check:dynamic": [ + "@check:tests" ], - "ci:php:fixer": "\"./.phive/php-cs-fixer\" --config=config/php-cs-fixer.php fix --dry-run -v --show-progress=dots --diff bin src tests config", - "ci:php:lint": "parallel-lint src tests config bin", - "ci:php:rector": "rector --no-progress-bar --dry-run --config=config/rector.php", - "ci:php:stan": "phpstan --no-progress --configuration=config/phpstan.neon", - "ci:static": [ - "@ci:composer:normalize", - "@ci:php:fixer", - "@ci:php:lint", - "@ci:php:rector", - "@ci:php:stan" + "check:php:fixer": "\"./.phive/php-cs-fixer\" --config=config/php-cs-fixer.php fix --dry-run -v --show-progress=dots --diff bin src tests config", + "check:php:lint": "parallel-lint src tests config bin", + "check:php:rector": "rector --no-progress-bar --dry-run --config=config/rector.php", + "check:php:stan": "phpstan --no-progress --configuration=config/phpstan.neon", + "check:static": [ + "@check:composer:normalize", + "@check:php:fixer", + "@check:php:lint", + "@check:php:rector", + "@check:php:stan" ], - "ci:tests": [ - "@ci:tests:unit" + "check:tests": [ + "@check:tests:unit" ], - "ci:tests:coverage": "phpunit --do-not-cache-result --coverage-clover=coverage.xml", - "ci:tests:sof": "phpunit --stop-on-failure --do-not-cache-result", - "ci:tests:unit": "phpunit --do-not-cache-result", + "check:tests:coverage": "phpunit --do-not-cache-result --coverage-clover=coverage.xml", + "check:tests:sof": "phpunit --stop-on-failure --do-not-cache-result", + "check:tests:unit": "phpunit --do-not-cache-result", "fix": [ "@fix:php" ], @@ -106,18 +106,18 @@ "phpstan:baseline": "phpstan --configuration=config/phpstan.neon --generate-baseline=config/phpstan-baseline.neon --allow-empty-baseline" }, "scripts-descriptions": { - "ci": "Runs all dynamic and static code checks.", - "ci:composer:normalize": "Checks the formatting and structure of the composer.json.", - "ci:dynamic": "Runs all dynamic code checks (i.e., currently, the unit tests).", - "ci:php:fixer": "Checks the code style with PHP CS Fixer.", - "ci:php:lint": "Checks the syntax of the PHP code.", - "ci:php:rector": "Checks the code for possible code updates and refactoring.", - "ci:php:stan": "Checks the types with PHPStan.", - "ci:static": "Runs all static code analysis checks for the code.", - "ci:tests": "Runs all dynamic tests (i.e., currently, the unit tests).", - "ci:tests:coverage": "Runs the unit tests with code coverage.", - "ci:tests:sof": "Runs the unit tests and stops at the first failure.", - "ci:tests:unit": "Runs all unit tests.", + "check": "Runs all dynamic and static code checks.", + "check:composer:normalize": "Checks the formatting and structure of the composer.json.", + "check:dynamic": "Runs all dynamic code checks (i.e., currently, the unit tests).", + "check:php:fixer": "Checks the code style with PHP CS Fixer.", + "check:php:lint": "Checks the syntax of the PHP code.", + "check:php:rector": "Checks the code for possible code updates and refactoring.", + "check:php:stan": "Checks the types with PHPStan.", + "check:static": "Runs all static code analysis checks for the code.", + "check:tests": "Runs all dynamic tests (i.e., currently, the unit tests).", + "check:tests:coverage": "Runs the unit tests with code coverage.", + "check:tests:sof": "Runs the unit tests and stops at the first failure.", + "check:tests:unit": "Runs all unit tests.", "fix": "Runs all fixers", "fix:composer:normalize": "Reformats and sorts the composer.json file.", "fix:php": "Autofixes all autofixable issues in the PHP code.", From 0adb83295105c68bbd45891fda6268c94104cc00 Mon Sep 17 00:00:00 2001 From: Oliver Klee Date: Sun, 16 Nov 2025 19:27:01 +0100 Subject: [PATCH 002/136] [FEATURE] Add line length code check and fixer (#1392) --- composer.json | 7 +++++++ config/phpcs.xml | 12 ++++++++++++ 2 files changed, 19 insertions(+) create mode 100644 config/phpcs.xml diff --git a/composer.json b/composer.json index 379b52206..5c6efd2c9 100644 --- a/composer.json +++ b/composer.json @@ -37,6 +37,7 @@ "rawr/phpunit-data-provider": "3.3.1", "rector/rector": "1.2.10 || 2.1.7", "rector/type-perfect": "1.0.0 || 2.1.0", + "squizlabs/php_codesniffer": "4.0.1", "thecodingmachine/phpstan-safe-rule": "1.2.0 || 1.4.1" }, "suggest": { @@ -75,6 +76,7 @@ "check:dynamic": [ "@check:tests" ], + "check:php:codesniffer": "phpcs --standard=config/phpcs.xml config src tests", "check:php:fixer": "\"./.phive/php-cs-fixer\" --config=config/php-cs-fixer.php fix --dry-run -v --show-progress=dots --diff bin src tests config", "check:php:lint": "parallel-lint src tests config bin", "check:php:rector": "rector --no-progress-bar --dry-run --config=config/rector.php", @@ -82,6 +84,7 @@ "check:static": [ "@check:composer:normalize", "@check:php:fixer", + "@check:php:codesniffer", "@check:php:lint", "@check:php:rector", "@check:php:stan" @@ -99,8 +102,10 @@ "fix:php": [ "@fix:composer:normalize", "@fix:php:rector", + "@fix:php:codesniffer", "@fix:php:fixer" ], + "fix:php:codesniffer": "phpcbf --standard=config/phpcs.xml config src tests", "fix:php:fixer": "\"./.phive/php-cs-fixer\" --config=config/php-cs-fixer.php fix bin src tests", "fix:php:rector": "rector --config=config/rector.php", "phpstan:baseline": "phpstan --configuration=config/phpstan.neon --generate-baseline=config/phpstan-baseline.neon --allow-empty-baseline" @@ -109,6 +114,7 @@ "check": "Runs all dynamic and static code checks.", "check:composer:normalize": "Checks the formatting and structure of the composer.json.", "check:dynamic": "Runs all dynamic code checks (i.e., currently, the unit tests).", + "check:php:codesniffer": "Checks the code style with PHP_CodeSniffer.", "check:php:fixer": "Checks the code style with PHP CS Fixer.", "check:php:lint": "Checks the syntax of the PHP code.", "check:php:rector": "Checks the code for possible code updates and refactoring.", @@ -121,6 +127,7 @@ "fix": "Runs all fixers", "fix:composer:normalize": "Reformats and sorts the composer.json file.", "fix:php": "Autofixes all autofixable issues in the PHP code.", + "fix:php:codesniffer": "Reformats the code with PHP_CodeSniffer.", "fix:php:fixer": "Fixes autofixable issues found by PHP CS Fixer.", "fix:php:rector": "Fixes autofixable issues found by Rector.", "phpstan:baseline": "Updates the PHPStan baseline file to match the code." diff --git a/config/phpcs.xml b/config/phpcs.xml new file mode 100644 index 000000000..bb8801e75 --- /dev/null +++ b/config/phpcs.xml @@ -0,0 +1,12 @@ + + + + + + + + + + + + From f5ad2e777f93cfeacd7fc35fef2ed1061e753299 Mon Sep 17 00:00:00 2001 From: Oliver Klee Date: Sun, 16 Nov 2025 19:27:47 +0100 Subject: [PATCH 003/136] [TASK] Update the development dependencies (#1381) --- .phive/phars.xml | 2 +- composer.json | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.phive/phars.xml b/.phive/phars.xml index d9ab49f38..8b34d5ce0 100644 --- a/.phive/phars.xml +++ b/.phive/phars.xml @@ -1,5 +1,5 @@ - + diff --git a/composer.json b/composer.json index 5c6efd2c9..849cc4f2b 100644 --- a/composer.json +++ b/composer.json @@ -30,12 +30,12 @@ "require-dev": { "php-parallel-lint/php-parallel-lint": "1.4.0", "phpstan/extension-installer": "1.4.3", - "phpstan/phpstan": "1.12.28 || 2.1.25", - "phpstan/phpstan-phpunit": "1.4.2 || 2.0.7", - "phpstan/phpstan-strict-rules": "1.6.2 || 2.0.6", + "phpstan/phpstan": "1.12.32 || 2.1.32", + "phpstan/phpstan-phpunit": "1.4.2 || 2.0.8", + "phpstan/phpstan-strict-rules": "1.6.2 || 2.0.7", "phpunit/phpunit": "8.5.48", "rawr/phpunit-data-provider": "3.3.1", - "rector/rector": "1.2.10 || 2.1.7", + "rector/rector": "1.2.10 || 2.2.8", "rector/type-perfect": "1.0.0 || 2.1.0", "squizlabs/php_codesniffer": "4.0.1", "thecodingmachine/phpstan-safe-rule": "1.2.0 || 1.4.1" From 7dd2d852b20c12d4779585d018c731edce34bc16 Mon Sep 17 00:00:00 2001 From: Oliver Klee Date: Mon, 17 Nov 2025 00:08:22 +0100 Subject: [PATCH 004/136] [TASK] Replace deprecated PHP-CS-Fixer rules (#1393) --- config/php-cs-fixer.php | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/config/php-cs-fixer.php b/config/php-cs-fixer.php index c10bf59ae..326105a0b 100644 --- a/config/php-cs-fixer.php +++ b/config/php-cs-fixer.php @@ -6,18 +6,18 @@ ->setRiskyAllowed(true) ->setRules( [ - '@PER-CS2.0' => true, - '@PER-CS2.0:risky' => true, - - '@PHPUnit50Migration:risky' => true, - '@PHPUnit52Migration:risky' => true, - '@PHPUnit54Migration:risky' => true, - '@PHPUnit55Migration:risky' => true, - '@PHPUnit56Migration:risky' => true, - '@PHPUnit57Migration:risky' => true, - '@PHPUnit60Migration:risky' => true, - '@PHPUnit75Migration:risky' => true, - '@PHPUnit84Migration:risky' => true, + '@PER-CS2x0' => true, + '@PER-CS2x0:risky' => true, + + '@PHPUnit5x0Migration:risky' => true, + '@PHPUnit5x2Migration:risky' => true, + '@PHPUnit5x4Migration:risky' => true, + '@PHPUnit5x5Migration:risky' => true, + '@PHPUnit5x6Migration:risky' => true, + '@PHPUnit5x7Migration:risky' => true, + '@PHPUnit6x0Migration:risky' => true, + '@PHPUnit7x5Migration:risky' => true, + '@PHPUnit8x4Migration:risky' => true, // overwrite the PER2 defaults to restore compatibility with PHP 7.x 'trailing_comma_in_multiline' => ['elements' => ['arrays']], From 3f2ec3d64595970003e7e065b1b6e919bcc799e0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 21 Nov 2025 12:07:57 +0100 Subject: [PATCH 005/136] [Dependabot] Bump actions/checkout from 5 to 6 (#1395) Bumps [actions/checkout](https://github.com/actions/checkout) from 5 to 6. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v5...v6) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: '6' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci.yml | 6 +++--- .github/workflows/codecoverage.yml | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 952c786ed..dbab016b1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,7 +20,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: Install PHP uses: shivammathur/setup-php@v2 @@ -63,7 +63,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: Install PHP uses: shivammathur/setup-php@v2 @@ -112,7 +112,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: Install PHP uses: shivammathur/setup-php@v2 diff --git a/.github/workflows/codecoverage.yml b/.github/workflows/codecoverage.yml index cefdd554a..457471e9d 100644 --- a/.github/workflows/codecoverage.yml +++ b/.github/workflows/codecoverage.yml @@ -22,7 +22,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: Install PHP uses: shivammathur/setup-php@v2 From c29c15a56036989c3beaaf01882cdb5ec442b40e Mon Sep 17 00:00:00 2001 From: Oliver Klee Date: Thu, 27 Nov 2025 19:49:38 +0100 Subject: [PATCH 006/136] [CLEANUP] Drop PHPDoc type annotations for constants (#1397) It has turned out that PHPStan does not need them. Fixes #1396 --- src/Parsing/ParserState.php | 3 --- src/Property/AtRule.php | 2 -- src/Property/KeyframeSelector.php | 2 -- src/Property/Selector.php | 2 -- src/Property/Selector/SpecificityCalculator.php | 4 ---- src/Value/CalcFunction.php | 7 ------- src/Value/Size.php | 8 -------- tests/Functional/Value/ValueTest.php | 2 -- tests/OutputFormatTest.php | 3 --- tests/Unit/Value/ValueTest.php | 2 -- 10 files changed, 35 deletions(-) diff --git a/src/Parsing/ParserState.php b/src/Parsing/ParserState.php index cc69ed974..864910fcb 100644 --- a/src/Parsing/ParserState.php +++ b/src/Parsing/ParserState.php @@ -16,9 +16,6 @@ */ class ParserState { - /** - * @var null - */ public const EOF = null; /** diff --git a/src/Property/AtRule.php b/src/Property/AtRule.php index 49a160a1a..7a00660f8 100644 --- a/src/Property/AtRule.php +++ b/src/Property/AtRule.php @@ -16,8 +16,6 @@ interface AtRule extends CSSListItem * Since there are more set rules than block rules, * we’re whitelisting the block rules and have anything else be treated as a set rule. * - * @var non-empty-string - * * @internal since 8.5.2 */ public const BLOCK_RULES = 'media/document/supports/region-style/font-feature-values'; diff --git a/src/Property/KeyframeSelector.php b/src/Property/KeyframeSelector.php index 47881771d..3d5af7b0b 100644 --- a/src/Property/KeyframeSelector.php +++ b/src/Property/KeyframeSelector.php @@ -11,8 +11,6 @@ class KeyframeSelector extends Selector * - comma is not allowed unless escaped or quoted; * - percentage value is allowed by itself. * - * @var non-empty-string - * * @internal since 8.5.2 */ public const SELECTOR_VALIDATION_RX = '/ diff --git a/src/Property/Selector.php b/src/Property/Selector.php index a647378e5..8f6290b4b 100644 --- a/src/Property/Selector.php +++ b/src/Property/Selector.php @@ -17,8 +17,6 @@ class Selector implements Renderable { /** - * @var non-empty-string - * * @internal since 8.5.2 */ public const SELECTOR_VALIDATION_RX = '/ diff --git a/src/Property/Selector/SpecificityCalculator.php b/src/Property/Selector/SpecificityCalculator.php index b2f1323e5..6c7bbccd5 100644 --- a/src/Property/Selector/SpecificityCalculator.php +++ b/src/Property/Selector/SpecificityCalculator.php @@ -15,8 +15,6 @@ final class SpecificityCalculator { /** * regexp for specificity calculations - * - * @var non-empty-string */ private const NON_ID_ATTRIBUTES_AND_PSEUDO_CLASSES_RX = '/ (\\.[\\w]+) # classes @@ -39,8 +37,6 @@ final class SpecificityCalculator /** * regexp for specificity calculations - * - * @var non-empty-string */ private const ELEMENTS_AND_PSEUDO_ELEMENTS_RX = '/ ((^|[\\s\\+\\>\\~]+)[\\w]+ # elements diff --git a/src/Value/CalcFunction.php b/src/Value/CalcFunction.php index dba6e1dd9..cd1648876 100644 --- a/src/Value/CalcFunction.php +++ b/src/Value/CalcFunction.php @@ -10,14 +10,7 @@ class CalcFunction extends CSSFunction { - /** - * @var int - */ private const T_OPERAND = 1; - - /** - * @var int - */ private const T_OPERATOR = 2; /** diff --git a/src/Value/Size.php b/src/Value/Size.php index eac736d79..e8e2a1d00 100644 --- a/src/Value/Size.php +++ b/src/Value/Size.php @@ -19,8 +19,6 @@ class Size extends PrimitiveValue { /** * vh/vw/vm(ax)/vmin/rem are absolute insofar as they don’t scale to the immediate parent (only the viewport) - * - * @var list */ private const ABSOLUTE_SIZE_UNITS = [ 'px', @@ -40,14 +38,8 @@ class Size extends PrimitiveValue 'rem', ]; - /** - * @var list - */ private const RELATIVE_SIZE_UNITS = ['%', 'em', 'ex', 'ch', 'fr']; - /** - * @var list - */ private const NON_SIZE_UNITS = ['deg', 'grad', 'rad', 's', 'ms', 'turn', 'Hz', 'kHz']; /** diff --git a/tests/Functional/Value/ValueTest.php b/tests/Functional/Value/ValueTest.php index 4d65ee2a8..b8dd5b4f1 100644 --- a/tests/Functional/Value/ValueTest.php +++ b/tests/Functional/Value/ValueTest.php @@ -20,8 +20,6 @@ final class ValueTest extends TestCase * the default set of delimiters for parsing most values * * @see \Sabberworm\CSS\Rule\Rule::listDelimiterForRule - * - * @var list */ private const DEFAULT_DELIMITERS = [',', ' ', '/']; diff --git a/tests/OutputFormatTest.php b/tests/OutputFormatTest.php index 3a8deb30e..cb0dfd42c 100644 --- a/tests/OutputFormatTest.php +++ b/tests/OutputFormatTest.php @@ -15,9 +15,6 @@ */ final class OutputFormatTest extends TestCase { - /** - * @var string - */ private const TEST_CSS = << */ private const DEFAULT_DELIMITERS = [',', ' ', '/']; From fe29992d5ef3c74e8c9927549cbf82072eea9d6c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 1 Dec 2025 12:49:32 +0100 Subject: [PATCH 007/136] [Dependabot] Update phpunit/phpunit requirement from 8.5.48 to 8.5.49 (#1401) Updates the requirements on [phpunit/phpunit](https://github.com/sebastianbergmann/phpunit) to permit the latest version. - [Release notes](https://github.com/sebastianbergmann/phpunit/releases) - [Changelog](https://github.com/sebastianbergmann/phpunit/blob/8.5.49/ChangeLog-8.5.md) - [Commits](https://github.com/sebastianbergmann/phpunit/compare/8.5.48...8.5.49) --- updated-dependencies: - dependency-name: phpunit/phpunit dependency-version: 8.5.49 dependency-type: direct:development ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 849cc4f2b..476b3bcb2 100644 --- a/composer.json +++ b/composer.json @@ -33,7 +33,7 @@ "phpstan/phpstan": "1.12.32 || 2.1.32", "phpstan/phpstan-phpunit": "1.4.2 || 2.0.8", "phpstan/phpstan-strict-rules": "1.6.2 || 2.0.7", - "phpunit/phpunit": "8.5.48", + "phpunit/phpunit": "8.5.49", "rawr/phpunit-data-provider": "3.3.1", "rector/rector": "1.2.10 || 2.2.8", "rector/type-perfect": "1.0.0 || 2.1.0", From b01197de48c9d26da55e8247c0cfccac640b1697 Mon Sep 17 00:00:00 2001 From: Simon Chester Date: Tue, 2 Dec 2025 15:56:41 +1300 Subject: [PATCH 008/136] [BUGFIX] Parse calc split over multiple lines (#1399) --- CHANGELOG.md | 2 + src/Value/CalcFunction.php | 7 +- tests/Unit/Value/CalcFunctionTest.php | 206 ++++++++++++++++++++++++++ 3 files changed, 212 insertions(+), 3 deletions(-) create mode 100644 tests/Unit/Value/CalcFunctionTest.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 8978fbb71..ded270b73 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,8 @@ Please also have a look at our ### Fixed - Use typesafe versions of PHP functions (#1379, #1380, #1382, #1383, #1384) +- Fix parsing of `calc` expressions when a newline immediately precedes or + follows a + or - operator (#1399) ### Documentation diff --git a/src/Value/CalcFunction.php b/src/Value/CalcFunction.php index cd1648876..f674ed151 100644 --- a/src/Value/CalcFunction.php +++ b/src/Value/CalcFunction.php @@ -8,6 +8,8 @@ use Sabberworm\CSS\Parsing\UnexpectedEOFException; use Sabberworm\CSS\Parsing\UnexpectedTokenException; +use function Safe\preg_match; + class CalcFunction extends CSSFunction { private const T_OPERAND = 1; @@ -60,9 +62,8 @@ public static function parse(ParserState $parserState, bool $ignoreCase = false) if (\in_array($parserState->peek(), $operators, true)) { if (($parserState->comes('-') || $parserState->comes('+'))) { if ( - $parserState->peek(1, -1) !== ' ' - || !($parserState->comes('- ') - || $parserState->comes('+ ')) + preg_match('/\\s/', $parserState->peek(1, -1)) !== 1 + || preg_match('/\\s/', $parserState->peek(1, 1)) !== 1 ) { throw new UnexpectedTokenException( " {$parserState->peek()} ", diff --git a/tests/Unit/Value/CalcFunctionTest.php b/tests/Unit/Value/CalcFunctionTest.php new file mode 100644 index 000000000..b87f742b5 --- /dev/null +++ b/tests/Unit/Value/CalcFunctionTest.php @@ -0,0 +1,206 @@ +parse($css); + + self::assertInstanceOf(CalcFunction::class, $calcFunction); + self::assertSame('calc', $calcFunction->getName()); + + $args = $calcFunction->getArguments(); + self::assertCount(1, $args); + self::assertInstanceOf(CalcRuleValueList::class, $args[0]); + + /** @var CalcRuleValueList $value */ + $value = $args[0]; + $components = $value->getListComponents(); + self::assertCount(3, $components); // 100%, -, 20px + + self::assertInstanceOf(Size::class, $components[0]); + self::assertSame(100.0, $components[0]->getSize()); + self::assertSame('%', $components[0]->getUnit()); + + self::assertSame('-', $components[1]); + + self::assertInstanceOf(Size::class, $components[2]); + self::assertSame(20.0, $components[2]->getSize()); + self::assertSame('px', $components[2]->getUnit()); + } + /** + * @test + */ + public function parseNestedCalc(): void + { + $css = 'calc(100% - calc(20px + 1em))'; + $calcFunction = $this->parse($css); + + /** @var CalcRuleValueList $value */ + $value = $calcFunction->getArguments()[0]; + $components = $value->getListComponents(); + + self::assertCount(3, $components); + self::assertSame('-', $components[1]); + + /** @var CalcFunction */ + $nestedCalc = $components[2]; + self::assertInstanceOf(CalcFunction::class, $nestedCalc); + + /** @var CalcRuleValueList $nestedValue */ + $nestedValue = $nestedCalc->getArguments()[0]; + self::assertInstanceOf(CalcRuleValueList::class, $nestedValue); + $nestedComponents = $nestedValue->getListComponents(); + + self::assertCount(3, $nestedComponents); + self::assertSame('+', $nestedComponents[1]); + } + + /** + * @test + */ + public function parseWithParentheses(): void + { + $css = 'calc((100% - 20px) * 2)'; + $calcFunction = $this->parse($css); + + /** @var CalcRuleValueList $value */ + $value = $calcFunction->getArguments()[0]; + $components = $value->getListComponents(); + + self::assertCount(7, $components); + self::assertSame('(', $components[0]); + self::assertInstanceOf(Size::class, $components[1]); // 100% + self::assertSame('-', $components[2]); + self::assertInstanceOf(Size::class, $components[3]); // 20px + self::assertSame(')', $components[4]); + self::assertSame('*', $components[5]); + self::assertInstanceOf(Size::class, $components[6]); // 2 + } + + /** + * @return array + */ + public function provideValidOperatorSyntax(): array + { + return [ + '+ op' => ['calc(100% + 20px)', 'calc(100% + 20px)'], + '- op' => ['calc(100% - 20px)', 'calc(100% - 20px)'], + '* op' => ['calc(100% * 20)', 'calc(100% * 20)'], + '* op no space' => ['calc(100%*20)', 'calc(100% * 20)'], + '/ op' => ['calc(100% / 20)', 'calc(100% / 20)'], + '/ op no space' => ['calc(100%/20)', 'calc(100% / 20)'], + ]; + } + + /** + * @test + * + * @dataProvider provideValidOperatorSyntax + */ + public function parseValidOperators(string $css, string $rendered): void + { + $calcFunction = $this->parse($css); + $output = $calcFunction->render(OutputFormat::create()); + self::assertSame($rendered, $output); + } + + /** + * @return array + */ + public function provideMultiline(): array + { + return [ + 'right newline' => ["calc(100% +\n20px)", 'calc(100% + 20px)'], + 'right and outer newline' => ["calc(\n100% +\n20px\n)", 'calc(100% + 20px)'], + 'left newline' => ["calc(100%\n+ 20px)", 'calc(100% + 20px)'], + 'both newline' => ["calc(100%\n+\n20px)", 'calc(100% + 20px)'], + 'tab whitespace' => ["calc(100%\t+\t20px)", 'calc(100% + 20px)'], + '- op' => ["calc(100%\n-\n20px)", 'calc(100% - 20px)'], + '/ op' => ["calc(100% /\n20)", 'calc(100% / 20)'], + ]; + } + + /** + * @test + * + * @dataProvider provideMultiline + */ + public function parseMultiline(string $css, string $rendered): void + { + $calcFunction = $this->parse($css); + $output = $calcFunction->render(OutputFormat::create()); + self::assertSame($rendered, $output); + } + + /** + * @return array + */ + public function provideInvalidSyntax(): array + { + return [ + 'missing space around -' => ['calc(100%-20px)'], + 'missing space around +' => ['calc(100%+20px)'], + 'invalid operator' => ['calc(100% ^ 20px)'], + ]; + } + + /** + * @test + * + * @dataProvider provideInvalidSyntax + */ + public function parseThrowsExceptionForInvalidSyntax(string $css): void + { + $this->expectException(UnexpectedTokenException::class); + $this->parse($css); + } + + /** + * @test + */ + public function parseThrowsExceptionIfCalledWithWrongFunctionName(): void + { + $css = 'wrong(100% - 20px)'; + $parserState = new ParserState($css, Settings::create()); + + $this->expectException(UnexpectedTokenException::class); + $this->expectExceptionMessage('calc'); + CalcFunction::parse($parserState); + } + + /** + * Parse provided CSS as a CalcFunction + * + * @param string $css + * @return CalcFunction + */ + private function parse(string $css): CalcFunction + { + $parserState = new ParserState($css, Settings::create()); + + $function = CalcFunction::parse($parserState); + self::assertInstanceOf(CalcFunction::class, $function); + return $function; + } +} From 961427932a7ddb97cdf39a4cddcad044db4c399e Mon Sep 17 00:00:00 2001 From: Simon Chester Date: Tue, 2 Dec 2025 22:12:50 +1300 Subject: [PATCH 009/136] [CLEANUP] Remove unneccesary `@var` PHPDoc annotations (#1402) --- tests/Unit/Value/CalcFunctionTest.php | 3 --- 1 file changed, 3 deletions(-) diff --git a/tests/Unit/Value/CalcFunctionTest.php b/tests/Unit/Value/CalcFunctionTest.php index b87f742b5..0eedba289 100644 --- a/tests/Unit/Value/CalcFunctionTest.php +++ b/tests/Unit/Value/CalcFunctionTest.php @@ -33,7 +33,6 @@ public function parseSimpleCalc(): void self::assertCount(1, $args); self::assertInstanceOf(CalcRuleValueList::class, $args[0]); - /** @var CalcRuleValueList $value */ $value = $args[0]; $components = $value->getListComponents(); self::assertCount(3, $components); // 100%, -, 20px @@ -63,11 +62,9 @@ public function parseNestedCalc(): void self::assertCount(3, $components); self::assertSame('-', $components[1]); - /** @var CalcFunction */ $nestedCalc = $components[2]; self::assertInstanceOf(CalcFunction::class, $nestedCalc); - /** @var CalcRuleValueList $nestedValue */ $nestedValue = $nestedCalc->getArguments()[0]; self::assertInstanceOf(CalcRuleValueList::class, $nestedValue); $nestedComponents = $nestedValue->getListComponents(); From 8be4011d7ba0d1e2027d6baaa727830b67d9c8f6 Mon Sep 17 00:00:00 2001 From: Simon Chester Date: Tue, 2 Dec 2025 23:58:35 +1300 Subject: [PATCH 010/136] [FEATURE] Parse container queries (#1400) --- CHANGELOG.md | 2 ++ src/Property/AtRule.php | 2 +- tests/CSSList/AtRuleBlockListTest.php | 3 +++ tests/Unit/Property/AtRuleTest.php | 22 ++++++++++++++++++++++ 4 files changed, 28 insertions(+), 1 deletion(-) create mode 100644 tests/Unit/Property/AtRuleTest.php diff --git a/CHANGELOG.md b/CHANGELOG.md index ded270b73..ab613dc3d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,8 @@ Please also have a look at our ### Added +- Support for CSS container queries (#1400) + ### Changed ### Deprecated diff --git a/src/Property/AtRule.php b/src/Property/AtRule.php index 7a00660f8..f5ae475ea 100644 --- a/src/Property/AtRule.php +++ b/src/Property/AtRule.php @@ -18,7 +18,7 @@ interface AtRule extends CSSListItem * * @internal since 8.5.2 */ - public const BLOCK_RULES = 'media/document/supports/region-style/font-feature-values'; + public const BLOCK_RULES = 'media/document/supports/region-style/font-feature-values/container'; /** * @return non-empty-string diff --git a/tests/CSSList/AtRuleBlockListTest.php b/tests/CSSList/AtRuleBlockListTest.php index 9a725b212..3539463bf 100644 --- a/tests/CSSList/AtRuleBlockListTest.php +++ b/tests/CSSList/AtRuleBlockListTest.php @@ -48,6 +48,9 @@ public static function provideSyntacticallyCorrectAtRule(): array } ', ], + 'container' => [ + '@container (min-width: 60rem) { .items { background: blue; } }', + ], ]; } diff --git a/tests/Unit/Property/AtRuleTest.php b/tests/Unit/Property/AtRuleTest.php new file mode 100644 index 000000000..53793bea8 --- /dev/null +++ b/tests/Unit/Property/AtRuleTest.php @@ -0,0 +1,22 @@ + Date: Tue, 2 Dec 2025 20:24:14 +0100 Subject: [PATCH 011/136] [DOCS] Polish the changelog (#1404) --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ab613dc3d..216525f57 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,7 @@ Please also have a look at our ### Added -- Support for CSS container queries (#1400) +- Add support for CSS container queries (#1400) ### Changed @@ -22,7 +22,7 @@ Please also have a look at our - Use typesafe versions of PHP functions (#1379, #1380, #1382, #1383, #1384) - Fix parsing of `calc` expressions when a newline immediately precedes or - follows a + or - operator (#1399) + follows a `+` or `-` operator (#1399) ### Documentation From a7e229b03d63233547c1febb48e71ba6e88548d8 Mon Sep 17 00:00:00 2001 From: Simon Chester Date: Wed, 3 Dec 2025 12:01:48 +1300 Subject: [PATCH 012/136] [CLEANUP] Tweak PHPDoc types in calc unit test (#1405) --- tests/Unit/Value/CalcFunctionTest.php | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/tests/Unit/Value/CalcFunctionTest.php b/tests/Unit/Value/CalcFunctionTest.php index 0eedba289..f4907d477 100644 --- a/tests/Unit/Value/CalcFunctionTest.php +++ b/tests/Unit/Value/CalcFunctionTest.php @@ -96,7 +96,7 @@ public function parseWithParentheses(): void } /** - * @return array + * @return array */ public function provideValidOperatorSyntax(): array { @@ -123,7 +123,7 @@ public function parseValidOperators(string $css, string $rendered): void } /** - * @return array + * @return array */ public function provideMultiline(): array { @@ -151,7 +151,7 @@ public function parseMultiline(string $css, string $rendered): void } /** - * @return array + * @return array */ public function provideInvalidSyntax(): array { @@ -188,9 +188,6 @@ public function parseThrowsExceptionIfCalledWithWrongFunctionName(): void /** * Parse provided CSS as a CalcFunction - * - * @param string $css - * @return CalcFunction */ private function parse(string $css): CalcFunction { From 3f57730f6c7445d3e2e1fc9c0cdbab98bf6c6124 Mon Sep 17 00:00:00 2001 From: JakeQZ Date: Thu, 4 Dec 2025 12:08:11 +0000 Subject: [PATCH 013/136] [CLEANUP] Tighten `DeclarationBlock::selectors` type (#1407) Use a local variable in `setSelectors()` rather than temporarily assigning the property with a type it's not supposed to have. Ensure that the property is set to a non-sparse numerical array. Also tighten the return type of `getSelectors()`. (Precursor to #1330.) --- CHANGELOG.md | 3 +++ src/RuleSet/DeclarationBlock.php | 19 ++++++++++++------- tests/Unit/RuleSet/DeclarationBlockTest.php | 15 +++++++++++++++ 3 files changed, 30 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 216525f57..0a06bcef4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,9 @@ Please also have a look at our ### Changed +- The array keys passed to `DeclarationBlock::setSelectors()` are no longer + preserved (#1407) + ### Deprecated ### Removed diff --git a/src/RuleSet/DeclarationBlock.php b/src/RuleSet/DeclarationBlock.php index 4d0775460..8a0f4e487 100644 --- a/src/RuleSet/DeclarationBlock.php +++ b/src/RuleSet/DeclarationBlock.php @@ -36,7 +36,7 @@ class DeclarationBlock implements CSSElement, CSSListItem, Positionable, RuleCon use Position; /** - * @var array + * @var list */ private $selectors = []; @@ -146,11 +146,13 @@ public static function parse(ParserState $parserState, ?CSSList $list = null): ? public function setSelectors($selectors, ?CSSList $list = null): void { if (\is_array($selectors)) { - $this->selectors = $selectors; + $selectorsToSet = $selectors; } else { - $this->selectors = \explode(',', $selectors); + $selectorsToSet = \explode(',', $selectors); } - foreach ($this->selectors as $key => $selector) { + + // Convert all items to a `Selector` if not already + foreach ($selectorsToSet as $key => $selector) { if (!($selector instanceof Selector)) { if ($list === null || !($list instanceof KeyFrame)) { if (!Selector::isValid($selector)) { @@ -160,7 +162,7 @@ public function setSelectors($selectors, ?CSSList $list = null): void 'custom' ); } - $this->selectors[$key] = new Selector($selector); + $selectorsToSet[$key] = new Selector($selector); } else { if (!KeyframeSelector::isValid($selector)) { throw new UnexpectedTokenException( @@ -169,10 +171,13 @@ public function setSelectors($selectors, ?CSSList $list = null): void 'custom' ); } - $this->selectors[$key] = new KeyframeSelector($selector); + $selectorsToSet[$key] = new KeyframeSelector($selector); } } } + + // Discard the keys and reindex the array + $this->selectors = \array_values($selectorsToSet); } /** @@ -195,7 +200,7 @@ public function removeSelector($selectorToRemove): bool } /** - * @return array + * @return list */ public function getSelectors(): array { diff --git a/tests/Unit/RuleSet/DeclarationBlockTest.php b/tests/Unit/RuleSet/DeclarationBlockTest.php index 4b20e9fc8..5f226846a 100644 --- a/tests/Unit/RuleSet/DeclarationBlockTest.php +++ b/tests/Unit/RuleSet/DeclarationBlockTest.php @@ -278,4 +278,19 @@ public function getRuleSetReturnsObjectWithLineNumberPassedToConstructor(?int $l self::assertSame($lineNumber, $result->getLineNumber()); } + + /** + * @test + * + * Any type of array may be passed to the method, but the resultant property should be a `list`. + */ + public function setSelectorsIgnoresKeys(): void + { + $subject = new DeclarationBlock(); + $subject->setSelectors(['Bob' => 'html', 'Mary' => 'body']); + + $result = $subject->getSelectors(); + + self::assertSame([0, 1], \array_keys($result)); + } } From 4a8a1c1811a5df09c4c584f4d122a7b9fb19b44d Mon Sep 17 00:00:00 2001 From: Oliver Klee Date: Thu, 4 Dec 2025 13:15:39 +0100 Subject: [PATCH 014/136] [DOCS] Sort changelog entries in reverse chronological order (#1408) To be consistent with the git log, new entries in the changelog should be first. This also makes it easier to see at a glance what was changed recently. --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0a06bcef4..ccdfc1ded 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,9 +23,9 @@ Please also have a look at our ### Fixed -- Use typesafe versions of PHP functions (#1379, #1380, #1382, #1383, #1384) - Fix parsing of `calc` expressions when a newline immediately precedes or follows a `+` or `-` operator (#1399) +- Use typesafe versions of PHP functions (#1379, #1380, #1382, #1383, #1384) ### Documentation From cb0292de5ba5351711b9c20914d9adf541ccab75 Mon Sep 17 00:00:00 2001 From: Oliver Klee Date: Thu, 4 Dec 2025 13:36:24 +0100 Subject: [PATCH 015/136] [TASK] Add coverage metadata to `AtRuleTest` (#1409) As interfaces cannot be covered using an `@covers` annotation, we need to use `@coversNothing` instead. This will allow us to require coverage metadata for testcases later down the road. --- tests/Unit/Property/AtRuleTest.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/Unit/Property/AtRuleTest.php b/tests/Unit/Property/AtRuleTest.php index 53793bea8..6094fbf83 100644 --- a/tests/Unit/Property/AtRuleTest.php +++ b/tests/Unit/Property/AtRuleTest.php @@ -7,6 +7,9 @@ use PHPUnit\Framework\TestCase; use Sabberworm\CSS\Property\AtRule; +/** + * @coversNothing + */ final class AtRuleTest extends TestCase { /** From 51a3c0acf0e26ae9782c25c1fae63a26708aca30 Mon Sep 17 00:00:00 2001 From: Oliver Klee Date: Thu, 4 Dec 2025 14:16:10 +0100 Subject: [PATCH 016/136] [TASK] Avoid heredoc in a test (#1410) This allows the testcase being autoformatted without breaking the tests for PHP 7.2 https://www.php.net/manual/en/language.types.string.php#language.types.string.syntax.heredoc --- tests/ParserTest.php | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/tests/ParserTest.php b/tests/ParserTest.php index b80280a77..a2bd4e703 100644 --- a/tests/ParserTest.php +++ b/tests/ParserTest.php @@ -570,11 +570,10 @@ public function selectorRemoval(): void public function comments(): void { $document = self::parsedStructureForFile('comments'); - $expected = <<render()); } From dc99bbde6c021dea62e77c831ffc3d9cbefb904b Mon Sep 17 00:00:00 2001 From: JakeQZ Date: Thu, 4 Dec 2025 17:30:36 +0000 Subject: [PATCH 017/136] [BUGFIX] Allow order-insensitve `removeDeclarationBlockBySelector` (#1406) Also internally avoid comparison with `==`. Resolves #1330 --- CHANGELOG.md | 1 + config/phpstan-baseline.neon | 6 ------ src/CSSList/CSSList.php | 32 +++++++++++++++++++++++++++++- tests/Unit/CSSList/CSSListTest.php | 16 +++++++++++++++ 4 files changed, 48 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ccdfc1ded..e35259471 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,7 @@ Please also have a look at our ### Fixed +- Allow `removeDeclarationBlockBySelector()` to be order-insensitve (#1406) - Fix parsing of `calc` expressions when a newline immediately precedes or follows a `+` or `-` operator (#1399) - Use typesafe versions of PHP functions (#1379, #1380, #1382, #1383, #1384) diff --git a/config/phpstan-baseline.neon b/config/phpstan-baseline.neon index 6205096ae..4c4add4da 100644 --- a/config/phpstan-baseline.neon +++ b/config/phpstan-baseline.neon @@ -1,11 +1,5 @@ parameters: ignoreErrors: - - - message: '#^Loose comparison via "\=\=" is not allowed\.$#' - identifier: equal.notAllowed - count: 1 - path: ../src/CSSList/CSSList.php - - message: '#^Parameter \#2 \$found of class Sabberworm\\CSS\\Parsing\\UnexpectedTokenException constructor expects string, Sabberworm\\CSS\\Value\\CSSFunction\|Sabberworm\\CSS\\Value\\CSSString\|Sabberworm\\CSS\\Value\\LineName\|Sabberworm\\CSS\\Value\\Size\|Sabberworm\\CSS\\Value\\URL given\.$#' identifier: argument.type diff --git a/src/CSSList/CSSList.php b/src/CSSList/CSSList.php index e09a03a98..56495f388 100644 --- a/src/CSSList/CSSList.php +++ b/src/CSSList/CSSList.php @@ -370,7 +370,7 @@ public function removeDeclarationBlockBySelector($selectors, bool $removeAll = f if (!($item instanceof DeclarationBlock)) { continue; } - if ($item->getSelectors() == $selectors) { + if (self::selectorsMatch($item->getSelectors(), $selectors)) { unset($this->contents[$key]); if (!$removeAll) { return; @@ -427,4 +427,34 @@ public function getContents(): array { return $this->contents; } + + /** + * @param list $selectors1 + * @param list $selectors2 + */ + private static function selectorsMatch(array $selectors1, array $selectors2): bool + { + $selectorStrings1 = self::getSelectorStrings($selectors1); + $selectorStrings2 = self::getSelectorStrings($selectors2); + + \sort($selectorStrings1); + \sort($selectorStrings2); + + return $selectorStrings1 === $selectorStrings2; + } + + /** + * @param list $selectors + * + * @return list + */ + private static function getSelectorStrings(array $selectors): array + { + return \array_map( + static function (Selector $selector): string { + return $selector->getSelector(); + }, + $selectors + ); + } } diff --git a/tests/Unit/CSSList/CSSListTest.php b/tests/Unit/CSSList/CSSListTest.php index ada176e9a..523ada093 100644 --- a/tests/Unit/CSSList/CSSListTest.php +++ b/tests/Unit/CSSList/CSSListTest.php @@ -239,6 +239,22 @@ public function removeDeclarationBlockBySelectorRemovesDeclarationBlockWithOutso self::assertSame([], $subject->getContents()); } + /** + * @test + */ + public function removeDeclarationBlockBySelectorRemovesDeclarationBlockWithSelectorsInReverseOrder(): void + { + $subject = new ConcreteCSSList(); + $declarationBlock = new DeclarationBlock(); + $declarationBlock->setSelectors(['html', 'body']); + $subject->setContents([$declarationBlock]); + self::assertNotSame([], $subject->getContents()); // make sure contents are set + + $subject->removeDeclarationBlockBySelector([new Selector('body'), new Selector('html')]); + + self::assertSame([], $subject->getContents()); + } + /** * @test */ From 00f637f6e98065bbfe2f083bc207bfdf1db08ea3 Mon Sep 17 00:00:00 2001 From: Oliver Klee Date: Fri, 5 Dec 2025 12:28:08 +0100 Subject: [PATCH 018/136] [TASK] Avoid heredoc in another test (#1411) This allows the testcase being autoformatted without breaking the tests for PHP 7.2. https://www.php.net/manual/en/language.types.string.php#language.types.string.syntax.heredoc --- tests/OutputFormatTest.php | 29 +++++++++++++---------------- 1 file changed, 13 insertions(+), 16 deletions(-) diff --git a/tests/OutputFormatTest.php b/tests/OutputFormatTest.php index cb0dfd42c..104ad7b5c 100644 --- a/tests/OutputFormatTest.php +++ b/tests/OutputFormatTest.php @@ -15,22 +15,19 @@ */ final class OutputFormatTest extends TestCase { - private const TEST_CSS = << Date: Sat, 6 Dec 2025 10:58:18 +0100 Subject: [PATCH 019/136] [Dependabot] Update phpunit/phpunit requirement from 8.5.49 to 8.5.50 (#1413) Updates the requirements on [phpunit/phpunit](https://github.com/sebastianbergmann/phpunit) to permit the latest version. - [Release notes](https://github.com/sebastianbergmann/phpunit/releases) - [Changelog](https://github.com/sebastianbergmann/phpunit/blob/8.5.50/ChangeLog-8.5.md) - [Commits](https://github.com/sebastianbergmann/phpunit/compare/8.5.49...8.5.50) --- updated-dependencies: - dependency-name: phpunit/phpunit dependency-version: 8.5.50 dependency-type: direct:development ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 476b3bcb2..caaf9a54a 100644 --- a/composer.json +++ b/composer.json @@ -33,7 +33,7 @@ "phpstan/phpstan": "1.12.32 || 2.1.32", "phpstan/phpstan-phpunit": "1.4.2 || 2.0.8", "phpstan/phpstan-strict-rules": "1.6.2 || 2.0.7", - "phpunit/phpunit": "8.5.49", + "phpunit/phpunit": "8.5.50", "rawr/phpunit-data-provider": "3.3.1", "rector/rector": "1.2.10 || 2.2.8", "rector/type-perfect": "1.0.0 || 2.1.0", From 49bf66f053a84daf6b34294c59f756b1cd8757e9 Mon Sep 17 00:00:00 2001 From: 8ctopus Date: Mon, 8 Dec 2025 18:59:46 +0400 Subject: [PATCH 020/136] [CLEANUP] Clean extra whitespace in CSS selector (#1398) --- CHANGELOG.md | 1 + src/Property/Selector.php | 8 ++++- tests/ParserTest.php | 2 +- tests/Unit/Property/SelectorTest.php | 47 ++++++++++++++++++++++++++++ 4 files changed, 56 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e35259471..3026b9364 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ Please also have a look at our ### Changed +- Cleanup extra whitespace in css selector (#1398) - The array keys passed to `DeclarationBlock::setSelectors()` are no longer preserved (#1407) diff --git a/src/Property/Selector.php b/src/Property/Selector.php index 8f6290b4b..b935b2a82 100644 --- a/src/Property/Selector.php +++ b/src/Property/Selector.php @@ -9,6 +9,7 @@ use Sabberworm\CSS\Renderable; use function Safe\preg_match; +use function Safe\preg_replace; /** * Class representing a single CSS selector. Selectors have to be split by the comma prior to being passed into this @@ -74,7 +75,12 @@ public function getSelector(): string public function setSelector(string $selector): void { - $this->selector = \trim($selector); + $selector = \trim($selector); + + $hasAttribute = \strpos($selector, '[') !== false; + + // Whitespace can't be adjusted within an attribute selector, as it would change its meaning + $this->selector = !$hasAttribute ? preg_replace('/\\s++/', ' ', $selector) : $selector; } /** diff --git a/tests/ParserTest.php b/tests/ParserTest.php index a2bd4e703..7604feb31 100644 --- a/tests/ParserTest.php +++ b/tests/ParserTest.php @@ -770,7 +770,7 @@ public function selectorIgnoresInFile(): void $document = self::parsedStructureForFile('selector-ignores', Settings::create()->withMultibyteSupport(true)); $expected = '.some[selectors-may=\'contain-a-{\'] {}' . "\n" - . '.this-selector .valid {width: 100px;}' + . '.this-selector .valid {width: 100px;}' . "\n" . '@media only screen and (min-width: 200px) {.test {prop: val;}}'; self::assertSame($expected, $document->render()); diff --git a/tests/Unit/Property/SelectorTest.php b/tests/Unit/Property/SelectorTest.php index e53e8274d..cfec36fba 100644 --- a/tests/Unit/Property/SelectorTest.php +++ b/tests/Unit/Property/SelectorTest.php @@ -141,4 +141,51 @@ public function isValidForInvalidSelectorReturnsFalse(string $selector): void { self::assertFalse(Selector::isValid($selector)); } + + /** + * @test + */ + public function cleansUpSpacesWithinSelector(): void + { + $selector = 'p > small'; + + $subject = new Selector($selector); + + self::assertSame('p > small', $subject->getSelector()); + } + + /** + * @test + */ + public function cleansUpTabsWithinSelector(): void + { + $selector = "p\t>\tsmall"; + + $subject = new Selector($selector); + + self::assertSame('p > small', $subject->getSelector()); + } + + /** + * @test + */ + public function cleansUpNewLineWithinSelector(): void + { + $selector = "p\n>\nsmall"; + + $subject = new Selector($selector); + + self::assertSame('p > small', $subject->getSelector()); + } + + + /** + * @test + */ + public function doesNotCleanupSpacesWithinAttributeSelector(): void + { + $subject = new Selector('a[title="extra space"]'); + + self::assertSame('a[title="extra space"]', $subject->getSelector()); + } } From 21712ccbb979b00d82fabb87286d5c435f958096 Mon Sep 17 00:00:00 2001 From: JakeQZ Date: Mon, 8 Dec 2025 16:54:36 +0000 Subject: [PATCH 021/136] [DOCS] Capitalize 'CSS' in the changelog (#1414) Also correct grammar of 'clean up', which is two separate words when used as a verb (though as a noun or adjective is a single word). --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3026b9364..c5882abcf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,7 +14,7 @@ Please also have a look at our ### Changed -- Cleanup extra whitespace in css selector (#1398) +- Clean up extra whitespace in CSS selector (#1398) - The array keys passed to `DeclarationBlock::setSelectors()` are no longer preserved (#1407) From 400edb2d5404fcb51a21a0d9c601f01bc637380d Mon Sep 17 00:00:00 2001 From: JakeQZ Date: Mon, 8 Dec 2025 20:18:11 +0000 Subject: [PATCH 022/136] [DEVELOPER] Disallow 'heredoc' and 'nowdoc' for strings (#1415) Part of #1412. --- config/phpcs.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/config/phpcs.xml b/config/phpcs.xml index bb8801e75..f4a05b783 100644 --- a/config/phpcs.xml +++ b/config/phpcs.xml @@ -9,4 +9,5 @@ + From e6b19f22fa06e0e336d702ecabfe2566f3401838 Mon Sep 17 00:00:00 2001 From: JakeQZ Date: Tue, 9 Dec 2025 08:36:02 +0000 Subject: [PATCH 023/136] [CLEANUP] Extract method `DeclarationBlock::parseSelectors` (#1416) This now does the selector-parsing that `parse()` used to do itself. Should help with #1324 and https://github.com/MyIntervals/PHP-CSS-Parser/pull/1398#issuecomment-3590773005 --- src/RuleSet/DeclarationBlock.php | 121 ++++++++++++++++++------------- 1 file changed, 69 insertions(+), 52 deletions(-) diff --git a/src/RuleSet/DeclarationBlock.php b/src/RuleSet/DeclarationBlock.php index 8a0f4e487..b5ce1eda3 100644 --- a/src/RuleSet/DeclarationBlock.php +++ b/src/RuleSet/DeclarationBlock.php @@ -4,6 +4,7 @@ namespace Sabberworm\CSS\RuleSet; +use Sabberworm\CSS\Comment\Comment; use Sabberworm\CSS\Comment\CommentContainer; use Sabberworm\CSS\CSSElement; use Sabberworm\CSS\CSSList\CSSList; @@ -65,58 +66,7 @@ public static function parse(ParserState $parserState, ?CSSList $list = null): ? $comments = []; $result = new DeclarationBlock($parserState->currentLine()); try { - $selectors = []; - $selectorParts = []; - $stringWrapperCharacter = null; - $functionNestingLevel = 0; - $consumedNextCharacter = false; - static $stopCharacters = ['{', '}', '\'', '"', '(', ')', ',']; - do { - if (!$consumedNextCharacter) { - $selectorParts[] = $parserState->consume(1); - } - $selectorParts[] = $parserState->consumeUntil($stopCharacters, false, false, $comments); - $nextCharacter = $parserState->peek(); - $consumedNextCharacter = false; - switch ($nextCharacter) { - case '\'': - // The fallthrough is intentional. - case '"': - if (!\is_string($stringWrapperCharacter)) { - $stringWrapperCharacter = $nextCharacter; - } elseif ($stringWrapperCharacter === $nextCharacter) { - if (\substr(\end($selectorParts), -1) !== '\\') { - $stringWrapperCharacter = null; - } - } - break; - case '(': - if (!\is_string($stringWrapperCharacter)) { - ++$functionNestingLevel; - } - break; - case ')': - if (!\is_string($stringWrapperCharacter)) { - if ($functionNestingLevel <= 0) { - throw new UnexpectedTokenException('anything but', ')'); - } - --$functionNestingLevel; - } - break; - case ',': - if (!\is_string($stringWrapperCharacter) && $functionNestingLevel === 0) { - $selectors[] = \implode('', $selectorParts); - $selectorParts = []; - $parserState->consume(1); - $consumedNextCharacter = true; - } - break; - } - } while (!\in_array($nextCharacter, ['{', '}'], true) || \is_string($stringWrapperCharacter)); - if ($functionNestingLevel !== 0) { - throw new UnexpectedTokenException(')', $nextCharacter); - } - $selectors[] = \implode('', $selectorParts); // add final or only selector + $selectors = self::parseSelectors($parserState, $comments); $result->setSelectors($selectors, $list); if ($parserState->comes('{')) { $parserState->consume(1); @@ -303,4 +253,71 @@ public function render(OutputFormat $outputFormat): string return $result; } + + /** + * @param array $comments + * + * @return list + * + * @throws UnexpectedTokenException + */ + private static function parseSelectors(ParserState $parserState, array &$comments): array + { + $selectors = []; + $selectorParts = []; + $stringWrapperCharacter = null; + $functionNestingLevel = 0; + $consumedNextCharacter = false; + static $stopCharacters = ['{', '}', '\'', '"', '(', ')', ',']; + + do { + if (!$consumedNextCharacter) { + $selectorParts[] = $parserState->consume(1); + } + $selectorParts[] = $parserState->consumeUntil($stopCharacters, false, false, $comments); + $nextCharacter = $parserState->peek(); + $consumedNextCharacter = false; + switch ($nextCharacter) { + case '\'': + // The fallthrough is intentional. + case '"': + if (!\is_string($stringWrapperCharacter)) { + $stringWrapperCharacter = $nextCharacter; + } elseif ($stringWrapperCharacter === $nextCharacter) { + if (\substr(\end($selectorParts), -1) !== '\\') { + $stringWrapperCharacter = null; + } + } + break; + case '(': + if (!\is_string($stringWrapperCharacter)) { + ++$functionNestingLevel; + } + break; + case ')': + if (!\is_string($stringWrapperCharacter)) { + if ($functionNestingLevel <= 0) { + throw new UnexpectedTokenException('anything but', ')'); + } + --$functionNestingLevel; + } + break; + case ',': + if (!\is_string($stringWrapperCharacter) && $functionNestingLevel === 0) { + $selectors[] = \implode('', $selectorParts); + $selectorParts = []; + $parserState->consume(1); + $consumedNextCharacter = true; + } + break; + } + } while (!\in_array($nextCharacter, ['{', '}'], true) || \is_string($stringWrapperCharacter)); + + if ($functionNestingLevel !== 0) { + throw new UnexpectedTokenException(')', $nextCharacter); + } + $selectors[] = \implode('', $selectorParts); // add final or only selector + + return $selectors; + } } From d1af3bb56cd8e061d2811c917934be21748eb863 Mon Sep 17 00:00:00 2001 From: Oliver Klee Date: Tue, 9 Dec 2025 13:18:32 +0100 Subject: [PATCH 024/136] [DOCS] Document how to create git tags (#1417) Copied from our sister project at https://github.com/MyIntervals/emogrifier/pull/1532 --- docs/release-checklist.md | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/docs/release-checklist.md b/docs/release-checklist.md index 48d50b7e6..5d673b898 100644 --- a/docs/release-checklist.md +++ b/docs/release-checklist.md @@ -13,3 +13,28 @@ [Releases tab](https://github.com/MyIntervals/PHP-CSS-Parser/releases), create a new release and copy the change log entries to the new release. 1. Post about the new release on social media. + +## Working with git tags + +List all tags: + +```bash +git tag +``` + +Locally create a tag from the current `HEAD` commit and push it to the git +remote `origin`: + +```bash +git tag -a v4.2.0 -m "Tag version 4.2.0" +git push --tags +``` + +Locally create a +[GPG-signed](https://git-scm.com/book/ms/v2/Git-Tools-Signing-Your-Work) tag +from the current `HEAD` commit and push it to the git remote `origin`: + +```bash +git tag -a -s v4.2.0 -m "Tag version 4.2.0" +git push --tags +``` From a5d1172ba7e4a0f2a8acb205c91c1fe2e848e819 Mon Sep 17 00:00:00 2001 From: JakeQZ Date: Tue, 9 Dec 2025 12:43:02 +0000 Subject: [PATCH 025/136] [CLEANUP] Tighten `consumeUntil` `$comments` parameter type (#1418) --- src/Parsing/ParserState.php | 2 +- src/RuleSet/DeclarationBlock.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Parsing/ParserState.php b/src/Parsing/ParserState.php index 864910fcb..722cd92c6 100644 --- a/src/Parsing/ParserState.php +++ b/src/Parsing/ParserState.php @@ -328,7 +328,7 @@ public function isEnd(): bool /** * @param list|string|self::EOF $stopCharacters - * @param array $comments + * @param list $comments * * @throws UnexpectedEOFException * @throws UnexpectedTokenException diff --git a/src/RuleSet/DeclarationBlock.php b/src/RuleSet/DeclarationBlock.php index b5ce1eda3..65a4862f0 100644 --- a/src/RuleSet/DeclarationBlock.php +++ b/src/RuleSet/DeclarationBlock.php @@ -255,7 +255,7 @@ public function render(OutputFormat $outputFormat): string } /** - * @param array $comments + * @param list $comments * * @return list * From 8d123a220a3f070d10c2054e39363635df0b4479 Mon Sep 17 00:00:00 2001 From: JakeQZ Date: Tue, 9 Dec 2025 18:44:35 +0000 Subject: [PATCH 026/136] [BUGFIX] Allow commas in attributes in `setSelectors` (#1419) This fixes an issue noted in #1324. Also add additional test data to fully exercise the code. --- CHANGELOG.md | 2 + src/RuleSet/DeclarationBlock.php | 23 ++++++- tests/Unit/RuleSet/DeclarationBlockTest.php | 70 +++++++++++++++++++++ 3 files changed, 93 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c5882abcf..74bdef76e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,8 @@ Please also have a look at our ### Fixed +- Support attribute selectors with values containing commas in + `DeclarationBlock::setSelectors()` (#1419) - Allow `removeDeclarationBlockBySelector()` to be order-insensitve (#1406) - Fix parsing of `calc` expressions when a newline immediately precedes or follows a `+` or `-` operator (#1399) diff --git a/src/RuleSet/DeclarationBlock.php b/src/RuleSet/DeclarationBlock.php index 65a4862f0..81590d518 100644 --- a/src/RuleSet/DeclarationBlock.php +++ b/src/RuleSet/DeclarationBlock.php @@ -20,6 +20,7 @@ use Sabberworm\CSS\Property\KeyframeSelector; use Sabberworm\CSS\Property\Selector; use Sabberworm\CSS\Rule\Rule; +use Sabberworm\CSS\Settings; /** * This class represents a `RuleSet` constrained by a `Selector`. @@ -98,7 +99,25 @@ public function setSelectors($selectors, ?CSSList $list = null): void if (\is_array($selectors)) { $selectorsToSet = $selectors; } else { - $selectorsToSet = \explode(',', $selectors); + // A string of comma-separated selectors requires parsing. + // Parse as if it's the opening part of a rule. + try { + $parserState = new ParserState($selectors . '{', Settings::create()); + $selectorsToSet = self::parseSelectors($parserState); + $parserState->consume('{'); // throw exception if this is not next + if (!$parserState->isEnd()) { + throw new UnexpectedTokenException('EOF', 'more'); + } + } catch (UnexpectedTokenException $exception) { + // The exception message from parsing may refer to the faux `{` block start token, + // which would be confusing. + // Rethrow with a more useful message, that also includes the selector(s) string that was passed. + throw new UnexpectedTokenException( + 'Selector(s) string is not valid.', + $selectors, + 'custom' + ); + } } // Convert all items to a `Selector` if not already @@ -261,7 +280,7 @@ public function render(OutputFormat $outputFormat): string * * @throws UnexpectedTokenException */ - private static function parseSelectors(ParserState $parserState, array &$comments): array + private static function parseSelectors(ParserState $parserState, array &$comments = []): array { $selectors = []; $selectorParts = []; diff --git a/tests/Unit/RuleSet/DeclarationBlockTest.php b/tests/Unit/RuleSet/DeclarationBlockTest.php index 5f226846a..685112d55 100644 --- a/tests/Unit/RuleSet/DeclarationBlockTest.php +++ b/tests/Unit/RuleSet/DeclarationBlockTest.php @@ -8,6 +8,7 @@ use Sabberworm\CSS\CSSElement; use Sabberworm\CSS\CSSList\CSSListItem; use Sabberworm\CSS\Parsing\ParserState; +use Sabberworm\CSS\Parsing\UnexpectedTokenException; use Sabberworm\CSS\Position\Positionable; use Sabberworm\CSS\Property\Selector; use Sabberworm\CSS\Rule\Rule; @@ -293,4 +294,73 @@ public function setSelectorsIgnoresKeys(): void self::assertSame([0, 1], \array_keys($result)); } + + /** + * @test + * + * @param non-empty-string $selector + * + * @dataProvider provideSelector + */ + public function setSelectorsSetsSingleSelectorProvidedAsString(string $selector): void + { + $subject = new DeclarationBlock(); + + $subject->setSelectors($selector); + + $result = $subject->getSelectors(); + self::assertSame([$selector], self::getSelectorsAsStrings($subject)); + } + + /** + * @test + * + * @param non-empty-string $firstSelector + * @param non-empty-string $secondSelector + * + * @dataProvider provideTwoSelectors + */ + public function setSelectorsSetsTwoCommaSeparatedSelectorsProvidedAsString( + string $firstSelector, + string $secondSelector + ): void { + $joinedSelectors = $firstSelector . ', ' . $secondSelector; + $subject = new DeclarationBlock(); + + $subject->setSelectors($joinedSelectors); + + $result = $subject->getSelectors(); + self::assertSame([$firstSelector, $secondSelector], self::getSelectorsAsStrings($subject)); + } + + /** + * Provides selectors that would be parsed without error in the context of full CSS, but are nonetheless invalid. + * + * @return array + */ + public static function provideInvalidStandaloneSelector(): array + { + return [ + 'rogue `{`' => ['a { b'], + 'rogue `}`' => ['a } b'], + ]; + } + + /** + * @test + * + * @param non-empty-string $selector + * + * @dataProvider provideInvalidSelector + * @dataProvider provideInvalidStandaloneSelector + */ + public function setSelectorsThrowsExceptionWithInvalidSelector(string $selector): void + { + $this->expectException(UnexpectedTokenException::class); + $this->expectExceptionMessageMatches('/^Selector\\(s\\) string is not valid. /'); + + $subject = new DeclarationBlock(); + + $subject->setSelectors($selector); + } } From 42502fd99a4cd0068a97163a0fde302592f385d9 Mon Sep 17 00:00:00 2001 From: JakeQZ Date: Thu, 11 Dec 2025 21:27:54 +0000 Subject: [PATCH 027/136] [BUGFIX] Parse comment(s) immediately preceding selector (#1421) Also parse consecutive comments. --- CHANGELOG.md | 2 + src/Parsing/ParserState.php | 23 +++++- tests/ParserTest.php | 7 +- tests/Unit/Parsing/ParserStateTest.php | 100 +++++++++++++++++++++++++ 4 files changed, 124 insertions(+), 8 deletions(-) create mode 100644 tests/Unit/Parsing/ParserStateTest.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 74bdef76e..ffbd588f0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,8 @@ Please also have a look at our ### Fixed +- Parse comment(s) immediately preceding a selector (#1421) +- Parse consecutive comments (#1421) - Support attribute selectors with values containing commas in `DeclarationBlock::setSelectors()` (#1419) - Allow `removeDeclarationBlockBySelector()` to be order-insensitve (#1406) diff --git a/src/Parsing/ParserState.php b/src/Parsing/ParserState.php index 722cd92c6..e17c3f3ab 100644 --- a/src/Parsing/ParserState.php +++ b/src/Parsing/ParserState.php @@ -343,6 +343,7 @@ public function consumeUntil( $consumedCharacters = ''; $start = $this->currentPosition; + $comments = \array_merge($comments, $this->consumeComments()); while (!$this->isEnd()) { $character = $this->consume(1); if (\in_array($character, $stopCharacters, true)) { @@ -354,10 +355,7 @@ public function consumeUntil( return $consumedCharacters; } $consumedCharacters .= $character; - $comment = $this->consumeComment(); - if ($comment instanceof Comment) { - $comments[] = $comment; - } + $comments = \array_merge($comments, $this->consumeComments()); } if (\in_array(self::EOF, $stopCharacters, true)) { @@ -455,4 +453,21 @@ private function strsplit(string $string): array return $result; } + + /** + * @return list + */ + private function consumeComments(): array + { + $comments = []; + + while (true) { + $comment = $this->consumeComment(); + if ($comment instanceof Comment) { + $comments[] = $comment; + } else { + return $comments; + } + } + } } diff --git a/tests/ParserTest.php b/tests/ParserTest.php index 7604feb31..5b360bdae 100644 --- a/tests/ParserTest.php +++ b/tests/ParserTest.php @@ -1048,10 +1048,9 @@ public function commentExtracting(): void $fooBarBlock = $nodes[1]; self::assertInstanceOf(Commentable::class, $fooBarBlock); $fooBarBlockComments = $fooBarBlock->getComments(); - // TODO Support comments in selectors. - // $this->assertCount(2, $fooBarBlockComments); - // $this->assertSame("* Number 4 *", $fooBarBlockComments[0]->getComment()); - // $this->assertSame("* Number 5 *", $fooBarBlockComments[1]->getComment()); + self::assertCount(2, $fooBarBlockComments); + self::assertSame(' Number 4 ', $fooBarBlockComments[0]->getComment()); + self::assertSame(' Number 5 ', $fooBarBlockComments[1]->getComment()); // Declaration rules. self::assertInstanceOf(DeclarationBlock::class, $fooBarBlock); diff --git a/tests/Unit/Parsing/ParserStateTest.php b/tests/Unit/Parsing/ParserStateTest.php new file mode 100644 index 000000000..245b083f8 --- /dev/null +++ b/tests/Unit/Parsing/ParserStateTest.php @@ -0,0 +1,100 @@ + + * } + * > + */ + public static function provideTextForConsumptionWithComments(): array + { + return [ + 'comment at start' => [ + 'text' => '/*comment*/hello{', + 'stopCharacter' => '{', + 'expectedConsumedText' => 'hello', + 'expectedComments' => ['comment'], + ], + 'comment at end' => [ + 'text' => 'hello/*comment*/{', + 'stopCharacter' => '{', + 'expectedConsumedText' => 'hello', + 'expectedComments' => ['comment'], + ], + 'comment in middle' => [ + 'text' => 'hell/*comment*/o{', + 'stopCharacter' => '{', + 'expectedConsumedText' => 'hello', + 'expectedComments' => ['comment'], + ], + 'two comments at start' => [ + 'text' => '/*comment1*//*comment2*/hello{', + 'stopCharacter' => '{', + 'expectedConsumedText' => 'hello', + 'expectedComments' => ['comment1', 'comment2'], + ], + 'two comments at end' => [ + 'text' => 'hello/*comment1*//*comment2*/{', + 'stopCharacter' => '{', + 'expectedConsumedText' => 'hello', + 'expectedComments' => ['comment1', 'comment2'], + ], + 'two comments interspersed' => [ + 'text' => 'he/*comment1*/ll/*comment2*/o{', + 'stopCharacter' => '{', + 'expectedConsumedText' => 'hello', + 'expectedComments' => ['comment1', 'comment2'], + ], + ]; + } + + /** + * @test + * + * @param non-empty-string $text + * @param non-empty-string $stopCharacter + * @param non-empty-string $expectedConsumedText + * @param non-empty-list $expectedComments + * + * @dataProvider provideTextForConsumptionWithComments + */ + public function consumeUntilExtractsComments( + string $text, + string $stopCharacter, + string $expectedConsumedText, + array $expectedComments + ): void { + $subject = new ParserState($text, Settings::create()); + + $comments = []; + $result = $subject->consumeUntil($stopCharacter, false, false, $comments); + + self::assertSame($expectedConsumedText, $result); + $commentsAsText = \array_map( + static function (Comment $comment): string { + return $comment->getComment(); + }, + $comments + ); + self::assertSame($expectedComments, $commentsAsText); + } +} From 1b1f91cf3ec01e121c9e6ed5834e85b42adb7b53 Mon Sep 17 00:00:00 2001 From: JakeQZ Date: Fri, 12 Dec 2025 07:44:08 +0000 Subject: [PATCH 028/136] [TASK] Add `ParserState::consumeIfComes` method (#1422) This, when used (in various places), will be more efficient than doing a `peek()` followed by a `consume()`. --- src/Parsing/ParserState.php | 21 +++++++++ tests/Unit/Parsing/ParserStateTest.php | 60 ++++++++++++++++++++++++++ 2 files changed, 81 insertions(+) diff --git a/src/Parsing/ParserState.php b/src/Parsing/ParserState.php index e17c3f3ab..df8253772 100644 --- a/src/Parsing/ParserState.php +++ b/src/Parsing/ParserState.php @@ -279,6 +279,27 @@ public function consume($value = 1): string return $result; } + /** + * If the possibly-expected next content is next, consume it. + * + * @param non-empty-string $nextContent + * + * @return bool whether the possibly-expected content was found and consumed + */ + public function consumeIfComes(string $nextContent): bool + { + $length = $this->strlen($nextContent); + if (!$this->streql($this->substr($this->currentPosition, $length), $nextContent)) { + return false; + } + + $numberOfLines = \substr_count($nextContent, "\n"); + $this->lineNumber += $numberOfLines; + $this->currentPosition += $this->strlen($nextContent); + + return true; + } + /** * @param string $expression * @param int<1, max>|null $maximumLength diff --git a/tests/Unit/Parsing/ParserStateTest.php b/tests/Unit/Parsing/ParserStateTest.php index 245b083f8..bd22508ab 100644 --- a/tests/Unit/Parsing/ParserStateTest.php +++ b/tests/Unit/Parsing/ParserStateTest.php @@ -97,4 +97,64 @@ static function (Comment $comment): string { ); self::assertSame($expectedComments, $commentsAsText); } + + /** + * @test + */ + public function consumeIfComesComsumesMatchingContent(): void + { + $subject = new ParserState('abc', Settings::create()); + + $subject->consumeIfComes('ab'); + + self::assertSame('c', $subject->peek()); + } + + /** + * @test + */ + public function consumeIfComesDoesNotComsumeNonMatchingContent(): void + { + $subject = new ParserState('a', Settings::create()); + + $subject->consumeIfComes('x'); + + self::assertSame('a', $subject->peek()); + } + + /** + * @test + */ + public function consumeIfComesReturnsTrueIfContentConsumed(): void + { + $subject = new ParserState('abc', Settings::create()); + + $result = $subject->consumeIfComes('ab'); + + self::assertTrue($result); + } + + /** + * @test + */ + public function consumeIfComesReturnsFalseIfContentNotConsumed(): void + { + $subject = new ParserState('a', Settings::create()); + + $result = $subject->consumeIfComes('x'); + + self::assertFalse($result); + } + + /** + * @test + */ + public function consumeIfComesUpdatesLineNumber(): void + { + $subject = new ParserState("\n", Settings::create()); + + $subject->consumeIfComes("\n"); + + self::assertSame(2, $subject->currentLine()); + } } From 024f1f352a9a87abb5956ea249bc7488c2023bcb Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 12 Dec 2025 12:07:26 +0100 Subject: [PATCH 029/136] [Dependabot] Bump actions/cache from 4 to 5 (#1423) Bumps [actions/cache](https://github.com/actions/cache) from 4 to 5. - [Release notes](https://github.com/actions/cache/releases) - [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md) - [Commits](https://github.com/actions/cache/compare/v4...v5) --- updated-dependencies: - dependency-name: actions/cache dependency-version: '5' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci.yml | 6 +++--- .github/workflows/codecoverage.yml | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index dbab016b1..b35ea69f9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -34,7 +34,7 @@ jobs: run: composer config --global --list - name: Cache dependencies installed with composer - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: ~/.cache/composer key: php${{ matrix.php-version }}-composer-${{ hashFiles('**/composer.json') }} @@ -77,7 +77,7 @@ jobs: run: composer config --global --list - name: Cache dependencies installed with composer - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: ~/.cache/composer key: php${{ matrix.php-version }}-composer-${{ hashFiles('**/composer.json') }} @@ -126,7 +126,7 @@ jobs: run: composer config --global --list - name: Cache dependencies installed with composer - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: ~/.cache/composer key: php${{ matrix.php-version }}-composer-${{ hashFiles('**/composer.json') }} diff --git a/.github/workflows/codecoverage.yml b/.github/workflows/codecoverage.yml index 457471e9d..15b1c0546 100644 --- a/.github/workflows/codecoverage.yml +++ b/.github/workflows/codecoverage.yml @@ -39,7 +39,7 @@ jobs: run: composer config --global --list - name: Cache dependencies installed with composer - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: ~/.cache/composer key: php${{ matrix.php-version }}-composer-${{ hashFiles('**/composer.json') }} From a2c978ccb43881c756f0503ff0523c9ba3d50bea Mon Sep 17 00:00:00 2001 From: JakeQZ Date: Fri, 12 Dec 2025 13:41:30 +0000 Subject: [PATCH 030/136] [CLEANUP] Extract method `DeclarationBlock::parseSelector` (#1420) Closes #1324 --- src/RuleSet/DeclarationBlock.php | 43 ++++++++++++++++++++++---------- 1 file changed, 30 insertions(+), 13 deletions(-) diff --git a/src/RuleSet/DeclarationBlock.php b/src/RuleSet/DeclarationBlock.php index 81590d518..456dc33b2 100644 --- a/src/RuleSet/DeclarationBlock.php +++ b/src/RuleSet/DeclarationBlock.php @@ -283,19 +283,33 @@ public function render(OutputFormat $outputFormat): string private static function parseSelectors(ParserState $parserState, array &$comments = []): array { $selectors = []; + + while (true) { + $selectors[] = self::parseSelector($parserState, $comments); + if (!$parserState->consumeIfComes(',')) { + break; + } + } + + return $selectors; + } + + /** + * @param list $comments + * + * @throws UnexpectedTokenException + */ + private static function parseSelector(ParserState $parserState, array &$comments = []): string + { $selectorParts = []; $stringWrapperCharacter = null; $functionNestingLevel = 0; - $consumedNextCharacter = false; static $stopCharacters = ['{', '}', '\'', '"', '(', ')', ',']; - do { - if (!$consumedNextCharacter) { - $selectorParts[] = $parserState->consume(1); - } + while (true) { + $selectorParts[] = $parserState->consume(1); $selectorParts[] = $parserState->consumeUntil($stopCharacters, false, false, $comments); $nextCharacter = $parserState->peek(); - $consumedNextCharacter = false; switch ($nextCharacter) { case '\'': // The fallthrough is intentional. @@ -321,22 +335,25 @@ private static function parseSelectors(ParserState $parserState, array &$comment --$functionNestingLevel; } break; + case '{': + // The fallthrough is intentional. + case '}': + if (!\is_string($stringWrapperCharacter)) { + break 2; + } + break; case ',': if (!\is_string($stringWrapperCharacter) && $functionNestingLevel === 0) { - $selectors[] = \implode('', $selectorParts); - $selectorParts = []; - $parserState->consume(1); - $consumedNextCharacter = true; + break 2; } break; } - } while (!\in_array($nextCharacter, ['{', '}'], true) || \is_string($stringWrapperCharacter)); + } if ($functionNestingLevel !== 0) { throw new UnexpectedTokenException(')', $nextCharacter); } - $selectors[] = \implode('', $selectorParts); // add final or only selector - return $selectors; + return \implode('', $selectorParts); } } From dada8f8b0783522f6c347a4daf7b3f7a5f8abf61 Mon Sep 17 00:00:00 2001 From: JakeQZ Date: Sat, 13 Dec 2025 09:12:19 +0000 Subject: [PATCH 031/136] [BUGFIX] Improve recovery parsing upon a rogue `}` (#1425) (My IDE thinks that the test results now updated should be parsed as such.) Also remove a duplicated test. --- CHANGELOG.md | 1 + src/RuleSet/DeclarationBlock.php | 2 +- tests/ParserTest.php | 11 ++---- tests/Unit/RuleSet/DeclarationBlockTest.php | 39 +++++++++++++++++++++ 4 files changed, 43 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ffbd588f0..caf9aa4e8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,7 @@ Please also have a look at our ### Fixed +- Improve recovery parsing when a rogue `}` is encountered (#1425) - Parse comment(s) immediately preceding a selector (#1421) - Parse consecutive comments (#1421) - Support attribute selectors with values containing commas in diff --git a/src/RuleSet/DeclarationBlock.php b/src/RuleSet/DeclarationBlock.php index 456dc33b2..7aa81f27f 100644 --- a/src/RuleSet/DeclarationBlock.php +++ b/src/RuleSet/DeclarationBlock.php @@ -74,7 +74,7 @@ public static function parse(ParserState $parserState, ?CSSList $list = null): ? } } catch (UnexpectedTokenException $e) { if ($parserState->getSettings()->usesLenientParsing()) { - if (!$parserState->comes('}')) { + if (!$parserState->consumeIfComes('}')) { $parserState->consumeUntil('}', false, true); } return null; diff --git a/tests/ParserTest.php b/tests/ParserTest.php index 5b360bdae..ffe278185 100644 --- a/tests/ParserTest.php +++ b/tests/ParserTest.php @@ -718,6 +718,7 @@ public function invalidSelectorsInFile(): void $document = self::parsedStructureForFile('invalid-selectors', Settings::create()->withMultibyteSupport(true)); $expected = '@keyframes mymove {from {top: 0px;}} #test {color: white;background: green;} +#test {display: block;background: red;color: white;} #test {display: block;background: white;color: black;}'; self::assertSame($expected, $document->render()); @@ -727,6 +728,7 @@ public function invalidSelectorsInFile(): void .super-menu > li:last-of-type {border-right-width: 0;} html[dir="rtl"] .super-menu > li:first-of-type {border-left-width: 1px;border-right-width: 0;} html[dir="rtl"] .super-menu > li:last-of-type {border-left-width: 0;}} +.super-menu.menu-floated {border-right-width: 1px;border-left-width: 1px;border-color: #5a4242;border-style: dotted;} body {background-color: red;}'; self::assertSame($expected, $document->render()); } @@ -740,15 +742,6 @@ public function selectorEscapesInFile(): void $expected = '#\\# {color: red;} .col-sm-1\\/5 {width: 20%;}'; self::assertSame($expected, $document->render()); - - $document = self::parsedStructureForFile('invalid-selectors-2', Settings::create()->withMultibyteSupport(true)); - $expected = '@media only screen and (max-width: 1215px) {.breadcrumb {padding-left: 10px;} - .super-menu > li:first-of-type {border-left-width: 0;} - .super-menu > li:last-of-type {border-right-width: 0;} - html[dir="rtl"] .super-menu > li:first-of-type {border-left-width: 1px;border-right-width: 0;} - html[dir="rtl"] .super-menu > li:last-of-type {border-left-width: 0;}} -body {background-color: red;}'; - self::assertSame($expected, $document->render()); } /** diff --git a/tests/Unit/RuleSet/DeclarationBlockTest.php b/tests/Unit/RuleSet/DeclarationBlockTest.php index 685112d55..caddbd134 100644 --- a/tests/Unit/RuleSet/DeclarationBlockTest.php +++ b/tests/Unit/RuleSet/DeclarationBlockTest.php @@ -192,6 +192,45 @@ public function parseSkipsBlockWithInvalidSelector(string $selector): void self::assertTrue($parserState->comes($nextCss)); } + /** + * @return array + */ + public static function provideClosingBrace(): array + { + return [ + 'as is' => ['}'], + 'with space before' => [' }'], + 'with newline before' => ["\n}"], + ]; + } + + /** + * @return DataProvider + */ + public static function provideInvalidSelectorAndClosingBrace(): DataProvider + { + return DataProvider::cross(self::provideInvalidSelector(), self::provideClosingBrace()); + } + + /** + * TODO: It's probably not the responsibility of `DeclarationBlock` to deal with this. + * + * @test + * + * @param non-empty-string $selector + * @param non-empty-string $closingBrace + * + * @dataProvider provideInvalidSelectorAndClosingBrace + */ + public function parseConsumesClosingBraceAfterInvalidSelector(string $selector, string $closingBrace): void + { + $parserState = new ParserState($selector . $closingBrace, Settings::create()); + + DeclarationBlock::parse($parserState); + + self::assertTrue($parserState->isEnd()); + } + /** * @return array */ From c7fb009174138e839dae360ac7e9489b12c638fb Mon Sep 17 00:00:00 2001 From: JakeQZ Date: Sat, 13 Dec 2025 19:43:00 +0000 Subject: [PATCH 032/136] [BUGFIX] Skip erroneous `}` when parsing `CSSList` (#1426) This allows parsing of the next item, if valid, rather than dropping it. --- CHANGELOG.md | 2 +- src/CSSList/CSSList.php | 3 ++- tests/Unit/CSSList/CSSListTest.php | 38 ++++++++++++++++++++++++++++++ 3 files changed, 41 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index caf9aa4e8..421d9419f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,7 +24,7 @@ Please also have a look at our ### Fixed -- Improve recovery parsing when a rogue `}` is encountered (#1425) +- Improve recovery parsing when a rogue `}` is encountered (#1425, #1426) - Parse comment(s) immediately preceding a selector (#1421) - Parse consecutive comments (#1421) - Support attribute selectors with values containing commas in diff --git a/src/CSSList/CSSList.php b/src/CSSList/CSSList.php index 56495f388..84ece7f8e 100644 --- a/src/CSSList/CSSList.php +++ b/src/CSSList/CSSList.php @@ -133,7 +133,8 @@ private static function parseListItem(ParserState $parserState, CSSList $list) } elseif ($parserState->comes('}')) { if ($isRoot) { if ($parserState->getSettings()->usesLenientParsing()) { - return DeclarationBlock::parse($parserState) ?? false; + $parserState->consume(1); + return self::parseListItem($parserState, $list); } else { throw new SourceException('Unopened {', $parserState->currentLine()); } diff --git a/tests/Unit/CSSList/CSSListTest.php b/tests/Unit/CSSList/CSSListTest.php index 523ada093..548acbb77 100644 --- a/tests/Unit/CSSList/CSSListTest.php +++ b/tests/Unit/CSSList/CSSListTest.php @@ -7,10 +7,15 @@ use PHPUnit\Framework\TestCase; use Sabberworm\CSS\Comment\Commentable; use Sabberworm\CSS\CSSElement; +use Sabberworm\CSS\CSSList\CSSList; use Sabberworm\CSS\CSSList\CSSListItem; +use Sabberworm\CSS\CSSList\Document; +use Sabberworm\CSS\OutputFormat; +use Sabberworm\CSS\Parsing\ParserState; use Sabberworm\CSS\Property\Selector; use Sabberworm\CSS\Renderable; use Sabberworm\CSS\RuleSet\DeclarationBlock; +use Sabberworm\CSS\Settings; use Sabberworm\CSS\Tests\Unit\CSSList\Fixtures\ConcreteCSSList; /** @@ -342,4 +347,37 @@ public function removeDeclarationBlockBySelectorRemovesMultipleBlocksWithStringS self::assertSame([], $subject->getContents()); } + + /** + * The content provided must (currently) be in the same format as the expected rendering. + * + * @return array + */ + public function provideValidContentForParsing(): array + { + return [ + 'at-import rule' => ['@import url("foo.css");'], + 'rule with declaration block' => ['a {color: green;}'], + ]; + } + + /** + * @test + * + * @param non-empty-string $followingContent + * + * @dataProvider provideValidContentForParsing + */ + public function parseListAtRootLevelSkipsErroneousClosingBraceAndParsesFollowingContent( + string $followingContent + ): void { + $parserState = new ParserState('}' . $followingContent, Settings::create()); + // The subject needs to be a `Document`, as that is currently the test for 'root level'. + // Otherwise `}` will be treated as 'end of list'. + $subject = new Document(); + + CSSList::parseList($parserState, $subject); + + self::assertSame($followingContent, $subject->render(new OutputFormat())); + } } From f129f4cffa21ddde7d825b3a6c65dec7900ebb0a Mon Sep 17 00:00:00 2001 From: Oliver Klee Date: Sun, 14 Dec 2025 18:53:26 +0100 Subject: [PATCH 033/136] [CLEANUP] Avoid multiline strings in `CommentTest` (#1428) --- tests/Comment/CommentTest.php | 83 ++++++++++++++++------------------- 1 file changed, 38 insertions(+), 45 deletions(-) diff --git a/tests/Comment/CommentTest.php b/tests/Comment/CommentTest.php index 52c3de550..042a509b0 100644 --- a/tests/Comment/CommentTest.php +++ b/tests/Comment/CommentTest.php @@ -19,40 +19,35 @@ final class CommentTest extends TestCase public function keepCommentsInOutput(): void { $cssDocument = TestsParserTest::parsedStructureForFile('comments'); - self::assertSame('/** Number 11 **/ -/** - * Comments - */ - -/* Hell */ -@import url("some/url.css") screen; + $expected1 = "/** Number 11 **/\n\n" + . "/**\n" + . " * Comments\n" + . " */\n\n" + . "/* Hell */\n" + . "@import url(\"some/url.css\") screen;\n\n" + . "/* Number 4 */\n\n" + . "/* Number 5 */\n" + . ".foo, #bar {\n" + . "\t/* Number 6 */\n" + . "\tbackground-color: #000;\n" + . "}\n\n" + . "@media screen {\n" + . "\t/** Number 10 **/\n" + . "\t#foo.bar {\n" + . "\t\t/** Number 10b **/\n" + . "\t\tposition: absolute;\n" + . "\t}\n" + . "}\n"; + self::assertSame($expected1, $cssDocument->render(OutputFormat::createPretty())); -/* Number 4 */ - -/* Number 5 */ -.foo, #bar { - /* Number 6 */ - background-color: #000; -} - -@media screen { - /** Number 10 **/ - #foo.bar { - /** Number 10b **/ - position: absolute; - } -} -', $cssDocument->render(OutputFormat::createPretty())); - self::assertSame( - '/** Number 11 **//**' . "\n" - . ' * Comments' . "\n" + $expected2 = "/** Number 11 **//**\n" + . " * Comments\n" . ' *//* Hell */@import url("some/url.css") screen;' . '/* Number 4 *//* Number 5 */.foo,#bar{' . '/* Number 6 */background-color:#000}@media screen{' - . '/** Number 10 **/#foo.bar{/** Number 10b **/position:absolute}}', - $cssDocument->render(OutputFormat::createCompact()->setRenderComments(true)) - ); + . '/** Number 10 **/#foo.bar{/** Number 10b **/position:absolute}}'; + self::assertSame($expected2, $cssDocument->render(OutputFormat::createCompact()->setRenderComments(true))); } /** @@ -61,24 +56,22 @@ public function keepCommentsInOutput(): void public function stripCommentsFromOutput(): void { $css = TestsParserTest::parsedStructureForFile('comments'); - self::assertSame(' -@import url("some/url.css") screen; -.foo, #bar { - background-color: #000; -} + $expected1 = "\n" + . "@import url(\"some/url.css\") screen;\n\n" + . ".foo, #bar {\n" . + "\tbackground-color: #000;\n" + . "}\n\n" + . "@media screen {\n" + . "\t#foo.bar {\n" + . "\t\tposition: absolute;\n" + . "\t}\n" + . "}\n"; + self::assertSame($expected1, $css->render(OutputFormat::createPretty()->setRenderComments(false))); -@media screen { - #foo.bar { - position: absolute; - } -} -', $css->render(OutputFormat::createPretty()->setRenderComments(false))); - self::assertSame( - '@import url("some/url.css") screen;' + $expected2 = '@import url("some/url.css") screen;' . '.foo,#bar{background-color:#000}' - . '@media screen{#foo.bar{position:absolute}}', - $css->render(OutputFormat::createCompact()) - ); + . '@media screen{#foo.bar{position:absolute}}'; + self::assertSame($expected2, $css->render(OutputFormat::createCompact())); } } From 704c17f06ee6dff80fd4abb04c4f79de4068fa33 Mon Sep 17 00:00:00 2001 From: JakeQZ Date: Sun, 14 Dec 2025 20:05:29 +0000 Subject: [PATCH 034/136] [TASK] Guard against infinite loop in `parseList` (#1427) This can't be tested because there are currently no cases that would fail the test without the change. It is there as a preventative double-lock measure to make sure any future code changes do not introduce an infinite loop. It's a slight unoptimization, as it adds what should be a redundant runtime check, but at miniscule performance cost versus the value of preventing a waste of CPU power and memory in the case that something goes wrong. --- src/CSSList/CSSList.php | 6 ++++++ src/Parsing/ParserState.php | 4 ++++ 2 files changed, 10 insertions(+) diff --git a/src/CSSList/CSSList.php b/src/CSSList/CSSList.php index 84ece7f8e..f18cd7725 100644 --- a/src/CSSList/CSSList.php +++ b/src/CSSList/CSSList.php @@ -72,9 +72,15 @@ public static function parseList(ParserState $parserState, CSSList $list): void $listItem = null; if ($usesLenientParsing) { try { + $positionBeforeParse = $parserState->currentColumn(); $listItem = self::parseListItem($parserState, $list); } catch (UnexpectedTokenException $e) { $listItem = false; + // If the failed parsing did not consume anything that was to come ... + if ($parserState->currentColumn() === $positionBeforeParse && !$parserState->isEnd()) { + // ... the unexpected token needs to be skipped, otherwise there'll be an infinite loop. + $parserState->consume(1); + } } } else { $listItem = self::parseListItem($parserState, $list); diff --git a/src/Parsing/ParserState.php b/src/Parsing/ParserState.php index df8253772..44f96a944 100644 --- a/src/Parsing/ParserState.php +++ b/src/Parsing/ParserState.php @@ -192,6 +192,10 @@ public function parseCharacter(bool $isForIdentifier): ?string * * @throws UnexpectedEOFException * @throws UnexpectedTokenException + * + * @phpstan-impure + * This method may change the state of the object by advancing the internal position; + * it does not simply 'get' a value. */ public function consumeWhiteSpace(): array { From 539c64caededf0ba25c7d03b5effd8bd862e6880 Mon Sep 17 00:00:00 2001 From: Oliver Klee Date: Sun, 14 Dec 2025 23:34:42 +0100 Subject: [PATCH 035/136] [CLEANUP] Avoid multiline strings in `OutputFormatTest` (#1429) Also avoid literal tabs. --- tests/OutputFormatTest.php | 129 ++++++++++++++++++++----------------- 1 file changed, 69 insertions(+), 60 deletions(-) diff --git a/tests/OutputFormatTest.php b/tests/OutputFormatTest.php index 104ad7b5c..6167e796b 100644 --- a/tests/OutputFormatTest.php +++ b/tests/OutputFormatTest.php @@ -51,8 +51,8 @@ protected function setUp(): void public function plain(): void { self::assertSame( - '.main, .test {font: italic normal bold 16px/1.2 "Helvetica",Verdana,sans-serif;background: white;} -@media screen {.main {background-size: 100% 100%;font-size: 1.3em;background-color: #fff;}}', + ".main, .test {font: italic normal bold 16px/1.2 \"Helvetica\",Verdana,sans-serif;background: white;}\n" + . '@media screen {.main {background-size: 100% 100%;font-size: 1.3em;background-color: #fff;}}', $this->document->render() ); } @@ -96,8 +96,8 @@ public function spaceAfterListArgumentSeparator(): void public function spaceAfterListArgumentSeparatorComplex(): void { self::assertSame( - '.main, .test {font: italic normal bold 16px/1.2 "Helvetica", Verdana, sans-serif;background: white;}' - . "\n@media screen {.main {background-size: 100% 100%;font-size: 1.3em;background-color: #fff;}}", + ".main, .test {font: italic normal bold 16px/1.2 \"Helvetica\",\tVerdana,\tsans-serif;background: white;}\n" + . '@media screen {.main {background-size: 100% 100%;font-size: 1.3em;background-color: #fff;}}', $this->document->render( OutputFormat::create() ->setSpaceAfterListArgumentSeparator(' ') @@ -116,9 +116,9 @@ public function spaceAfterListArgumentSeparatorComplex(): void public function spaceAfterSelectorSeparator(): void { self::assertSame( - '.main, -.test {font: italic normal bold 16px/1.2 "Helvetica",Verdana,sans-serif;background: white;} -@media screen {.main {background-size: 100% 100%;font-size: 1.3em;background-color: #fff;}}', + ".main,\n" + . ".test {font: italic normal bold 16px/1.2 \"Helvetica\",Verdana,sans-serif;background: white;}\n" + . '@media screen {.main {background-size: 100% 100%;font-size: 1.3em;background-color: #fff;}}', $this->document->render(OutputFormat::create()->setSpaceAfterSelectorSeparator("\n")) ); } @@ -129,8 +129,8 @@ public function spaceAfterSelectorSeparator(): void public function stringQuotingType(): void { self::assertSame( - '.main, .test {font: italic normal bold 16px/1.2 \'Helvetica\',Verdana,sans-serif;background: white;} -@media screen {.main {background-size: 100% 100%;font-size: 1.3em;background-color: #fff;}}', + ".main, .test {font: italic normal bold 16px/1.2 'Helvetica',Verdana,sans-serif;background: white;}\n" + . '@media screen {.main {background-size: 100% 100%;font-size: 1.3em;background-color: #fff;}}', $this->document->render(OutputFormat::create()->setStringQuotingType("'")) ); } @@ -141,8 +141,8 @@ public function stringQuotingType(): void public function rGBHashNotation(): void { self::assertSame( - '.main, .test {font: italic normal bold 16px/1.2 "Helvetica",Verdana,sans-serif;background: white;} -@media screen {.main {background-size: 100% 100%;font-size: 1.3em;background-color: rgb(255,255,255);}}', + ".main, .test {font: italic normal bold 16px/1.2 \"Helvetica\",Verdana,sans-serif;background: white;}\n" + . '@media screen {.main {background-size: 100% 100%;font-size: 1.3em;background-color: rgb(255,255,255);}}', $this->document->render(OutputFormat::create()->setRGBHashNotation(false)) ); } @@ -153,8 +153,8 @@ public function rGBHashNotation(): void public function semicolonAfterLastRule(): void { self::assertSame( - '.main, .test {font: italic normal bold 16px/1.2 "Helvetica",Verdana,sans-serif;background: white} -@media screen {.main {background-size: 100% 100%;font-size: 1.3em;background-color: #fff}}', + ".main, .test {font: italic normal bold 16px/1.2 \"Helvetica\",Verdana,sans-serif;background: white}\n" + . '@media screen {.main {background-size: 100% 100%;font-size: 1.3em;background-color: #fff}}', $this->document->render(OutputFormat::create()->setSemicolonAfterLastRule(false)) ); } @@ -165,8 +165,8 @@ public function semicolonAfterLastRule(): void public function spaceAfterRuleName(): void { self::assertSame( - '.main, .test {font: italic normal bold 16px/1.2 "Helvetica",Verdana,sans-serif;background: white;} -@media screen {.main {background-size: 100% 100%;font-size: 1.3em;background-color: #fff;}}', + ".main, .test {font:\titalic normal bold 16px/1.2 \"Helvetica\",Verdana,sans-serif;background:\twhite;}\n" + . "@media screen {.main {background-size:\t100% 100%;font-size:\t1.3em;background-color:\t#fff;}}", $this->document->render(OutputFormat::create()->setSpaceAfterRuleName("\t")) ); } @@ -181,15 +181,18 @@ public function spaceRules(): void ->setSpaceBetweenRules("\n") ->setSpaceAfterRules("\n"); - self::assertSame('.main, .test { - font: italic normal bold 16px/1.2 "Helvetica",Verdana,sans-serif; - background: white; -} -@media screen {.main { - background-size: 100% 100%; - font-size: 1.3em; - background-color: #fff; - }}', $this->document->render($outputFormat)); + self::assertSame( + ".main, .test {\n" + . "\tfont: italic normal bold 16px/1.2 \"Helvetica\",Verdana,sans-serif;\n" + . "\tbackground: white;\n" + . "}\n" + . "@media screen {.main {\n" + . "\t\tbackground-size: 100% 100%;\n" + . "\t\tfont-size: 1.3em;\n" + . "\t\tbackground-color: #fff;\n" + . "\t}}", + $this->document->render($outputFormat) + ); } /** @@ -202,12 +205,14 @@ public function spaceBlocks(): void ->setSpaceBetweenBlocks("\n") ->setSpaceAfterBlocks("\n"); - self::assertSame(' -.main, .test {font: italic normal bold 16px/1.2 "Helvetica",Verdana,sans-serif;background: white;} -@media screen { - .main {background-size: 100% 100%;font-size: 1.3em;background-color: #fff;} -} -', $this->document->render($outputFormat)); + self::assertSame( + "\n" + . ".main, .test {font: italic normal bold 16px/1.2 \"Helvetica\",Verdana,sans-serif;background: white;}\n" + . "@media screen {\n" + . "\t.main {background-size: 100% 100%;font-size: 1.3em;background-color: #fff;}\n" + . "}\n", + $this->document->render($outputFormat) + ); } /** @@ -223,19 +228,21 @@ public function spaceBoth(): void ->setSpaceBetweenBlocks("\n") ->setSpaceAfterBlocks("\n"); - self::assertSame(' -.main, .test { - font: italic normal bold 16px/1.2 "Helvetica",Verdana,sans-serif; - background: white; -} -@media screen { - .main { - background-size: 100% 100%; - font-size: 1.3em; - background-color: #fff; - } -} -', $this->document->render($outputFormat)); + self::assertSame( + "\n" + . ".main, .test {\n" + . "\tfont: italic normal bold 16px/1.2 \"Helvetica\",Verdana,sans-serif;\n" + . "\tbackground: white;\n" + . "}\n" + . "@media screen {\n" + . "\t.main {\n" + . "\t\tbackground-size: 100% 100%;\n" + . "\t\tfont-size: 1.3em;\n" + . "\t\tbackground-color: #fff;\n" + . "\t}\n" + . "}\n", + $this->document->render($outputFormat) + ); } /** @@ -267,19 +274,21 @@ public function indentation(): void ->setSpaceAfterBlocks("\n") ->setIndentation(''); - self::assertSame(' -.main, .test { -font: italic normal bold 16px/1.2 "Helvetica",Verdana,sans-serif; -background: white; -} -@media screen { -.main { -background-size: 100% 100%; -font-size: 1.3em; -background-color: #fff; -} -} -', $this->document->render($outputFormat)); + self::assertSame( + "\n" + . ".main, .test {\n" + . "font: italic normal bold 16px/1.2 \"Helvetica\",Verdana,sans-serif;\n" + . "background: white;\n" + . "}\n" + . "@media screen {\n" + . ".main {\n" + . "background-size: 100% 100%;\n" + . "font-size: 1.3em;\n" + . "background-color: #fff;\n" + . "}\n" + . "}\n", + $this->document->render($outputFormat) + ); } /** @@ -291,8 +300,8 @@ public function spaceBeforeBraces(): void ->setSpaceBeforeOpeningBrace(''); self::assertSame( - '.main, .test{font: italic normal bold 16px/1.2 "Helvetica",Verdana,sans-serif;background: white;} -@media screen{.main{background-size: 100% 100%;font-size: 1.3em;background-color: #fff;}}', + ".main, .test{font: italic normal bold 16px/1.2 \"Helvetica\",Verdana,sans-serif;background: white;}\n" + . '@media screen{.main{background-size: 100% 100%;font-size: 1.3em;background-color: #fff;}}', $this->document->render($outputFormat) ); } @@ -310,8 +319,8 @@ public function ignoreExceptionsOff(): void $firstDeclarationBlock = $declarationBlocks[0]; $firstDeclarationBlock->removeSelector('.main'); self::assertSame( - '.test {font: italic normal bold 16px/1.2 "Helvetica",Verdana,sans-serif;background: white;} -@media screen {.main {background-size: 100% 100%;font-size: 1.3em;background-color: #fff;}}', + ".test {font: italic normal bold 16px/1.2 \"Helvetica\",Verdana,sans-serif;background: white;}\n" + . '@media screen {.main {background-size: 100% 100%;font-size: 1.3em;background-color: #fff;}}', $this->document->render($outputFormat) ); $firstDeclarationBlock->removeSelector('.test'); From e94cb16b0cca8fdb4fd387dffe07c294d865dde4 Mon Sep 17 00:00:00 2001 From: JakeQZ Date: Mon, 15 Dec 2025 07:26:36 +0000 Subject: [PATCH 036/136] [TASK] Consume to EOF in `DeclarationBlock::parse()` upon failure (#1431) This is not the right long-term fix - see #1430. For now it is a sticking-plaster to help complete #1424. --- src/RuleSet/DeclarationBlock.php | 2 +- tests/Unit/RuleSet/DeclarationBlockTest.php | 40 +++++++++++++++++++++ 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/src/RuleSet/DeclarationBlock.php b/src/RuleSet/DeclarationBlock.php index 7aa81f27f..6e2e1a60d 100644 --- a/src/RuleSet/DeclarationBlock.php +++ b/src/RuleSet/DeclarationBlock.php @@ -75,7 +75,7 @@ public static function parse(ParserState $parserState, ?CSSList $list = null): ? } catch (UnexpectedTokenException $e) { if ($parserState->getSettings()->usesLenientParsing()) { if (!$parserState->consumeIfComes('}')) { - $parserState->consumeUntil('}', false, true); + $parserState->consumeUntil(['}', ParserState::EOF], false, true); } return null; } else { diff --git a/tests/Unit/RuleSet/DeclarationBlockTest.php b/tests/Unit/RuleSet/DeclarationBlockTest.php index caddbd134..6b1b2865f 100644 --- a/tests/Unit/RuleSet/DeclarationBlockTest.php +++ b/tests/Unit/RuleSet/DeclarationBlockTest.php @@ -231,6 +231,46 @@ public function parseConsumesClosingBraceAfterInvalidSelector(string $selector, self::assertTrue($parserState->isEnd()); } + /** + * @return array + */ + public static function provideOptionalWhitespace(): array + { + return [ + 'none' => [''], + 'space' => [' '], + 'newline' => ["\n"], + ]; + } + + /** + * @return DataProvider + */ + public static function provideInvalidSelectorAndOptionalWhitespace(): DataProvider + { + return DataProvider::cross(self::provideInvalidSelector(), self::provideOptionalWhitespace()); + } + + /** + * TODO: It's probably not the responsibility of `DeclarationBlock` to deal with this. + * + * @test + * + * @param non-empty-string $selector + * + * @dataProvider provideInvalidSelectorAndOptionalWhitespace + */ + public function parseConsumesToEofIfNoClosingBraceAfterInvalidSelector( + string $selector, + string $optionalWhitespace + ): void { + $parserState = new ParserState($selector . $optionalWhitespace, Settings::create()); + + DeclarationBlock::parse($parserState); + + self::assertTrue($parserState->isEnd()); + } + /** * @return array */ From e58b67c8a85921f6e83530b76581dae6fe32523a Mon Sep 17 00:00:00 2001 From: Oliver Klee Date: Mon, 15 Dec 2025 17:32:20 +0100 Subject: [PATCH 037/136] [CLEANUP] Reformat the code with the PER-2 configuration (#1391) --- src/Comment/Comment.php | 2 +- src/RuleSet/DeclarationBlock.php | 12 ++++++------ tests/OutputFormatTest.php | 24 ++++++++++++------------ tests/Unit/Value/CalcFunctionTest.php | 1 + 4 files changed, 20 insertions(+), 19 deletions(-) diff --git a/src/Comment/Comment.php b/src/Comment/Comment.php index 33188988f..6f29682c2 100644 --- a/src/Comment/Comment.php +++ b/src/Comment/Comment.php @@ -5,9 +5,9 @@ namespace Sabberworm\CSS\Comment; use Sabberworm\CSS\OutputFormat; -use Sabberworm\CSS\Renderable; use Sabberworm\CSS\Position\Position; use Sabberworm\CSS\Position\Positionable; +use Sabberworm\CSS\Renderable; class Comment implements Positionable, Renderable { diff --git a/src/RuleSet/DeclarationBlock.php b/src/RuleSet/DeclarationBlock.php index 6e2e1a60d..2cd2ef997 100644 --- a/src/RuleSet/DeclarationBlock.php +++ b/src/RuleSet/DeclarationBlock.php @@ -190,9 +190,9 @@ public function addRule(Rule $ruleToAdd, ?Rule $sibling = null): void } /** - * @see RuleSet::getRules() - * * @return array, Rule> + * + * @see RuleSet::getRules() */ public function getRules(?string $searchPattern = null): array { @@ -200,9 +200,9 @@ public function getRules(?string $searchPattern = null): array } /** - * @see RuleSet::setRules() - * * @param array $rules + * + * @see RuleSet::setRules() */ public function setRules(array $rules): void { @@ -210,9 +210,9 @@ public function setRules(array $rules): void } /** - * @see RuleSet::getRulesAssoc() - * * @return array + * + * @see RuleSet::getRulesAssoc() */ public function getRulesAssoc(?string $searchPattern = null): array { diff --git a/tests/OutputFormatTest.php b/tests/OutputFormatTest.php index 6167e796b..20c463e14 100644 --- a/tests/OutputFormatTest.php +++ b/tests/OutputFormatTest.php @@ -16,18 +16,18 @@ final class OutputFormatTest extends TestCase { private const TEST_CSS = "\n" - . ".main, .test {\n" - . "\tfont: italic normal bold 16px/1.2 \"Helvetica\", Verdana, sans-serif;\n" - . "\tbackground: white;\n" - . "}\n" - . "\n" - . "@media screen {\n" - . "\t.main {\n" - . "\t\tbackground-size: 100% 100%;\n" - . "\t\tfont-size: 1.3em;\n" - . "\t\tbackground-color: #fff;\n" - . "\t}\n" - . "}\n"; + . ".main, .test {\n" + . "\tfont: italic normal bold 16px/1.2 \"Helvetica\", Verdana, sans-serif;\n" + . "\tbackground: white;\n" + . "}\n" + . "\n" + . "@media screen {\n" + . "\t.main {\n" + . "\t\tbackground-size: 100% 100%;\n" + . "\t\tfont-size: 1.3em;\n" + . "\t\tbackground-color: #fff;\n" + . "\t}\n" + . "}\n"; /** * @var Parser diff --git a/tests/Unit/Value/CalcFunctionTest.php b/tests/Unit/Value/CalcFunctionTest.php index f4907d477..65f23e131 100644 --- a/tests/Unit/Value/CalcFunctionTest.php +++ b/tests/Unit/Value/CalcFunctionTest.php @@ -47,6 +47,7 @@ public function parseSimpleCalc(): void self::assertSame(20.0, $components[2]->getSize()); self::assertSame('px', $components[2]->getUnit()); } + /** * @test */ From 18a6467625be4e4ef2de44ce7cd447d47b14fc50 Mon Sep 17 00:00:00 2001 From: Oliver Klee Date: Mon, 15 Dec 2025 18:05:53 +0100 Subject: [PATCH 038/136] [CLEANUP] Standardize escaped linefeeds and tabs in literals (#1432) Trailing linefeeds should always be part of the previous line, and leading tabs should be part of the following line. --- tests/OutputFormatTest.php | 7 +-- tests/ParserTest.php | 122 +++++++++++++------------------------ 2 files changed, 44 insertions(+), 85 deletions(-) diff --git a/tests/OutputFormatTest.php b/tests/OutputFormatTest.php index 20c463e14..dce67db92 100644 --- a/tests/OutputFormatTest.php +++ b/tests/OutputFormatTest.php @@ -19,8 +19,7 @@ final class OutputFormatTest extends TestCase . ".main, .test {\n" . "\tfont: italic normal bold 16px/1.2 \"Helvetica\", Verdana, sans-serif;\n" . "\tbackground: white;\n" - . "}\n" - . "\n" + . "}\n\n" . "@media screen {\n" . "\t.main {\n" . "\t\tbackground-size: 100% 100%;\n" @@ -84,8 +83,8 @@ public function spaceAfterListArgumentSeparator(): void { self::assertSame( '.main, .test {font: italic normal bold 16px/ 1.2 ' - . '"Helvetica", Verdana, sans-serif;background: white;}' - . "\n@media screen {.main {background-size: 100% 100%;font-size: 1.3em;background-color: #fff;}}", + . "\"Helvetica\", Verdana, sans-serif;background: white;}\n" + . '@media screen {.main {background-size: 100% 100%;font-size: 1.3em;background-color: #fff;}}', $this->document->render(OutputFormat::create()->setSpaceAfterListArgumentSeparator(' ')) ); } diff --git a/tests/ParserTest.php b/tests/ParserTest.php index ffe278185..97e0d09b5 100644 --- a/tests/ParserTest.php +++ b/tests/ParserTest.php @@ -156,14 +156,11 @@ public function colorParsing(): void } self::assertSame( '#mine {color: red;border-color: #0a64e6;border-color: rgba(10,100,231,.3);outline-color: #222;' - . 'background-color: #232323;}' - . "\n" - . '#yours {background-color: hsl(220,10%,220%);background-color: hsla(220,10%,220%,.3);}' - . "\n" + . "background-color: #232323;}\n" + . "#yours {background-color: hsl(220,10%,220%);background-color: hsla(220,10%,220%,.3);}\n" . '#variables {background-color: rgb(var(--some-rgb));background-color: rgb(var(--r),var(--g),var(--b));' . 'background-color: rgb(255,var(--g),var(--b));background-color: rgb(255,255,var(--b));' - . 'background-color: rgb(255,var(--rg));background-color: hsl(var(--some-hsl));}' - . "\n" + . "background-color: rgb(255,var(--rg));background-color: hsl(var(--some-hsl));}\n" . '#variables-alpha {background-color: rgba(var(--some-rgb),.1);' . 'background-color: rgba(var(--some-rg),255,.1);background-color: hsla(var(--some-hsl),.1);}', $document->render() @@ -265,35 +262,21 @@ public function manipulation(): void { $document = self::parsedStructureForFile('atrules'); self::assertSame( - '@charset "utf-8";' - . "\n" - . '@font-face {font-family: "CrassRoots";src: url("../media/cr.ttf");}' - . "\n" - . 'html, body {font-size: -.6em;}' - . "\n" - . '@keyframes mymove {from {top: 0px;}' - . "\n\t" - . 'to {top: 200px;}}' - . "\n" - . '@-moz-keyframes some-move {from {top: 0px;}' - . "\n\t" - . 'to {top: 200px;}}' - . "\n" + "@charset \"utf-8\";\n" + . "@font-face {font-family: \"CrassRoots\";src: url(\"../media/cr.ttf\");}\n" + . "html, body {font-size: -.6em;}\n" + . "@keyframes mymove {from {top: 0px;}\n" + . "\tto {top: 200px;}}\n" + . "@-moz-keyframes some-move {from {top: 0px;}\n" + . "\tto {top: 200px;}}\n" . '@supports ( (perspective: 10px) or (-moz-perspective: 10px) or (-webkit-perspective: 10px) or ' - . '(-ms-perspective: 10px) or (-o-perspective: 10px) ) {body {font-family: "Helvetica";}}' - . "\n" - . '@page :pseudo-class {margin: 2in;}' - . "\n" - . '@-moz-document url(https://www.w3.org/),' - . "\n" - . ' url-prefix(https://www.w3.org/Style/),' - . "\n" - . ' domain(mozilla.org),' - . "\n" - . ' regexp("https:.*") {body {color: purple;background: yellow;}}' - . "\n" - . '@media screen and (orientation: landscape) {@-ms-viewport {width: 1024px;height: 768px;}}' - . "\n" + . "(-ms-perspective: 10px) or (-o-perspective: 10px) ) {body {font-family: \"Helvetica\";}}\n" + . "@page :pseudo-class {margin: 2in;}\n" + . "@-moz-document url(https://www.w3.org/),\n" + . " url-prefix(https://www.w3.org/Style/),\n" + . " domain(mozilla.org),\n" + . " regexp(\"https:.*\") {body {color: purple;background: yellow;}}\n" + . "@media screen and (orientation: landscape) {@-ms-viewport {width: 1024px;height: 768px;}}\n" . '@region-style #intro {p {color: blue;}}', $document->render() ); @@ -304,35 +287,21 @@ public function manipulation(): void } } self::assertSame( - '@charset "utf-8";' - . "\n" - . '@font-face {font-family: "CrassRoots";src: url("../media/cr.ttf");}' - . "\n" - . '#my_id html, #my_id body {font-size: -.6em;}' - . "\n" - . '@keyframes mymove {from {top: 0px;}' - . "\n\t" - . 'to {top: 200px;}}' - . "\n" - . '@-moz-keyframes some-move {from {top: 0px;}' - . "\n\t" - . 'to {top: 200px;}}' - . "\n" + "@charset \"utf-8\";\n" + . "@font-face {font-family: \"CrassRoots\";src: url(\"../media/cr.ttf\");}\n" + . "#my_id html, #my_id body {font-size: -.6em;}\n" + . "@keyframes mymove {from {top: 0px;}\n" + . "\tto {top: 200px;}}\n" + . "@-moz-keyframes some-move {from {top: 0px;}\n" + . "\tto {top: 200px;}}\n" . '@supports ( (perspective: 10px) or (-moz-perspective: 10px) or (-webkit-perspective: 10px) ' - . 'or (-ms-perspective: 10px) or (-o-perspective: 10px) ) {#my_id body {font-family: "Helvetica";}}' - . "\n" - . '@page :pseudo-class {margin: 2in;}' - . "\n" - . '@-moz-document url(https://www.w3.org/),' - . "\n" - . ' url-prefix(https://www.w3.org/Style/),' - . "\n" - . ' domain(mozilla.org),' - . "\n" - . ' regexp("https:.*") {#my_id body {color: purple;background: yellow;}}' - . "\n" - . '@media screen and (orientation: landscape) {@-ms-viewport {width: 1024px;height: 768px;}}' - . "\n" + . "or (-ms-perspective: 10px) or (-o-perspective: 10px) ) {#my_id body {font-family: \"Helvetica\";}}\n" + . "@page :pseudo-class {margin: 2in;}\n" + . "@-moz-document url(https://www.w3.org/),\n" + . " url-prefix(https://www.w3.org/Style/),\n" + . " domain(mozilla.org),\n" + . " regexp(\"https:.*\") {#my_id body {color: purple;background: yellow;}}\n" + . "@media screen and (orientation: landscape) {@-ms-viewport {width: 1024px;height: 768px;}}\n" . '@region-style #intro {#my_id p {color: blue;}}', $document->render(OutputFormat::create()->setRenderComments(false)) ); @@ -438,18 +407,14 @@ public function slashedValues(): void public function functionSyntax(): void { $document = self::parsedStructureForFile('functions'); - $expected = 'div.main {background-image: linear-gradient(#000,#fff);}' - . "\n" + $expected = "div.main {background-image: linear-gradient(#000,#fff);}\n" . '.collapser::before, .collapser::-moz-before, .collapser::-webkit-before {content: "»";font-size: 1.2em;' . 'margin-right: .2em;-moz-transition-property: -moz-transform;-moz-transition-duration: .2s;' - . '-moz-transform-origin: center 60%;}' - . "\n" + . "-moz-transform-origin: center 60%;}\n" . '.collapser.expanded::before, .collapser.expanded::-moz-before,' - . ' .collapser.expanded::-webkit-before {-moz-transform: rotate(90deg);}' - . "\n" + . " .collapser.expanded::-webkit-before {-moz-transform: rotate(90deg);}\n" . '.collapser + * {height: 0;overflow: hidden;-moz-transition-property: height;' - . '-moz-transition-duration: .3s;}' - . "\n" + . "-moz-transition-duration: .3s;}\n" . '.collapser.expanded + * {height: auto;}'; self::assertSame($expected, $document->render()); @@ -761,10 +726,8 @@ public function identifierEscapesInFile(): void public function selectorIgnoresInFile(): void { $document = self::parsedStructureForFile('selector-ignores', Settings::create()->withMultibyteSupport(true)); - $expected = '.some[selectors-may=\'contain-a-{\'] {}' - . "\n" - . '.this-selector .valid {width: 100px;}' - . "\n" + $expected = ".some[selectors-may='contain-a-{'] {}\n" + . ".this-selector .valid {width: 100px;}\n" . '@media only screen and (min-width: 200px) {.test {prop: val;}}'; self::assertSame($expected, $document->render()); } @@ -778,11 +741,9 @@ public function keyframeSelectors(): void 'keyframe-selector-validation', Settings::create()->withMultibyteSupport(true) ); - $expected = '@-webkit-keyframes zoom {0% {-webkit-transform: scale(1,1);}' - . "\n\t" - . '50% {-webkit-transform: scale(1.2,1.2);}' - . "\n\t" - . '100% {-webkit-transform: scale(1,1);}}'; + $expected = "@-webkit-keyframes zoom {0% {-webkit-transform: scale(1,1);}\n" + . "\t50% {-webkit-transform: scale(1.2,1.2);}\n" + . "\t100% {-webkit-transform: scale(1,1);}}"; self::assertSame($expected, $document->render()); } @@ -812,8 +773,7 @@ public function calcFailure(): void public function urlInFileMbOff(): void { $document = self::parsedStructureForFile('url', Settings::create()->withMultibyteSupport(false)); - $expected = 'body {background: #fff url("https://somesite.com/images/someimage.gif") repeat top center;}' - . "\n" + $expected = "body {background: #fff url(\"https://somesite.com/images/someimage.gif\") repeat top center;}\n" . 'body {background-url: url("https://somesite.com/images/someimage.gif");}'; self::assertSame($expected, $document->render()); } From 515317db03ee09c69809b7b014422312df218650 Mon Sep 17 00:00:00 2001 From: JakeQZ Date: Tue, 16 Dec 2025 08:43:50 +0000 Subject: [PATCH 039/136] [BUGFIX] Reject selector comprising only whitespace (#1433) --- CHANGELOG.md | 1 + src/RuleSet/DeclarationBlock.php | 7 ++- tests/Unit/RuleSet/DeclarationBlockTest.php | 58 ++++++++++++++++++--- 3 files changed, 57 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 421d9419f..bcc114faf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,7 @@ Please also have a look at our ### Fixed +- Reject selector comprising only whitespace (#1433) - Improve recovery parsing when a rogue `}` is encountered (#1425, #1426) - Parse comment(s) immediately preceding a selector (#1421) - Parse consecutive comments (#1421) diff --git a/src/RuleSet/DeclarationBlock.php b/src/RuleSet/DeclarationBlock.php index 2cd2ef997..7d669ce8e 100644 --- a/src/RuleSet/DeclarationBlock.php +++ b/src/RuleSet/DeclarationBlock.php @@ -354,6 +354,11 @@ private static function parseSelector(ParserState $parserState, array &$comments throw new UnexpectedTokenException(')', $nextCharacter); } - return \implode('', $selectorParts); + $selector = \trim(\implode('', $selectorParts)); + if ($selector === '') { + throw new UnexpectedTokenException('selector', $nextCharacter, 'literal', $parserState->currentLine()); + } + + return $selector; } } diff --git a/tests/Unit/RuleSet/DeclarationBlockTest.php b/tests/Unit/RuleSet/DeclarationBlockTest.php index 6b1b2865f..97dcb2212 100644 --- a/tests/Unit/RuleSet/DeclarationBlockTest.php +++ b/tests/Unit/RuleSet/DeclarationBlockTest.php @@ -159,20 +159,43 @@ public function parsesTwoCommaSeparatedSelectors(string $firstSelector, string $ } /** - * @return array + * @return array */ - public static function provideInvalidSelector(): array + public static function provideInvalidSelectorAndExpectedExceptionMessage(): array { // TODO: the `parse` method consumes the first character without inspection, - // so the 'lone' test strings are prefixed with a space. + // so some of the test strings are prefixed with a space. return [ - 'lone `(`' => [' ('], - 'lone `)`' => [' )'], - 'unclosed `(`' => [':not(#your-mug'], - 'extra `)`' => [':not(#your-mug))'], + 'no selector' => [' ', 'Token “selector” (literal) not found. Got “{”. [line no: 1]'], + 'lone `(`' => [' (', 'Token “)” (literal) not found. Got “{”.'], + 'lone `)`' => [' )', 'Token “anything but” (literal) not found. Got “)”.'], + 'lone `,`' => [' ,', 'Token “selector” (literal) not found. Got “,”. [line no: 1]'], + 'unclosed `(`' => [':not(#your-mug', 'Token “)” (literal) not found. Got “{”.'], + 'extra `)`' => [':not(#your-mug))', 'Token “anything but” (literal) not found. Got “)”.'], + '`,` missing left operand' => [' , a', 'Token “selector” (literal) not found. Got “,”. [line no: 1]'], + '`,` missing right operand' => ['a,', 'Token “selector” (literal) not found. Got “{”. [line no: 1]'], ]; } + /** + * @return array + */ + public static function provideInvalidSelector(): array + { + // Re-use the set of invalid selectors, but remove the expected exception message for tests that don't need it. + return \array_map( + /** + * @param array{0: string, 1: non-empty-string} + * + * @return array<{0: string}> + */ + static function (array $testData): array { + return [$testData[0]]; + }, + self::provideInvalidSelectorAndExpectedExceptionMessage() + ); + } + /** * @test * @@ -192,6 +215,25 @@ public function parseSkipsBlockWithInvalidSelector(string $selector): void self::assertTrue($parserState->comes($nextCss)); } + /** + * @test + * + * @param non-empty-string $expectedExceptionMessage + * + * @dataProvider provideInvalidSelectorAndExpectedExceptionMessage + */ + public function parseInStrictModeThrowsExceptionWithInvalidSelector( + string $selector, + string $expectedExceptionMessage + ): void { + $this->expectException(UnexpectedTokenException::class); + $this->expectExceptionMessage($expectedExceptionMessage); + + $parserState = new ParserState($selector . ' {}', Settings::create()->beStrict()); + + $subject = DeclarationBlock::parse($parserState); + } + /** * @return array */ @@ -436,7 +478,7 @@ public static function provideInvalidStandaloneSelector(): array public function setSelectorsThrowsExceptionWithInvalidSelector(string $selector): void { $this->expectException(UnexpectedTokenException::class); - $this->expectExceptionMessageMatches('/^Selector\\(s\\) string is not valid. /'); + $this->expectExceptionMessageMatches('/^Selector\\(s\\) string is not valid./'); $subject = new DeclarationBlock(); From ca51e51df5ccfe866b074db43dc0132dea147e22 Mon Sep 17 00:00:00 2001 From: JakeQZ Date: Tue, 16 Dec 2025 10:44:51 +0000 Subject: [PATCH 040/136] [BUGFIX] Parse comment(s) immediately preceding selector (part 2) (#1424) Now comments with no whitespace before the selector are parsed and extracted. `setSelectors()` also receives the same fix, for the unlikely case when someone calls it with a selector string beginning with a comment. --- CHANGELOG.md | 2 +- src/RuleSet/DeclarationBlock.php | 2 +- tests/Unit/RuleSet/DeclarationBlockTest.php | 94 +++++++++++++++++++-- 3 files changed, 87 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bcc114faf..f60db312b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,7 +26,7 @@ Please also have a look at our - Reject selector comprising only whitespace (#1433) - Improve recovery parsing when a rogue `}` is encountered (#1425, #1426) -- Parse comment(s) immediately preceding a selector (#1421) +- Parse comment(s) immediately preceding a selector (#1421, #1424) - Parse consecutive comments (#1421) - Support attribute selectors with values containing commas in `DeclarationBlock::setSelectors()` (#1419) diff --git a/src/RuleSet/DeclarationBlock.php b/src/RuleSet/DeclarationBlock.php index 7d669ce8e..c8cdb6d48 100644 --- a/src/RuleSet/DeclarationBlock.php +++ b/src/RuleSet/DeclarationBlock.php @@ -307,7 +307,6 @@ private static function parseSelector(ParserState $parserState, array &$comments static $stopCharacters = ['{', '}', '\'', '"', '(', ')', ',']; while (true) { - $selectorParts[] = $parserState->consume(1); $selectorParts[] = $parserState->consumeUntil($stopCharacters, false, false, $comments); $nextCharacter = $parserState->peek(); switch ($nextCharacter) { @@ -348,6 +347,7 @@ private static function parseSelector(ParserState $parserState, array &$comments } break; } + $selectorParts[] = $parserState->consume(1); } if ($functionNestingLevel !== 0) { diff --git a/tests/Unit/RuleSet/DeclarationBlockTest.php b/tests/Unit/RuleSet/DeclarationBlockTest.php index 97dcb2212..9eaab9563 100644 --- a/tests/Unit/RuleSet/DeclarationBlockTest.php +++ b/tests/Unit/RuleSet/DeclarationBlockTest.php @@ -5,6 +5,7 @@ namespace Sabberworm\CSS\Tests\Unit\RuleSet; use PHPUnit\Framework\TestCase; +use Sabberworm\CSS\Comment\Comment; use Sabberworm\CSS\CSSElement; use Sabberworm\CSS\CSSList\CSSListItem; use Sabberworm\CSS\Parsing\ParserState; @@ -158,21 +159,85 @@ public function parsesTwoCommaSeparatedSelectors(string $firstSelector, string $ self::assertSame([$firstSelector, $secondSelector], self::getSelectorsAsStrings($subject)); } + /** + * @return array + */ + public static function provideSelectorWithAndWithoutComment(): array + { + return [ + 'comment before' => ['/*comment*/body', 'body'], + 'comment after' => ['body/*comment*/', 'body'], + 'comment within' => ['./*comment*/teapot', '.teapot'], + 'comment within function' => [':not(#your-mug,/*comment*/.their-mug)', ':not(#your-mug,.their-mug)'], + ]; + } + + /** + * @test + * + * @param non-empty-string $selectorWith + * @param non-empty-string $selectorWithout + * + * @dataProvider provideSelectorWithAndWithoutComment + */ + public function parsesSelectorWithComment(string $selectorWith, string $selectorWithout): void + { + $subject = DeclarationBlock::parse(new ParserState($selectorWith . ' {}', Settings::create())); + + self::assertInstanceOf(DeclarationBlock::class, $subject); + self::assertSame([$selectorWithout], self::getSelectorsAsStrings($subject)); + } + + /** + * @test + * + * @param non-empty-string $selector + * + * @dataProvider provideSelectorWithAndWithoutComment + */ + public function parseExtractsCommentFromSelector(string $selector): void + { + $subject = DeclarationBlock::parse(new ParserState($selector . ' {}', Settings::create())); + + self::assertInstanceOf(DeclarationBlock::class, $subject); + self::assertSame(['comment'], self::getCommentsAsStrings($subject)); + } + + /** + * @test + */ + public function parsesSelectorWithTwoComments(): void + { + $subject = DeclarationBlock::parse(new ParserState('/*comment1*/a/*comment2*/ {}', Settings::create())); + + self::assertInstanceOf(DeclarationBlock::class, $subject); + self::assertSame(['a'], self::getSelectorsAsStrings($subject)); + } + + /** + * @test + */ + public function parseExtractsTwoCommentsFromSelector(): void + { + $subject = DeclarationBlock::parse(new ParserState('/*comment1*/a/*comment2*/ {}', Settings::create())); + + self::assertInstanceOf(DeclarationBlock::class, $subject); + self::assertSame(['comment1', 'comment2'], self::getCommentsAsStrings($subject)); + } + /** * @return array */ public static function provideInvalidSelectorAndExpectedExceptionMessage(): array { - // TODO: the `parse` method consumes the first character without inspection, - // so some of the test strings are prefixed with a space. return [ - 'no selector' => [' ', 'Token “selector” (literal) not found. Got “{”. [line no: 1]'], - 'lone `(`' => [' (', 'Token “)” (literal) not found. Got “{”.'], - 'lone `)`' => [' )', 'Token “anything but” (literal) not found. Got “)”.'], - 'lone `,`' => [' ,', 'Token “selector” (literal) not found. Got “,”. [line no: 1]'], + 'no selector' => ['', 'Token “selector” (literal) not found. Got “{”. [line no: 1]'], + 'lone `(`' => ['(', 'Token “)” (literal) not found. Got “{”.'], + 'lone `)`' => [')', 'Token “anything but” (literal) not found. Got “)”.'], + 'lone `,`' => [',', 'Token “selector” (literal) not found. Got “,”. [line no: 1]'], 'unclosed `(`' => [':not(#your-mug', 'Token “)” (literal) not found. Got “{”.'], 'extra `)`' => [':not(#your-mug))', 'Token “anything but” (literal) not found. Got “)”.'], - '`,` missing left operand' => [' , a', 'Token “selector” (literal) not found. Got “,”. [line no: 1]'], + '`,` missing left operand' => [', a', 'Token “selector” (literal) not found. Got “,”. [line no: 1]'], '`,` missing right operand' => ['a,', 'Token “selector” (literal) not found. Got “{”. [line no: 1]'], ]; } @@ -199,8 +264,6 @@ static function (array $testData): array { /** * @test * - * @param non-empty-string $selector - * * @dataProvider provideInvalidSelector */ public function parseSkipsBlockWithInvalidSelector(string $selector): void @@ -326,6 +389,19 @@ static function (Selector $selectorObject): string { ); } + /** + * @return list + */ + private static function getCommentsAsStrings(DeclarationBlock $declarationBlock): array + { + return \array_map( + static function (Comment $comment): string { + return $comment->getComment(); + }, + $declarationBlock->getComments() + ); + } + /** * @test */ From fea9a1d43cdea2ba803c539bcb550e8040cdbdb3 Mon Sep 17 00:00:00 2001 From: JakeQZ Date: Wed, 17 Dec 2025 07:22:25 +0000 Subject: [PATCH 041/136] [CLEANUP] Reorder methods in `DeclarationBlockTest` (#1434) Move the private helper methods to the end, as they are used in multiple tests both above and below their current location. --- tests/Unit/RuleSet/DeclarationBlockTest.php | 52 ++++++++++----------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/tests/Unit/RuleSet/DeclarationBlockTest.php b/tests/Unit/RuleSet/DeclarationBlockTest.php index 9eaab9563..7800ba73e 100644 --- a/tests/Unit/RuleSet/DeclarationBlockTest.php +++ b/tests/Unit/RuleSet/DeclarationBlockTest.php @@ -376,32 +376,6 @@ public function parseConsumesToEofIfNoClosingBraceAfterInvalidSelector( self::assertTrue($parserState->isEnd()); } - /** - * @return array - */ - private static function getSelectorsAsStrings(DeclarationBlock $declarationBlock): array - { - return \array_map( - static function (Selector $selectorObject): string { - return $selectorObject->getSelector(); - }, - $declarationBlock->getSelectors() - ); - } - - /** - * @return list - */ - private static function getCommentsAsStrings(DeclarationBlock $declarationBlock): array - { - return \array_map( - static function (Comment $comment): string { - return $comment->getComment(); - }, - $declarationBlock->getComments() - ); - } - /** * @test */ @@ -560,4 +534,30 @@ public function setSelectorsThrowsExceptionWithInvalidSelector(string $selector) $subject->setSelectors($selector); } + + /** + * @return array + */ + private static function getSelectorsAsStrings(DeclarationBlock $declarationBlock): array + { + return \array_map( + static function (Selector $selectorObject): string { + return $selectorObject->getSelector(); + }, + $declarationBlock->getSelectors() + ); + } + + /** + * @return list + */ + private static function getCommentsAsStrings(DeclarationBlock $declarationBlock): array + { + return \array_map( + static function (Comment $comment): string { + return $comment->getComment(); + }, + $declarationBlock->getComments() + ); + } } From 3500d422bb98f4c16de87664eb116dec406d2421 Mon Sep 17 00:00:00 2001 From: JakeQZ Date: Wed, 17 Dec 2025 07:23:47 +0000 Subject: [PATCH 042/136] [FEATURE] Provide line number in exception message (#1435) ... for mismatched parentheses in selector. --- CHANGELOG.md | 2 ++ src/RuleSet/DeclarationBlock.php | 9 +++++++-- tests/Unit/RuleSet/DeclarationBlockTest.php | 8 ++++---- 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f60db312b..d75a0e766 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,8 @@ Please also have a look at our ### Added +- Provide line number in exception message for mismatched parentheses in + selector (#1435) - Add support for CSS container queries (#1400) ### Changed diff --git a/src/RuleSet/DeclarationBlock.php b/src/RuleSet/DeclarationBlock.php index c8cdb6d48..a3921815f 100644 --- a/src/RuleSet/DeclarationBlock.php +++ b/src/RuleSet/DeclarationBlock.php @@ -329,7 +329,12 @@ private static function parseSelector(ParserState $parserState, array &$comments case ')': if (!\is_string($stringWrapperCharacter)) { if ($functionNestingLevel <= 0) { - throw new UnexpectedTokenException('anything but', ')'); + throw new UnexpectedTokenException( + 'anything but', + ')', + 'literal', + $parserState->currentLine() + ); } --$functionNestingLevel; } @@ -351,7 +356,7 @@ private static function parseSelector(ParserState $parserState, array &$comments } if ($functionNestingLevel !== 0) { - throw new UnexpectedTokenException(')', $nextCharacter); + throw new UnexpectedTokenException(')', $nextCharacter, 'literal', $parserState->currentLine()); } $selector = \trim(\implode('', $selectorParts)); diff --git a/tests/Unit/RuleSet/DeclarationBlockTest.php b/tests/Unit/RuleSet/DeclarationBlockTest.php index 7800ba73e..d28e1195b 100644 --- a/tests/Unit/RuleSet/DeclarationBlockTest.php +++ b/tests/Unit/RuleSet/DeclarationBlockTest.php @@ -232,11 +232,11 @@ public static function provideInvalidSelectorAndExpectedExceptionMessage(): arra { return [ 'no selector' => ['', 'Token “selector” (literal) not found. Got “{”. [line no: 1]'], - 'lone `(`' => ['(', 'Token “)” (literal) not found. Got “{”.'], - 'lone `)`' => [')', 'Token “anything but” (literal) not found. Got “)”.'], + 'lone `(`' => ['(', 'Token “)” (literal) not found. Got “{”. [line no: 1]'], + 'lone `)`' => [')', 'Token “anything but” (literal) not found. Got “)”. [line no: 1]'], 'lone `,`' => [',', 'Token “selector” (literal) not found. Got “,”. [line no: 1]'], - 'unclosed `(`' => [':not(#your-mug', 'Token “)” (literal) not found. Got “{”.'], - 'extra `)`' => [':not(#your-mug))', 'Token “anything but” (literal) not found. Got “)”.'], + 'unclosed `(`' => [':not(#your-mug', 'Token “)” (literal) not found. Got “{”. [line no: 1]'], + 'extra `)`' => [':not(#your-mug))', 'Token “anything but” (literal) not found. Got “)”. [line no: 1]'], '`,` missing left operand' => [', a', 'Token “selector” (literal) not found. Got “,”. [line no: 1]'], '`,` missing right operand' => ['a,', 'Token “selector” (literal) not found. Got “{”. [line no: 1]'], ]; From 0ae1fde22bc2f66363ebdd77095e6f1d35118bc8 Mon Sep 17 00:00:00 2001 From: JakeQZ Date: Thu, 18 Dec 2025 08:34:14 +0000 Subject: [PATCH 043/136] [CLEANUP] Tighten a type annotation in `DeclarationBlockTest` (#1436) --- tests/Unit/RuleSet/DeclarationBlockTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Unit/RuleSet/DeclarationBlockTest.php b/tests/Unit/RuleSet/DeclarationBlockTest.php index d28e1195b..980ea4004 100644 --- a/tests/Unit/RuleSet/DeclarationBlockTest.php +++ b/tests/Unit/RuleSet/DeclarationBlockTest.php @@ -536,7 +536,7 @@ public function setSelectorsThrowsExceptionWithInvalidSelector(string $selector) } /** - * @return array + * @return list */ private static function getSelectorsAsStrings(DeclarationBlock $declarationBlock): array { From b74a00045edb02520803d9ec8f53aa6dfe5c5204 Mon Sep 17 00:00:00 2001 From: JakeQZ Date: Wed, 31 Dec 2025 12:31:57 +0000 Subject: [PATCH 044/136] [TASK] Add `getArrayRepresentation()` to `Renderable` (#1447) This method will make it easier to test the parsing independently of the rendering. Placeholder implementations are currently all stubs that will throw an exception, confirmed by tests. They can be implemented as and when required, with the tests updated. Part of #1440. --- src/CSSList/CSSList.php | 10 ++++++++++ src/Comment/Comment.php | 10 ++++++++++ src/Property/CSSNamespace.php | 10 ++++++++++ src/Property/Charset.php | 10 ++++++++++ src/Property/Import.php | 10 ++++++++++ src/Property/Selector.php | 10 ++++++++++ src/Renderable.php | 7 +++++++ src/Rule/Rule.php | 10 ++++++++++ src/RuleSet/DeclarationBlock.php | 10 ++++++++++ src/RuleSet/RuleSet.php | 10 ++++++++++ src/Value/Value.php | 10 ++++++++++ tests/Unit/CSSList/CSSListTest.php | 12 ++++++++++++ tests/Unit/Comment/CommentTest.php | 12 ++++++++++++ tests/Unit/Property/CSSNamespaceTest.php | 10 ++++++++++ tests/Unit/Property/CharsetTest.php | 10 ++++++++++ tests/Unit/Property/ImportTest.php | 10 ++++++++++ tests/Unit/Property/SelectorTest.php | 12 ++++++++++++ tests/Unit/Rule/RuleTest.php | 12 ++++++++++++ tests/Unit/RuleSet/DeclarationBlockTest.php | 10 ++++++++++ tests/Unit/RuleSet/RuleSetTest.php | 10 ++++++++++ tests/Unit/Value/ValueTest.php | 12 ++++++++++++ 21 files changed, 217 insertions(+) diff --git a/src/CSSList/CSSList.php b/src/CSSList/CSSList.php index f18cd7725..7d4c7efae 100644 --- a/src/CSSList/CSSList.php +++ b/src/CSSList/CSSList.php @@ -435,6 +435,16 @@ public function getContents(): array return $this->contents; } + /** + * @internal + * + * @return array>> + */ + public function getArrayRepresentation(): array + { + throw new \BadMethodCallException('`getArrayRepresentation` is not yet implemented for `' . self::class . '`'); + } + /** * @param list $selectors1 * @param list $selectors2 diff --git a/src/Comment/Comment.php b/src/Comment/Comment.php index 6f29682c2..e09b033b6 100644 --- a/src/Comment/Comment.php +++ b/src/Comment/Comment.php @@ -46,4 +46,14 @@ public function render(OutputFormat $outputFormat): string { return '/*' . $this->commentText . '*/'; } + + /** + * @internal + * + * @return array>> + */ + public function getArrayRepresentation(): array + { + throw new \BadMethodCallException('`getArrayRepresentation` is not yet implemented for `' . self::class . '`'); + } } diff --git a/src/Property/CSSNamespace.php b/src/Property/CSSNamespace.php index 66164a318..cb96d78ba 100644 --- a/src/Property/CSSNamespace.php +++ b/src/Property/CSSNamespace.php @@ -94,4 +94,14 @@ public function atRuleArgs(): array } return $result; } + + /** + * @internal + * + * @return array>> + */ + public function getArrayRepresentation(): array + { + throw new \BadMethodCallException('`getArrayRepresentation` is not yet implemented for `' . self::class . '`'); + } } diff --git a/src/Property/Charset.php b/src/Property/Charset.php index c9488ad1e..9f2ae0500 100644 --- a/src/Property/Charset.php +++ b/src/Property/Charset.php @@ -71,4 +71,14 @@ public function atRuleArgs(): CSSString { return $this->charset; } + + /** + * @internal + * + * @return array>> + */ + public function getArrayRepresentation(): array + { + throw new \BadMethodCallException('`getArrayRepresentation` is not yet implemented for `' . self::class . '`'); + } } diff --git a/src/Property/Import.php b/src/Property/Import.php index a9dabe979..366042481 100644 --- a/src/Property/Import.php +++ b/src/Property/Import.php @@ -82,4 +82,14 @@ public function getMediaQuery(): ?string { return $this->mediaQuery; } + + /** + * @internal + * + * @return array>> + */ + public function getArrayRepresentation(): array + { + throw new \BadMethodCallException('`getArrayRepresentation` is not yet implemented for `' . self::class . '`'); + } } diff --git a/src/Property/Selector.php b/src/Property/Selector.php index b935b2a82..093f2bcf4 100644 --- a/src/Property/Selector.php +++ b/src/Property/Selector.php @@ -95,4 +95,14 @@ public function render(OutputFormat $outputFormat): string { return $this->getSelector(); } + + /** + * @internal + * + * @return array>> + */ + public function getArrayRepresentation(): array + { + throw new \BadMethodCallException('`getArrayRepresentation` is not yet implemented for `' . self::class . '`'); + } } diff --git a/src/Renderable.php b/src/Renderable.php index 9ebf9a9b9..1385abd92 100644 --- a/src/Renderable.php +++ b/src/Renderable.php @@ -7,4 +7,11 @@ interface Renderable { public function render(OutputFormat $outputFormat): string; + + /** + * @internal + * + * @return array>> + */ + public function getArrayRepresentation(): array; } diff --git a/src/Rule/Rule.php b/src/Rule/Rule.php index 96ef1b4da..c43cb414b 100644 --- a/src/Rule/Rule.php +++ b/src/Rule/Rule.php @@ -197,4 +197,14 @@ public function render(OutputFormat $outputFormat): string $result .= ';'; return $result; } + + /** + * @internal + * + * @return array>> + */ + public function getArrayRepresentation(): array + { + throw new \BadMethodCallException('`getArrayRepresentation` is not yet implemented for `' . self::class . '`'); + } } diff --git a/src/RuleSet/DeclarationBlock.php b/src/RuleSet/DeclarationBlock.php index a3921815f..386d7651c 100644 --- a/src/RuleSet/DeclarationBlock.php +++ b/src/RuleSet/DeclarationBlock.php @@ -273,6 +273,16 @@ public function render(OutputFormat $outputFormat): string return $result; } + /** + * @internal + * + * @return array>> + */ + public function getArrayRepresentation(): array + { + throw new \BadMethodCallException('`getArrayRepresentation` is not yet implemented for `' . self::class . '`'); + } + /** * @param list $comments * diff --git a/src/RuleSet/RuleSet.php b/src/RuleSet/RuleSet.php index 521a6ae9b..64bd8c4db 100644 --- a/src/RuleSet/RuleSet.php +++ b/src/RuleSet/RuleSet.php @@ -331,6 +331,16 @@ protected function renderRules(OutputFormat $outputFormat): string return $formatter->removeLastSemicolon($result); } + /** + * @internal + * + * @return array>> + */ + public function getArrayRepresentation(): array + { + throw new \BadMethodCallException('`getArrayRepresentation` is not yet implemented for `' . self::class . '`'); + } + /** * @return int negative if `$first` is before `$second`; zero if they have the same position; positive otherwise * diff --git a/src/Value/Value.php b/src/Value/Value.php index 263c420da..59c49c85f 100644 --- a/src/Value/Value.php +++ b/src/Value/Value.php @@ -181,6 +181,16 @@ public static function parsePrimitiveValue(ParserState $parserState) return $value; } + /** + * @internal + * + * @return array>> + */ + public function getArrayRepresentation(): array + { + throw new \BadMethodCallException('`getArrayRepresentation` is not yet implemented for `' . self::class . '`'); + } + /** * @throws UnexpectedEOFException * @throws UnexpectedTokenException diff --git a/tests/Unit/CSSList/CSSListTest.php b/tests/Unit/CSSList/CSSListTest.php index 548acbb77..3ab8d19f1 100644 --- a/tests/Unit/CSSList/CSSListTest.php +++ b/tests/Unit/CSSList/CSSListTest.php @@ -380,4 +380,16 @@ public function parseListAtRootLevelSkipsErroneousClosingBraceAndParsesFollowing self::assertSame($followingContent, $subject->render(new OutputFormat())); } + + /** + * @test + */ + public function getArrayRepresentationThrowsException(): void + { + $this->expectException(\BadMethodCallException::class); + + $subject = new ConcreteCSSList(); + + $subject->getArrayRepresentation(); + } } diff --git a/tests/Unit/Comment/CommentTest.php b/tests/Unit/Comment/CommentTest.php index 69572e903..841420825 100644 --- a/tests/Unit/Comment/CommentTest.php +++ b/tests/Unit/Comment/CommentTest.php @@ -77,4 +77,16 @@ public function getLineNumberReturnsLineNumberProvidedToConstructor(): void self::assertSame($lineNumber, $subject->getLineNumber()); } + + /** + * @test + */ + public function getArrayRepresentationThrowsException(): void + { + $this->expectException(\BadMethodCallException::class); + + $subject = new Comment(); + + $subject->getArrayRepresentation(); + } } diff --git a/tests/Unit/Property/CSSNamespaceTest.php b/tests/Unit/Property/CSSNamespaceTest.php index 2e4d99222..44f5c7d1f 100644 --- a/tests/Unit/Property/CSSNamespaceTest.php +++ b/tests/Unit/Property/CSSNamespaceTest.php @@ -31,4 +31,14 @@ public function implementsCSSListItem(): void { self::assertInstanceOf(CSSListItem::class, $this->subject); } + + /** + * @test + */ + public function getArrayRepresentationThrowsException(): void + { + $this->expectException(\BadMethodCallException::class); + + $this->subject->getArrayRepresentation(); + } } diff --git a/tests/Unit/Property/CharsetTest.php b/tests/Unit/Property/CharsetTest.php index e0645f5ef..6f8fa60d6 100644 --- a/tests/Unit/Property/CharsetTest.php +++ b/tests/Unit/Property/CharsetTest.php @@ -31,4 +31,14 @@ public function implementsCSSListItem(): void { self::assertInstanceOf(CSSListItem::class, $this->subject); } + + /** + * @test + */ + public function getArrayRepresentationThrowsException(): void + { + $this->expectException(\BadMethodCallException::class); + + $this->subject->getArrayRepresentation(); + } } diff --git a/tests/Unit/Property/ImportTest.php b/tests/Unit/Property/ImportTest.php index 4ec028e3f..3164bc201 100644 --- a/tests/Unit/Property/ImportTest.php +++ b/tests/Unit/Property/ImportTest.php @@ -32,4 +32,14 @@ public function implementsCSSListItem(): void { self::assertInstanceOf(CSSListItem::class, $this->subject); } + + /** + * @test + */ + public function getArrayRepresentationThrowsException(): void + { + $this->expectException(\BadMethodCallException::class); + + $this->subject->getArrayRepresentation(); + } } diff --git a/tests/Unit/Property/SelectorTest.php b/tests/Unit/Property/SelectorTest.php index cfec36fba..3aca38515 100644 --- a/tests/Unit/Property/SelectorTest.php +++ b/tests/Unit/Property/SelectorTest.php @@ -188,4 +188,16 @@ public function doesNotCleanupSpacesWithinAttributeSelector(): void self::assertSame('a[title="extra space"]', $subject->getSelector()); } + + /** + * @test + */ + public function getArrayRepresentationThrowsException(): void + { + $this->expectException(\BadMethodCallException::class); + + $subject = new Selector('a'); + + $subject->getArrayRepresentation(); + } } diff --git a/tests/Unit/Rule/RuleTest.php b/tests/Unit/Rule/RuleTest.php index 008bcfc18..3a00bd751 100644 --- a/tests/Unit/Rule/RuleTest.php +++ b/tests/Unit/Rule/RuleTest.php @@ -70,4 +70,16 @@ static function ($component): string { self::assertSame($expectedTypeClassnames, $actualClassnames); } + + /** + * @test + */ + public function getArrayRepresentationThrowsException(): void + { + $this->expectException(\BadMethodCallException::class); + + $subject = new Rule('todo'); + + $subject->getArrayRepresentation(); + } } diff --git a/tests/Unit/RuleSet/DeclarationBlockTest.php b/tests/Unit/RuleSet/DeclarationBlockTest.php index 980ea4004..b9c908b49 100644 --- a/tests/Unit/RuleSet/DeclarationBlockTest.php +++ b/tests/Unit/RuleSet/DeclarationBlockTest.php @@ -535,6 +535,16 @@ public function setSelectorsThrowsExceptionWithInvalidSelector(string $selector) $subject->setSelectors($selector); } + /** + * @test + */ + public function getArrayRepresentationThrowsException(): void + { + $this->expectException(\BadMethodCallException::class); + + $this->subject->getArrayRepresentation(); + } + /** * @return list */ diff --git a/tests/Unit/RuleSet/RuleSetTest.php b/tests/Unit/RuleSet/RuleSetTest.php index 92c056ca0..76cf7b2e6 100644 --- a/tests/Unit/RuleSet/RuleSetTest.php +++ b/tests/Unit/RuleSet/RuleSetTest.php @@ -79,4 +79,14 @@ public function getLineNumberReturnsLineNumberPassedToConstructor(?int $lineNumb self::assertSame($lineNumber, $result); } + + /** + * @test + */ + public function getArrayRepresentationThrowsException(): void + { + $this->expectException(\BadMethodCallException::class); + + $this->subject->getArrayRepresentation(); + } } diff --git a/tests/Unit/Value/ValueTest.php b/tests/Unit/Value/ValueTest.php index b7279c6e9..87c282c01 100644 --- a/tests/Unit/Value/ValueTest.php +++ b/tests/Unit/Value/ValueTest.php @@ -141,4 +141,16 @@ public function parsesArithmeticWithMalformedOperandsInFunctions(string $leftOpe $subject->render(OutputFormat::createCompact()) ); } + + /** + * @test + */ + public function getArrayRepresentationThrowsException(): void + { + $this->expectException(\BadMethodCallException::class); + + $subject = new ConcreteValue(); + + $subject->getArrayRepresentation(); + } } From 32b7faccf3edd328b1067cda83a8be85cffdf67d Mon Sep 17 00:00:00 2001 From: Oliver Klee Date: Thu, 1 Jan 2026 18:23:15 +0100 Subject: [PATCH 045/136] [TASK] Add more stub tests for `getArrayRepresentation()` (#1448) Part of #1440 --- tests/Unit/CSSList/AtRuleBlockListTest.php | 12 +++++++++ tests/Unit/CSSList/CSSBlockListTest.php | 12 +++++++++ tests/Unit/CSSList/DocumentTest.php | 12 +++++++++ tests/Unit/CSSList/KeyFrameTest.php | 12 +++++++++ tests/Unit/Property/KeyframeSelectorTest.php | 27 +++++++++++++++++++ tests/Unit/RuleSet/AtRuleSetTest.php | 10 +++++++ tests/Unit/Value/CSSFunctionTest.php | 28 ++++++++++++++++++++ tests/Unit/Value/CSSStringTest.php | 12 +++++++++ tests/Unit/Value/CalcFunctionTest.php | 12 +++++++++ tests/Unit/Value/CalcRuleValueListTest.php | 12 +++++++++ tests/Unit/Value/ColorTest.php | 12 +++++++++ tests/Unit/Value/LineNameTest.php | 28 ++++++++++++++++++++ tests/Unit/Value/RuleValueListTest.php | 28 ++++++++++++++++++++ tests/Unit/Value/SizeTest.php | 12 +++++++++ tests/Unit/Value/URLTest.php | 12 +++++++++ 15 files changed, 241 insertions(+) create mode 100644 tests/Unit/Property/KeyframeSelectorTest.php create mode 100644 tests/Unit/Value/CSSFunctionTest.php create mode 100644 tests/Unit/Value/LineNameTest.php create mode 100644 tests/Unit/Value/RuleValueListTest.php diff --git a/tests/Unit/CSSList/AtRuleBlockListTest.php b/tests/Unit/CSSList/AtRuleBlockListTest.php index 3b61e437e..416c8819f 100644 --- a/tests/Unit/CSSList/AtRuleBlockListTest.php +++ b/tests/Unit/CSSList/AtRuleBlockListTest.php @@ -144,4 +144,16 @@ public function isRootListAlwaysReturnsFalse(): void self::assertFalse($subject->isRootList()); } + + /** + * @test + */ + public function getArrayRepresentationThrowsException(): void + { + $this->expectException(\BadMethodCallException::class); + + $subject = new AtRuleBlockList('media'); + + $subject->getArrayRepresentation(); + } } diff --git a/tests/Unit/CSSList/CSSBlockListTest.php b/tests/Unit/CSSList/CSSBlockListTest.php index ce7e54477..50b6420ef 100644 --- a/tests/Unit/CSSList/CSSBlockListTest.php +++ b/tests/Unit/CSSList/CSSBlockListTest.php @@ -471,4 +471,16 @@ public function getAllValuesWithSearchInFunctionArgumentsReturnsValuesInFunction self::assertSame([$value1, $value2], $result); } + + /** + * @test + */ + public function getArrayRepresentationThrowsException(): void + { + $this->expectException(\BadMethodCallException::class); + + $subject = new ConcreteCSSBlockList(); + + $subject->getArrayRepresentation(); + } } diff --git a/tests/Unit/CSSList/DocumentTest.php b/tests/Unit/CSSList/DocumentTest.php index 77e8aa81c..c1d0f614a 100644 --- a/tests/Unit/CSSList/DocumentTest.php +++ b/tests/Unit/CSSList/DocumentTest.php @@ -63,4 +63,16 @@ public function isRootListAlwaysReturnsTrue(): void self::assertTrue($subject->isRootList()); } + + /** + * @test + */ + public function getArrayRepresentationThrowsException(): void + { + $this->expectException(\BadMethodCallException::class); + + $subject = new Document(); + + $subject->getArrayRepresentation(); + } } diff --git a/tests/Unit/CSSList/KeyFrameTest.php b/tests/Unit/CSSList/KeyFrameTest.php index e191a440e..b77f295db 100644 --- a/tests/Unit/CSSList/KeyFrameTest.php +++ b/tests/Unit/CSSList/KeyFrameTest.php @@ -108,4 +108,16 @@ public function getVendorKeyFrameByDefaultReturnsKeyframes(): void self::assertSame('keyframes', $subject->getVendorKeyFrame()); } + + /** + * @test + */ + public function getArrayRepresentationThrowsException(): void + { + $this->expectException(\BadMethodCallException::class); + + $subject = new KeyFrame(); + + $subject->getArrayRepresentation(); + } } diff --git a/tests/Unit/Property/KeyframeSelectorTest.php b/tests/Unit/Property/KeyframeSelectorTest.php new file mode 100644 index 000000000..2f7cebacb --- /dev/null +++ b/tests/Unit/Property/KeyframeSelectorTest.php @@ -0,0 +1,27 @@ +expectException(\BadMethodCallException::class); + + $subject = new KeyframeSelector('a'); + + $subject->getArrayRepresentation(); + } +} diff --git a/tests/Unit/RuleSet/AtRuleSetTest.php b/tests/Unit/RuleSet/AtRuleSetTest.php index 0e3e0c974..74c60a04a 100644 --- a/tests/Unit/RuleSet/AtRuleSetTest.php +++ b/tests/Unit/RuleSet/AtRuleSetTest.php @@ -30,4 +30,14 @@ public function implementsCSSListItem(): void { self::assertInstanceOf(CSSListItem::class, $this->subject); } + + /** + * @test + */ + public function getArrayRepresentationThrowsException(): void + { + $this->expectException(\BadMethodCallException::class); + + $this->subject->getArrayRepresentation(); + } } diff --git a/tests/Unit/Value/CSSFunctionTest.php b/tests/Unit/Value/CSSFunctionTest.php new file mode 100644 index 000000000..8600c569e --- /dev/null +++ b/tests/Unit/Value/CSSFunctionTest.php @@ -0,0 +1,28 @@ +expectException(\BadMethodCallException::class); + + $subject = new CSSFunction('filter', []); + + $subject->getArrayRepresentation(); + } +} diff --git a/tests/Unit/Value/CSSStringTest.php b/tests/Unit/Value/CSSStringTest.php index c54ecdd75..d967d2841 100644 --- a/tests/Unit/Value/CSSStringTest.php +++ b/tests/Unit/Value/CSSStringTest.php @@ -80,4 +80,16 @@ public function getLineNumberReturnsLineNumberProvidedToConstructor(): void self::assertSame($lineNumber, $subject->getLineNumber()); } + + /** + * @test + */ + public function getArrayRepresentationThrowsException(): void + { + $this->expectException(\BadMethodCallException::class); + + $subject = new CSSString(''); + + $subject->getArrayRepresentation(); + } } diff --git a/tests/Unit/Value/CalcFunctionTest.php b/tests/Unit/Value/CalcFunctionTest.php index 65f23e131..330c43b72 100644 --- a/tests/Unit/Value/CalcFunctionTest.php +++ b/tests/Unit/Value/CalcFunctionTest.php @@ -198,4 +198,16 @@ private function parse(string $css): CalcFunction self::assertInstanceOf(CalcFunction::class, $function); return $function; } + + /** + * @test + */ + public function getArrayRepresentationThrowsException(): void + { + $this->expectException(\BadMethodCallException::class); + + $subject = new CalcFunction('calc', []); + + $subject->getArrayRepresentation(); + } } diff --git a/tests/Unit/Value/CalcRuleValueListTest.php b/tests/Unit/Value/CalcRuleValueListTest.php index 7314e3c51..a02c05f7d 100644 --- a/tests/Unit/Value/CalcRuleValueListTest.php +++ b/tests/Unit/Value/CalcRuleValueListTest.php @@ -56,4 +56,16 @@ public function separatorAlwaysIsComma(): void self::assertSame(',', $subject->getListSeparator()); } + + /** + * @test + */ + public function getArrayRepresentationThrowsException(): void + { + $this->expectException(\BadMethodCallException::class); + + $subject = new CalcRuleValueList(); + + $subject->getArrayRepresentation(); + } } diff --git a/tests/Unit/Value/ColorTest.php b/tests/Unit/Value/ColorTest.php index aaa257553..0311f9e5e 100644 --- a/tests/Unit/Value/ColorTest.php +++ b/tests/Unit/Value/ColorTest.php @@ -471,4 +471,16 @@ public function throwsExceptionWithInvalidColor(string $color): void Color::parse(new ParserState($color, Settings::create())); } + + /** + * @test + */ + public function getArrayRepresentationThrowsException(): void + { + $this->expectException(\BadMethodCallException::class); + + $subject = new Color([]); + + $subject->getArrayRepresentation(); + } } diff --git a/tests/Unit/Value/LineNameTest.php b/tests/Unit/Value/LineNameTest.php new file mode 100644 index 000000000..e48670f8a --- /dev/null +++ b/tests/Unit/Value/LineNameTest.php @@ -0,0 +1,28 @@ +expectException(\BadMethodCallException::class); + + $subject = new LineName(); + + $subject->getArrayRepresentation(); + } +} diff --git a/tests/Unit/Value/RuleValueListTest.php b/tests/Unit/Value/RuleValueListTest.php new file mode 100644 index 000000000..e82d255ab --- /dev/null +++ b/tests/Unit/Value/RuleValueListTest.php @@ -0,0 +1,28 @@ +expectException(\BadMethodCallException::class); + + $subject = new RuleValueList(); + + $subject->getArrayRepresentation(); + } +} diff --git a/tests/Unit/Value/SizeTest.php b/tests/Unit/Value/SizeTest.php index c80f788cd..fe86cf761 100644 --- a/tests/Unit/Value/SizeTest.php +++ b/tests/Unit/Value/SizeTest.php @@ -98,4 +98,16 @@ public function parsesUnit(string $unit): void self::assertSame($unit, $parsedSize->getUnit()); } + + /** + * @test + */ + public function getArrayRepresentationThrowsException(): void + { + $this->expectException(\BadMethodCallException::class); + + $subject = new Size(1); + + $subject->getArrayRepresentation(); + } } diff --git a/tests/Unit/Value/URLTest.php b/tests/Unit/Value/URLTest.php index 66654e647..8b4c08809 100644 --- a/tests/Unit/Value/URLTest.php +++ b/tests/Unit/Value/URLTest.php @@ -80,4 +80,16 @@ public function getLineNumberReturnsLineNumberProvidedToConstructor(): void self::assertSame($lineNumber, $subject->getLineNumber()); } + + /** + * @test + */ + public function getArrayRepresentationThrowsException(): void + { + $this->expectException(\BadMethodCallException::class); + + $subject = new URL(new CSSString('http://example.com')); + + $subject->getArrayRepresentation(); + } } From c82f7045ef2065e75da796dcd3ef579d49ff0d17 Mon Sep 17 00:00:00 2001 From: Oliver Klee Date: Thu, 1 Jan 2026 18:25:50 +0100 Subject: [PATCH 046/136] [CLEANUP] Reorder some PHPDoc annotations (#1449) This avoids unrelated changes by autoformatting in the future. Part of #1440 --- src/CSSList/CSSList.php | 4 ++-- src/Comment/Comment.php | 4 ++-- src/Property/CSSNamespace.php | 4 ++-- src/Property/Charset.php | 4 ++-- src/Property/Import.php | 4 ++-- src/Property/Selector.php | 4 ++-- src/Renderable.php | 4 ++-- src/Rule/Rule.php | 4 ++-- src/RuleSet/DeclarationBlock.php | 4 ++-- src/RuleSet/RuleSet.php | 4 ++-- src/Value/Value.php | 4 ++-- 11 files changed, 22 insertions(+), 22 deletions(-) diff --git a/src/CSSList/CSSList.php b/src/CSSList/CSSList.php index 7d4c7efae..ddca103a1 100644 --- a/src/CSSList/CSSList.php +++ b/src/CSSList/CSSList.php @@ -436,9 +436,9 @@ public function getContents(): array } /** - * @internal - * * @return array>> + * + * @internal */ public function getArrayRepresentation(): array { diff --git a/src/Comment/Comment.php b/src/Comment/Comment.php index e09b033b6..b840d6e7f 100644 --- a/src/Comment/Comment.php +++ b/src/Comment/Comment.php @@ -48,9 +48,9 @@ public function render(OutputFormat $outputFormat): string } /** - * @internal - * * @return array>> + * + * @internal */ public function getArrayRepresentation(): array { diff --git a/src/Property/CSSNamespace.php b/src/Property/CSSNamespace.php index cb96d78ba..cb80526c9 100644 --- a/src/Property/CSSNamespace.php +++ b/src/Property/CSSNamespace.php @@ -96,9 +96,9 @@ public function atRuleArgs(): array } /** - * @internal - * * @return array>> + * + * @internal */ public function getArrayRepresentation(): array { diff --git a/src/Property/Charset.php b/src/Property/Charset.php index 9f2ae0500..431150b08 100644 --- a/src/Property/Charset.php +++ b/src/Property/Charset.php @@ -73,9 +73,9 @@ public function atRuleArgs(): CSSString } /** - * @internal - * * @return array>> + * + * @internal */ public function getArrayRepresentation(): array { diff --git a/src/Property/Import.php b/src/Property/Import.php index 366042481..95d1612bc 100644 --- a/src/Property/Import.php +++ b/src/Property/Import.php @@ -84,9 +84,9 @@ public function getMediaQuery(): ?string } /** - * @internal - * * @return array>> + * + * @internal */ public function getArrayRepresentation(): array { diff --git a/src/Property/Selector.php b/src/Property/Selector.php index 093f2bcf4..72c8555e9 100644 --- a/src/Property/Selector.php +++ b/src/Property/Selector.php @@ -97,9 +97,9 @@ public function render(OutputFormat $outputFormat): string } /** - * @internal - * * @return array>> + * + * @internal */ public function getArrayRepresentation(): array { diff --git a/src/Renderable.php b/src/Renderable.php index 1385abd92..560388d1c 100644 --- a/src/Renderable.php +++ b/src/Renderable.php @@ -9,9 +9,9 @@ interface Renderable public function render(OutputFormat $outputFormat): string; /** - * @internal - * * @return array>> + * + * @internal */ public function getArrayRepresentation(): array; } diff --git a/src/Rule/Rule.php b/src/Rule/Rule.php index c43cb414b..d1be3cb62 100644 --- a/src/Rule/Rule.php +++ b/src/Rule/Rule.php @@ -199,9 +199,9 @@ public function render(OutputFormat $outputFormat): string } /** - * @internal - * * @return array>> + * + * @internal */ public function getArrayRepresentation(): array { diff --git a/src/RuleSet/DeclarationBlock.php b/src/RuleSet/DeclarationBlock.php index 386d7651c..c10d2d233 100644 --- a/src/RuleSet/DeclarationBlock.php +++ b/src/RuleSet/DeclarationBlock.php @@ -274,9 +274,9 @@ public function render(OutputFormat $outputFormat): string } /** - * @internal - * * @return array>> + * + * @internal */ public function getArrayRepresentation(): array { diff --git a/src/RuleSet/RuleSet.php b/src/RuleSet/RuleSet.php index 64bd8c4db..b79ed4d8e 100644 --- a/src/RuleSet/RuleSet.php +++ b/src/RuleSet/RuleSet.php @@ -332,9 +332,9 @@ protected function renderRules(OutputFormat $outputFormat): string } /** - * @internal - * * @return array>> + * + * @internal */ public function getArrayRepresentation(): array { diff --git a/src/Value/Value.php b/src/Value/Value.php index 59c49c85f..b53c918eb 100644 --- a/src/Value/Value.php +++ b/src/Value/Value.php @@ -182,9 +182,9 @@ public static function parsePrimitiveValue(ParserState $parserState) } /** - * @internal - * * @return array>> + * + * @internal */ public function getArrayRepresentation(): array { From 6604f352dcad94e47836a080c03e81a57d61e2c7 Mon Sep 17 00:00:00 2001 From: Oliver Klee Date: Thu, 1 Jan 2026 18:26:43 +0100 Subject: [PATCH 047/136] [BUGFIX] Fix test name wording (#1450) --- tests/Unit/Comment/CommentTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Unit/Comment/CommentTest.php b/tests/Unit/Comment/CommentTest.php index 841420825..c149b9a49 100644 --- a/tests/Unit/Comment/CommentTest.php +++ b/tests/Unit/Comment/CommentTest.php @@ -47,7 +47,7 @@ public function getCommentInitiallyReturnsCommentPassedToConstructor(): void /** * @test */ - public function setCommentSetsComments(): void + public function setCommentSetsComment(): void { $comment = 'There is no spoon.'; $subject = new Comment(); From ecde963ceb5d0ed966cf3230d59ce54abcf17e9c Mon Sep 17 00:00:00 2001 From: JakeQZ Date: Fri, 2 Jan 2026 20:00:04 +0000 Subject: [PATCH 048/136] [TASK] Add trait for `getArrayRepresentation` to populate `class` (#1451) Part of #1440. --- src/ShortClassNameProvider.php | 21 +++++++++++++++ .../ConcreteShortClassNameProvider.php | 22 ++++++++++++++++ tests/Unit/ShortClassNameProviderTest.php | 26 +++++++++++++++++++ 3 files changed, 69 insertions(+) create mode 100644 src/ShortClassNameProvider.php create mode 100644 tests/Unit/Fixtures/ConcreteShortClassNameProvider.php create mode 100644 tests/Unit/ShortClassNameProviderTest.php diff --git a/src/ShortClassNameProvider.php b/src/ShortClassNameProvider.php new file mode 100644 index 000000000..f305e1326 --- /dev/null +++ b/src/ShortClassNameProvider.php @@ -0,0 +1,21 @@ +getShortName(); + } +} diff --git a/tests/Unit/Fixtures/ConcreteShortClassNameProvider.php b/tests/Unit/Fixtures/ConcreteShortClassNameProvider.php new file mode 100644 index 000000000..70dd10fd6 --- /dev/null +++ b/tests/Unit/Fixtures/ConcreteShortClassNameProvider.php @@ -0,0 +1,22 @@ +getShortClassName(); + } +} diff --git a/tests/Unit/ShortClassNameProviderTest.php b/tests/Unit/ShortClassNameProviderTest.php new file mode 100644 index 000000000..c5d5694fa --- /dev/null +++ b/tests/Unit/ShortClassNameProviderTest.php @@ -0,0 +1,26 @@ +getTheShortClassName(); + + self::assertSame('ConcreteShortClassNameProvider', $result); + } +} From 80cb4c4d659561304dc520797a16b208bc8bbfe5 Mon Sep 17 00:00:00 2001 From: JakeQZ Date: Sat, 3 Jan 2026 09:08:08 +0000 Subject: [PATCH 049/136] [TASK] Implement `getArrayRepresentation()` for `Size` (#1452) Part of #1440. --- src/Value/Size.php | 18 ++++++++++++++++++ tests/Unit/Value/SizeTest.php | 33 ++++++++++++++++++++++++++++++--- 2 files changed, 48 insertions(+), 3 deletions(-) diff --git a/src/Value/Size.php b/src/Value/Size.php index e8e2a1d00..34dd47b13 100644 --- a/src/Value/Size.php +++ b/src/Value/Size.php @@ -8,6 +8,7 @@ use Sabberworm\CSS\Parsing\ParserState; use Sabberworm\CSS\Parsing\UnexpectedEOFException; use Sabberworm\CSS\Parsing\UnexpectedTokenException; +use Sabberworm\CSS\ShortClassNameProvider; use function Safe\preg_match; use function Safe\preg_replace; @@ -17,6 +18,8 @@ */ class Size extends PrimitiveValue { + use ShortClassNameProvider; + /** * vh/vw/vm(ax)/vmin/rem are absolute insofar as they don’t scale to the immediate parent (only the viewport) */ @@ -202,4 +205,19 @@ public function render(OutputFormat $outputFormat): string return preg_replace(["/$decimalPoint/", '/^(-?)0\\./'], ['.', '$1.'], $size) . ($this->unit ?? ''); } + + /** + * @return array>> + * + * @internal + */ + public function getArrayRepresentation(): array + { + return [ + 'class' => $this->getShortClassName(), + // 'number' is the official W3C terminology (not 'size') + 'number' => $this->size, + 'unit' => $this->unit, + ]; + } } diff --git a/tests/Unit/Value/SizeTest.php b/tests/Unit/Value/SizeTest.php index fe86cf761..189946f66 100644 --- a/tests/Unit/Value/SizeTest.php +++ b/tests/Unit/Value/SizeTest.php @@ -102,12 +102,39 @@ public function parsesUnit(string $unit): void /** * @test */ - public function getArrayRepresentationThrowsException(): void + public function getArrayRepresentationIncludesClassName(): void { - $this->expectException(\BadMethodCallException::class); + $subject = new Size(1); + + $result = $subject->getArrayRepresentation(); + self::assertArrayHasKey('class', $result); + self::assertSame('Size', $result['class']); + } + + /** + * @test + */ + public function getArrayRepresentationIncludesNumber(): void + { $subject = new Size(1); - $subject->getArrayRepresentation(); + $result = $subject->getArrayRepresentation(); + + self::assertArrayHasKey('number', $result); + self::assertSame(1.0, $result['number']); + } + + /** + * @test + */ + public function getArrayRepresentationIncludesUnit(): void + { + $subject = new Size(1, 'px'); + + $result = $subject->getArrayRepresentation(); + + self::assertArrayHasKey('unit', $result); + self::assertSame('px', $result['unit']); } } From b6e4d25e63759dca967ed6743d054c3e90464cae Mon Sep 17 00:00:00 2001 From: Oliver Klee Date: Sat, 3 Jan 2026 18:23:41 +0100 Subject: [PATCH 050/136] [TASK] Implement `getArrayRepresentation()` for `Comment` (#1454) Part of #1440. --- src/Comment/Comment.php | 9 ++++++++- tests/Unit/Comment/CommentTest.php | 23 +++++++++++++++++++---- 2 files changed, 27 insertions(+), 5 deletions(-) diff --git a/src/Comment/Comment.php b/src/Comment/Comment.php index b840d6e7f..d85b2e8a8 100644 --- a/src/Comment/Comment.php +++ b/src/Comment/Comment.php @@ -8,10 +8,12 @@ use Sabberworm\CSS\Position\Position; use Sabberworm\CSS\Position\Positionable; use Sabberworm\CSS\Renderable; +use Sabberworm\CSS\ShortClassNameProvider; class Comment implements Positionable, Renderable { use Position; + use ShortClassNameProvider; /** * @var string @@ -54,6 +56,11 @@ public function render(OutputFormat $outputFormat): string */ public function getArrayRepresentation(): array { - throw new \BadMethodCallException('`getArrayRepresentation` is not yet implemented for `' . self::class . '`'); + return [ + 'class' => $this->getShortClassName(), + // "contents" is the term used in the W3C specs: + // https://www.w3.org/TR/CSS22/syndata.html#comments + 'contents' => $this->commentText, + ]; } } diff --git a/tests/Unit/Comment/CommentTest.php b/tests/Unit/Comment/CommentTest.php index c149b9a49..cacab2360 100644 --- a/tests/Unit/Comment/CommentTest.php +++ b/tests/Unit/Comment/CommentTest.php @@ -81,12 +81,27 @@ public function getLineNumberReturnsLineNumberProvidedToConstructor(): void /** * @test */ - public function getArrayRepresentationThrowsException(): void + public function getArrayRepresentationIncludesClassName(): void { - $this->expectException(\BadMethodCallException::class); - $subject = new Comment(); - $subject->getArrayRepresentation(); + $result = $subject->getArrayRepresentation(); + + self::assertArrayHasKey('class', $result); + self::assertSame('Comment', $result['class']); + } + + /** + * @test + */ + public function getArrayRepresentationIncludesContents(): void + { + $contents = 'War. War never changes.'; + $subject = new Comment($contents); + + $result = $subject->getArrayRepresentation(); + + self::assertArrayHasKey('contents', $result); + self::assertSame($contents, $result['contents']); } } From 7566715961bfb6dedc6e8f3feb194eb16481a3a3 Mon Sep 17 00:00:00 2001 From: JakeQZ Date: Sun, 4 Jan 2026 09:18:31 +0000 Subject: [PATCH 051/136] [TASK] Improve exception message for `Value::getArrayRepresentation` (#1456) Having the leaf class identified eases debugging during development. Part of #1440. --- src/Value/Value.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Value/Value.php b/src/Value/Value.php index b53c918eb..1ea2d2723 100644 --- a/src/Value/Value.php +++ b/src/Value/Value.php @@ -188,7 +188,9 @@ public static function parsePrimitiveValue(ParserState $parserState) */ public function getArrayRepresentation(): array { - throw new \BadMethodCallException('`getArrayRepresentation` is not yet implemented for `' . self::class . '`'); + throw new \BadMethodCallException( + '`getArrayRepresentation` is not yet implemented for `' . static::class . '`' + ); } /** From 0a2c87553898de1391c2c70d24069d5a50b4bb42 Mon Sep 17 00:00:00 2001 From: JakeQZ Date: Sun, 4 Jan 2026 09:22:05 +0000 Subject: [PATCH 052/136] [TASK] Implement `getArrayRepresentation()` for `ValueList` (#1455) Note that `ValueList` is an abstract class. The implementation can be reused by the concrete subclasses. In the case of `LineName` and `RuleValueList` it can be inherited as is, because they don't have any additional properties. `CSSFunction` will need to add the `name` property. For now, exception-throwing stubs are added to the subclasses, pending implementation on a case-by-case basis. Part of #1440. --- src/Value/CSSFunction.php | 10 ++ src/Value/LineName.php | 10 ++ src/Value/RuleValueList.php | 10 ++ src/Value/ValueList.php | 28 ++++++ .../Unit/Value/Fixtures/ConcreteValueList.php | 9 ++ tests/Unit/Value/ValueListTest.php | 98 +++++++++++++++++++ 6 files changed, 165 insertions(+) create mode 100644 tests/Unit/Value/Fixtures/ConcreteValueList.php create mode 100644 tests/Unit/Value/ValueListTest.php diff --git a/src/Value/CSSFunction.php b/src/Value/CSSFunction.php index 86b56d9b1..a8b8ead73 100644 --- a/src/Value/CSSFunction.php +++ b/src/Value/CSSFunction.php @@ -113,4 +113,14 @@ public function render(OutputFormat $outputFormat): string $arguments = parent::render($outputFormat); return "{$this->name}({$arguments})"; } + + /** + * @return array>> + * + * @internal + */ + public function getArrayRepresentation(): array + { + throw new \BadMethodCallException('`getArrayRepresentation` is not yet implemented for `' . self::class . '`'); + } } diff --git a/src/Value/LineName.php b/src/Value/LineName.php index 763cc48ea..d16c60dbf 100644 --- a/src/Value/LineName.php +++ b/src/Value/LineName.php @@ -56,4 +56,14 @@ public function render(OutputFormat $outputFormat): string { return '[' . parent::render(OutputFormat::createCompact()) . ']'; } + + /** + * @return array>> + * + * @internal + */ + public function getArrayRepresentation(): array + { + throw new \BadMethodCallException('`getArrayRepresentation` is not yet implemented for `' . self::class . '`'); + } } diff --git a/src/Value/RuleValueList.php b/src/Value/RuleValueList.php index 37aa7cd66..604e13003 100644 --- a/src/Value/RuleValueList.php +++ b/src/Value/RuleValueList.php @@ -19,4 +19,14 @@ public function __construct(string $separator = ',', ?int $lineNumber = null) { parent::__construct([], $separator, $lineNumber); } + + /** + * @return array>> + * + * @internal + */ + public function getArrayRepresentation(): array + { + throw new \BadMethodCallException('`getArrayRepresentation` is not yet implemented for `' . self::class . '`'); + } } diff --git a/src/Value/ValueList.php b/src/Value/ValueList.php index 6f85f895e..b39449cfa 100644 --- a/src/Value/ValueList.php +++ b/src/Value/ValueList.php @@ -5,6 +5,7 @@ namespace Sabberworm\CSS\Value; use Sabberworm\CSS\OutputFormat; +use Sabberworm\CSS\ShortClassNameProvider; /** * A `ValueList` represents a lists of `Value`s, separated by some separation character @@ -14,6 +15,8 @@ */ abstract class ValueList extends Value { + use ShortClassNameProvider; + /** * @var array * @@ -93,4 +96,29 @@ public function render(OutputFormat $outputFormat): string $this->components ); } + + /** + * @return array>> + * + * @internal + */ + public function getArrayRepresentation(): array + { + return [ + 'class' => $this->getShortClassName(), + 'components' => \array_map( + /** + * @parm Value|string $component + */ + function ($component): array { + if (\is_string($component)) { + return ['class' => 'string', 'value' => $component]; + } + return $component->getArrayRepresentation(); + }, + $this->components + ), + 'separator' => $this->separator, + ]; + } } diff --git a/tests/Unit/Value/Fixtures/ConcreteValueList.php b/tests/Unit/Value/Fixtures/ConcreteValueList.php new file mode 100644 index 000000000..a9659d30c --- /dev/null +++ b/tests/Unit/Value/Fixtures/ConcreteValueList.php @@ -0,0 +1,9 @@ +getArrayRepresentation(); + + self::assertArrayHasKey('class', $result); + self::assertSame('ConcreteValueList', $result['class']); + } + + /** + * @test + */ + public function getArrayRepresentationIncludesStringComponent(): void + { + $subject = new ConcreteValueList(['Helvetica']); + + $result = $subject->getArrayRepresentation(); + + self::assertArrayHasKey('components', $result); + self::assertIsArray($result['components']); + self::assertArrayHasKey(0, $result['components']); + self::assertArrayHasKey('value', $result['components'][0]); + self::assertSame('Helvetica', $result['components'][0]['value']); + } + + /** + * @test + */ + public function getArrayRepresentationIncludesValueComponent(): void + { + $subject = new ConcreteValueList([new Size(1)]); + + $result = $subject->getArrayRepresentation(); + + self::assertArrayHasKey('components', $result); + self::assertIsArray($result['components']); + self::assertArrayHasKey(0, $result['components']); + self::assertArrayHasKey('class', $result['components'][0]); + self::assertSame('Size', $result['components'][0]['class']); + } + + /** + * @test + */ + public function getArrayRepresentationIncludesMultipleMixedComponents(): void + { + $subject = new ConcreteValueList([new Size(1), '+', new Size(2)]); + + $result = $subject->getArrayRepresentation(); + + self::assertArrayHasKey('components', $result); + self::assertIsArray($result['components']); + + self::assertArrayHasKey(0, $result['components']); + self::assertArrayHasKey('class', $result['components'][0]); + self::assertSame('Size', $result['components'][0]['class']); + + self::assertArrayHasKey(1, $result['components']); + self::assertArrayHasKey('value', $result['components'][1]); + self::assertSame('+', $result['components'][1]['value']); + + self::assertArrayHasKey(2, $result['components']); + self::assertArrayHasKey('class', $result['components'][2]); + self::assertSame('Size', $result['components'][2]['class']); + } + + /** + * @test + */ + public function getArrayRepresentationIncludesSeparator(): void + { + $subject = new ConcreteValueList(); + + $result = $subject->getArrayRepresentation(); + + self::assertArrayHasKey('separator', $result); + self::assertSame(',', $result['separator']); + } +} From 09adec3737008431244a6e9005bcb1ea5e2ae089 Mon Sep 17 00:00:00 2001 From: JakeQZ Date: Sun, 4 Jan 2026 11:08:51 +0000 Subject: [PATCH 053/136] [TASK] Implement `RuleValueList::getArrayRepresentation()` (#1457) The class has no additional properties, so this is a simple case of removing the stub method so it inherits its parent implementation, and adding a test. A stub exception-throwing method is added (pushed down the chain) to the subclass `CalcRuleValueList` pending implementation. Part of #1440. --- src/Value/CalcRuleValueList.php | 10 ++++++++++ src/Value/RuleValueList.php | 10 ---------- tests/Unit/Value/RuleValueListTest.php | 9 +++++---- 3 files changed, 15 insertions(+), 14 deletions(-) diff --git a/src/Value/CalcRuleValueList.php b/src/Value/CalcRuleValueList.php index f904f12c7..f110452e7 100644 --- a/src/Value/CalcRuleValueList.php +++ b/src/Value/CalcRuleValueList.php @@ -20,4 +20,14 @@ public function render(OutputFormat $outputFormat): string { return $outputFormat->getFormatter()->implode(' ', $this->components); } + + /** + * @return array>> + * + * @internal + */ + public function getArrayRepresentation(): array + { + throw new \BadMethodCallException('`getArrayRepresentation` is not yet implemented for `' . self::class . '`'); + } } diff --git a/src/Value/RuleValueList.php b/src/Value/RuleValueList.php index 604e13003..37aa7cd66 100644 --- a/src/Value/RuleValueList.php +++ b/src/Value/RuleValueList.php @@ -19,14 +19,4 @@ public function __construct(string $separator = ',', ?int $lineNumber = null) { parent::__construct([], $separator, $lineNumber); } - - /** - * @return array>> - * - * @internal - */ - public function getArrayRepresentation(): array - { - throw new \BadMethodCallException('`getArrayRepresentation` is not yet implemented for `' . self::class . '`'); - } } diff --git a/tests/Unit/Value/RuleValueListTest.php b/tests/Unit/Value/RuleValueListTest.php index e82d255ab..4c116003b 100644 --- a/tests/Unit/Value/RuleValueListTest.php +++ b/tests/Unit/Value/RuleValueListTest.php @@ -17,12 +17,13 @@ final class RuleValueListTest extends TestCase /** * @test */ - public function getArrayRepresentationThrowsException(): void + public function getArrayRepresentationIncludesClassName(): void { - $this->expectException(\BadMethodCallException::class); - $subject = new RuleValueList(); - $subject->getArrayRepresentation(); + $result = $subject->getArrayRepresentation(); + + self::assertArrayHasKey('class', $result); + self::assertSame('RuleValueList', $result['class']); } } From 95263d1c6a38fd2378ff3e38b3435c181063fa8c Mon Sep 17 00:00:00 2001 From: JakeQZ Date: Sun, 4 Jan 2026 12:24:52 +0000 Subject: [PATCH 054/136] [TASK] Implement `CSSFunction::getArrayRepresentation()` (#1458) Move exception-throwing stubs down the chain to be implemented IDC. Part of #1440. --- src/Value/CSSFunction.php | 6 +++++- src/Value/CalcFunction.php | 10 ++++++++++ src/Value/Color.php | 10 ++++++++++ tests/Unit/Value/CSSFunctionTest.php | 20 +++++++++++++++++--- 4 files changed, 42 insertions(+), 4 deletions(-) diff --git a/src/Value/CSSFunction.php b/src/Value/CSSFunction.php index a8b8ead73..018a9997a 100644 --- a/src/Value/CSSFunction.php +++ b/src/Value/CSSFunction.php @@ -121,6 +121,10 @@ public function render(OutputFormat $outputFormat): string */ public function getArrayRepresentation(): array { - throw new \BadMethodCallException('`getArrayRepresentation` is not yet implemented for `' . self::class . '`'); + $result = parent::getArrayRepresentation(); + + $result['name'] = $this->name; + + return $result; } } diff --git a/src/Value/CalcFunction.php b/src/Value/CalcFunction.php index f674ed151..f77b9c416 100644 --- a/src/Value/CalcFunction.php +++ b/src/Value/CalcFunction.php @@ -96,4 +96,14 @@ public static function parse(ParserState $parserState, bool $ignoreCase = false) } return new CalcFunction($function, $list, ',', $parserState->currentLine()); } + + /** + * @return array>> + * + * @internal + */ + public function getArrayRepresentation(): array + { + throw new \BadMethodCallException('`getArrayRepresentation` is not yet implemented for `' . self::class . '`'); + } } diff --git a/src/Value/Color.php b/src/Value/Color.php index 63bccab8b..a7fe74db2 100644 --- a/src/Value/Color.php +++ b/src/Value/Color.php @@ -239,6 +239,16 @@ public function render(OutputFormat $outputFormat): string return parent::render($outputFormat); } + /** + * @return array>> + * + * @internal + */ + public function getArrayRepresentation(): array + { + throw new \BadMethodCallException('`getArrayRepresentation` is not yet implemented for `' . self::class . '`'); + } + private function shouldRenderAsHex(OutputFormat $outputFormat): bool { return diff --git a/tests/Unit/Value/CSSFunctionTest.php b/tests/Unit/Value/CSSFunctionTest.php index 8600c569e..c8dc549fc 100644 --- a/tests/Unit/Value/CSSFunctionTest.php +++ b/tests/Unit/Value/CSSFunctionTest.php @@ -17,12 +17,26 @@ final class CSSFunctionTest extends TestCase /** * @test */ - public function getArrayRepresentationThrowsException(): void + public function getArrayRepresentationIncludesClassName(): void { - $this->expectException(\BadMethodCallException::class); + $subject = new CSSFunction('filter', []); + + $result = $subject->getArrayRepresentation(); + self::assertArrayHasKey('class', $result); + self::assertSame('CSSFunction', $result['class']); + } + + /** + * @test + */ + public function getArrayRepresentationIncludesFunctionName(): void + { $subject = new CSSFunction('filter', []); - $subject->getArrayRepresentation(); + $result = $subject->getArrayRepresentation(); + + self::assertArrayHasKey('name', $result); + self::assertSame('filter', $result['name']); } } From 7538c81699f98063624d850b2d87ac6f75fc271a Mon Sep 17 00:00:00 2001 From: Oliver Klee Date: Sun, 4 Jan 2026 19:03:51 +0100 Subject: [PATCH 055/136] [TASK] Implement `CSSString::getArrayRepresentation()` (#1459) Part of #1440. --- src/Value/CSSString.php | 17 +++++++++++++++++ tests/Unit/Value/CSSStringTest.php | 23 +++++++++++++++++++---- 2 files changed, 36 insertions(+), 4 deletions(-) diff --git a/src/Value/CSSString.php b/src/Value/CSSString.php index 569311d77..fcfee0c42 100644 --- a/src/Value/CSSString.php +++ b/src/Value/CSSString.php @@ -9,6 +9,7 @@ use Sabberworm\CSS\Parsing\SourceException; use Sabberworm\CSS\Parsing\UnexpectedEOFException; use Sabberworm\CSS\Parsing\UnexpectedTokenException; +use Sabberworm\CSS\ShortClassNameProvider; use function Safe\preg_match; @@ -19,6 +20,8 @@ */ class CSSString extends PrimitiveValue { + use ShortClassNameProvider; + /** * @var string */ @@ -94,4 +97,18 @@ public function render(OutputFormat $outputFormat): string $string = \str_replace("\n", '\\A', $string); return $outputFormat->getStringQuotingType() . $string . $outputFormat->getStringQuotingType(); } + + /** + * @return array>> + * + * @internal + */ + public function getArrayRepresentation(): array + { + return [ + 'class' => $this->getShortClassName(), + // We're using the term "contents" here to make the difference to the class more clear. + 'contents' => $this->string, + ]; + } } diff --git a/tests/Unit/Value/CSSStringTest.php b/tests/Unit/Value/CSSStringTest.php index d967d2841..2fd30b18a 100644 --- a/tests/Unit/Value/CSSStringTest.php +++ b/tests/Unit/Value/CSSStringTest.php @@ -84,12 +84,27 @@ public function getLineNumberReturnsLineNumberProvidedToConstructor(): void /** * @test */ - public function getArrayRepresentationThrowsException(): void + public function getArrayRepresentationIncludesClassName(): void { - $this->expectException(\BadMethodCallException::class); - $subject = new CSSString(''); - $subject->getArrayRepresentation(); + $result = $subject->getArrayRepresentation(); + + self::assertArrayHasKey('class', $result); + self::assertSame('CSSString', $result['class']); + } + + /** + * @test + */ + public function getArrayRepresentationIncludesContents(): void + { + $contents = 'What is love?'; + $subject = new CSSString($contents); + + $result = $subject->getArrayRepresentation(); + + self::assertArrayHasKey('contents', $result); + self::assertSame($contents, $result['contents']); } } From 1b5be340ec29e5438e79dae3d1019dea76908883 Mon Sep 17 00:00:00 2001 From: JakeQZ Date: Sun, 4 Jan 2026 19:51:21 +0000 Subject: [PATCH 056/136] [TASK] Add PHP_Codesniffer to CI checks (#1460) --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b35ea69f9..72f25f4aa 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -105,6 +105,7 @@ jobs: command: - composer:normalize - php:fixer + - php:codesniffer - php:stan - php:rector php-version: From 99217b8e99b7a971e4fe3a3437d6303ebe27d044 Mon Sep 17 00:00:00 2001 From: JakeQZ Date: Sun, 4 Jan 2026 23:49:05 +0000 Subject: [PATCH 057/136] [TASK] Use `getArrayRepresentation()` in `ValueTest` (#1441) ... in `parsesArithmeticInFunctions()` test. Follows on from #1440. --- tests/Unit/Value/ValueTest.php | 38 ++++++++++++++++++++++++++++++++-- 1 file changed, 36 insertions(+), 2 deletions(-) diff --git a/tests/Unit/Value/ValueTest.php b/tests/Unit/Value/ValueTest.php index 87c282c01..5b34831b9 100644 --- a/tests/Unit/Value/ValueTest.php +++ b/tests/Unit/Value/ValueTest.php @@ -60,8 +60,42 @@ public function parsesArithmeticInFunctions(string $operator): void self::DEFAULT_DELIMITERS ); - self::assertInstanceOf(CSSFunction::class, $subject); - self::assertSame('max(300px,50vh ' . $operator . ' 10px)', $subject->render(OutputFormat::createCompact())); + $result = $subject->getArrayRepresentation(); + self::assertSame( + [ + 'class' => 'CSSFunction', + 'components' => [ + [ + 'class' => 'Size', + 'number' => 300.0, + 'unit' => 'px', + ], + [ + 'class' => 'RuleValueList', + 'components' => [ + [ + 'class' => 'Size', + 'number' => 50.0, + 'unit' => 'vh', + ], + [ + 'class' => 'string', + 'value' => $operator, + ], + [ + 'class' => 'Size', + 'number' => 10.0, + 'unit' => 'px', + ], + ], + 'separator' => ' ', + ], + ], + 'separator' => ',', + 'name' => 'max', + ], + $result + ); } /** From 870a709629c1207ae3b27d1d9fb5cf01e0009dbf Mon Sep 17 00:00:00 2001 From: JakeQZ Date: Mon, 5 Jan 2026 09:03:13 +0000 Subject: [PATCH 058/136] [TASK] Order `CSSFunction` array representation logically (#1461) --- src/Value/CSSFunction.php | 12 +++++++----- tests/Unit/Value/ValueTest.php | 2 +- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/Value/CSSFunction.php b/src/Value/CSSFunction.php index 018a9997a..f5951bd21 100644 --- a/src/Value/CSSFunction.php +++ b/src/Value/CSSFunction.php @@ -121,10 +121,12 @@ public function render(OutputFormat $outputFormat): string */ public function getArrayRepresentation(): array { - $result = parent::getArrayRepresentation(); - - $result['name'] = $this->name; - - return $result; + return \array_merge( + [ + 'class' => 'placeholder', + 'name' => $this->name, + ], + parent::getArrayRepresentation() + ); } } diff --git a/tests/Unit/Value/ValueTest.php b/tests/Unit/Value/ValueTest.php index 5b34831b9..b97e5f657 100644 --- a/tests/Unit/Value/ValueTest.php +++ b/tests/Unit/Value/ValueTest.php @@ -64,6 +64,7 @@ public function parsesArithmeticInFunctions(string $operator): void self::assertSame( [ 'class' => 'CSSFunction', + 'name' => 'max', 'components' => [ [ 'class' => 'Size', @@ -92,7 +93,6 @@ public function parsesArithmeticInFunctions(string $operator): void ], ], 'separator' => ',', - 'name' => 'max', ], $result ); From 3c27af37b9b3636586289104a2e5d462ba979f6b Mon Sep 17 00:00:00 2001 From: Oliver Klee Date: Mon, 5 Jan 2026 19:45:40 +0100 Subject: [PATCH 059/136] [BUGFIX] Loosen the type annotation for `getArrayRepresentation` (#1463) Part of #1440. --- src/CSSList/CSSList.php | 2 +- src/Comment/Comment.php | 2 +- src/Property/CSSNamespace.php | 2 +- src/Property/Charset.php | 2 +- src/Property/Import.php | 2 +- src/Property/Selector.php | 2 +- src/Renderable.php | 2 +- src/Rule/Rule.php | 2 +- src/RuleSet/DeclarationBlock.php | 2 +- src/RuleSet/RuleSet.php | 2 +- src/Value/CSSFunction.php | 2 +- src/Value/CSSString.php | 2 +- src/Value/CalcFunction.php | 2 +- src/Value/CalcRuleValueList.php | 2 +- src/Value/Color.php | 2 +- src/Value/LineName.php | 2 +- src/Value/Size.php | 2 +- src/Value/Value.php | 2 +- src/Value/ValueList.php | 2 +- 19 files changed, 19 insertions(+), 19 deletions(-) diff --git a/src/CSSList/CSSList.php b/src/CSSList/CSSList.php index ddca103a1..48e27c686 100644 --- a/src/CSSList/CSSList.php +++ b/src/CSSList/CSSList.php @@ -436,7 +436,7 @@ public function getContents(): array } /** - * @return array>> + * @return array|null> * * @internal */ diff --git a/src/Comment/Comment.php b/src/Comment/Comment.php index d85b2e8a8..cba49d925 100644 --- a/src/Comment/Comment.php +++ b/src/Comment/Comment.php @@ -50,7 +50,7 @@ public function render(OutputFormat $outputFormat): string } /** - * @return array>> + * @return array|null> * * @internal */ diff --git a/src/Property/CSSNamespace.php b/src/Property/CSSNamespace.php index cb80526c9..1801904a3 100644 --- a/src/Property/CSSNamespace.php +++ b/src/Property/CSSNamespace.php @@ -96,7 +96,7 @@ public function atRuleArgs(): array } /** - * @return array>> + * @return array|null> * * @internal */ diff --git a/src/Property/Charset.php b/src/Property/Charset.php index 431150b08..50c84928d 100644 --- a/src/Property/Charset.php +++ b/src/Property/Charset.php @@ -73,7 +73,7 @@ public function atRuleArgs(): CSSString } /** - * @return array>> + * @return array|null> * * @internal */ diff --git a/src/Property/Import.php b/src/Property/Import.php index 95d1612bc..d7c24d7af 100644 --- a/src/Property/Import.php +++ b/src/Property/Import.php @@ -84,7 +84,7 @@ public function getMediaQuery(): ?string } /** - * @return array>> + * @return array|null> * * @internal */ diff --git a/src/Property/Selector.php b/src/Property/Selector.php index 72c8555e9..b9cf9bbc4 100644 --- a/src/Property/Selector.php +++ b/src/Property/Selector.php @@ -97,7 +97,7 @@ public function render(OutputFormat $outputFormat): string } /** - * @return array>> + * @return array|null> * * @internal */ diff --git a/src/Renderable.php b/src/Renderable.php index 560388d1c..2c3cf0635 100644 --- a/src/Renderable.php +++ b/src/Renderable.php @@ -9,7 +9,7 @@ interface Renderable public function render(OutputFormat $outputFormat): string; /** - * @return array>> + * @return array|null> * * @internal */ diff --git a/src/Rule/Rule.php b/src/Rule/Rule.php index d1be3cb62..27b6bd9a7 100644 --- a/src/Rule/Rule.php +++ b/src/Rule/Rule.php @@ -199,7 +199,7 @@ public function render(OutputFormat $outputFormat): string } /** - * @return array>> + * @return array|null> * * @internal */ diff --git a/src/RuleSet/DeclarationBlock.php b/src/RuleSet/DeclarationBlock.php index c10d2d233..ed226676a 100644 --- a/src/RuleSet/DeclarationBlock.php +++ b/src/RuleSet/DeclarationBlock.php @@ -274,7 +274,7 @@ public function render(OutputFormat $outputFormat): string } /** - * @return array>> + * @return array|null> * * @internal */ diff --git a/src/RuleSet/RuleSet.php b/src/RuleSet/RuleSet.php index b79ed4d8e..576ae23f7 100644 --- a/src/RuleSet/RuleSet.php +++ b/src/RuleSet/RuleSet.php @@ -332,7 +332,7 @@ protected function renderRules(OutputFormat $outputFormat): string } /** - * @return array>> + * @return array|null> * * @internal */ diff --git a/src/Value/CSSFunction.php b/src/Value/CSSFunction.php index f5951bd21..30228340a 100644 --- a/src/Value/CSSFunction.php +++ b/src/Value/CSSFunction.php @@ -115,7 +115,7 @@ public function render(OutputFormat $outputFormat): string } /** - * @return array>> + * @return array|null> * * @internal */ diff --git a/src/Value/CSSString.php b/src/Value/CSSString.php index fcfee0c42..16bc90226 100644 --- a/src/Value/CSSString.php +++ b/src/Value/CSSString.php @@ -99,7 +99,7 @@ public function render(OutputFormat $outputFormat): string } /** - * @return array>> + * @return array|null> * * @internal */ diff --git a/src/Value/CalcFunction.php b/src/Value/CalcFunction.php index f77b9c416..3d85b22a7 100644 --- a/src/Value/CalcFunction.php +++ b/src/Value/CalcFunction.php @@ -98,7 +98,7 @@ public static function parse(ParserState $parserState, bool $ignoreCase = false) } /** - * @return array>> + * @return array|null> * * @internal */ diff --git a/src/Value/CalcRuleValueList.php b/src/Value/CalcRuleValueList.php index f110452e7..5396dfdd5 100644 --- a/src/Value/CalcRuleValueList.php +++ b/src/Value/CalcRuleValueList.php @@ -22,7 +22,7 @@ public function render(OutputFormat $outputFormat): string } /** - * @return array>> + * @return array|null> * * @internal */ diff --git a/src/Value/Color.php b/src/Value/Color.php index a7fe74db2..df491692c 100644 --- a/src/Value/Color.php +++ b/src/Value/Color.php @@ -240,7 +240,7 @@ public function render(OutputFormat $outputFormat): string } /** - * @return array>> + * @return array|null> * * @internal */ diff --git a/src/Value/LineName.php b/src/Value/LineName.php index d16c60dbf..7b7690f88 100644 --- a/src/Value/LineName.php +++ b/src/Value/LineName.php @@ -58,7 +58,7 @@ public function render(OutputFormat $outputFormat): string } /** - * @return array>> + * @return array|null> * * @internal */ diff --git a/src/Value/Size.php b/src/Value/Size.php index 34dd47b13..ad6d794cc 100644 --- a/src/Value/Size.php +++ b/src/Value/Size.php @@ -207,7 +207,7 @@ public function render(OutputFormat $outputFormat): string } /** - * @return array>> + * @return array|null> * * @internal */ diff --git a/src/Value/Value.php b/src/Value/Value.php index 1ea2d2723..1ea78811f 100644 --- a/src/Value/Value.php +++ b/src/Value/Value.php @@ -182,7 +182,7 @@ public static function parsePrimitiveValue(ParserState $parserState) } /** - * @return array>> + * @return array|null> * * @internal */ diff --git a/src/Value/ValueList.php b/src/Value/ValueList.php index b39449cfa..9490a8bd1 100644 --- a/src/Value/ValueList.php +++ b/src/Value/ValueList.php @@ -98,7 +98,7 @@ public function render(OutputFormat $outputFormat): string } /** - * @return array>> + * @return array|null> * * @internal */ From 2b61cd568861afbe5fca15c361ecac0cb2a98d0b Mon Sep 17 00:00:00 2001 From: Oliver Klee Date: Mon, 5 Jan 2026 23:37:42 +0100 Subject: [PATCH 060/136] [TASK] Implement `URL::getArrayRepresentation()` (#1462) Part of #1440. --- src/Value/URL.php | 18 ++++++++++++++++++ tests/Unit/Value/URLTest.php | 23 +++++++++++++++++++---- 2 files changed, 37 insertions(+), 4 deletions(-) diff --git a/src/Value/URL.php b/src/Value/URL.php index f6e7b974a..55ee04f17 100644 --- a/src/Value/URL.php +++ b/src/Value/URL.php @@ -9,12 +9,15 @@ use Sabberworm\CSS\Parsing\SourceException; use Sabberworm\CSS\Parsing\UnexpectedEOFException; use Sabberworm\CSS\Parsing\UnexpectedTokenException; +use Sabberworm\CSS\ShortClassNameProvider; /** * This class represents URLs in CSS. `URL`s always output in `URL("")` notation. */ class URL extends PrimitiveValue { + use ShortClassNameProvider; + /** * @var CSSString */ @@ -80,4 +83,19 @@ public function render(OutputFormat $outputFormat): string { return "url({$this->url->render($outputFormat)})"; } + + /** + * @return array|null> + * + * @internal + */ + public function getArrayRepresentation(): array + { + return [ + 'class' => $this->getShortClassName(), + // We're using the term "uri" here to match the wording used in the specs: + // https://www.w3.org/TR/CSS22/syndata.html#uri + 'uri' => $this->url->getArrayRepresentation(), + ]; + } } diff --git a/tests/Unit/Value/URLTest.php b/tests/Unit/Value/URLTest.php index 8b4c08809..9cdda5dc2 100644 --- a/tests/Unit/Value/URLTest.php +++ b/tests/Unit/Value/URLTest.php @@ -84,12 +84,27 @@ public function getLineNumberReturnsLineNumberProvidedToConstructor(): void /** * @test */ - public function getArrayRepresentationThrowsException(): void + public function getArrayRepresentationIncludesClassName(): void { - $this->expectException(\BadMethodCallException::class); + $subject = new URL(new CSSString('https://example.com')); - $subject = new URL(new CSSString('http://example.com')); + $result = $subject->getArrayRepresentation(); + + self::assertArrayHasKey('class', $result); + self::assertSame('URL', $result['class']); + } + + /** + * @test + */ + public function getArrayRepresentationIncludesUri(): void + { + $uri = 'https://example.com'; + $subject = new URL(new CSSString($uri)); + + $result = $subject->getArrayRepresentation(); - $subject->getArrayRepresentation(); + self::assertArrayHasKey('uri', $result); + self::assertSame(['class' => 'CSSString', 'contents' => $uri], $result['uri']); } } From 6f07de239d510d81bf6131c39f164d9a65847e12 Mon Sep 17 00:00:00 2001 From: Oliver Klee Date: Sat, 17 Jan 2026 19:38:02 +0100 Subject: [PATCH 061/136] [CLEANUP] Narrow type of `OutputFormat::stringQuotingType` (#1465) --- src/OutputFormat.php | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/OutputFormat.php b/src/OutputFormat.php index 5c493865a..c9b46c5c9 100644 --- a/src/OutputFormat.php +++ b/src/OutputFormat.php @@ -7,9 +7,7 @@ final class OutputFormat { /** - * Value format: `"` means double-quote, `'` means single-quote - * - * @var non-empty-string + * @var '"'|"'" */ private $stringQuotingType = '"'; @@ -181,7 +179,7 @@ final class OutputFormat private $indentationLevel = 0; /** - * @return non-empty-string + * @return '"'|"'" * * @internal */ @@ -191,7 +189,7 @@ public function getStringQuotingType(): string } /** - * @param non-empty-string $quotingType + * @param '"'|"'" $quotingType * * @return $this fluent interface */ From b36a6ac7b20ea76f03ae0763ebda302f305b0b15 Mon Sep 17 00:00:00 2001 From: 8ctopus Date: Sun, 18 Jan 2026 16:58:24 +0400 Subject: [PATCH 062/136] [BUGFIX] Do not escape characters that do not need escaping in CSS string (#1444) --- CHANGELOG.md | 1 + src/Value/CSSString.php | 13 +++++++++- tests/ParserTest.php | 4 +-- tests/Unit/Value/CSSStringTest.php | 39 ++++++++++++++++++++++++++++++ tests/fixtures/unicode.css | 2 +- 5 files changed, 55 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d75a0e766..f14bb939d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,7 @@ Please also have a look at our ### Fixed +- Do not escape characters that do not need escaping in CSS string (#1444) - Reject selector comprising only whitespace (#1433) - Improve recovery parsing when a rogue `}` is encountered (#1425, #1426) - Parse comment(s) immediately preceding a selector (#1421, #1424) diff --git a/src/Value/CSSString.php b/src/Value/CSSString.php index 16bc90226..c13906ebc 100644 --- a/src/Value/CSSString.php +++ b/src/Value/CSSString.php @@ -93,7 +93,7 @@ public function getString(): string */ public function render(OutputFormat $outputFormat): string { - $string = \addslashes($this->string); + $string = $this->escape($this->string, $outputFormat->getStringQuotingType()); $string = \str_replace("\n", '\\A', $string); return $outputFormat->getStringQuotingType() . $string . $outputFormat->getStringQuotingType(); } @@ -111,4 +111,15 @@ public function getArrayRepresentation(): array 'contents' => $this->string, ]; } + + /** + * @param "'"|'"' $quote + */ + private function escape(string $string, string $quote): string + { + $charactersToEscape = '\\'; + $charactersToEscape .= ($quote === '"' ? '"' : "'"); + + return \addcslashes($string, $charactersToEscape); + } } diff --git a/tests/ParserTest.php b/tests/ParserTest.php index 97e0d09b5..d05dc8a88 100644 --- a/tests/ParserTest.php +++ b/tests/ParserTest.php @@ -206,10 +206,10 @@ public function unicodeParsing(): void self::assertSame('"\\"\\""', $firstContentRuleAsString); } if ($selector === '.test-9') { - self::assertSame('"\\"\\\'"', $firstContentRuleAsString); + self::assertSame('"\\"\'"', $firstContentRuleAsString); } if ($selector === '.test-10') { - self::assertSame('"\\\'\\\\"', $firstContentRuleAsString); + self::assertSame('"\'\\\\"', $firstContentRuleAsString); } if ($selector === '.test-11') { self::assertSame('"test"', $firstContentRuleAsString); diff --git a/tests/Unit/Value/CSSStringTest.php b/tests/Unit/Value/CSSStringTest.php index 2fd30b18a..309fcc15e 100644 --- a/tests/Unit/Value/CSSStringTest.php +++ b/tests/Unit/Value/CSSStringTest.php @@ -5,6 +5,7 @@ namespace Sabberworm\CSS\Tests\Unit\Value; use PHPUnit\Framework\TestCase; +use Sabberworm\CSS\OutputFormat; use Sabberworm\CSS\Value\CSSString; use Sabberworm\CSS\Value\PrimitiveValue; use Sabberworm\CSS\Value\Value; @@ -107,4 +108,42 @@ public function getArrayRepresentationIncludesContents(): void self::assertArrayHasKey('contents', $result); self::assertSame($contents, $result['contents']); } + + /** + * @test + */ + public function doesNotEscapeSingleQuotesThatDoNotNeedToBeEscaped(): void + { + $input = "data:image/svg+xml,%3Csvg width='24' height='24' viewBox='0 0 24 24' fill='none'" . + " xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd'" . + " d='M14.3145 2 3V11.9987H17.5687L18 8.20761H14.3145L14.32 6.31012C14.32 5.32134 14.4207" . + ' 4.79153 15.9426 4.79153H17.977V1H14.7223C10.8129 1 9.43687 2.83909 9.43687 ' . + " 5.93187V8.20804H7V11.9991H9.43687V23H14.3145Z' fill='black'/%3E%3C/svg%3E%0A"; + + $outputFormat = OutputFormat::createPretty(); + + self::assertSame("\"{$input}\"", (new CSSString($input))->render($outputFormat)); + + $outputFormat->setStringQuotingType("'"); + + $expected = str_replace("'", "\\'", $input); + + self::assertSame("'{$expected}'", (new CSSString($input))->render($outputFormat)); + } + + /** + * @test + */ + public function doesNotEscapeDoubleQuotesThatDoNotNeedToBeEscaped(): void + { + $input = '"Hello World"'; + + $outputFormat = OutputFormat::createPretty(); + + self::assertSame('"\\"Hello World\\""', (new CSSString($input))->render($outputFormat)); + + $outputFormat->setStringQuotingType("'"); + + self::assertSame("'{$input}'", (new CSSString($input))->render($outputFormat)); + } } diff --git a/tests/fixtures/unicode.css b/tests/fixtures/unicode.css index 24823200b..0c1bd57f2 100644 --- a/tests/fixtures/unicode.css +++ b/tests/fixtures/unicode.css @@ -5,7 +5,7 @@ .test-6 { content: "\00A5" } /* Same as "¥" */ .test-7 { content: '\a' } /* Same as "\A" (Newline) */ .test-8 { content: "\"\22" } /* Same as "\"\"" */ -.test-9 { content: "\"\27" } /* Same as ""\"\'"" */ +.test-9 { content: "\"\27" } /* Same as "\"'" */ .test-10 { content: "\'\\" } /* Same as "'\" */ .test-11 { content: "\test" } /* Same as "test" */ From ae4c5afc3ce1521fa0adf8c93a43d737ba0348d5 Mon Sep 17 00:00:00 2001 From: Oliver Klee Date: Mon, 19 Jan 2026 08:46:00 +0100 Subject: [PATCH 063/136] [CLEANUP] Refactor and enhance `CSSString::escape()` (#1467) Fixes #1466 --- src/Value/CSSString.php | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/Value/CSSString.php b/src/Value/CSSString.php index c13906ebc..8271671f2 100644 --- a/src/Value/CSSString.php +++ b/src/Value/CSSString.php @@ -93,9 +93,9 @@ public function getString(): string */ public function render(OutputFormat $outputFormat): string { - $string = $this->escape($this->string, $outputFormat->getStringQuotingType()); - $string = \str_replace("\n", '\\A', $string); - return $outputFormat->getStringQuotingType() . $string . $outputFormat->getStringQuotingType(); + return $outputFormat->getStringQuotingType() + . $this->escape($this->string, $outputFormat) + . $outputFormat->getStringQuotingType(); } /** @@ -112,14 +112,14 @@ public function getArrayRepresentation(): array ]; } - /** - * @param "'"|'"' $quote - */ - private function escape(string $string, string $quote): string + private function escape(string $string, OutputFormat $outputFormat): string { $charactersToEscape = '\\'; - $charactersToEscape .= ($quote === '"' ? '"' : "'"); + $charactersToEscape .= ($outputFormat->getStringQuotingType() === '"' ? '"' : "'"); + $withEscapedQuotes = \addcslashes($string, $charactersToEscape); + + $withNewlineEncoded = \str_replace("\n", '\\A', $withEscapedQuotes); - return \addcslashes($string, $charactersToEscape); + return $withNewlineEncoded; } } From bea90126e0b63cb075686a61b9f3e061f4cb625d Mon Sep 17 00:00:00 2001 From: Oliver Klee Date: Mon, 19 Jan 2026 08:52:03 +0100 Subject: [PATCH 064/136] [TASK] Implement `Charset::getArrayRepresentation()` (#1468) Part of #1440. --- src/Property/Charset.php | 7 ++++++- tests/Unit/Property/CharsetTest.php | 21 ++++++++++++++++++--- 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/src/Property/Charset.php b/src/Property/Charset.php index 50c84928d..107768081 100644 --- a/src/Property/Charset.php +++ b/src/Property/Charset.php @@ -8,6 +8,7 @@ use Sabberworm\CSS\OutputFormat; use Sabberworm\CSS\Position\Position; use Sabberworm\CSS\Position\Positionable; +use Sabberworm\CSS\ShortClassNameProvider; use Sabberworm\CSS\Value\CSSString; /** @@ -22,6 +23,7 @@ class Charset implements AtRule, Positionable { use CommentContainer; use Position; + use ShortClassNameProvider; /** * @var CSSString @@ -79,6 +81,9 @@ public function atRuleArgs(): CSSString */ public function getArrayRepresentation(): array { - throw new \BadMethodCallException('`getArrayRepresentation` is not yet implemented for `' . self::class . '`'); + return [ + 'class' => $this->getShortClassName(), + 'charset' => $this->charset->getArrayRepresentation(), + ]; } } diff --git a/tests/Unit/Property/CharsetTest.php b/tests/Unit/Property/CharsetTest.php index 6f8fa60d6..3fc385af0 100644 --- a/tests/Unit/Property/CharsetTest.php +++ b/tests/Unit/Property/CharsetTest.php @@ -35,10 +35,25 @@ public function implementsCSSListItem(): void /** * @test */ - public function getArrayRepresentationThrowsException(): void + public function getArrayRepresentationIncludesClassName(): void { - $this->expectException(\BadMethodCallException::class); + $result = $this->subject->getArrayRepresentation(); - $this->subject->getArrayRepresentation(); + self::assertArrayHasKey('class', $result); + self::assertSame('Charset', $result['class']); + } + + /** + * @test + */ + public function getArrayRepresentationIncludesCharset(): void + { + $charset = 'iso-8859-15'; + $subject = new Charset(new CSSString($charset)); + + $result = $subject->getArrayRepresentation(); + + self::assertArrayHasKey('charset', $result); + self::assertSame(['class' => 'CSSString', 'contents' => $charset], $result['charset']); } } From 1911017430956d45768fadb6d04cff85d2edd754 Mon Sep 17 00:00:00 2001 From: JakeQZ Date: Fri, 23 Jan 2026 14:14:04 +0000 Subject: [PATCH 065/136] [TASK] Reorder methods in `SelectorTest` (#1472) Move a data provider in preparation for #1470. --- tests/Unit/Property/SelectorTest.php | 40 ++++++++++++++-------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/tests/Unit/Property/SelectorTest.php b/tests/Unit/Property/SelectorTest.php index 3aca38515..63abb6c30 100644 --- a/tests/Unit/Property/SelectorTest.php +++ b/tests/Unit/Property/SelectorTest.php @@ -66,6 +66,26 @@ public static function provideSelectorsAndSpecificities(): array ]; } + /** + * @return array + */ + public static function provideInvalidSelectors(): array + { + return [ + // This is currently broken. + // 'empty string' => [''], + 'percent sign' => ['%'], + // This is currently broken. + // 'hash only' => ['#'], + // This is currently broken. + // 'dot only' => ['.'], + 'slash' => ['/'], + 'less-than sign' => ['<'], + // This is currently broken. + // 'whitespace only' => [" \t\n\r"], + ]; + } + /** * @test * @@ -112,26 +132,6 @@ public function isValidForValidSelectorReturnsTrue(string $selector): void self::assertTrue(Selector::isValid($selector)); } - /** - * @return array - */ - public static function provideInvalidSelectors(): array - { - return [ - // This is currently broken. - // 'empty string' => [''], - 'percent sign' => ['%'], - // This is currently broken. - // 'hash only' => ['#'], - // This is currently broken. - // 'dot only' => ['.'], - 'slash' => ['/'], - 'less-than sign' => ['<'], - // This is currently broken. - // 'whitespace only' => [" \t\n\r"], - ]; - } - /** * @test * From 60271c344b29d5c0a232481a84b789859cc1ca87 Mon Sep 17 00:00:00 2001 From: JakeQZ Date: Fri, 23 Jan 2026 14:15:20 +0000 Subject: [PATCH 066/136] [TASK] Test more valid selectors in `SelectorTest` (#1473) Also improve some of the data provider descriptions, and reorder the datasets more logically. --- tests/Unit/Property/SelectorTest.php | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/tests/Unit/Property/SelectorTest.php b/tests/Unit/Property/SelectorTest.php index 63abb6c30..33ea01bb6 100644 --- a/tests/Unit/Property/SelectorTest.php +++ b/tests/Unit/Property/SelectorTest.php @@ -53,16 +53,23 @@ public function setSelectorOverwritesSelectorProvidedToConstructor(): void public static function provideSelectorsAndSpecificities(): array { return [ - 'element' => ['a', 1], - 'element and descendant with pseudo-selector' => ['ol li::before', 3], + 'type' => ['a', 1], 'class' => ['.highlighted', 10], - 'element with class' => ['li.green', 11], - 'class with pseudo-selector' => ['.help:hover', 20], + 'type with class' => ['li.green', 11], + 'pseudo-class' => [':hover', 10], + 'type with pseudo-class' => ['a:hover', 11], + 'class with pseudo-class' => ['.help:hover', 20], 'ID' => ['#file', 100], - 'ID and descendant class' => ['#test .help', 110], + 'ID and descendent class' => ['#test .help', 110], + 'type with ID' => ['h2#my-mug', 101], + 'pseudo-element' => ['::before', 1], + 'type with pseudo-element' => ['li::before', 2], + 'type and descendent type with pseudo-element' => ['ol li::before', 3], '`not`' => [':not(#your-mug)', 100], // TODO, broken: The specificity should be the highest of the `:not` arguments, not the sum. '`not` with multiple arguments' => [':not(#your-mug, .their-mug)', 110], + 'attribute with `"`' => ['[alt="{}()[]\\"\',"]', 10], + 'attribute with `\'`' => ['[alt=\'{}()[]"\\\',\']', 10], ]; } From 7983af6823a81b27db5c962b418b8ab90d1c9cb5 Mon Sep 17 00:00:00 2001 From: JakeQZ Date: Fri, 23 Jan 2026 14:25:30 +0000 Subject: [PATCH 067/136] [CLEANUP] Tighten type annotations in `SelectorTest` (#1474) --- tests/Unit/Property/SelectorTest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/Unit/Property/SelectorTest.php b/tests/Unit/Property/SelectorTest.php index 33ea01bb6..2b67dce45 100644 --- a/tests/Unit/Property/SelectorTest.php +++ b/tests/Unit/Property/SelectorTest.php @@ -48,7 +48,7 @@ public function setSelectorOverwritesSelectorProvidedToConstructor(): void } /** - * @return array}> + * @return array}> */ public static function provideSelectorsAndSpecificities(): array { @@ -74,7 +74,7 @@ public static function provideSelectorsAndSpecificities(): array } /** - * @return array + * @return array */ public static function provideInvalidSelectors(): array { From 68670ab79795b66ab2168c2ef4c62bee7a6e02c0 Mon Sep 17 00:00:00 2001 From: Oliver Klee Date: Fri, 23 Jan 2026 19:13:23 +0100 Subject: [PATCH 068/136] [CLEANUP] Simplify tests that use `getArrayRepresenation()` (#1476) We don't need to check for `arrayKeyExists` as PHPUnit will warn anyway if the code tries to access an array element that does not exist. --- tests/Unit/Comment/CommentTest.php | 2 -- tests/Unit/Property/CharsetTest.php | 10 +++++++--- tests/Unit/Value/CSSFunctionTest.php | 2 -- tests/Unit/Value/CSSStringTest.php | 2 -- tests/Unit/Value/RuleValueListTest.php | 1 - tests/Unit/Value/SizeTest.php | 3 --- tests/Unit/Value/URLTest.php | 10 +++++++--- tests/Unit/Value/ValueListTest.php | 21 --------------------- 8 files changed, 14 insertions(+), 37 deletions(-) diff --git a/tests/Unit/Comment/CommentTest.php b/tests/Unit/Comment/CommentTest.php index cacab2360..9c2d1e9a1 100644 --- a/tests/Unit/Comment/CommentTest.php +++ b/tests/Unit/Comment/CommentTest.php @@ -87,7 +87,6 @@ public function getArrayRepresentationIncludesClassName(): void $result = $subject->getArrayRepresentation(); - self::assertArrayHasKey('class', $result); self::assertSame('Comment', $result['class']); } @@ -101,7 +100,6 @@ public function getArrayRepresentationIncludesContents(): void $result = $subject->getArrayRepresentation(); - self::assertArrayHasKey('contents', $result); self::assertSame($contents, $result['contents']); } } diff --git a/tests/Unit/Property/CharsetTest.php b/tests/Unit/Property/CharsetTest.php index 3fc385af0..bdbc1994a 100644 --- a/tests/Unit/Property/CharsetTest.php +++ b/tests/Unit/Property/CharsetTest.php @@ -39,7 +39,6 @@ public function getArrayRepresentationIncludesClassName(): void { $result = $this->subject->getArrayRepresentation(); - self::assertArrayHasKey('class', $result); self::assertSame('Charset', $result['class']); } @@ -53,7 +52,12 @@ public function getArrayRepresentationIncludesCharset(): void $result = $subject->getArrayRepresentation(); - self::assertArrayHasKey('charset', $result); - self::assertSame(['class' => 'CSSString', 'contents' => $charset], $result['charset']); + self::assertSame( + [ + 'class' => 'CSSString', + 'contents' => $charset, + ], + $result['charset'] + ); } } diff --git a/tests/Unit/Value/CSSFunctionTest.php b/tests/Unit/Value/CSSFunctionTest.php index c8dc549fc..4ec56afd2 100644 --- a/tests/Unit/Value/CSSFunctionTest.php +++ b/tests/Unit/Value/CSSFunctionTest.php @@ -23,7 +23,6 @@ public function getArrayRepresentationIncludesClassName(): void $result = $subject->getArrayRepresentation(); - self::assertArrayHasKey('class', $result); self::assertSame('CSSFunction', $result['class']); } @@ -36,7 +35,6 @@ public function getArrayRepresentationIncludesFunctionName(): void $result = $subject->getArrayRepresentation(); - self::assertArrayHasKey('name', $result); self::assertSame('filter', $result['name']); } } diff --git a/tests/Unit/Value/CSSStringTest.php b/tests/Unit/Value/CSSStringTest.php index 309fcc15e..6f6b5018f 100644 --- a/tests/Unit/Value/CSSStringTest.php +++ b/tests/Unit/Value/CSSStringTest.php @@ -91,7 +91,6 @@ public function getArrayRepresentationIncludesClassName(): void $result = $subject->getArrayRepresentation(); - self::assertArrayHasKey('class', $result); self::assertSame('CSSString', $result['class']); } @@ -105,7 +104,6 @@ public function getArrayRepresentationIncludesContents(): void $result = $subject->getArrayRepresentation(); - self::assertArrayHasKey('contents', $result); self::assertSame($contents, $result['contents']); } diff --git a/tests/Unit/Value/RuleValueListTest.php b/tests/Unit/Value/RuleValueListTest.php index 4c116003b..9284c4dbc 100644 --- a/tests/Unit/Value/RuleValueListTest.php +++ b/tests/Unit/Value/RuleValueListTest.php @@ -23,7 +23,6 @@ public function getArrayRepresentationIncludesClassName(): void $result = $subject->getArrayRepresentation(); - self::assertArrayHasKey('class', $result); self::assertSame('RuleValueList', $result['class']); } } diff --git a/tests/Unit/Value/SizeTest.php b/tests/Unit/Value/SizeTest.php index 189946f66..bebed016b 100644 --- a/tests/Unit/Value/SizeTest.php +++ b/tests/Unit/Value/SizeTest.php @@ -108,7 +108,6 @@ public function getArrayRepresentationIncludesClassName(): void $result = $subject->getArrayRepresentation(); - self::assertArrayHasKey('class', $result); self::assertSame('Size', $result['class']); } @@ -121,7 +120,6 @@ public function getArrayRepresentationIncludesNumber(): void $result = $subject->getArrayRepresentation(); - self::assertArrayHasKey('number', $result); self::assertSame(1.0, $result['number']); } @@ -134,7 +132,6 @@ public function getArrayRepresentationIncludesUnit(): void $result = $subject->getArrayRepresentation(); - self::assertArrayHasKey('unit', $result); self::assertSame('px', $result['unit']); } } diff --git a/tests/Unit/Value/URLTest.php b/tests/Unit/Value/URLTest.php index 9cdda5dc2..c8c632683 100644 --- a/tests/Unit/Value/URLTest.php +++ b/tests/Unit/Value/URLTest.php @@ -90,7 +90,6 @@ public function getArrayRepresentationIncludesClassName(): void $result = $subject->getArrayRepresentation(); - self::assertArrayHasKey('class', $result); self::assertSame('URL', $result['class']); } @@ -104,7 +103,12 @@ public function getArrayRepresentationIncludesUri(): void $result = $subject->getArrayRepresentation(); - self::assertArrayHasKey('uri', $result); - self::assertSame(['class' => 'CSSString', 'contents' => $uri], $result['uri']); + self::assertSame( + [ + 'class' => 'CSSString', + 'contents' => $uri, + ], + $result['uri'] + ); } } diff --git a/tests/Unit/Value/ValueListTest.php b/tests/Unit/Value/ValueListTest.php index 4db1b1a04..b711fabbe 100644 --- a/tests/Unit/Value/ValueListTest.php +++ b/tests/Unit/Value/ValueListTest.php @@ -22,7 +22,6 @@ public function getArrayRepresentationIncludesClassName(): void $result = $subject->getArrayRepresentation(); - self::assertArrayHasKey('class', $result); self::assertSame('ConcreteValueList', $result['class']); } @@ -35,10 +34,6 @@ public function getArrayRepresentationIncludesStringComponent(): void $result = $subject->getArrayRepresentation(); - self::assertArrayHasKey('components', $result); - self::assertIsArray($result['components']); - self::assertArrayHasKey(0, $result['components']); - self::assertArrayHasKey('value', $result['components'][0]); self::assertSame('Helvetica', $result['components'][0]['value']); } @@ -51,10 +46,6 @@ public function getArrayRepresentationIncludesValueComponent(): void $result = $subject->getArrayRepresentation(); - self::assertArrayHasKey('components', $result); - self::assertIsArray($result['components']); - self::assertArrayHasKey(0, $result['components']); - self::assertArrayHasKey('class', $result['components'][0]); self::assertSame('Size', $result['components'][0]['class']); } @@ -67,19 +58,8 @@ public function getArrayRepresentationIncludesMultipleMixedComponents(): void $result = $subject->getArrayRepresentation(); - self::assertArrayHasKey('components', $result); - self::assertIsArray($result['components']); - - self::assertArrayHasKey(0, $result['components']); - self::assertArrayHasKey('class', $result['components'][0]); self::assertSame('Size', $result['components'][0]['class']); - - self::assertArrayHasKey(1, $result['components']); - self::assertArrayHasKey('value', $result['components'][1]); self::assertSame('+', $result['components'][1]['value']); - - self::assertArrayHasKey(2, $result['components']); - self::assertArrayHasKey('class', $result['components'][2]); self::assertSame('Size', $result['components'][2]['class']); } @@ -92,7 +72,6 @@ public function getArrayRepresentationIncludesSeparator(): void $result = $subject->getArrayRepresentation(); - self::assertArrayHasKey('separator', $result); self::assertSame(',', $result['separator']); } } From ae80d04ec4ef948cc2b77306e59d244e3e85627d Mon Sep 17 00:00:00 2001 From: JakeQZ Date: Fri, 23 Jan 2026 19:19:49 +0000 Subject: [PATCH 069/136] [TASK] Add `Selector::parse()` (#1475) The implementation is based on `DeclarationBlock::parseSelector()`, which will be updated to use this instead in #1470. Part of #1325. --- src/Property/Selector.php | 105 ++++++++++++++++- tests/Unit/Property/SelectorTest.php | 162 +++++++++++++++++++++++++++ 2 files changed, 266 insertions(+), 1 deletion(-) diff --git a/src/Property/Selector.php b/src/Property/Selector.php index b9cf9bbc4..d74554535 100644 --- a/src/Property/Selector.php +++ b/src/Property/Selector.php @@ -4,7 +4,10 @@ namespace Sabberworm\CSS\Property; +use Sabberworm\CSS\Comment\Comment; use Sabberworm\CSS\OutputFormat; +use Sabberworm\CSS\Parsing\ParserState; +use Sabberworm\CSS\Parsing\UnexpectedTokenException; use Sabberworm\CSS\Property\Selector\SpecificityCalculator; use Sabberworm\CSS\Renderable; @@ -63,11 +66,111 @@ public static function isValid(string $selector): bool return $numberOfMatches === 1; } - public function __construct(string $selector) + final public function __construct(string $selector) { $this->setSelector($selector); } + /** + * @param list $comments + * + * @throws UnexpectedTokenException + * + * @internal + */ + public static function parse(ParserState $parserState, array &$comments = []): self + { + $selectorParts = []; + $stringWrapperCharacter = null; + $functionNestingLevel = 0; + static $stopCharacters = ['{', '}', '\'', '"', '(', ')', ',', ParserState::EOF, '']; + + while (true) { + $selectorParts[] = $parserState->consumeUntil($stopCharacters, false, false, $comments); + $nextCharacter = $parserState->peek(); + switch ($nextCharacter) { + case '': + // EOF + break 2; + case '\'': + // The fallthrough is intentional. + case '"': + if (!\is_string($stringWrapperCharacter)) { + $stringWrapperCharacter = $nextCharacter; + } elseif ($stringWrapperCharacter === $nextCharacter) { + if (\substr(\end($selectorParts), -1) !== '\\') { + $stringWrapperCharacter = null; + } + } + break; + case '(': + if (!\is_string($stringWrapperCharacter)) { + ++$functionNestingLevel; + } + break; + case ')': + if (!\is_string($stringWrapperCharacter)) { + if ($functionNestingLevel <= 0) { + throw new UnexpectedTokenException( + 'anything but', + ')', + 'literal', + $parserState->currentLine() + ); + } + --$functionNestingLevel; + } + break; + case ',': + if (!\is_string($stringWrapperCharacter) && $functionNestingLevel === 0) { + break 2; + } + break; + case '{': + // The fallthrough is intentional. + case '}': + if (!\is_string($stringWrapperCharacter)) { + break 2; + } + break; + default: + // This will never happen unless something gets broken in `ParserState`. + throw new \UnexpectedValueException( + 'Unexpected character \'' . $nextCharacter + . '\' returned from `ParserState::peek()` in `Selector::parse()`' + ); + } + $selectorParts[] = $parserState->consume(1); + } + + if ($functionNestingLevel !== 0) { + throw new UnexpectedTokenException(')', $nextCharacter, 'literal', $parserState->currentLine()); + } + if (\is_string($stringWrapperCharacter)) { + throw new UnexpectedTokenException( + $stringWrapperCharacter, + $nextCharacter, + 'literal', + $parserState->currentLine() + ); + } + + $selector = \trim(\implode('', $selectorParts)); + if ($selector === '') { + throw new UnexpectedTokenException('selector', $nextCharacter, 'literal', $parserState->currentLine()); + } + if (!self::isValid($selector)) { + throw new UnexpectedTokenException( + "Selector did not match '" . static::SELECTOR_VALIDATION_RX . "'.", + $selector, + 'custom', + $parserState->currentLine() + ); + } + + return new static($selector); + } + public function getSelector(): string { return $this->selector; diff --git a/tests/Unit/Property/SelectorTest.php b/tests/Unit/Property/SelectorTest.php index 2b67dce45..4427e59ee 100644 --- a/tests/Unit/Property/SelectorTest.php +++ b/tests/Unit/Property/SelectorTest.php @@ -5,8 +5,13 @@ namespace Sabberworm\CSS\Tests\Unit\Property; use PHPUnit\Framework\TestCase; +use Sabberworm\CSS\Comment\Comment; +use Sabberworm\CSS\Parsing\ParserState; +use Sabberworm\CSS\Parsing\UnexpectedTokenException; use Sabberworm\CSS\Property\Selector; use Sabberworm\CSS\Renderable; +use Sabberworm\CSS\Settings; +use TRegx\PhpUnit\DataProviders\DataProvider; /** * @covers \Sabberworm\CSS\Property\Selector @@ -73,6 +78,21 @@ public static function provideSelectorsAndSpecificities(): array ]; } + /** + * @test + * + * @param non-empty-string $selector + * + * @dataProvider provideSelectorsAndSpecificities + */ + public function parsesValidSelector(string $selector): void + { + $result = Selector::parse(new ParserState($selector, Settings::create())); + + self::assertInstanceOf(Selector::class, $result); + self::assertSame($result->getSelector(), $selector); + } + /** * @return array */ @@ -93,6 +113,148 @@ public static function provideInvalidSelectors(): array ]; } + /** + * @return array + */ + public static function provideInvalidSelectorsForParse(): array + { + return [ + 'empty string' => [''], + 'space' => [' '], + 'tab' => ["\t"], + 'line feed' => ["\n"], + 'carriage return' => ["\r"], + 'a `:not` missing the closing brace' => [':not(a'], + 'a `:not` missing the opening brace' => [':not a)'], + 'attribute value missing closing single quote' => ['a[href=\'#top]'], + 'attribute value missing closing double quote' => ['a[href="#top]'], + 'attribute value with mismatched quotes, single quote opening' => ['a[href=\'#top"]'], + 'attribute value with mismatched quotes, double quote opening' => ['a[href="#top\']'], + ]; + } + + /** + * @test + * + * @param non-empty-string $selector + * + * @dataProvider provideInvalidSelectors + * @dataProvider provideInvalidSelectorsForParse + */ + public function parseThrowsExceptionWithInvalidSelector(string $selector): void + { + $this->expectException(UnexpectedTokenException::class); + + Selector::parse(new ParserState($selector, Settings::create())); + } + + /** + * @return array + */ + public static function provideStopCharacters(): array + { + return [ + ',' => [','], + '{' => ['{'], + '}' => ['}'], + ]; + } + + /** + * @return DataProvider + */ + public function provideStopCharactersAndValidSelectors(): DataProvider + { + return DataProvider::cross(self::provideStopCharacters(), self::provideSelectorsAndSpecificities()); + } + + /** + * @test + * + * @param non-empty-string $stopCharacter + * @param non-empty-string $selector + * + * @dataProvider provideStopCharactersAndValidSelectors + */ + public function parseDoesNotConsumeStopCharacter(string $stopCharacter, string $selector): void + { + $subject = new ParserState($selector . $stopCharacter, Settings::create()); + + Selector::parse($subject); + + self::assertSame($stopCharacter, $subject->peek()); + } + + /** + * @return array + */ + public static function provideSelectorsWithAndWithoutComment(): array + { + return [ + 'comment before' => ['/*comment*/body', 'body'], + 'comment after' => ['body/*comment*/', 'body'], + 'comment within' => ['./*comment*/teapot', '.teapot'], + 'comment within function' => [':not(#your-mug,/*comment*/.their-mug)', ':not(#your-mug,.their-mug)'], + ]; + } + + /** + * @test + * + * @param non-empty-string $selectorWith + * @param non-empty-string $selectorWithout + * + * @dataProvider provideSelectorsWithAndWithoutComment + */ + public function parsesSelectorWithComment(string $selectorWith, string $selectorWithout): void + { + $result = Selector::parse(new ParserState($selectorWith, Settings::create())); + + self::assertInstanceOf(Selector::class, $result); + self::assertSame($selectorWithout, $result->getSelector()); + } + + /** + * @test + * + * @param non-empty-string $selector + * + * @dataProvider provideSelectorsWithAndWithoutComment + */ + public function parseExtractsCommentFromSelector(string $selector): void + { + $result = []; + Selector::parse(new ParserState($selector, Settings::create()), $result); + + self::assertInstanceOf(Comment::class, $result[0]); + self::assertSame('comment', $result[0]->getComment()); + } + + /** + * @test + */ + public function parsesSelectorWithTwoComments(): void + { + $result = Selector::parse(new ParserState('/*comment1*/a/*comment2*/', Settings::create())); + + self::assertInstanceOf(Selector::class, $result); + self::assertSame('a', $result->getSelector()); + } + + /** + * @test + */ + public function parseExtractsTwoCommentsFromSelector(): void + { + $result = []; + Selector::parse(new ParserState('/*comment1*/a/*comment2*/', Settings::create()), $result); + + self::assertInstanceOf(Comment::class, $result[0]); + self::assertSame('comment1', $result[0]->getComment()); + self::assertInstanceOf(Comment::class, $result[1]); + self::assertSame('comment2', $result[1]->getComment()); + } + /** * @test * From 21bb0eb9f9426357da8d60aaa9773f8514ab4a2f Mon Sep 17 00:00:00 2001 From: JakeQZ Date: Fri, 23 Jan 2026 19:57:54 +0000 Subject: [PATCH 070/136] [TASK] Use `Selector::parse()` (#1470) This replaces `DeclarationBlock::parseSelector()`. Part of #1325. --- src/RuleSet/DeclarationBlock.php | 88 +++----------------------------- 1 file changed, 7 insertions(+), 81 deletions(-) diff --git a/src/RuleSet/DeclarationBlock.php b/src/RuleSet/DeclarationBlock.php index ed226676a..323ad6417 100644 --- a/src/RuleSet/DeclarationBlock.php +++ b/src/RuleSet/DeclarationBlock.php @@ -67,7 +67,7 @@ public static function parse(ParserState $parserState, ?CSSList $list = null): ? $comments = []; $result = new DeclarationBlock($parserState->currentLine()); try { - $selectors = self::parseSelectors($parserState, $comments); + $selectors = self::parseSelectors($parserState, $list, $comments); $result->setSelectors($selectors, $list); if ($parserState->comes('{')) { $parserState->consume(1); @@ -100,11 +100,9 @@ public function setSelectors($selectors, ?CSSList $list = null): void $selectorsToSet = $selectors; } else { // A string of comma-separated selectors requires parsing. - // Parse as if it's the opening part of a rule. try { - $parserState = new ParserState($selectors . '{', Settings::create()); - $selectorsToSet = self::parseSelectors($parserState); - $parserState->consume('{'); // throw exception if this is not next + $parserState = new ParserState($selectors, Settings::create()); + $selectorsToSet = self::parseSelectors($parserState, $list); if (!$parserState->isEnd()) { throw new UnexpectedTokenException('EOF', 'more'); } @@ -286,16 +284,17 @@ public function getArrayRepresentation(): array /** * @param list $comments * - * @return list + * @return list * * @throws UnexpectedTokenException */ - private static function parseSelectors(ParserState $parserState, array &$comments = []): array + private static function parseSelectors(ParserState $parserState, ?CSSList $list, array &$comments = []): array { + $selectorClass = $list instanceof KeyFrame ? KeyFrameSelector::class : Selector::class; $selectors = []; while (true) { - $selectors[] = self::parseSelector($parserState, $comments); + $selectors[] = $selectorClass::parse($parserState, $comments); if (!$parserState->consumeIfComes(',')) { break; } @@ -303,77 +302,4 @@ private static function parseSelectors(ParserState $parserState, array &$comment return $selectors; } - - /** - * @param list $comments - * - * @throws UnexpectedTokenException - */ - private static function parseSelector(ParserState $parserState, array &$comments = []): string - { - $selectorParts = []; - $stringWrapperCharacter = null; - $functionNestingLevel = 0; - static $stopCharacters = ['{', '}', '\'', '"', '(', ')', ',']; - - while (true) { - $selectorParts[] = $parserState->consumeUntil($stopCharacters, false, false, $comments); - $nextCharacter = $parserState->peek(); - switch ($nextCharacter) { - case '\'': - // The fallthrough is intentional. - case '"': - if (!\is_string($stringWrapperCharacter)) { - $stringWrapperCharacter = $nextCharacter; - } elseif ($stringWrapperCharacter === $nextCharacter) { - if (\substr(\end($selectorParts), -1) !== '\\') { - $stringWrapperCharacter = null; - } - } - break; - case '(': - if (!\is_string($stringWrapperCharacter)) { - ++$functionNestingLevel; - } - break; - case ')': - if (!\is_string($stringWrapperCharacter)) { - if ($functionNestingLevel <= 0) { - throw new UnexpectedTokenException( - 'anything but', - ')', - 'literal', - $parserState->currentLine() - ); - } - --$functionNestingLevel; - } - break; - case '{': - // The fallthrough is intentional. - case '}': - if (!\is_string($stringWrapperCharacter)) { - break 2; - } - break; - case ',': - if (!\is_string($stringWrapperCharacter) && $functionNestingLevel === 0) { - break 2; - } - break; - } - $selectorParts[] = $parserState->consume(1); - } - - if ($functionNestingLevel !== 0) { - throw new UnexpectedTokenException(')', $nextCharacter, 'literal', $parserState->currentLine()); - } - - $selector = \trim(\implode('', $selectorParts)); - if ($selector === '') { - throw new UnexpectedTokenException('selector', $nextCharacter, 'literal', $parserState->currentLine()); - } - - return $selector; - } } From 0135077b2c397993b795ef77d42e737c1a2421af Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 24 Jan 2026 16:53:35 +0000 Subject: [PATCH 071/136] [Dependabot] Update phpunit/phpunit requirement from 8.5.50 to 8.5.51 (#1479) Updates the requirements on [phpunit/phpunit](https://github.com/sebastianbergmann/phpunit) to permit the latest version. - [Release notes](https://github.com/sebastianbergmann/phpunit/releases) - [Changelog](https://github.com/sebastianbergmann/phpunit/blob/8.5.51/ChangeLog-8.5.md) - [Commits](https://github.com/sebastianbergmann/phpunit/compare/8.5.50...8.5.51) --- updated-dependencies: - dependency-name: phpunit/phpunit dependency-version: 8.5.51 dependency-type: direct:development ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index caaf9a54a..49cc7519c 100644 --- a/composer.json +++ b/composer.json @@ -33,7 +33,7 @@ "phpstan/phpstan": "1.12.32 || 2.1.32", "phpstan/phpstan-phpunit": "1.4.2 || 2.0.8", "phpstan/phpstan-strict-rules": "1.6.2 || 2.0.7", - "phpunit/phpunit": "8.5.50", + "phpunit/phpunit": "8.5.51", "rawr/phpunit-data-provider": "3.3.1", "rector/rector": "1.2.10 || 2.2.8", "rector/type-perfect": "1.0.0 || 2.1.0", From 6455dd1868cb5e8e56f30b3e8727ed6d6bcd0c5e Mon Sep 17 00:00:00 2001 From: JakeQZ Date: Sat, 24 Jan 2026 19:36:27 +0000 Subject: [PATCH 072/136] [TASK] Have `comsumeWhiteSpace()` return the consumed (#1477) Instead, have the comments array passed by reference to be appended to. This avoids various methods having to do an `array_merge`. Also, knowing if any whitespace was consumed will be needed for #1471. --- src/CSSList/CSSList.php | 5 +- src/Parsing/ParserState.php | 14 ++- src/Rule/Rule.php | 5 +- src/RuleSet/RuleSet.php | 3 +- tests/Unit/Parsing/ParserStateTest.php | 146 +++++++++++++++++++++++++ 5 files changed, 163 insertions(+), 10 deletions(-) diff --git a/src/CSSList/CSSList.php b/src/CSSList/CSSList.php index 48e27c686..62f7fe654 100644 --- a/src/CSSList/CSSList.php +++ b/src/CSSList/CSSList.php @@ -68,7 +68,7 @@ public static function parseList(ParserState $parserState, CSSList $list): void $usesLenientParsing = $parserState->getSettings()->usesLenientParsing(); $comments = []; while (!$parserState->isEnd()) { - $comments = \array_merge($comments, $parserState->consumeWhiteSpace()); + $parserState->consumeWhiteSpace($comments); $listItem = null; if ($usesLenientParsing) { try { @@ -93,7 +93,8 @@ public static function parseList(ParserState $parserState, CSSList $list): void $listItem->addComments($comments); $list->append($listItem); } - $comments = $parserState->consumeWhiteSpace(); + $comments = []; + $parserState->consumeWhiteSpace($comments); } $list->addComments($comments); if (!$isRoot && !$usesLenientParsing) { diff --git a/src/Parsing/ParserState.php b/src/Parsing/ParserState.php index 44f96a944..3451fe0c4 100644 --- a/src/Parsing/ParserState.php +++ b/src/Parsing/ParserState.php @@ -188,7 +188,11 @@ public function parseCharacter(bool $isForIdentifier): ?string } /** - * @return list + * Consumes whitespace and/or comments until the next non-whitespace character that isn't a slash opening a comment. + * + * @param list $comments Any comments consumed will be appended to this array. + * + * @return string the whitespace consumed, without the comments * * @throws UnexpectedEOFException * @throws UnexpectedTokenException @@ -197,12 +201,12 @@ public function parseCharacter(bool $isForIdentifier): ?string * This method may change the state of the object by advancing the internal position; * it does not simply 'get' a value. */ - public function consumeWhiteSpace(): array + public function consumeWhiteSpace(array &$comments = []): string { - $comments = []; + $consumed = ''; do { while (preg_match('/\\s/isSu', $this->peek()) === 1) { - $this->consume(1); + $consumed .= $this->consume(1); } if ($this->parserSettings->usesLenientParsing()) { try { @@ -219,7 +223,7 @@ public function consumeWhiteSpace(): array } } while ($comment instanceof Comment); - return $comments; + return $consumed; } /** diff --git a/src/Rule/Rule.php b/src/Rule/Rule.php index 27b6bd9a7..26e68b231 100644 --- a/src/Rule/Rule.php +++ b/src/Rule/Rule.php @@ -65,14 +65,15 @@ public function __construct(string $rule, ?int $lineNumber = null, ?int $columnN */ public static function parse(ParserState $parserState, array $commentsBeforeRule = []): Rule { - $comments = \array_merge($commentsBeforeRule, $parserState->consumeWhiteSpace()); + $comments = $commentsBeforeRule; + $parserState->consumeWhiteSpace($comments); $rule = new Rule( $parserState->parseIdentifier(!$parserState->comes('--')), $parserState->currentLine(), $parserState->currentColumn() ); + $parserState->consumeWhiteSpace($comments); $rule->setComments($comments); - $rule->addComments($parserState->consumeWhiteSpace()); $parserState->consume(':'); $value = Value::parseValue($parserState, self::listDelimiterForRule($rule->getRule())); $rule->setValue($value); diff --git a/src/RuleSet/RuleSet.php b/src/RuleSet/RuleSet.php index 576ae23f7..e1086328c 100644 --- a/src/RuleSet/RuleSet.php +++ b/src/RuleSet/RuleSet.php @@ -59,7 +59,8 @@ public static function parseRuleSet(ParserState $parserState, RuleSet $ruleSet): $parserState->consume(';'); } while (true) { - $commentsBeforeRule = $parserState->consumeWhiteSpace(); + $commentsBeforeRule = []; + $parserState->consumeWhiteSpace($commentsBeforeRule); if ($parserState->comes('}')) { break; } diff --git a/tests/Unit/Parsing/ParserStateTest.php b/tests/Unit/Parsing/ParserStateTest.php index bd22508ab..ebc3c7fa7 100644 --- a/tests/Unit/Parsing/ParserStateTest.php +++ b/tests/Unit/Parsing/ParserStateTest.php @@ -8,6 +8,7 @@ use Sabberworm\CSS\Comment\Comment; use Sabberworm\CSS\Parsing\ParserState; use Sabberworm\CSS\Settings; +use TRegx\PhpUnit\DataProviders\DataProvider; /** * @covers \Sabberworm\CSS\Parsing\ParserState @@ -157,4 +158,149 @@ public function consumeIfComesUpdatesLineNumber(): void self::assertSame(2, $subject->currentLine()); } + + /** + * @return array + */ + public static function provideContentWhichMayHaveWhitespaceOrCommentsAndExpectedConsumption(): array + { + return [ + 'nothing' => ['', ''], + 'space' => [' ', ' '], + 'tab' => ["\t", "\t"], + 'line feed' => ["\n", "\n"], + 'carriage return' => ["\r", "\r"], + 'two spaces' => [' ', ' '], + 'comment' => ['/*hello*/', ''], + 'comment with space to the left' => [' /*hello*/', ' '], + 'comment with space to the right' => ['/*hello*/ ', ' '], + 'two comments' => ['/*hello*//*bye*/', ''], + 'two comments with space between' => ['/*hello*/ /*bye*/', ' '], + 'two comments with line feed between' => ["/*hello*/\n/*bye*/", "\n"], + ]; + } + + /** + * @test + * + * @dataProvider provideContentWhichMayHaveWhitespaceOrCommentsAndExpectedConsumption + */ + public function consumeWhiteSpaceReturnsTheConsumed( + string $whitespaceMaybeWithComments, + string $expectedConsumption + ): void { + $subject = new ParserState($whitespaceMaybeWithComments, Settings::create()); + + $result = $subject->consumeWhiteSpace(); + + self::assertSame($expectedConsumption, $result); + } + + /** + * @test + */ + public function consumeWhiteSpaceExtractsComment(): void + { + $commentText = 'Did they get you to trade your heroes for ghosts?'; + $subject = new ParserState('/*' . $commentText . '*/', Settings::create()); + + $result = []; + $subject->consumeWhiteSpace($result); + + self::assertInstanceOf(Comment::class, $result[0]); + self::assertSame($commentText, $result[0]->getComment()); + } + + /** + * @test + */ + public function consumeWhiteSpaceExtractsTwoComments(): void + { + $commentText1 = 'Hot ashes for trees? Hot air for a cool breeze?'; + $commentText2 = 'Cold comfort for change? Did you exchange'; + $subject = new ParserState('/*' . $commentText1 . '*//*' . $commentText2 . '*/', Settings::create()); + + $result = []; + $subject->consumeWhiteSpace($result); + + self::assertInstanceOf(Comment::class, $result[0]); + self::assertSame($commentText1, $result[0]->getComment()); + self::assertInstanceOf(Comment::class, $result[1]); + self::assertSame($commentText2, $result[1]->getComment()); + } + + /** + * @return array + */ + public static function provideWhitespace(): array + { + return [ + 'space' => [' '], + 'tab' => ["\t"], + 'line feed' => ["\n"], + 'carriage return' => ["\r"], + 'two spaces' => [' '], + ]; + } + + /** + * @test + * + * @param non-empty-string $whitespace + * + * @dataProvider provideWhitespace + */ + public function consumeWhiteSpaceExtractsCommentWithSurroundingWhitespace(string $whitespace): void + { + $commentText = 'A walk-on part in the war for a lead role in a cage?'; + $subject = new ParserState($whitespace . '/*' . $commentText . '*/' . $whitespace, Settings::create()); + + $result = []; + $subject->consumeWhiteSpace($result); + + self::assertInstanceOf(Comment::class, $result[0]); + self::assertSame($commentText, $result[0]->getComment()); + } + + /** + * @return array + */ + public static function provideNonWhitespace(): array + { + return [ + 'number' => ['7'], + 'uppercase letter' => ['B'], + 'lowercase letter' => ['c'], + 'symbol' => ['@'], + 'sequence of non-whitespace characters' => ['Hello!'], + 'sequence of characters including space which isn\'t first' => ['Oh no!'], + ]; + } + + /** + * @return DataProvider + */ + public function provideNonWhitespaceAndContentWithPossibleWhitespaceOrComments(): DataProvider + { + return DataProvider::cross( + self::provideNonWhitespace(), + self::provideContentWhichMayHaveWhitespaceOrCommentsAndExpectedConsumption() + ); + } + + /** + * @test + * + * @param non-empty-string $nonWhitespace + * + * @dataProvider provideNonWhitespaceAndContentWithPossibleWhitespaceOrComments + */ + public function consumeWhiteSpaceStopsAtNonWhitespace(string $nonWhitespace, string $whitespace): void + { + $subject = new ParserState($whitespace . $nonWhitespace, Settings::create()); + + $subject->consumeWhiteSpace(); + + self::assertTrue($subject->comes($nonWhitespace)); + } } From 416f6a7fe878302223a7b1ce8ccf1915f1934ec7 Mon Sep 17 00:00:00 2001 From: Oliver Klee Date: Sun, 25 Jan 2026 19:13:12 +0100 Subject: [PATCH 073/136] [TASK] Implement `Value::getArrayRepresentation()` (#1480) This implementation might get removed if all subclasses have their own implementation without needing the parent class implementation. Part of #1440. --- src/Value/Value.php | 8 +++++--- tests/Unit/Value/ValueTest.php | 8 ++++---- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/Value/Value.php b/src/Value/Value.php index 1ea78811f..67065d370 100644 --- a/src/Value/Value.php +++ b/src/Value/Value.php @@ -11,6 +11,7 @@ use Sabberworm\CSS\Parsing\UnexpectedTokenException; use Sabberworm\CSS\Position\Position; use Sabberworm\CSS\Position\Positionable; +use Sabberworm\CSS\ShortClassNameProvider; use function Safe\preg_match; @@ -21,6 +22,7 @@ abstract class Value implements CSSElement, Positionable { use Position; + use ShortClassNameProvider; /** * @param int<1, max>|null $lineNumber @@ -188,9 +190,9 @@ public static function parsePrimitiveValue(ParserState $parserState) */ public function getArrayRepresentation(): array { - throw new \BadMethodCallException( - '`getArrayRepresentation` is not yet implemented for `' . static::class . '`' - ); + return [ + 'class' => $this->getShortClassName(), + ]; } /** diff --git a/tests/Unit/Value/ValueTest.php b/tests/Unit/Value/ValueTest.php index b97e5f657..dfe46917e 100644 --- a/tests/Unit/Value/ValueTest.php +++ b/tests/Unit/Value/ValueTest.php @@ -179,12 +179,12 @@ public function parsesArithmeticWithMalformedOperandsInFunctions(string $leftOpe /** * @test */ - public function getArrayRepresentationThrowsException(): void + public function getArrayRepresentationIncludesClassName(): void { - $this->expectException(\BadMethodCallException::class); - $subject = new ConcreteValue(); - $subject->getArrayRepresentation(); + $result = $subject->getArrayRepresentation(); + + self::assertSame('ConcreteValue', $result['class']); } } From 96410045cfd5e94442b58297886dc6e890346f55 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 27 Jan 2026 10:12:00 +0100 Subject: [PATCH 074/136] [Dependabot] Update phpunit/phpunit requirement from 8.5.51 to 8.5.52 (#1483) Updates the requirements on [phpunit/phpunit](https://github.com/sebastianbergmann/phpunit) to permit the latest version. - [Release notes](https://github.com/sebastianbergmann/phpunit/releases) - [Changelog](https://github.com/sebastianbergmann/phpunit/blob/8.5.52/ChangeLog-8.5.md) - [Commits](https://github.com/sebastianbergmann/phpunit/compare/8.5.51...8.5.52) --- updated-dependencies: - dependency-name: phpunit/phpunit dependency-version: 8.5.52 dependency-type: direct:development ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 49cc7519c..e037dcb53 100644 --- a/composer.json +++ b/composer.json @@ -33,7 +33,7 @@ "phpstan/phpstan": "1.12.32 || 2.1.32", "phpstan/phpstan-phpunit": "1.4.2 || 2.0.8", "phpstan/phpstan-strict-rules": "1.6.2 || 2.0.7", - "phpunit/phpunit": "8.5.51", + "phpunit/phpunit": "8.5.52", "rawr/phpunit-data-provider": "3.3.1", "rector/rector": "1.2.10 || 2.2.8", "rector/type-perfect": "1.0.0 || 2.1.0", From f78fca507b88cd2049f039020c5cf4ef11d212b4 Mon Sep 17 00:00:00 2001 From: JakeQZ Date: Thu, 29 Jan 2026 10:43:31 +0000 Subject: [PATCH 075/136] [TASK] Add `SelectorComponent` interface (#1478) This will be implemented by `CompoundSelector` and `Combinator` to break up the components of a selector. Part of #1325. --- src/Property/Selector/SelectorComponent.php | 39 +++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 src/Property/Selector/SelectorComponent.php diff --git a/src/Property/Selector/SelectorComponent.php b/src/Property/Selector/SelectorComponent.php new file mode 100644 index 000000000..2e105cf11 --- /dev/null +++ b/src/Property/Selector/SelectorComponent.php @@ -0,0 +1,39 @@ + + */ + public function getSpecificity(): int; +} From ba1dd905f59d9d1fae10dab7f30f63281136c18f Mon Sep 17 00:00:00 2001 From: JakeQZ Date: Sat, 31 Jan 2026 09:25:46 +0000 Subject: [PATCH 076/136] [TASK] Rename `SelectorComponent` to `Component` (#1486) It's already in the `Selector` namespace, so having 'Selector' in the name is redundant. --- src/Property/Selector/{SelectorComponent.php => Component.php} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename src/Property/Selector/{SelectorComponent.php => Component.php} (97%) diff --git a/src/Property/Selector/SelectorComponent.php b/src/Property/Selector/Component.php similarity index 97% rename from src/Property/Selector/SelectorComponent.php rename to src/Property/Selector/Component.php index 2e105cf11..bb8d343dc 100644 --- a/src/Property/Selector/SelectorComponent.php +++ b/src/Property/Selector/Component.php @@ -20,7 +20,7 @@ * @see https://developer.mozilla.org/en-US/docs/Web/CSS/Guides/Selectors/Selector_structure * @see https://www.w3.org/TR/selectors-4/#structure */ -interface SelectorComponent +interface Component { /** * @return non-empty-string From 9fd06ecfa73faba625ee64a4d8a06d52fb91c6a7 Mon Sep 17 00:00:00 2001 From: samuil-banti-wpenigne Date: Tue, 3 Feb 2026 19:02:54 +0200 Subject: [PATCH 077/136] [BUGFIX] Support escaped quotes selector in selectors (#1485) Tailwind allows selectors that contain quotes like this one: .before\:content-\[\'\'\]:before --- CHANGELOG.md | 1 + src/Property/Selector.php | 11 ++-- tests/Unit/Property/SelectorTest.php | 84 ++++++++++++++++++++++++++++ 3 files changed, 92 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f14bb939d..0386882fc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ Please also have a look at our ### Added +- Add support for escaped quotes in the selectors (#1485) - Provide line number in exception message for mismatched parentheses in selector (#1435) - Add support for CSS container queries (#1400) diff --git a/src/Property/Selector.php b/src/Property/Selector.php index d74554535..6cce27000 100644 --- a/src/Property/Selector.php +++ b/src/Property/Selector.php @@ -95,10 +95,13 @@ public static function parse(ParserState $parserState, array &$comments = []): s case '\'': // The fallthrough is intentional. case '"': - if (!\is_string($stringWrapperCharacter)) { - $stringWrapperCharacter = $nextCharacter; - } elseif ($stringWrapperCharacter === $nextCharacter) { - if (\substr(\end($selectorParts), -1) !== '\\') { + $lastPart = \end($selectorParts); + $backslashCount = \strspn(\strrev($lastPart), '\\'); + $quoteIsEscaped = ($backslashCount % 2 === 1); + if (!$quoteIsEscaped) { + if (!\is_string($stringWrapperCharacter)) { + $stringWrapperCharacter = $nextCharacter; + } elseif ($stringWrapperCharacter === $nextCharacter) { $stringWrapperCharacter = null; } } diff --git a/tests/Unit/Property/SelectorTest.php b/tests/Unit/Property/SelectorTest.php index 4427e59ee..970fd1364 100644 --- a/tests/Unit/Property/SelectorTest.php +++ b/tests/Unit/Property/SelectorTest.php @@ -369,4 +369,88 @@ public function getArrayRepresentationThrowsException(): void $subject->getArrayRepresentation(); } + + /** + * @return array + */ + public static function provideSelectorsWithEscapedQuotes(): array + { + return [ + 'escaped double quote in double-quoted attribute' => ['a[href="test\\"value"]'], + 'escaped single quote in single-quoted attribute' => ['a[href=\'test\\\'value\']'], + 'multiple escaped double quotes in double-quoted attribute' => ['a[title="say \\"hello\\" world"]'], + 'multiple escaped single quotes in single-quoted attribute' => ['a[title=\'say \\\'hello\\\' world\']'], + 'escaped quote at start of attribute value' => ['a[data-test="\\"start"]'], + 'escaped quote at end of attribute value' => ['a[data-test="end\\""]'], + 'escaped backslash followed by quote' => ['a[data-test="test\\\\"]'], + 'escaped backslash before escaped quote' => ['a[data-test="test\\\\\\"value"]'], + 'triple backslash before quote' => ['a[data-test="test\\\\\\""]'], + ]; + } + + /** + * @test + * + * @param non-empty-string $selector + * + * @dataProvider provideSelectorsWithEscapedQuotes + */ + public function parsesSelectorsWithEscapedQuotes(string $selector): void + { + $result = Selector::parse(new ParserState($selector, Settings::create())); + + self::assertInstanceOf(Selector::class, $result); + self::assertSame($selector, $result->getSelector()); + } + + /** + * @test + * + * @param non-empty-string $selector + * + * @dataProvider provideSelectorsWithEscapedQuotes + */ + public function isValidForSelectorsWithEscapedQuotesReturnsTrue(string $selector): void + { + self::assertTrue(Selector::isValid($selector)); + } + + /** + * @test + */ + public function parsingAttributeWithEscapedQuoteDoesNotPrematurelyCloseString(): void + { + $selector = 'input[placeholder="Enter \\"quoted\\" text here"]'; + + $result = Selector::parse(new ParserState($selector, Settings::create())); + + self::assertInstanceOf(Selector::class, $result); + self::assertSame($selector, $result->getSelector()); + } + + /** + * @test + */ + public function parseDistinguishesEscapedFromUnescapedQuotes(): void + { + // One backslash = escaped quote (should not close string) + $selector = 'a[data-value="test\\"more"]'; + + $result = Selector::parse(new ParserState($selector, Settings::create())); + + self::assertSame($selector, $result->getSelector()); + } + + /** + * @test + */ + public function parseHandlesEvenNumberOfBackslashesBeforeQuote(): void + { + // Two backslashes = escaped backslash + unescaped quote (should close string) + $selector = 'a[data-value="test\\\\"]'; + + $result = Selector::parse(new ParserState($selector, Settings::create())); + + self::assertSame($selector, $result->getSelector()); + } } From 46147e24c2de031ebe7e8f10ee869c3f0d480aae Mon Sep 17 00:00:00 2001 From: JakeQZ Date: Wed, 4 Feb 2026 08:54:20 +0000 Subject: [PATCH 078/136] [TASK] Test escaped quotes in selectors outside of strings (#1489) The extra datasets cover the kind of selectors reported in the OP of #1485. --- CHANGELOG.md | 2 +- tests/Unit/Property/SelectorTest.php | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0386882fc..7e58bc83e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,7 @@ Please also have a look at our ### Added -- Add support for escaped quotes in the selectors (#1485) +- Add support for escaped quotes in the selectors (#1485, #1489) - Provide line number in exception message for mismatched parentheses in selector (#1435) - Add support for CSS container queries (#1400) diff --git a/tests/Unit/Property/SelectorTest.php b/tests/Unit/Property/SelectorTest.php index 970fd1364..59a53962b 100644 --- a/tests/Unit/Property/SelectorTest.php +++ b/tests/Unit/Property/SelectorTest.php @@ -385,6 +385,10 @@ public static function provideSelectorsWithEscapedQuotes(): array 'escaped backslash followed by quote' => ['a[data-test="test\\\\"]'], 'escaped backslash before escaped quote' => ['a[data-test="test\\\\\\"value"]'], 'triple backslash before quote' => ['a[data-test="test\\\\\\""]'], + 'escaped single quotes in selector itself, with other escaped characters' + => ['.before\\:content-\\[\\\'\\\'\\]:before'], + 'escaped double quotes in selector itself, with other escaped characters' + => ['.before\\:content-\\[\\"\\"\\]:before'], ]; } From 5f1cc1c7c05bba09e2039542f5b0dfbed79c0094 Mon Sep 17 00:00:00 2001 From: JakeQZ Date: Wed, 4 Feb 2026 08:54:59 +0000 Subject: [PATCH 079/136] [CLEANUP] Combine some test methods in `SelectorTest` (#1490) Multiple data providers can be specified for a test method which does exactly the same thing as another, to avoid having the 'another'. --- tests/Unit/Property/SelectorTest.php | 29 ++-------------------------- 1 file changed, 2 insertions(+), 27 deletions(-) diff --git a/tests/Unit/Property/SelectorTest.php b/tests/Unit/Property/SelectorTest.php index 59a53962b..79cfe9980 100644 --- a/tests/Unit/Property/SelectorTest.php +++ b/tests/Unit/Property/SelectorTest.php @@ -84,6 +84,7 @@ public static function provideSelectorsAndSpecificities(): array * @param non-empty-string $selector * * @dataProvider provideSelectorsAndSpecificities + * @dataProvider provideSelectorsWithEscapedQuotes */ public function parsesValidSelector(string $selector): void { @@ -295,6 +296,7 @@ public function getSpecificityReturnsSpecificityOfSelectorLastProvidedViaSetSele * @test * * @dataProvider provideSelectorsAndSpecificities + * @dataProvider provideSelectorsWithEscapedQuotes */ public function isValidForValidSelectorReturnsTrue(string $selector): void { @@ -392,33 +394,6 @@ public static function provideSelectorsWithEscapedQuotes(): array ]; } - /** - * @test - * - * @param non-empty-string $selector - * - * @dataProvider provideSelectorsWithEscapedQuotes - */ - public function parsesSelectorsWithEscapedQuotes(string $selector): void - { - $result = Selector::parse(new ParserState($selector, Settings::create())); - - self::assertInstanceOf(Selector::class, $result); - self::assertSame($selector, $result->getSelector()); - } - - /** - * @test - * - * @param non-empty-string $selector - * - * @dataProvider provideSelectorsWithEscapedQuotes - */ - public function isValidForSelectorsWithEscapedQuotesReturnsTrue(string $selector): void - { - self::assertTrue(Selector::isValid($selector)); - } - /** * @test */ From 0066316b54ee0b3f597f5bd466e0f0d56529fbec Mon Sep 17 00:00:00 2001 From: JakeQZ Date: Wed, 4 Feb 2026 08:55:27 +0000 Subject: [PATCH 080/136] [CLEANUP] Correct assert argument order in `SelectorTest` (#1491) The first parameter is `$expected`, the second `$actual`. --- tests/Unit/Property/SelectorTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Unit/Property/SelectorTest.php b/tests/Unit/Property/SelectorTest.php index 79cfe9980..13d4db628 100644 --- a/tests/Unit/Property/SelectorTest.php +++ b/tests/Unit/Property/SelectorTest.php @@ -91,7 +91,7 @@ public function parsesValidSelector(string $selector): void $result = Selector::parse(new ParserState($selector, Settings::create())); self::assertInstanceOf(Selector::class, $result); - self::assertSame($result->getSelector(), $selector); + self::assertSame($selector, $result->getSelector()); } /** From 104b94d599e68afb59963a609f1d1eb5688da848 Mon Sep 17 00:00:00 2001 From: JakeQZ Date: Wed, 4 Feb 2026 21:08:37 +0000 Subject: [PATCH 081/136] [TASK] Add `Selector\Combinator` class (#1487) --- src/Property/Selector/Combinator.php | 114 +++++++ .../Unit/Property/Selector/CombinatorTest.php | 300 ++++++++++++++++++ 2 files changed, 414 insertions(+) create mode 100644 src/Property/Selector/Combinator.php create mode 100644 tests/Unit/Property/Selector/CombinatorTest.php diff --git a/src/Property/Selector/Combinator.php b/src/Property/Selector/Combinator.php new file mode 100644 index 000000000..4c2f66940 --- /dev/null +++ b/src/Property/Selector/Combinator.php @@ -0,0 +1,114 @@ +`, `+`, or `~`). + * + * @phpstan-type ValidCombinatorValue ' '|'>'|'+'|'~' + */ +class Combinator implements Component, Renderable +{ + use ShortClassNameProvider; + + /** + * @var ValidCombinatorValue + */ + private $value; + + /** + * @param ValidCombinatorValue $value + */ + public function __construct(string $value) + { + $this->setValue($value); + } + + /** + * @param list $comments + * + * @throws UnexpectedTokenException + * + * @internal + */ + public static function parse(ParserState $parserState, array &$comments = []): self + { + $consumedWhitespace = $parserState->consumeWhiteSpace($comments); + + $nextToken = $parserState->peek(); + if (\in_array($nextToken, ['>', '+', '~'], true)) { + $value = $nextToken; + $parserState->consume(1); + $parserState->consumeWhiteSpace($comments); + } elseif ($consumedWhitespace !== '') { + $value = ' '; + } else { + throw new UnexpectedTokenException( + 'combinator', + $nextToken, + 'literal', + $parserState->currentLine() + ); + } + + return new self($value); + } + + /** + * @return ValidCombinatorValue + */ + public function getValue(): string + { + return $this->value; + } + + /** + * @param non-empty-string $value + * + * @throws \UnexpectedValueException if `$value` is not either space, '>', '+' or '~' + */ + public function setValue(string $value): void + { + if (!\in_array($value, [' ', '>', '+', '~'], true)) { + throw new \UnexpectedValueException('`' . $value . '` is not a valid selector combinator.'); + } + + $this->value = $value; + } + + /** + * @return int<0, max> + */ + public function getSpecificity(): int + { + return 0; + } + + public function render(OutputFormat $outputFormat): string + { + // TODO: allow optional spacing controlled via OutputFormat + return $this->getValue(); + } + + /** + * @return array|null> + * + * @internal + */ + public function getArrayRepresentation(): array + { + return [ + 'class' => $this->getShortClassName(), + 'value' => $this->value, + ]; + } +} diff --git a/tests/Unit/Property/Selector/CombinatorTest.php b/tests/Unit/Property/Selector/CombinatorTest.php new file mode 100644 index 000000000..07434581c --- /dev/null +++ b/tests/Unit/Property/Selector/CombinatorTest.php @@ -0,0 +1,300 @@ +')); + } + + /** + * @test + */ + public function implementsSelectorComponent(): void + { + self::assertInstanceOf(Component::class, new Combinator('>')); + } + + /** + * @return array + */ + public static function provideValidValue(): array + { + return [ + 'descendent' => [' '], + 'child' => ['>'], + 'next sibling' => ['+'], + 'subsequent sibling' => ['~'], + ]; + } + + /** + * @return array + */ + public static function provideInvalidValue(): array + { + return [ + 'other symbol' => ['@'], + 'uppercase letter' => ['Z'], + 'lowercase letter' => ['j'], + 'number' => ['1'], + 'sequence' => ['abc?123'], + ]; + } + + /** + * @test + * + * @param ValidCombinatorValue $combinator + * + * @dataProvider provideValidValue + */ + public function parsesValidCombinator(string $combinator): void + { + $result = Combinator::parse(new ParserState($combinator, Settings::create())); + + self::assertSame($combinator, $result->getArrayRepresentation()['value']); + } + + /** + * @test + * + * @param non-empty-string $selectorComponent + * + * @dataProvider provideInvalidValue + */ + public function parseThrowsExceptionWithInvalidCombinator(string $selectorComponent): void + { + $this->expectException(UnexpectedTokenException::class); + + Combinator::parse(new ParserState($selectorComponent, Settings::create())); + } + + /** + * @test + * + * @param ValidCombinatorValue $combinator + * + * @dataProvider provideValidValue + */ + public function parsesCombinatorWithCommentBefore(string $combinator): void + { + $result = Combinator::parse(new ParserState('/*comment*/' . $combinator, Settings::create())); + + self::assertSame($combinator, $result->getArrayRepresentation()['value']); + } + + /** + * @test + * + * @param ValidCombinatorValue $combinator + * + * @dataProvider provideValidValue + */ + public function parsesCombinatorWithCommentAfter(string $combinator): void + { + $result = Combinator::parse(new ParserState($combinator . '/*comment*/', Settings::create())); + + self::assertSame($combinator, $result->getArrayRepresentation()['value']); + } + + /** + * @test + * + * @param ValidCombinatorValue $combinator + * + * @dataProvider provideValidValue + */ + public function parseExtractsCommentBefore(string $combinator): void + { + $result = []; + Combinator::parse(new ParserState('/*comment*/' . $combinator, Settings::create()), $result); + + self::assertSame('comment', $result[0]->getArrayRepresentation()['contents']); + } + + /** + * @test + * + * @param ValidCombinatorValue $combinator + * + * @dataProvider provideValidValue + */ + public function parseExtractsCommentAfter(string $combinator): void + { + $result = []; + Combinator::parse(new ParserState($combinator . '/*comment*/', Settings::create()), $result); + + self::assertSame('comment', $result[0]->getArrayRepresentation()['contents']); + } + + /** + * @test + * + * @param ValidCombinatorValue $value + * + * @dataProvider provideValidValue + */ + public function constructsWithValueProvided(string $value): void + { + $subject = new Combinator($value); + + self::assertSame($value, $subject->getArrayRepresentation()['value']); + } + + /** + * @test + * + * @param non-empty-string $value + * + * @dataProvider provideInvalidValue + */ + public function constructorThrowsExceptionWithInvalidValue(string $value): void + { + $this->expectException(\UnexpectedValueException::class); + + new Combinator($value); + } + + /** + * @test + * + * @param ValidCombinatorValue $value + * + * @dataProvider provideValidValue + */ + public function setValueSetsValueProvided(string $value): void + { + $subject = new Combinator('>'); + + $subject->setValue($value); + + self::assertSame($value, $subject->getArrayRepresentation()['value']); + } + + /** + * @test + * + * @param non-empty-string $value + * + * @dataProvider provideInvalidValue + */ + public function setValueThrowsExceptionWithInvalidValue(string $value): void + { + $this->expectException(\UnexpectedValueException::class); + + $subject = new Combinator('>'); + + $subject->setValue($value); + } + + /** + * @test + * + * @param ValidCombinatorValue $value + * + * @dataProvider provideValidValue + */ + public function getValueReturnsValueProvidedToConstructor(string $value): void + { + $subject = new Combinator($value); + + $result = $subject->getValue(); + + self::assertSame($value, $result); + } + + /** + * @test + * + * @param ValidCombinatorValue $value + * + * @dataProvider provideValidValue + */ + public function getValueReturnsValueProvidedToSetValue(string $value): void + { + $subject = new Combinator('>'); + $subject->setValue($value); + + $result = $subject->getValue(); + + self::assertSame($value, $result); + } + + /** + * @test + * + * @param ValidCombinatorValue $value + * + * @dataProvider provideValidValue + */ + public function hasNoSpecificity(string $value): void + { + $subject = new Combinator($value); + + self::assertSame(0, $subject->getSpecificity()); + } + + /** + * @test + * + * @param ValidCombinatorValue $value + * + * @dataProvider provideValidValue + */ + public function renderReturnsValueProvided(string $value): void + { + $subject = new Combinator($value); + + self::assertSame($value, $subject->render(OutputFormat::create())); + } + + /** + * @test + */ + public function getArrayRepresentationIncludesClassName(): void + { + $subject = new Combinator('>'); + + $result = $subject->getArrayRepresentation(); + + self::assertSame('Combinator', $result['class']); + } + + /** + * @test + * + * @param ValidCombinatorValue $value + * + * @dataProvider provideValidValue + */ + public function getArrayRepresentationIncludesValue(string $value): void + { + $subject = new Combinator($value); + + $result = $subject->getArrayRepresentation(); + + self::assertSame($value, $result['value']); + } +} From 34df8f91d1d7d054b9e7148cfe09ed61c7a2a299 Mon Sep 17 00:00:00 2001 From: JakeQZ Date: Wed, 4 Feb 2026 21:20:03 +0000 Subject: [PATCH 082/136] [CLEANUP] Reorder methods in `SelectorTest` (#1492) Group the parsing test methods together, with the two data providers used coming before them. --- tests/Unit/Property/SelectorTest.php | 122 +++++++++++++-------------- 1 file changed, 61 insertions(+), 61 deletions(-) diff --git a/tests/Unit/Property/SelectorTest.php b/tests/Unit/Property/SelectorTest.php index 13d4db628..c18915c05 100644 --- a/tests/Unit/Property/SelectorTest.php +++ b/tests/Unit/Property/SelectorTest.php @@ -78,6 +78,28 @@ public static function provideSelectorsAndSpecificities(): array ]; } + /** + * @return array + */ + public static function provideSelectorsWithEscapedQuotes(): array + { + return [ + 'escaped double quote in double-quoted attribute' => ['a[href="test\\"value"]'], + 'escaped single quote in single-quoted attribute' => ['a[href=\'test\\\'value\']'], + 'multiple escaped double quotes in double-quoted attribute' => ['a[title="say \\"hello\\" world"]'], + 'multiple escaped single quotes in single-quoted attribute' => ['a[title=\'say \\\'hello\\\' world\']'], + 'escaped quote at start of attribute value' => ['a[data-test="\\"start"]'], + 'escaped quote at end of attribute value' => ['a[data-test="end\\""]'], + 'escaped backslash followed by quote' => ['a[data-test="test\\\\"]'], + 'escaped backslash before escaped quote' => ['a[data-test="test\\\\\\"value"]'], + 'triple backslash before quote' => ['a[data-test="test\\\\\\""]'], + 'escaped single quotes in selector itself, with other escaped characters' + => ['.before\\:content-\\[\\\'\\\'\\]:before'], + 'escaped double quotes in selector itself, with other escaped characters' + => ['.before\\:content-\\[\\"\\"\\]:before'], + ]; + } + /** * @test * @@ -94,6 +116,45 @@ public function parsesValidSelector(string $selector): void self::assertSame($selector, $result->getSelector()); } + /** + * @test + */ + public function parsingAttributeWithEscapedQuoteDoesNotPrematurelyCloseString(): void + { + $selector = 'input[placeholder="Enter \\"quoted\\" text here"]'; + + $result = Selector::parse(new ParserState($selector, Settings::create())); + + self::assertInstanceOf(Selector::class, $result); + self::assertSame($selector, $result->getSelector()); + } + + /** + * @test + */ + public function parseDistinguishesEscapedFromUnescapedQuotes(): void + { + // One backslash = escaped quote (should not close string) + $selector = 'a[data-value="test\\"more"]'; + + $result = Selector::parse(new ParserState($selector, Settings::create())); + + self::assertSame($selector, $result->getSelector()); + } + + /** + * @test + */ + public function parseHandlesEvenNumberOfBackslashesBeforeQuote(): void + { + // Two backslashes = escaped backslash + unescaped quote (should close string) + $selector = 'a[data-value="test\\\\"]'; + + $result = Selector::parse(new ParserState($selector, Settings::create())); + + self::assertSame($selector, $result->getSelector()); + } + /** * @return array */ @@ -371,65 +432,4 @@ public function getArrayRepresentationThrowsException(): void $subject->getArrayRepresentation(); } - - /** - * @return array - */ - public static function provideSelectorsWithEscapedQuotes(): array - { - return [ - 'escaped double quote in double-quoted attribute' => ['a[href="test\\"value"]'], - 'escaped single quote in single-quoted attribute' => ['a[href=\'test\\\'value\']'], - 'multiple escaped double quotes in double-quoted attribute' => ['a[title="say \\"hello\\" world"]'], - 'multiple escaped single quotes in single-quoted attribute' => ['a[title=\'say \\\'hello\\\' world\']'], - 'escaped quote at start of attribute value' => ['a[data-test="\\"start"]'], - 'escaped quote at end of attribute value' => ['a[data-test="end\\""]'], - 'escaped backslash followed by quote' => ['a[data-test="test\\\\"]'], - 'escaped backslash before escaped quote' => ['a[data-test="test\\\\\\"value"]'], - 'triple backslash before quote' => ['a[data-test="test\\\\\\""]'], - 'escaped single quotes in selector itself, with other escaped characters' - => ['.before\\:content-\\[\\\'\\\'\\]:before'], - 'escaped double quotes in selector itself, with other escaped characters' - => ['.before\\:content-\\[\\"\\"\\]:before'], - ]; - } - - /** - * @test - */ - public function parsingAttributeWithEscapedQuoteDoesNotPrematurelyCloseString(): void - { - $selector = 'input[placeholder="Enter \\"quoted\\" text here"]'; - - $result = Selector::parse(new ParserState($selector, Settings::create())); - - self::assertInstanceOf(Selector::class, $result); - self::assertSame($selector, $result->getSelector()); - } - - /** - * @test - */ - public function parseDistinguishesEscapedFromUnescapedQuotes(): void - { - // One backslash = escaped quote (should not close string) - $selector = 'a[data-value="test\\"more"]'; - - $result = Selector::parse(new ParserState($selector, Settings::create())); - - self::assertSame($selector, $result->getSelector()); - } - - /** - * @test - */ - public function parseHandlesEvenNumberOfBackslashesBeforeQuote(): void - { - // Two backslashes = escaped backslash + unescaped quote (should close string) - $selector = 'a[data-value="test\\\\"]'; - - $result = Selector::parse(new ParserState($selector, Settings::create())); - - self::assertSame($selector, $result->getSelector()); - } } From 2f9902851d31214c63ff32413e451709702f8e26 Mon Sep 17 00:00:00 2001 From: JakeQZ Date: Wed, 4 Feb 2026 23:22:12 +0000 Subject: [PATCH 083/136] [TASK] Add `CompoundSelector` class (#1488) Part of #1325. --- src/Property/Selector/CompoundSelector.php | 256 +++++++++ .../Selector/CompoundSelectorTest.php | 494 ++++++++++++++++++ 2 files changed, 750 insertions(+) create mode 100644 src/Property/Selector/CompoundSelector.php create mode 100644 tests/Unit/Property/Selector/CompoundSelectorTest.php diff --git a/src/Property/Selector/CompoundSelector.php b/src/Property/Selector/CompoundSelector.php new file mode 100644 index 000000000..1073a6cb2 --- /dev/null +++ b/src/Property/Selector/CompoundSelector.php @@ -0,0 +1,256 @@ +`, `+`, `~`) before being passed to this class. + */ +class CompoundSelector implements Renderable, Component +{ + use ShortClassNameProvider; + + private const PARSER_STOP_CHARACTERS = [ + '{', + '}', + '\'', + '"', + '(', + ')', + ',', + ' ', + "\t", + "\n", + "\r", + '>', + '+', + '~', + ParserState::EOF, + '', // `ParserState::peek()` returns empty string rather than `ParserState::EOF` when end of string is reached + ]; + + private const SELECTOR_VALIDATION_RX = '/ + ^ + # not starting with whitespace + (?!\\s) + (?: + (?: + # any sequence of valid unescaped characters, except quotes + [a-zA-Z0-9\\x{00A0}-\\x{FFFF}_^$|*=\\[\\]()\\-\\.:#,\\s]++ + | + # one or more escaped characters + (?:\\\\.)++ + | + # quoted text, like in `[id="example"]` + (?: + # opening quote + ([\'"]) + (?: + # sequence of characters except closing quote or backslash + (?:(?!\\g{-1}|\\\\).)++ + | + # one or more escaped characters + (?:\\\\.)++ + )*+ # zero or more times + # closing quote or end (unmatched quote is currently allowed) + (?:\\g{-1}|$) + ) + )++ # one or more times + | + # keyframe animation progress percentage (e.g. 50%) + (?:\\d++%) + ) + # not ending with whitespace + (?setValue($value); + } + + /** + * @param list $comments + * + * @throws UnexpectedTokenException + * + * @internal + */ + public static function parse(ParserState $parserState, array &$comments = []): self + { + $selectorParts = []; + $stringWrapperCharacter = null; + $functionNestingLevel = 0; + + while (true) { + $selectorParts[] = $parserState->consumeUntil(self::PARSER_STOP_CHARACTERS, false, false, $comments); + $nextCharacter = $parserState->peek(); + switch ($nextCharacter) { + case '': + // EOF + break 2; + case '\'': + // The fallthrough is intentional. + case '"': + $lastPart = \end($selectorParts); + $backslashCount = \strspn(\strrev($lastPart), '\\'); + $quoteIsEscaped = ($backslashCount % 2 === 1); + if (!$quoteIsEscaped) { + if (!\is_string($stringWrapperCharacter)) { + $stringWrapperCharacter = $nextCharacter; + } elseif ($stringWrapperCharacter === $nextCharacter) { + $stringWrapperCharacter = null; + } + } + break; + case '(': + if (!\is_string($stringWrapperCharacter)) { + ++$functionNestingLevel; + } + break; + case ')': + if (!\is_string($stringWrapperCharacter)) { + if ($functionNestingLevel <= 0) { + throw new UnexpectedTokenException( + 'anything but', + ')', + 'literal', + $parserState->currentLine() + ); + } + --$functionNestingLevel; + } + break; + case '{': + // The fallthrough is intentional. + case '}': + if (!\is_string($stringWrapperCharacter)) { + break 2; + } + break; + case ',': + // The fallthrough is intentional. + case ' ': + // The fallthrough is intentional. + case "\t": + // The fallthrough is intentional. + case "\n": + // The fallthrough is intentional. + case "\r": + // The fallthrough is intentional. + case '>': + // The fallthrough is intentional. + case '+': + // The fallthrough is intentional. + case '~': + if (!\is_string($stringWrapperCharacter) && $functionNestingLevel === 0) { + break 2; + } + break; + } + $selectorParts[] = $parserState->consume(1); + } + + if ($functionNestingLevel !== 0) { + throw new UnexpectedTokenException(')', $nextCharacter, 'literal', $parserState->currentLine()); + } + if (\is_string($stringWrapperCharacter)) { + throw new UnexpectedTokenException( + $stringWrapperCharacter, + $nextCharacter, + 'literal', + $parserState->currentLine() + ); + } + + $value = \implode('', $selectorParts); + if ($value === '') { + throw new UnexpectedTokenException('selector', $nextCharacter, 'literal', $parserState->currentLine()); + } + if (!self::isValid($value)) { + throw new UnexpectedTokenException( + 'Selector component is not valid:', + '`' . $value . '`', + 'custom', + $parserState->currentLine() + ); + } + + return new self($value); + } + + /** + * @return non-empty-string + */ + public function getValue(): string + { + return $this->value; + } + + /** + * @param non-empty-string $value + * + * @throws \UnexpectedValueException if `$value` contains invalid characters or has surrounding whitespce + */ + public function setValue(string $value): void + { + if (!self::isValid($value)) { + throw new \UnexpectedValueException('`' . $value . '` is not a valid compound selector.'); + } + + $this->value = $value; + } + + /** + * @return int<0, max> + */ + public function getSpecificity(): int + { + return SpecificityCalculator::calculate($this->value); + } + + public function render(OutputFormat $outputFormat): string + { + return $this->getValue(); + } + + /** + * @return array|null> + * + * @internal + */ + public function getArrayRepresentation(): array + { + return [ + 'class' => $this->getShortClassName(), + 'value' => $this->value, + ]; + } + + private static function isValid(string $value): bool + { + $numberOfMatches = preg_match(self::SELECTOR_VALIDATION_RX, $value); + + return $numberOfMatches === 1; + } +} diff --git a/tests/Unit/Property/Selector/CompoundSelectorTest.php b/tests/Unit/Property/Selector/CompoundSelectorTest.php new file mode 100644 index 000000000..1e3f6bd6a --- /dev/null +++ b/tests/Unit/Property/Selector/CompoundSelectorTest.php @@ -0,0 +1,494 @@ +}> + */ + public static function provideCompoundSelectorAndSpecificity(): array + { + return [ + 'type' => ['a', 1], + 'class' => ['.highlighted', 10], + 'type with class' => ['li.green', 11], + 'pseudo-class' => [':hover', 10], + 'type with pseudo-class' => ['a:hover', 11], + 'class with pseudo-class' => ['.help:hover', 20], + 'ID' => ['#file', 100], + 'type with ID' => ['h2#my-mug', 101], + 'pseudo-element' => ['::before', 1], + 'type with pseudo-element' => ['li::before', 2], + '`not`' => [':not(#your-mug)', 100], + // TODO, broken: The specificity should be the highest of the `:not` arguments, not the sum. + '`not` with multiple arguments' => [':not(#your-mug, .their-mug)', 110], + 'attribute with `"`' => ['[alt="{}()[]\\"\',"]', 10], + 'attribute with `\'`' => ['[alt=\'{}()[]"\\\',\']', 10], + ]; + } + + /** + * @return array + */ + public static function provideCompoundSelectorWithEscapedQuotes(): array + { + return [ + 'escaped double quote in double-quoted attribute' => ['a[href="test\\"value"]'], + 'escaped single quote in single-quoted attribute' => ["a[href='test\\'value']"], + 'multiple escaped double quotes in double-quoted attribute' => ['a[title="say \\"hello\\" world"]'], + 'multiple escaped single quotes in single-quoted attribute' => ["a[title='say \\'hello\\' world']"], + 'escaped quote at start of attribute value' => ['a[data-test="\\"start"]'], + 'escaped quote at end of attribute value' => ['a[data-test="end\\""]'], + 'escaped backslash followed by quote' => ['a[data-test="test\\\\"]'], + 'escaped backslash before escaped quote' => ['a[data-test="test\\\\\\"value"]'], + 'triple backslash before quote' => ['a[data-test="test\\\\\\""]'], + 'escaped single quotes in selector itself, with other escaped characters' + => [".before\\:content-\\[\\'\\'\\]:before"], + 'escaped double quotes in selector itself, with other escaped characters' + => ['.before\\:content-\\[\\"\\"\\]:before'], + ]; + } + + /** + * @test + * + * @param non-empty-string $compoundSelector + * + * @dataProvider provideCompoundSelectorAndSpecificity + * @dataProvider provideCompoundSelectorWithEscapedQuotes + */ + public function parsesValidCompoundSelector(string $compoundSelector): void + { + $result = CompoundSelector::parse(new ParserState($compoundSelector, Settings::create())); + + self::assertInstanceOf(CompoundSelector::class, $result); + self::assertSame($compoundSelector, $result->getValue()); + } + + /** + * @test + */ + public function parsingAttributeWithEscapedQuoteDoesNotPrematurelyCloseString(): void + { + $compoundSelector = 'input[placeholder="Enter \\"quoted\\" text here"]'; + + $result = CompoundSelector::parse(new ParserState($compoundSelector, Settings::create())); + + self::assertInstanceOf(CompoundSelector::class, $result); + self::assertSame($compoundSelector, $result->getValue()); + } + + /** + * @test + */ + public function parseDistinguishesEscapedFromUnescapedQuotes(): void + { + // One backslash = escaped quote (should not close string) + $compoundSelector = 'a[data-value="test\\"more"]'; + + $result = CompoundSelector::parse(new ParserState($compoundSelector, Settings::create())); + + self::assertSame($compoundSelector, $result->getValue()); + } + + /** + * @test + */ + public function parseHandlesEvenNumberOfBackslashesBeforeQuote(): void + { + // Two backslashes = escaped backslash + unescaped quote (should close string) + $compoundSelector = 'a[data-value="test\\\\"]'; + + $result = CompoundSelector::parse(new ParserState($compoundSelector, Settings::create())); + + self::assertSame($compoundSelector, $result->getValue()); + } + + /** + * @return array + */ + public static function provideInvalidCompoundSelector(): array + { + return [ + 'empty string' => [''], + 'space' => [' '], + 'tab' => ["\t"], + 'line feed' => ["\n"], + 'carriage return' => ["\r"], + 'percent sign' => ['%'], + // This is currently broken. + // 'hash only' => ['#'], + // This is currently broken. + // 'dot only' => ['.'], + 'slash' => ['/'], + 'less-than sign' => ['<'], + 'with space before' => [' a:hover'], + ]; + } + + /** + * The validation checks on `setValue()` are not able to pick up these, because it does not perform any parsing. + * + * @return array + */ + public static function provideInvalidCompoundSelectorForParse(): array + { + return [ + 'a `:not` missing the closing brace' => [':not(a'], + 'a `:not` missing the opening brace' => [':nota)'], + 'attribute value missing closing single quote' => ["a[href='#top]"], + 'attribute value missing closing double quote' => ['a[href="#top]'], + 'attribute value with mismatched quotes, single quote opening' => ['a[href=\'#top"]'], + 'attribute value with mismatched quotes, double quote opening' => ['a[href="#top\']'], + ]; + } + + /** + * This is valid in a parsing context, but not when passed to `setValue()` or the constructor. + * + * @return array + */ + public static function provideInvalidCompoundSelectorForSetValue(): array + { + return [ + 'with space after' => ['a:hover '], + ]; + } + + /** + * @test + * + * @param non-empty-string $compoundSelector + * + * @dataProvider provideInvalidCompoundSelector + * @dataProvider provideInvalidCompoundSelectorForParse + */ + public function parseThrowsExceptionWithInvalidCompoundSelector(string $compoundSelector): void + { + $this->expectException(UnexpectedTokenException::class); + + CompoundSelector::parse(new ParserState($compoundSelector, Settings::create())); + } + + /** + * @return array + */ + public static function provideStopCharacters(): array + { + return [ + ',' => [','], + '{' => ['{'], + '}' => ['}'], + 'space' => [' '], + 'tab' => ["\t"], + 'line feed' => ["\n"], + 'carriage return' => ["\r"], + 'child combinator' => ['>'], + 'next-sibling combinator' => ['+'], + 'subsequent-sibling combinator' => ['~'], + ]; + } + + /** + * @return DataProvider + */ + public function provideStopCharactersAndValidCompoundSelector(): DataProvider + { + return DataProvider::cross(self::provideStopCharacters(), self::provideCompoundSelectorAndSpecificity()); + } + + /** + * @test + * + * @param non-empty-string $stopCharacter + * @param non-empty-string $compoundSelector + * + * @dataProvider provideStopCharactersAndValidCompoundSelector + */ + public function parseDoesNotConsumeStopCharacter(string $stopCharacter, string $compoundSelector): void + { + $subject = new ParserState($compoundSelector . $stopCharacter, Settings::create()); + + CompoundSelector::parse($subject); + + self::assertSame($stopCharacter, $subject->peek()); + } + + /** + * @return array + */ + public static function provideCompoundSelectorWithAndWithoutComment(): array + { + return [ + 'comment before' => ['/*comment*/body.page', 'body.page'], + 'comment after' => ['body.page/*comment*/', 'body.page'], + 'comment within' => ['p./*comment*/teapot', 'p.teapot'], + 'comment within function' => ['p:not(#your-mug,/*comment*/.their-mug)', 'p:not(#your-mug,.their-mug)'], + ]; + } + + /** + * @test + * + * @param non-empty-string $compoundSelectorWith + * @param non-empty-string $compoundSelectorWithout + * + * @dataProvider provideCompoundSelectorWithAndWithoutComment + */ + public function parsesCompoundSelectorWithComment( + string $compoundSelectorWith, + string $compoundSelectorWithout + ): void { + $result = CompoundSelector::parse(new ParserState($compoundSelectorWith, Settings::create())); + + self::assertInstanceOf(CompoundSelector::class, $result); + self::assertSame($compoundSelectorWithout, $result->getValue()); + } + + /** + * @test + * + * @param non-empty-string $compoundSelector + * + * @dataProvider provideCompoundSelectorWithAndWithoutComment + */ + public function parseExtractsCommentFromCompoundSelector(string $compoundSelector): void + { + $result = []; + CompoundSelector::parse(new ParserState($compoundSelector, Settings::create()), $result); + + self::assertInstanceOf(Comment::class, $result[0]); + self::assertSame('comment', $result[0]->getComment()); + } + + /** + * @test + */ + public function parsesCompoundSelectorWithTwoComments(): void + { + $result = CompoundSelector::parse(new ParserState('/*comment1*/a:hover/*comment2*/', Settings::create())); + + self::assertInstanceOf(CompoundSelector::class, $result); + self::assertSame('a:hover', $result->getValue()); + } + + /** + * @test + */ + public function parseExtractsTwoCommentsFromCompoundSelector(): void + { + $result = []; + CompoundSelector::parse(new ParserState('/*comment1*/a:hover/*comment2*/', Settings::create()), $result); + + self::assertInstanceOf(Comment::class, $result[0]); + self::assertSame('comment1', $result[0]->getComment()); + self::assertInstanceOf(Comment::class, $result[1]); + self::assertSame('comment2', $result[1]->getComment()); + } + + /** + * @test + * + * @param non-empty-string $value + * + * @dataProvider provideCompoundSelectorAndSpecificity + */ + public function constructsWithValueProvided(string $value): void + { + $subject = new CompoundSelector($value); + + self::assertSame($value, $subject->getArrayRepresentation()['value']); + } + + /** + * @test + * + * @param non-empty-string $value + * + * @dataProvider provideInvalidCompoundSelector + * @dataProvider provideInvalidCompoundSelectorForSetValue + */ + public function constructorThrowsExceptionWithInvalidValue(string $value): void + { + $this->expectException(\UnexpectedValueException::class); + + new CompoundSelector($value); + } + + /** + * @test + * + * @param non-empty-string $value + * + * @dataProvider provideCompoundSelectorAndSpecificity + */ + public function setValueSetsValueProvided(string $value): void + { + $subject = new CompoundSelector('p.intro'); + + $subject->setValue($value); + + self::assertSame($value, $subject->getArrayRepresentation()['value']); + } + + /** + * @test + * + * @param non-empty-string $value + * + * @dataProvider provideInvalidCompoundSelector + * @dataProvider provideInvalidCompoundSelectorForSetValue + */ + public function setValueThrowsExceptionWithInvalidValue(string $value): void + { + $this->expectException(\UnexpectedValueException::class); + + $subject = new CompoundSelector('p.intro'); + + $subject->setValue($value); + } + + /** + * @test + * + * @param non-empty-string $value + * + * @dataProvider provideCompoundSelectorAndSpecificity + */ + public function getValueReturnsValueProvidedToConstructor(string $value): void + { + $subject = new CompoundSelector($value); + + $result = $subject->getValue(); + + self::assertSame($value, $result); + } + + /** + * @test + * + * @param non-empty-string $value + * + * @dataProvider provideCompoundSelectorAndSpecificity + */ + public function getValueReturnsValueProvidedToSetValue(string $value): void + { + $subject = new CompoundSelector('p.intro'); + $subject->setValue($value); + + $result = $subject->getValue(); + + self::assertSame($value, $result); + } + + /** + * @test + * + * @param non-empty-string $compoundSelector + * @param int<0, max> $expectedSpecificity + * + * @dataProvider provideCompoundSelectorAndSpecificity + */ + public function getSpecificityByDefaultReturnsSpecificityOfCompoundSelectorProvidedToConstructor( + string $compoundSelector, + int $expectedSpecificity + ): void { + $subject = new CompoundSelector($compoundSelector); + + self::assertSame($expectedSpecificity, $subject->getSpecificity()); + } + + /** + * @test + * + * @param non-empty-string $compoundSelector + * @param int<0, max> $expectedSpecificity + * + * @dataProvider provideCompoundSelectorAndSpecificity + */ + public function getSpecificityReturnsSpecificityOfCompoundSelectorLastProvidedViaSetValue( + string $compoundSelector, + int $expectedSpecificity + ): void { + $subject = new CompoundSelector('p.intro'); + + $subject->setValue($compoundSelector); + + self::assertSame($expectedSpecificity, $subject->getSpecificity()); + } + + /** + * @test + * + * @param non-empty-string $value + * + * @dataProvider provideCompoundSelectorAndSpecificity + */ + public function renderReturnsValueProvided(string $value): void + { + $subject = new CompoundSelector($value); + + self::assertSame($value, $subject->render(OutputFormat::create())); + } + + /** + * @test + */ + public function getArrayRepresentationIncludesClassName(): void + { + $subject = new CompoundSelector('a:hover'); + + $result = $subject->getArrayRepresentation(); + + self::assertSame('CompoundSelector', $result['class']); + } + + /** + * @test + * + * @param non-empty-string $value + * + * @dataProvider provideCompoundSelectorAndSpecificity + */ + public function getArrayRepresentationIncludesValue(string $value): void + { + $subject = new CompoundSelector($value); + + $result = $subject->getArrayRepresentation(); + + self::assertSame($value, $result['value']); + } +} From 6f398f8ad1c142f536729654f283145ab6f94319 Mon Sep 17 00:00:00 2001 From: JakeQZ Date: Thu, 5 Feb 2026 09:22:54 +0000 Subject: [PATCH 084/136] [TASK] Use `SelectorComponent` and `Combinator` in `Selector::parse()` (#1471) Ultimately `Selector` will be represented by instances of these classes rather than a string. This change is a stepping-stone towards that goal, to use the parsing functionality of those classes, but for now just converting the parsed result to a single string for internal representation. Part of #1325. --- src/Property/Selector.php | 114 ++++++++++++-------------------------- 1 file changed, 36 insertions(+), 78 deletions(-) diff --git a/src/Property/Selector.php b/src/Property/Selector.php index 6cce27000..e5a8c9de4 100644 --- a/src/Property/Selector.php +++ b/src/Property/Selector.php @@ -8,6 +8,8 @@ use Sabberworm\CSS\OutputFormat; use Sabberworm\CSS\Parsing\ParserState; use Sabberworm\CSS\Parsing\UnexpectedTokenException; +use Sabberworm\CSS\Property\Selector\Combinator; +use Sabberworm\CSS\Property\Selector\CompoundSelector; use Sabberworm\CSS\Property\Selector\SpecificityCalculator; use Sabberworm\CSS\Renderable; @@ -80,98 +82,54 @@ final public function __construct(string $selector) */ public static function parse(ParserState $parserState, array &$comments = []): self { - $selectorParts = []; - $stringWrapperCharacter = null; - $functionNestingLevel = 0; - static $stopCharacters = ['{', '}', '\'', '"', '(', ')', ',', ParserState::EOF, '']; + // Whitespace is a descendent combinator, not allowed around a compound selector. + // (It is allowed within, e.g. as part of a string or within a function like `:not()`.) + // Gobble any up now to get a clean start. + $parserState->consumeWhiteSpace($comments); + $selectorParts = []; while (true) { - $selectorParts[] = $parserState->consumeUntil($stopCharacters, false, false, $comments); - $nextCharacter = $parserState->peek(); - switch ($nextCharacter) { - case '': - // EOF - break 2; - case '\'': - // The fallthrough is intentional. - case '"': - $lastPart = \end($selectorParts); - $backslashCount = \strspn(\strrev($lastPart), '\\'); - $quoteIsEscaped = ($backslashCount % 2 === 1); - if (!$quoteIsEscaped) { - if (!\is_string($stringWrapperCharacter)) { - $stringWrapperCharacter = $nextCharacter; - } elseif ($stringWrapperCharacter === $nextCharacter) { - $stringWrapperCharacter = null; - } - } - break; - case '(': - if (!\is_string($stringWrapperCharacter)) { - ++$functionNestingLevel; - } - break; - case ')': - if (!\is_string($stringWrapperCharacter)) { - if ($functionNestingLevel <= 0) { - throw new UnexpectedTokenException( - 'anything but', - ')', - 'literal', - $parserState->currentLine() - ); - } - --$functionNestingLevel; - } + try { + $selectorParts[] = CompoundSelector::parse($parserState, $comments); + } catch (UnexpectedTokenException $e) { + if ($selectorParts !== [] && \end($selectorParts)->getValue() === ' ') { + // The whitespace was not a descendent combinator, and was, in fact, arbitrary, + // after the end of the selector. Discard it. + \array_pop($selectorParts); break; - case ',': - if (!\is_string($stringWrapperCharacter) && $functionNestingLevel === 0) { - break 2; - } - break; - case '{': - // The fallthrough is intentional. - case '}': - if (!\is_string($stringWrapperCharacter)) { - break 2; - } - break; - default: - // This will never happen unless something gets broken in `ParserState`. - throw new \UnexpectedValueException( - 'Unexpected character \'' . $nextCharacter - . '\' returned from `ParserState::peek()` in `Selector::parse()`' - ); + } else { + throw $e; + } + } + try { + $selectorParts[] = Combinator::parse($parserState, $comments); + } catch (UnexpectedTokenException $e) { + // End of selector has been reached. + break; } - $selectorParts[] = $parserState->consume(1); } - if ($functionNestingLevel !== 0) { - throw new UnexpectedTokenException(')', $nextCharacter, 'literal', $parserState->currentLine()); - } - if (\is_string($stringWrapperCharacter)) { + // Check that the selector has been fully parsed: + if (!\in_array($parserState->peek(), ['{', '}', ',', ''], true)) { throw new UnexpectedTokenException( - $stringWrapperCharacter, - $nextCharacter, + '`,`, `{`, `}` or EOF', + $parserState->peek(5), 'literal', $parserState->currentLine() ); } - $selector = \trim(\implode('', $selectorParts)); - if ($selector === '') { - throw new UnexpectedTokenException('selector', $nextCharacter, 'literal', $parserState->currentLine()); - } - if (!self::isValid($selector)) { - throw new UnexpectedTokenException( - "Selector did not match '" . static::SELECTOR_VALIDATION_RX . "'.", - $selector, - 'custom', - $parserState->currentLine() - ); + $selectorString = ''; + foreach ($selectorParts as $selectorPart) { + $selectorPartValue = $selectorPart->getValue(); + if (\in_array($selectorPartValue, ['>', '+', '~'], true)) { + $selectorString .= ' ' . $selectorPartValue . ' '; + } else { + $selectorString .= $selectorPartValue; + } } - return new static($selector); + return new static($selectorString); } public function getSelector(): string From 9dd05edb050b46da69d8ac04729384af4a97f810 Mon Sep 17 00:00:00 2001 From: JakeQZ Date: Thu, 5 Feb 2026 18:05:32 +0000 Subject: [PATCH 085/136] [TASK] Extract method `Selector::parseComponents` (#1493) This will allow the conversion of a selector string into an array of `Selector\Component` objects to also be used by the `setSelector()` method. Part of #1325. --- src/Property/Selector.php | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/src/Property/Selector.php b/src/Property/Selector.php index e5a8c9de4..dcecf387f 100644 --- a/src/Property/Selector.php +++ b/src/Property/Selector.php @@ -9,6 +9,7 @@ use Sabberworm\CSS\Parsing\ParserState; use Sabberworm\CSS\Parsing\UnexpectedTokenException; use Sabberworm\CSS\Property\Selector\Combinator; +use Sabberworm\CSS\Property\Selector\Component; use Sabberworm\CSS\Property\Selector\CompoundSelector; use Sabberworm\CSS\Property\Selector\SpecificityCalculator; use Sabberworm\CSS\Renderable; @@ -76,11 +77,11 @@ final public function __construct(string $selector) /** * @param list $comments * - * @throws UnexpectedTokenException + * @return list * - * @internal + * @throws UnexpectedTokenException */ - public static function parse(ParserState $parserState, array &$comments = []): self + private static function parseComponents(ParserState $parserState, array &$comments = []): array { // Whitespace is a descendent combinator, not allowed around a compound selector. // (It is allowed within, e.g. as part of a string or within a function like `:not()`.) @@ -109,6 +110,20 @@ public static function parse(ParserState $parserState, array &$comments = []): s } } + return $selectorParts; + } + + /** + * @param list $comments + * + * @throws UnexpectedTokenException + * + * @internal + */ + public static function parse(ParserState $parserState, array &$comments = []): self + { + $selectorParts = self::parseComponents($parserState, $comments); + // Check that the selector has been fully parsed: if (!\in_array($parserState->peek(), ['{', '}', ',', ''], true)) { throw new UnexpectedTokenException( From b324449a64fde217c2b678ff8028e93e57d3c5ad Mon Sep 17 00:00:00 2001 From: JakeQZ Date: Thu, 5 Feb 2026 19:26:26 +0000 Subject: [PATCH 086/136] [TASK] Have `Selector\Component` extend `Renderable` (#1494) This will allow the `render()` method to be called on `Selector\Component` objects without knowing their specific type. Part of #1325. --- src/Property/Selector/Combinator.php | 3 +-- src/Property/Selector/Component.php | 4 +++- src/Property/Selector/CompoundSelector.php | 3 +-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Property/Selector/Combinator.php b/src/Property/Selector/Combinator.php index 4c2f66940..ab9f6b801 100644 --- a/src/Property/Selector/Combinator.php +++ b/src/Property/Selector/Combinator.php @@ -8,7 +8,6 @@ use Sabberworm\CSS\OutputFormat; use Sabberworm\CSS\Parsing\ParserState; use Sabberworm\CSS\Parsing\UnexpectedTokenException; -use Sabberworm\CSS\Renderable; use Sabberworm\CSS\ShortClassNameProvider; /** @@ -16,7 +15,7 @@ * * @phpstan-type ValidCombinatorValue ' '|'>'|'+'|'~' */ -class Combinator implements Component, Renderable +class Combinator implements Component { use ShortClassNameProvider; diff --git a/src/Property/Selector/Component.php b/src/Property/Selector/Component.php index bb8d343dc..faab5b9a2 100644 --- a/src/Property/Selector/Component.php +++ b/src/Property/Selector/Component.php @@ -4,6 +4,8 @@ namespace Sabberworm\CSS\Property\Selector; +use Sabberworm\CSS\Renderable; + /** * This interface is for a class that represents a part of a selector which is either a compound selector (or a simple * selector, which is effectively a compound selector without any compounding) or a selector combinator. @@ -20,7 +22,7 @@ * @see https://developer.mozilla.org/en-US/docs/Web/CSS/Guides/Selectors/Selector_structure * @see https://www.w3.org/TR/selectors-4/#structure */ -interface Component +interface Component extends Renderable { /** * @return non-empty-string diff --git a/src/Property/Selector/CompoundSelector.php b/src/Property/Selector/CompoundSelector.php index 1073a6cb2..4e0b85210 100644 --- a/src/Property/Selector/CompoundSelector.php +++ b/src/Property/Selector/CompoundSelector.php @@ -8,7 +8,6 @@ use Sabberworm\CSS\OutputFormat; use Sabberworm\CSS\Parsing\ParserState; use Sabberworm\CSS\Parsing\UnexpectedTokenException; -use Sabberworm\CSS\Renderable; use Sabberworm\CSS\ShortClassNameProvider; use function Safe\preg_match; @@ -17,7 +16,7 @@ * Class representing a CSS compound selector. * Selectors have to be split at combinators (space, `>`, `+`, `~`) before being passed to this class. */ -class CompoundSelector implements Renderable, Component +class CompoundSelector implements Component { use ShortClassNameProvider; From 3282587a859774e138645b70f9ea263a90e9a907 Mon Sep 17 00:00:00 2001 From: JakeQZ Date: Thu, 5 Feb 2026 20:13:26 +0000 Subject: [PATCH 087/136] [TASK] Render selector combinators with surrounding spaces by default (#1495) Part of #1325. --- src/Property/Selector/Combinator.php | 2 +- .../Unit/Property/Selector/CombinatorTest.php | 41 ++++++++++--------- 2 files changed, 22 insertions(+), 21 deletions(-) diff --git a/src/Property/Selector/Combinator.php b/src/Property/Selector/Combinator.php index ab9f6b801..c4393fc6c 100644 --- a/src/Property/Selector/Combinator.php +++ b/src/Property/Selector/Combinator.php @@ -95,7 +95,7 @@ public function getSpecificity(): int public function render(OutputFormat $outputFormat): string { // TODO: allow optional spacing controlled via OutputFormat - return $this->getValue(); + return $this->value !== ' ' ? (' ' . $this->value . ' ') : ' '; } /** diff --git a/tests/Unit/Property/Selector/CombinatorTest.php b/tests/Unit/Property/Selector/CombinatorTest.php index 07434581c..215748346 100644 --- a/tests/Unit/Property/Selector/CombinatorTest.php +++ b/tests/Unit/Property/Selector/CombinatorTest.php @@ -37,15 +37,15 @@ public function implementsSelectorComponent(): void } /** - * @return array + * @return array */ - public static function provideValidValue(): array + public static function provideValidValueAndDefaultRendering(): array { return [ - 'descendent' => [' '], - 'child' => ['>'], - 'next sibling' => ['+'], - 'subsequent sibling' => ['~'], + 'descendent' => [' ', ' '], + 'child' => ['>', ' > '], + 'next sibling' => ['+', ' + '], + 'subsequent sibling' => ['~', ' ~ '], ]; } @@ -68,7 +68,7 @@ public static function provideInvalidValue(): array * * @param ValidCombinatorValue $combinator * - * @dataProvider provideValidValue + * @dataProvider provideValidValueAndDefaultRendering */ public function parsesValidCombinator(string $combinator): void { @@ -96,7 +96,7 @@ public function parseThrowsExceptionWithInvalidCombinator(string $selectorCompon * * @param ValidCombinatorValue $combinator * - * @dataProvider provideValidValue + * @dataProvider provideValidValueAndDefaultRendering */ public function parsesCombinatorWithCommentBefore(string $combinator): void { @@ -110,7 +110,7 @@ public function parsesCombinatorWithCommentBefore(string $combinator): void * * @param ValidCombinatorValue $combinator * - * @dataProvider provideValidValue + * @dataProvider provideValidValueAndDefaultRendering */ public function parsesCombinatorWithCommentAfter(string $combinator): void { @@ -124,7 +124,7 @@ public function parsesCombinatorWithCommentAfter(string $combinator): void * * @param ValidCombinatorValue $combinator * - * @dataProvider provideValidValue + * @dataProvider provideValidValueAndDefaultRendering */ public function parseExtractsCommentBefore(string $combinator): void { @@ -139,7 +139,7 @@ public function parseExtractsCommentBefore(string $combinator): void * * @param ValidCombinatorValue $combinator * - * @dataProvider provideValidValue + * @dataProvider provideValidValueAndDefaultRendering */ public function parseExtractsCommentAfter(string $combinator): void { @@ -154,7 +154,7 @@ public function parseExtractsCommentAfter(string $combinator): void * * @param ValidCombinatorValue $value * - * @dataProvider provideValidValue + * @dataProvider provideValidValueAndDefaultRendering */ public function constructsWithValueProvided(string $value): void { @@ -182,7 +182,7 @@ public function constructorThrowsExceptionWithInvalidValue(string $value): void * * @param ValidCombinatorValue $value * - * @dataProvider provideValidValue + * @dataProvider provideValidValueAndDefaultRendering */ public function setValueSetsValueProvided(string $value): void { @@ -214,7 +214,7 @@ public function setValueThrowsExceptionWithInvalidValue(string $value): void * * @param ValidCombinatorValue $value * - * @dataProvider provideValidValue + * @dataProvider provideValidValueAndDefaultRendering */ public function getValueReturnsValueProvidedToConstructor(string $value): void { @@ -230,7 +230,7 @@ public function getValueReturnsValueProvidedToConstructor(string $value): void * * @param ValidCombinatorValue $value * - * @dataProvider provideValidValue + * @dataProvider provideValidValueAndDefaultRendering */ public function getValueReturnsValueProvidedToSetValue(string $value): void { @@ -247,7 +247,7 @@ public function getValueReturnsValueProvidedToSetValue(string $value): void * * @param ValidCombinatorValue $value * - * @dataProvider provideValidValue + * @dataProvider provideValidValueAndDefaultRendering */ public function hasNoSpecificity(string $value): void { @@ -260,14 +260,15 @@ public function hasNoSpecificity(string $value): void * @test * * @param ValidCombinatorValue $value + * @param non-empty-string $expectedRendering * - * @dataProvider provideValidValue + * @dataProvider provideValidValueAndDefaultRendering */ - public function renderReturnsValueProvided(string $value): void + public function renderReturnsDefaultRenderingForValueProvided(string $value, string $expectedRendering): void { $subject = new Combinator($value); - self::assertSame($value, $subject->render(OutputFormat::create())); + self::assertSame($expectedRendering, $subject->render(OutputFormat::create())); } /** @@ -287,7 +288,7 @@ public function getArrayRepresentationIncludesClassName(): void * * @param ValidCombinatorValue $value * - * @dataProvider provideValidValue + * @dataProvider provideValidValueAndDefaultRendering */ public function getArrayRepresentationIncludesValue(string $value): void { From 600d16381dae3295dee7fb525ec8a94c52fc32e5 Mon Sep 17 00:00:00 2001 From: JakeQZ Date: Thu, 5 Feb 2026 23:43:32 +0000 Subject: [PATCH 088/136] [CLEANUP] Untighten a parameter definition in `SelectorTest` (#1497) One of the data providers includes an empty string in its provision, so `non-empty-string` is not appropriate. --- tests/Unit/Property/SelectorTest.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/Unit/Property/SelectorTest.php b/tests/Unit/Property/SelectorTest.php index c18915c05..9aa620312 100644 --- a/tests/Unit/Property/SelectorTest.php +++ b/tests/Unit/Property/SelectorTest.php @@ -198,8 +198,6 @@ public static function provideInvalidSelectorsForParse(): array /** * @test * - * @param non-empty-string $selector - * * @dataProvider provideInvalidSelectors * @dataProvider provideInvalidSelectorsForParse */ From b67aac0a558f40e664f835f17ce35d5be4a8d9d6 Mon Sep 17 00:00:00 2001 From: JakeQZ Date: Fri, 6 Feb 2026 09:13:08 +0000 Subject: [PATCH 089/136] [TASK] Throw exception upon invalid selector (#1498) ... being passed to `Selector` via the constructor or `setSelector()` --- CHANGELOG.md | 2 ++ src/Property/Selector.php | 10 ++++++++ tests/Unit/Property/SelectorTest.php | 36 ++++++++++++++++++++++++++++ 3 files changed, 48 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7e58bc83e..29df562ae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,8 @@ Please also have a look at our ### Changed +- `Selector::setSelector()` and `Selector` constructor will now throw exception + upon provision of an invalid selectior (#1498) - Clean up extra whitespace in CSS selector (#1398) - The array keys passed to `DeclarationBlock::setSelectors()` are no longer preserved (#1407) diff --git a/src/Property/Selector.php b/src/Property/Selector.php index dcecf387f..01db122ed 100644 --- a/src/Property/Selector.php +++ b/src/Property/Selector.php @@ -69,6 +69,9 @@ public static function isValid(string $selector): bool return $numberOfMatches === 1; } + /** + * @throws \UnexpectedValueException if the selector is not valid + */ final public function __construct(string $selector) { $this->setSelector($selector); @@ -152,8 +155,15 @@ public function getSelector(): string return $this->selector; } + /** + * @throws \UnexpectedValueException if the selector is not valid + */ public function setSelector(string $selector): void { + if (!self::isValid($selector)) { + throw new \UnexpectedValueException("Selector `$selector` is not valid."); + } + $selector = \trim($selector); $hasAttribute = \strpos($selector, '[') !== false; diff --git a/tests/Unit/Property/SelectorTest.php b/tests/Unit/Property/SelectorTest.php index 9aa620312..1e537951b 100644 --- a/tests/Unit/Property/SelectorTest.php +++ b/tests/Unit/Property/SelectorTest.php @@ -315,6 +315,42 @@ public function parseExtractsTwoCommentsFromSelector(): void self::assertSame('comment2', $result[1]->getComment()); } + /** + * @test + */ + public function canConstructObjectWithEmptyState(): void + { + $subject = new Selector(''); + + self::assertSame('', $subject->getSelector()); + } + + /** + * @test + * + * @dataProvider provideInvalidSelectors + */ + public function constructorThrowsExceptionWithInvalidSelector(string $selector): void + { + $this->expectException(\UnexpectedValueException::class); + + new Selector($selector); + } + + /** + * @test + * + * @dataProvider provideInvalidSelectors + */ + public function setSelectorThrowsExceptionWithInvalidSelector(string $selector): void + { + $this->expectException(\UnexpectedValueException::class); + + $subject = new Selector('a'); + + $subject->setSelector($selector); + } + /** * @test * From e4c7f2ceba893cc53932c09f74a6a6e4ea79442a Mon Sep 17 00:00:00 2001 From: JakeQZ Date: Sat, 7 Feb 2026 09:02:52 +0000 Subject: [PATCH 090/136] [TASK] Implement `Selector::getArrayRepresentation()` (#1499) This does not include the string value, because it will be changed in #1496. --- src/Property/Selector.php | 7 ++++++- tests/Unit/Property/KeyframeSelectorTest.php | 8 ++++---- tests/Unit/Property/SelectorTest.php | 8 ++++---- 3 files changed, 14 insertions(+), 9 deletions(-) diff --git a/src/Property/Selector.php b/src/Property/Selector.php index 01db122ed..57dee8a79 100644 --- a/src/Property/Selector.php +++ b/src/Property/Selector.php @@ -13,6 +13,7 @@ use Sabberworm\CSS\Property\Selector\CompoundSelector; use Sabberworm\CSS\Property\Selector\SpecificityCalculator; use Sabberworm\CSS\Renderable; +use Sabberworm\CSS\ShortClassNameProvider; use function Safe\preg_match; use function Safe\preg_replace; @@ -23,6 +24,8 @@ */ class Selector implements Renderable { + use ShortClassNameProvider; + /** * @internal since 8.5.2 */ @@ -192,6 +195,8 @@ public function render(OutputFormat $outputFormat): string */ public function getArrayRepresentation(): array { - throw new \BadMethodCallException('`getArrayRepresentation` is not yet implemented for `' . self::class . '`'); + return [ + 'class' => $this->getShortClassName(), + ]; } } diff --git a/tests/Unit/Property/KeyframeSelectorTest.php b/tests/Unit/Property/KeyframeSelectorTest.php index 2f7cebacb..4533e6773 100644 --- a/tests/Unit/Property/KeyframeSelectorTest.php +++ b/tests/Unit/Property/KeyframeSelectorTest.php @@ -16,12 +16,12 @@ final class KeyframeSelectorTest extends TestCase /** * @test */ - public function getArrayRepresentationThrowsException(): void + public function getArrayRepresentationIncludesClassName(): void { - $this->expectException(\BadMethodCallException::class); + $subject = new KeyframeSelector('50%'); - $subject = new KeyframeSelector('a'); + $result = $subject->getArrayRepresentation(); - $subject->getArrayRepresentation(); + self::assertSame('KeyframeSelector', $result['class']); } } diff --git a/tests/Unit/Property/SelectorTest.php b/tests/Unit/Property/SelectorTest.php index 1e537951b..9ecc4e1d5 100644 --- a/tests/Unit/Property/SelectorTest.php +++ b/tests/Unit/Property/SelectorTest.php @@ -458,12 +458,12 @@ public function doesNotCleanupSpacesWithinAttributeSelector(): void /** * @test */ - public function getArrayRepresentationThrowsException(): void + public function getArrayRepresentationIncludesClassName(): void { - $this->expectException(\BadMethodCallException::class); - $subject = new Selector('a'); - $subject->getArrayRepresentation(); + $result = $subject->getArrayRepresentation(); + + self::assertSame('Selector', $result['class']); } } From e57672c9107bd957b804766e567bf1cf2ad29469 Mon Sep 17 00:00:00 2001 From: JakeQZ Date: Mon, 9 Feb 2026 12:42:36 +0000 Subject: [PATCH 091/136] [TASK] Ensure `Selector` content is not whitespace-only (#1502) --- CHANGELOG.md | 2 +- src/Property/Selector.php | 13 +++++++++++-- tests/Unit/Property/SelectorTest.php | 24 ++++++------------------ 3 files changed, 18 insertions(+), 21 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 29df562ae..179a6cfa7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,7 +18,7 @@ Please also have a look at our ### Changed - `Selector::setSelector()` and `Selector` constructor will now throw exception - upon provision of an invalid selectior (#1498) + upon provision of an invalid selectior (#1498, #1502) - Clean up extra whitespace in CSS selector (#1398) - The array keys passed to `DeclarationBlock::setSelectors()` are no longer preserved (#1407) diff --git a/src/Property/Selector.php b/src/Property/Selector.php index 57dee8a79..4474d7225 100644 --- a/src/Property/Selector.php +++ b/src/Property/Selector.php @@ -31,6 +31,8 @@ class Selector implements Renderable */ public const SELECTOR_VALIDATION_RX = '/ ^( + # not whitespace only + (?!\\s*+$) (?: # any sequence of valid unescaped characters, except quotes [a-zA-Z0-9\\x{00A0}-\\x{FFFF}_^$|*=~\\[\\]()\\-\\s\\.:#+>,]++ @@ -57,7 +59,7 @@ class Selector implements Renderable /ux'; /** - * @var string + * @var non-empty-string */ private $selector; @@ -73,6 +75,8 @@ public static function isValid(string $selector): bool } /** + * @param non-empty-string $selector + * * @throws \UnexpectedValueException if the selector is not valid */ final public function __construct(string $selector) @@ -83,7 +87,7 @@ final public function __construct(string $selector) /** * @param list $comments * - * @return list + * @return non-empty-list * * @throws UnexpectedTokenException */ @@ -153,12 +157,17 @@ public static function parse(ParserState $parserState, array &$comments = []): s return new static($selectorString); } + /** + * @return non-empty-string + */ public function getSelector(): string { return $this->selector; } /** + * @param non-empty-string $selector + * * @throws \UnexpectedValueException if the selector is not valid */ public function setSelector(string $selector): void diff --git a/tests/Unit/Property/SelectorTest.php b/tests/Unit/Property/SelectorTest.php index 9ecc4e1d5..12e2312bd 100644 --- a/tests/Unit/Property/SelectorTest.php +++ b/tests/Unit/Property/SelectorTest.php @@ -161,8 +161,11 @@ public function parseHandlesEvenNumberOfBackslashesBeforeQuote(): void public static function provideInvalidSelectors(): array { return [ - // This is currently broken. - // 'empty string' => [''], + 'empty string' => [''], + 'space' => [' '], + 'tab' => ["\t"], + 'line feed' => ["\n"], + 'carriage return' => ["\r"], 'percent sign' => ['%'], // This is currently broken. // 'hash only' => ['#'], @@ -176,16 +179,11 @@ public static function provideInvalidSelectors(): array } /** - * @return array + * @return array */ public static function provideInvalidSelectorsForParse(): array { return [ - 'empty string' => [''], - 'space' => [' '], - 'tab' => ["\t"], - 'line feed' => ["\n"], - 'carriage return' => ["\r"], 'a `:not` missing the closing brace' => [':not(a'], 'a `:not` missing the opening brace' => [':not a)'], 'attribute value missing closing single quote' => ['a[href=\'#top]'], @@ -315,16 +313,6 @@ public function parseExtractsTwoCommentsFromSelector(): void self::assertSame('comment2', $result[1]->getComment()); } - /** - * @test - */ - public function canConstructObjectWithEmptyState(): void - { - $subject = new Selector(''); - - self::assertSame('', $subject->getSelector()); - } - /** * @test * From dd1b3de2b19e99df9486a5b537a721ff11f0d312 Mon Sep 17 00:00:00 2001 From: JakeQZ Date: Mon, 9 Feb 2026 18:26:59 +0000 Subject: [PATCH 092/136] [CLEANUP] Remove superfluous commented test in `SelectorTest` (#1503) Rejecting whitespace-only selectors was fixed in #1502. --- tests/Unit/Property/SelectorTest.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/Unit/Property/SelectorTest.php b/tests/Unit/Property/SelectorTest.php index 12e2312bd..58a98fa6f 100644 --- a/tests/Unit/Property/SelectorTest.php +++ b/tests/Unit/Property/SelectorTest.php @@ -173,8 +173,6 @@ public static function provideInvalidSelectors(): array // 'dot only' => ['.'], 'slash' => ['/'], 'less-than sign' => ['<'], - // This is currently broken. - // 'whitespace only' => [" \t\n\r"], ]; } From 8e87b8a6f2f0c082decc0f69aabba20f71310437 Mon Sep 17 00:00:00 2001 From: JakeQZ Date: Mon, 9 Feb 2026 23:11:20 +0000 Subject: [PATCH 093/136] [FEATURE] Represent `Selector` as `Component` objects (#1496) Part of #1325. --- CHANGELOG.md | 3 + src/Property/Selector.php | 96 ++++++++---- tests/Unit/Property/KeyframeSelectorTest.php | 13 ++ tests/Unit/Property/SelectorTest.php | 147 ++++++++++++++++++- 4 files changed, 227 insertions(+), 32 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 179a6cfa7..f14ec7044 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,9 @@ Please also have a look at our ### Changed +- `Selector` is now represented as a sequence of `Selector\Component` objects + which can be accessed via `getComponents()`, manipulated individually, or set + via `setComponents()` (#1478, #1486, #1487, #1488, #1494, #1496) - `Selector::setSelector()` and `Selector` constructor will now throw exception upon provision of an invalid selectior (#1498, #1502) - Clean up extra whitespace in CSS selector (#1398) diff --git a/src/Property/Selector.php b/src/Property/Selector.php index 4474d7225..66c061ada 100644 --- a/src/Property/Selector.php +++ b/src/Property/Selector.php @@ -11,12 +11,11 @@ use Sabberworm\CSS\Property\Selector\Combinator; use Sabberworm\CSS\Property\Selector\Component; use Sabberworm\CSS\Property\Selector\CompoundSelector; -use Sabberworm\CSS\Property\Selector\SpecificityCalculator; use Sabberworm\CSS\Renderable; +use Sabberworm\CSS\Settings; use Sabberworm\CSS\ShortClassNameProvider; use function Safe\preg_match; -use function Safe\preg_replace; /** * Class representing a single CSS selector. Selectors have to be split by the comma prior to being passed into this @@ -59,9 +58,9 @@ class Selector implements Renderable /ux'; /** - * @var non-empty-string + * @var non-empty-list */ - private $selector; + private $components; /** * @internal since V8.8.0 @@ -75,13 +74,18 @@ public static function isValid(string $selector): bool } /** - * @param non-empty-string $selector + * @param non-empty-string|non-empty-list $selector + * Providing a string is deprecated in version 9.2 and will not work from v10.0 * - * @throws \UnexpectedValueException if the selector is not valid + * @throws UnexpectedTokenException if the selector is not valid */ - final public function __construct(string $selector) + final public function __construct($selector) { - $this->setSelector($selector); + if (\is_string($selector)) { + $this->setSelector($selector); + } else { + $this->setComponents($selector); + } } /** @@ -144,44 +148,62 @@ public static function parse(ParserState $parserState, array &$comments = []): s ); } - $selectorString = ''; - foreach ($selectorParts as $selectorPart) { - $selectorPartValue = $selectorPart->getValue(); - if (\in_array($selectorPartValue, ['>', '+', '~'], true)) { - $selectorString .= ' ' . $selectorPartValue . ' '; - } else { - $selectorString .= $selectorPartValue; - } - } + return new static($selectorParts); + } - return new static($selectorString); + /** + * @return non-empty-list + */ + public function getComponents(): array + { + return $this->components; + } + + /** + * @param non-empty-list $components + * This should be an alternating sequence of `CompoundSelector` and `Combinator`, starting and ending with a + * `CompoundSelector`, and may be a single `CompoundSelector`. + */ + public function setComponents(array $components): self + { + $this->components = $components; + + return $this; } /** * @return non-empty-string + * + * @deprecated in version 9.2, will be removed in v10.0. Use either `getComponents()` or `render()` instead. */ public function getSelector(): string { - return $this->selector; + return $this->render(new OutputFormat()); } /** * @param non-empty-string $selector * - * @throws \UnexpectedValueException if the selector is not valid + * @throws UnexpectedTokenException if the selector is not valid + * + * @deprecated in version 9.2, will be removed in v10.0. Use `setComponents()` instead. */ public function setSelector(string $selector): void { - if (!self::isValid($selector)) { - throw new \UnexpectedValueException("Selector `$selector` is not valid."); - } + $parserState = new ParserState($selector, Settings::create()); - $selector = \trim($selector); + $components = self::parseComponents($parserState); - $hasAttribute = \strpos($selector, '[') !== false; + // Check that the selector has been fully parsed: + if (!$parserState->isEnd()) { + throw new UnexpectedTokenException( + 'EOF', + $parserState->peek(5), + 'literal' + ); + } - // Whitespace can't be adjusted within an attribute selector, as it would change its meaning - $this->selector = !$hasAttribute ? preg_replace('/\\s++/', ' ', $selector) : $selector; + $this->components = $components; } /** @@ -189,12 +211,22 @@ public function setSelector(string $selector): void */ public function getSpecificity(): int { - return SpecificityCalculator::calculate($this->selector); + return \array_sum(\array_map( + static function (Component $component): int { + return $component->getSpecificity(); + }, + $this->components + )); } public function render(OutputFormat $outputFormat): string { - return $this->getSelector(); + return \implode('', \array_map( + static function (Component $component) use ($outputFormat): string { + return $component->render($outputFormat); + }, + $this->components + )); } /** @@ -206,6 +238,12 @@ public function getArrayRepresentation(): array { return [ 'class' => $this->getShortClassName(), + 'components' => \array_map( + static function (Component $component): array { + return $component->getArrayRepresentation(); + }, + $this->components + ), ]; } } diff --git a/tests/Unit/Property/KeyframeSelectorTest.php b/tests/Unit/Property/KeyframeSelectorTest.php index 4533e6773..e73bf2323 100644 --- a/tests/Unit/Property/KeyframeSelectorTest.php +++ b/tests/Unit/Property/KeyframeSelectorTest.php @@ -6,6 +6,7 @@ use PHPUnit\Framework\TestCase; use Sabberworm\CSS\Property\KeyframeSelector; +use Sabberworm\CSS\Property\Selector\CompoundSelector; /** * @covers \Sabberworm\CSS\Property\KeyframeSelector @@ -24,4 +25,16 @@ public function getArrayRepresentationIncludesClassName(): void self::assertSame('KeyframeSelector', $result['class']); } + + /** + * @test + */ + public function getArrayRepresentationIncludesComponent(): void + { + $subject = new KeyframeSelector([new CompoundSelector('50%')]); + + $result = $subject->getArrayRepresentation(); + + self::assertSame('50%', $result['components'][0]['value']); + } } diff --git a/tests/Unit/Property/SelectorTest.php b/tests/Unit/Property/SelectorTest.php index 58a98fa6f..d22190da7 100644 --- a/tests/Unit/Property/SelectorTest.php +++ b/tests/Unit/Property/SelectorTest.php @@ -9,6 +9,9 @@ use Sabberworm\CSS\Parsing\ParserState; use Sabberworm\CSS\Parsing\UnexpectedTokenException; use Sabberworm\CSS\Property\Selector; +use Sabberworm\CSS\Property\Selector\Combinator; +use Sabberworm\CSS\Property\Selector\Component; +use Sabberworm\CSS\Property\Selector\CompoundSelector; use Sabberworm\CSS\Renderable; use Sabberworm\CSS\Settings; use TRegx\PhpUnit\DataProviders\DataProvider; @@ -315,10 +318,11 @@ public function parseExtractsTwoCommentsFromSelector(): void * @test * * @dataProvider provideInvalidSelectors + * @dataProvider provideInvalidSelectorsForParse */ public function constructorThrowsExceptionWithInvalidSelector(string $selector): void { - $this->expectException(\UnexpectedValueException::class); + $this->expectException(UnexpectedTokenException::class); new Selector($selector); } @@ -327,16 +331,141 @@ public function constructorThrowsExceptionWithInvalidSelector(string $selector): * @test * * @dataProvider provideInvalidSelectors + * @dataProvider provideInvalidSelectorsForParse */ public function setSelectorThrowsExceptionWithInvalidSelector(string $selector): void { - $this->expectException(\UnexpectedValueException::class); + $this->expectException(UnexpectedTokenException::class); $subject = new Selector('a'); $subject->setSelector($selector); } + /** + * @return array< + * non-empty-string, + * array{ + * 0: non-empty-list, + * 1: non-empty-list + * } + * > + */ + public static function provideComponentsAndArrayRepresentation(): array + { + return [ + 'simple selector' => [ + [new CompoundSelector('p')], + [ + [ + 'class' => 'CompoundSelector', + 'value' => 'p', + ], + ], + ], + 'selector with combinator' => [ + [ + new CompoundSelector('ul'), + new Combinator('>'), + new CompoundSelector('li'), + ], + [ + [ + 'class' => 'CompoundSelector', + 'value' => 'ul', + ], + [ + 'class' => 'Combinator', + 'value' => '>', + ], + [ + 'class' => 'CompoundSelector', + 'value' => 'li', + ], + ], + ], + ]; + } + + /** + * @test + * + * @param non-empty-list $components + * @param non-empty-list $expectedRepresenation + * + * @dataProvider provideComponentsAndArrayRepresentation + */ + public function constructsWithComponentsProvided(array $components, array $expectedRepresenation): void + { + $subject = new Selector($components); + + $representation = $subject->getArrayRepresentation()['components']; + self::assertSame($expectedRepresenation, $representation); + } + + /** + * @test + */ + public function setComponentsProvidesFluentInterface(): void + { + $subject = new Selector([new CompoundSelector('p')]); + + $result = $subject->setComponents([new CompoundSelector('li')]); + + self::assertSame($subject, $result); + } + + /** + * @test + * + * @param non-empty-list $components + * @param non-empty-list $expectedRepresenation + * + * @dataProvider provideComponentsAndArrayRepresentation + */ + public function setComponentsSetsComponentsProvided(array $components, array $expectedRepresenation): void + { + $subject = new Selector([new CompoundSelector('p')]); + + $subject->setComponents($components); + + $representation = $subject->getArrayRepresentation()['components']; + self::assertSame($expectedRepresenation, $representation); + } + + /** + * @test + * + * @param non-empty-list $components + * + * @dataProvider provideComponentsAndArrayRepresentation + */ + public function getComponentsReturnsComponentsProvidedToConstructor(array $components): void + { + $subject = new Selector($components); + + $result = $subject->getComponents(); + + self::assertSame($components, $result); + } + + /** + * @test + * + * @param non-empty-list $components + * + * @dataProvider provideComponentsAndArrayRepresentation + */ + public function getComponentsReturnsComponentsSet(array $components): void + { + $subject = new Selector([new CompoundSelector('p')]); + $subject->setComponents($components); + + $result = $subject->getComponents(); + + self::assertSame($components, $result); + } + /** * @test * @@ -446,10 +575,22 @@ public function doesNotCleanupSpacesWithinAttributeSelector(): void */ public function getArrayRepresentationIncludesClassName(): void { - $subject = new Selector('a'); + $subject = new Selector([new CompoundSelector('p')]); $result = $subject->getArrayRepresentation(); self::assertSame('Selector', $result['class']); } + + /** + * @test + */ + public function getArrayRepresentationIncludesComponent(): void + { + $subject = new Selector([new CompoundSelector('p.test')]); + + $result = $subject->getArrayRepresentation(); + + self::assertSame('p.test', $result['components'][0]['value']); + } } From 252bbe1c6ebd873b05785fb0390ed926256fec76 Mon Sep 17 00:00:00 2001 From: JakeQZ Date: Tue, 10 Feb 2026 11:05:13 +0000 Subject: [PATCH 094/136] [FEATURE] Add `OutputFormat::setSpaceAroundSelectorCombinator()` (#1504) Resolves #1446 --- CHANGELOG.md | 1 + src/OutputFormat.php | 27 +++++++++++ src/Property/Selector/Combinator.php | 10 +++- tests/Unit/OutputFormatTest.php | 47 +++++++++++++++++++ .../Unit/Property/Selector/CombinatorTest.php | 46 ++++++++++++++++++ 5 files changed, 129 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f14ec7044..60e79baed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ Please also have a look at our ### Added +- Add `OutputFormat::setSpaceAroundSelectorCombinator()` (#1504) - Add support for escaped quotes in the selectors (#1485, #1489) - Provide line number in exception message for mismatched parentheses in selector (#1435) diff --git a/src/OutputFormat.php b/src/OutputFormat.php index c9b46c5c9..4f7ba0b19 100644 --- a/src/OutputFormat.php +++ b/src/OutputFormat.php @@ -92,6 +92,11 @@ final class OutputFormat */ private $spaceAfterSelectorSeparator = ' '; + /** + * @var string + */ + private $spaceAroundSelectorCombinator = ' '; + /** * This is what’s inserted before the separator in value lists, by default. * @@ -434,6 +439,27 @@ public function setSpaceAfterSelectorSeparator(string $whitespace): self return $this; } + /** + * @internal + */ + public function getSpaceAroundSelectorCombinator(): string + { + return $this->spaceAroundSelectorCombinator; + } + + /** + * The spacing set is also used for the descendent combinator, which is whitespace only, + * unless an empty string is set, in which case a space will be used. + * + * @return $this fluent interface + */ + public function setSpaceAroundSelectorCombinator(string $whitespace): self + { + $this->spaceAroundSelectorCombinator = $whitespace; + + return $this; + } + /** * @internal */ @@ -724,6 +750,7 @@ public static function createCompact(): self ->setSpaceAfterRuleName('') ->setSpaceBeforeOpeningBrace('') ->setSpaceAfterSelectorSeparator('') + ->setSpaceAroundSelectorCombinator('') ->setSemicolonAfterLastRule(false) ->setRenderComments(false); diff --git a/src/Property/Selector/Combinator.php b/src/Property/Selector/Combinator.php index c4393fc6c..15f63e2a7 100644 --- a/src/Property/Selector/Combinator.php +++ b/src/Property/Selector/Combinator.php @@ -94,8 +94,14 @@ public function getSpecificity(): int public function render(OutputFormat $outputFormat): string { - // TODO: allow optional spacing controlled via OutputFormat - return $this->value !== ' ' ? (' ' . $this->value . ' ') : ' '; + $spacing = $outputFormat->getSpaceAroundSelectorCombinator(); + if ($this->value === ' ') { + $rendering = $spacing !== '' ? $spacing : ' '; + } else { + $rendering = $spacing . $this->value . $spacing; + } + + return $rendering; } /** diff --git a/tests/Unit/OutputFormatTest.php b/tests/Unit/OutputFormatTest.php index 1a043e1da..145ae49d0 100644 --- a/tests/Unit/OutputFormatTest.php +++ b/tests/Unit/OutputFormatTest.php @@ -414,6 +414,33 @@ public function setSpaceAfterSelectorSeparatorProvidesFluentInterface(): void self::assertSame($this->subject, $this->subject->setSpaceAfterSelectorSeparator(' ')); } + /** + * @test + */ + public function getSpaceAroundSelectorCombinatorInitiallyReturnsSpace(): void + { + self::assertSame(' ', $this->subject->getSpaceAroundSelectorCombinator()); + } + + /** + * @test + */ + public function setSpaceAroundSelectorCombinatorSetsSpaceAroundSelectorCombinator(): void + { + $value = ' '; + $this->subject->setSpaceAroundSelectorCombinator($value); + + self::assertSame($value, $this->subject->getSpaceAroundSelectorCombinator()); + } + + /** + * @test + */ + public function setSpaceAroundSelectorCombinatorProvidesFluentInterface(): void + { + self::assertSame($this->subject, $this->subject->setSpaceAroundSelectorCombinator(' ')); + } + /** * @test */ @@ -1024,6 +1051,16 @@ public function createCompactReturnsInstanceWithSpaceAfterSelectorSeparatorSetTo self::assertSame('', $newInstance->getSpaceAfterSelectorSeparator()); } + /** + * @test + */ + public function createCompactReturnsInstanceWithSpaceAroundSelectorCombinatorSetToEmptyString(): void + { + $newInstance = OutputFormat::createCompact(); + + self::assertSame('', $newInstance->getSpaceAroundSelectorCombinator()); + } + /** * @test */ @@ -1163,6 +1200,16 @@ public function createPrettyReturnsInstanceWithSpaceAfterSelectorSeparatorSetToS self::assertSame(' ', $newInstance->getSpaceAfterSelectorSeparator()); } + /** + * @test + */ + public function createPrettyReturnsInstanceWithSpaceAroundSelectorCombinatorSetToSpace(): void + { + $newInstance = OutputFormat::createPretty(); + + self::assertSame(' ', $newInstance->getSpaceAroundSelectorCombinator()); + } + /** * @test */ diff --git a/tests/Unit/Property/Selector/CombinatorTest.php b/tests/Unit/Property/Selector/CombinatorTest.php index 215748346..e416678d1 100644 --- a/tests/Unit/Property/Selector/CombinatorTest.php +++ b/tests/Unit/Property/Selector/CombinatorTest.php @@ -271,6 +271,52 @@ public function renderReturnsDefaultRenderingForValueProvided(string $value, str self::assertSame($expectedRendering, $subject->render(OutputFormat::create())); } + /** + * @return array + */ + public static function provideSpacing(): array + { + return [ + 'empty string' => [''], + 'space' => [' '], + 'newline' => ["\n"], + 'carriage return' => ["\r"], + 'tab' => ["\t"], + ]; + } + + /** + * @test + * + * @dataProvider provideSpacing + */ + public function renderIncludesSpacingSetInOutputFormat(string $spacing): void + { + $subject = new Combinator('>'); + $outputFormat = (OutputFormat::create())->setSpaceAroundSelectorCombinator($spacing); + + $result = $subject->render($outputFormat); + + $expectedRendering = $spacing . '>' . $spacing; + self::assertSame($expectedRendering, $result); + } + + /** + * @test + * + * @dataProvider provideSpacing + */ + public function renderReturnsSpacingSetInOutputFormatForDescendentCombinatorOrSpaceIfNeeded(string $spacing): void + { + $subject = new Combinator(' '); + $outputFormat = (OutputFormat::create())->setSpaceAroundSelectorCombinator($spacing); + + $result = $subject->render($outputFormat); + + $expectedRendering = $spacing !== '' ? $spacing : ' '; + self::assertSame($expectedRendering, $result); + } + /** * @test */ From 1ce3447653dd81b21ccdabd9d6e9552824af76bf Mon Sep 17 00:00:00 2001 From: JakeQZ Date: Thu, 12 Feb 2026 12:39:03 +0000 Subject: [PATCH 095/136] [TASK] Rename `Rule::$rule` to `Rule::$propertyName` (#1506) This has become a misnomer. Rules are the outer enclosing blocks. Deprecate the existing setters and getters and add new ones. (The name of the `Rule` class itself is also a misnomer.) Ref: https://developer.mozilla.org/en-US/docs/Web/API/CSS_Object_Model/CSS_Declaration --- CHANGELOG.md | 5 +++ src/Rule/Rule.php | 50 +++++++++++++++++++--------- tests/Functional/Value/ValueTest.php | 2 +- tests/Unit/Value/ValueTest.php | 2 +- 4 files changed, 42 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 60e79baed..106d3717b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,8 @@ Please also have a look at our ### Changed +- `Rule::setRule()` and `getRule()` are replaced with `setPropertyName()` and + `getPropertyName()` (#1506) - `Selector` is now represented as a sequence of `Selector\Component` objects which can be accessed via `getComponents()`, manipulated individually, or set via `setComponents()` (#1478, #1486, #1487, #1488, #1494, #1496) @@ -29,6 +31,9 @@ Please also have a look at our ### Deprecated +- `Rule::setRule()` and `getRule()` are deprecated and replaced with + `setPropertyName()` and `getPropertyName()` (#1506) + ### Removed ### Fixed diff --git a/src/Rule/Rule.php b/src/Rule/Rule.php index 26e68b231..804a58904 100644 --- a/src/Rule/Rule.php +++ b/src/Rule/Rule.php @@ -20,7 +20,7 @@ use function Safe\preg_match; /** - * `Rule`s just have a string key (the rule) and a 'Value'. + * `Rule`s just have a string key (the property name) and a 'Value'. * * In CSS, `Rule`s are expressed as follows: “key: value[0][0] value[0][1], value[1][0] value[1][1];” */ @@ -32,7 +32,7 @@ class Rule implements Commentable, CSSElement, Positionable /** * @var non-empty-string */ - private $rule; + private $propertyName; /** * @var RuleValueList|string|null @@ -45,13 +45,13 @@ class Rule implements Commentable, CSSElement, Positionable private $isImportant = false; /** - * @param non-empty-string $rule + * @param non-empty-string $propertyName * @param int<1, max>|null $lineNumber * @param int<0, max>|null $columnNumber */ - public function __construct(string $rule, ?int $lineNumber = null, ?int $columnNumber = null) + public function __construct(string $propertyName, ?int $lineNumber = null, ?int $columnNumber = null) { - $this->rule = $rule; + $this->propertyName = $propertyName; $this->setPosition($lineNumber, $columnNumber); } @@ -75,7 +75,7 @@ public static function parse(ParserState $parserState, array $commentsBeforeRule $parserState->consumeWhiteSpace($comments); $rule->setComments($comments); $parserState->consume(':'); - $value = Value::parseValue($parserState, self::listDelimiterForRule($rule->getRule())); + $value = Value::parseValue($parserState, self::getDelimitersForPropertyValue($rule->getRule())); $rule->setValue($value); $parserState->consumeWhiteSpace(); if ($parserState->comes('!')) { @@ -97,17 +97,17 @@ public static function parse(ParserState $parserState, array $commentsBeforeRule * The first item is the innermost separator (or, put another way, the highest-precedence operator). * The sequence continues to the outermost separator (or lowest-precedence operator). * - * @param non-empty-string $rule + * @param non-empty-string $propertyName * * @return list */ - private static function listDelimiterForRule(string $rule): array + private static function getDelimitersForPropertyValue(string $propertyName): array { - if (preg_match('/^font($|-)/', $rule) === 1) { + if (preg_match('/^font($|-)/', $propertyName) === 1) { return [',', '/', ' ']; } - switch ($rule) { + switch ($propertyName) { case 'src': return [' ', ',']; default: @@ -116,19 +116,39 @@ private static function listDelimiterForRule(string $rule): array } /** - * @param non-empty-string $rule + * @param non-empty-string $propertyName */ - public function setRule(string $rule): void + public function setPropertyName(string $propertyName): void { - $this->rule = $rule; + $this->propertyName = $propertyName; } /** * @return non-empty-string */ + public function getPropertyName(): string + { + return $this->propertyName; + } + + /** + * @param non-empty-string $propertyName + * + * @deprecated in v9.2, will be removed in v10.0; use `setPropertyName()` instead. + */ + public function setRule(string $propertyName): void + { + $this->propertyName = $propertyName; + } + + /** + * @return non-empty-string + * + * @deprecated in v9.2, will be removed in v10.0; use `getPropertyName()` instead. + */ public function getRule(): string { - return $this->rule; + return $this->propertyName; } /** @@ -186,7 +206,7 @@ public function getIsImportant(): bool public function render(OutputFormat $outputFormat): string { $formatter = $outputFormat->getFormatter(); - $result = "{$formatter->comments($this)}{$this->rule}:{$formatter->spaceAfterRuleName()}"; + $result = "{$formatter->comments($this)}{$this->propertyName}:{$formatter->spaceAfterRuleName()}"; if ($this->value instanceof Value) { // Can also be a ValueList $result .= $this->value->render($outputFormat); } else { diff --git a/tests/Functional/Value/ValueTest.php b/tests/Functional/Value/ValueTest.php index b8dd5b4f1..287361a8e 100644 --- a/tests/Functional/Value/ValueTest.php +++ b/tests/Functional/Value/ValueTest.php @@ -19,7 +19,7 @@ final class ValueTest extends TestCase /** * the default set of delimiters for parsing most values * - * @see \Sabberworm\CSS\Rule\Rule::listDelimiterForRule + * @see \Sabberworm\CSS\Rule\Rule::getDelimitersForPropertyValue */ private const DEFAULT_DELIMITERS = [',', ' ', '/']; diff --git a/tests/Unit/Value/ValueTest.php b/tests/Unit/Value/ValueTest.php index dfe46917e..e3ab12c70 100644 --- a/tests/Unit/Value/ValueTest.php +++ b/tests/Unit/Value/ValueTest.php @@ -21,7 +21,7 @@ final class ValueTest extends TestCase /** * the default set of delimiters for parsing most values * - * @see \Sabberworm\CSS\Rule\Rule::listDelimiterForRule + * @see \Sabberworm\CSS\Rule\Rule::getDelimitersForPropertyValue */ private const DEFAULT_DELIMITERS = [',', ' ', '/']; From 802cebe832a6aaed432c6bf3fb1bb4e38d14c66e Mon Sep 17 00:00:00 2001 From: JakeQZ Date: Thu, 12 Feb 2026 15:00:20 +0000 Subject: [PATCH 096/136] [TASK] Rename `Rule\Rule` to `Property\Declaration` (#1508) A property declaration is not a rule. Ref: https://developer.mozilla.org/en-US/docs/Web/API/CSS_Object_Model/CSS_Declaration Add a `class_alias` to avoid a breaking change, and deprecate the original class name. --- config/phpstan-baseline.neon | 6 +- config/phpstan.neon | 3 + src/Property/Declaration.php | 231 +++++++++++++++++++++++++ src/Rule/Rule.php | 225 +----------------------- tests/UnitDeprecated/Rule/RuleTest.php | 25 +++ 5 files changed, 266 insertions(+), 224 deletions(-) create mode 100644 src/Property/Declaration.php create mode 100644 tests/UnitDeprecated/Rule/RuleTest.php diff --git a/config/phpstan-baseline.neon b/config/phpstan-baseline.neon index 4c4add4da..ea84804b7 100644 --- a/config/phpstan-baseline.neon +++ b/config/phpstan-baseline.neon @@ -79,7 +79,7 @@ parameters: path: ../tests/ParserTest.php - - message: '#^Parameter \#1 \$value of method Sabberworm\\CSS\\Rule\\Rule\:\:setValue\(\) expects Sabberworm\\CSS\\Value\\RuleValueList\|string\|null, Sabberworm\\CSS\\Value\\Size given\.$#' + message: '#^Parameter \#1 \$value of method Sabberworm\\CSS\\Property\\Declaration\:\:setValue\(\) expects Sabberworm\\CSS\\Value\\RuleValueList\|string\|null, Sabberworm\\CSS\\Value\\Size given\.$#' identifier: argument.type count: 3 path: ../tests/RuleSet/DeclarationBlockTest.php @@ -91,13 +91,13 @@ parameters: path: ../tests/Unit/CSSList/AtRuleBlockListTest.php - - message: '#^Parameter \#1 \$value of method Sabberworm\\CSS\\Rule\\Rule\:\:setValue\(\) expects Sabberworm\\CSS\\Value\\RuleValueList\|string\|null, Sabberworm\\CSS\\Value\\CSSFunction given\.$#' + message: '#^Parameter \#1 \$value of method Sabberworm\\CSS\\Property\\Declaration\:\:setValue\(\) expects Sabberworm\\CSS\\Value\\RuleValueList\|string\|null, Sabberworm\\CSS\\Value\\CSSFunction given\.$#' identifier: argument.type count: 2 path: ../tests/Unit/CSSList/CSSBlockListTest.php - - message: '#^Parameter \#1 \$value of method Sabberworm\\CSS\\Rule\\Rule\:\:setValue\(\) expects Sabberworm\\CSS\\Value\\RuleValueList\|string\|null, Sabberworm\\CSS\\Value\\CSSString given\.$#' + message: '#^Parameter \#1 \$value of method Sabberworm\\CSS\\Property\\Declaration\:\:setValue\(\) expects Sabberworm\\CSS\\Value\\RuleValueList\|string\|null, Sabberworm\\CSS\\Value\\CSSString given\.$#' identifier: argument.type count: 10 path: ../tests/Unit/CSSList/CSSBlockListTest.php diff --git a/config/phpstan.neon b/config/phpstan.neon index 55ba682af..cfdfefe1d 100644 --- a/config/phpstan.neon +++ b/config/phpstan.neon @@ -15,6 +15,9 @@ parameters: - %currentWorkingDirectory%/src/ - %currentWorkingDirectory%/tests/ + bootstrapFiles: + - %currentWorkingDirectory%/src/Rule/Rule.php + type_perfect: no_mixed_property: true no_mixed_caller: true diff --git a/src/Property/Declaration.php b/src/Property/Declaration.php new file mode 100644 index 000000000..079ab48db --- /dev/null +++ b/src/Property/Declaration.php @@ -0,0 +1,231 @@ +|null $lineNumber + * @param int<0, max>|null $columnNumber + */ + public function __construct(string $propertyName, ?int $lineNumber = null, ?int $columnNumber = null) + { + $this->propertyName = $propertyName; + $this->setPosition($lineNumber, $columnNumber); + } + + /** + * @param list $commentsBefore + * + * @throws UnexpectedEOFException + * @throws UnexpectedTokenException + * + * @internal since V8.8.0 + */ + public static function parse(ParserState $parserState, array $commentsBefore = []): self + { + $comments = $commentsBefore; + $parserState->consumeWhiteSpace($comments); + $declaration = new self( + $parserState->parseIdentifier(!$parserState->comes('--')), + $parserState->currentLine(), + $parserState->currentColumn() + ); + $parserState->consumeWhiteSpace($comments); + $declaration->setComments($comments); + $parserState->consume(':'); + $value = Value::parseValue($parserState, self::getDelimitersForPropertyValue($declaration->getPropertyName())); + $declaration->setValue($value); + $parserState->consumeWhiteSpace(); + if ($parserState->comes('!')) { + $parserState->consume('!'); + $parserState->consumeWhiteSpace(); + $parserState->consume('important'); + $declaration->setIsImportant(true); + } + $parserState->consumeWhiteSpace(); + while ($parserState->comes(';')) { + $parserState->consume(';'); + } + + return $declaration; + } + + /** + * Returns a list of delimiters (or separators). + * The first item is the innermost separator (or, put another way, the highest-precedence operator). + * The sequence continues to the outermost separator (or lowest-precedence operator). + * + * @param non-empty-string $propertyName + * + * @return list + */ + private static function getDelimitersForPropertyValue(string $propertyName): array + { + if (preg_match('/^font($|-)/', $propertyName) === 1) { + return [',', '/', ' ']; + } + + switch ($propertyName) { + case 'src': + return [' ', ',']; + default: + return [',', ' ', '/']; + } + } + + /** + * @param non-empty-string $propertyName + */ + public function setPropertyName(string $propertyName): void + { + $this->propertyName = $propertyName; + } + + /** + * @return non-empty-string + */ + public function getPropertyName(): string + { + return $this->propertyName; + } + + /** + * @param non-empty-string $propertyName + * + * @deprecated in v9.2, will be removed in v10.0; use `setPropertyName()` instead. + */ + public function setRule(string $propertyName): void + { + $this->propertyName = $propertyName; + } + + /** + * @return non-empty-string + * + * @deprecated in v9.2, will be removed in v10.0; use `getPropertyName()` instead. + */ + public function getRule(): string + { + return $this->propertyName; + } + + /** + * @return RuleValueList|string|null + */ + public function getValue() + { + return $this->value; + } + + /** + * @param RuleValueList|string|null $value + */ + public function setValue($value): void + { + $this->value = $value; + } + + /** + * Adds a value to the existing value. Value will be appended if a `RuleValueList` exists of the given type. + * Otherwise, the existing value will be wrapped by one. + * + * @param RuleValueList|array $value + */ + public function addValue($value, string $type = ' '): void + { + if (!\is_array($value)) { + $value = [$value]; + } + if (!($this->value instanceof RuleValueList) || $this->value->getListSeparator() !== $type) { + $currentValue = $this->value; + $this->value = new RuleValueList($type, $this->getLineNumber()); + if ($currentValue !== null && $currentValue !== '') { + $this->value->addListComponent($currentValue); + } + } + foreach ($value as $valueItem) { + $this->value->addListComponent($valueItem); + } + } + + public function setIsImportant(bool $isImportant): void + { + $this->isImportant = $isImportant; + } + + public function getIsImportant(): bool + { + return $this->isImportant; + } + + /** + * @return non-empty-string + */ + public function render(OutputFormat $outputFormat): string + { + $formatter = $outputFormat->getFormatter(); + $result = "{$formatter->comments($this)}{$this->propertyName}:{$formatter->spaceAfterRuleName()}"; + if ($this->value instanceof Value) { // Can also be a ValueList + $result .= $this->value->render($outputFormat); + } else { + $result .= $this->value; + } + if ($this->isImportant) { + $result .= ' !important'; + } + $result .= ';'; + return $result; + } + + /** + * @return array|null> + * + * @internal + */ + public function getArrayRepresentation(): array + { + throw new \BadMethodCallException('`getArrayRepresentation` is not yet implemented for `' . self::class . '`'); + } +} diff --git a/src/Rule/Rule.php b/src/Rule/Rule.php index 804a58904..bed19cf2a 100644 --- a/src/Rule/Rule.php +++ b/src/Rule/Rule.php @@ -4,228 +4,11 @@ namespace Sabberworm\CSS\Rule; -use Sabberworm\CSS\Comment\Comment; -use Sabberworm\CSS\Comment\Commentable; -use Sabberworm\CSS\Comment\CommentContainer; -use Sabberworm\CSS\CSSElement; -use Sabberworm\CSS\OutputFormat; -use Sabberworm\CSS\Parsing\ParserState; -use Sabberworm\CSS\Parsing\UnexpectedEOFException; -use Sabberworm\CSS\Parsing\UnexpectedTokenException; -use Sabberworm\CSS\Position\Position; -use Sabberworm\CSS\Position\Positionable; -use Sabberworm\CSS\Value\RuleValueList; -use Sabberworm\CSS\Value\Value; +use Sabberworm\CSS\Property\Declaration; -use function Safe\preg_match; +use function Safe\class_alias; /** - * `Rule`s just have a string key (the property name) and a 'Value'. - * - * In CSS, `Rule`s are expressed as follows: “key: value[0][0] value[0][1], value[1][0] value[1][1];” + * @deprecated in v9.2, will be removed in v10.0. Use `Property\Declaration` instead, which is a direct replacement. */ -class Rule implements Commentable, CSSElement, Positionable -{ - use CommentContainer; - use Position; - - /** - * @var non-empty-string - */ - private $propertyName; - - /** - * @var RuleValueList|string|null - */ - private $value; - - /** - * @var bool - */ - private $isImportant = false; - - /** - * @param non-empty-string $propertyName - * @param int<1, max>|null $lineNumber - * @param int<0, max>|null $columnNumber - */ - public function __construct(string $propertyName, ?int $lineNumber = null, ?int $columnNumber = null) - { - $this->propertyName = $propertyName; - $this->setPosition($lineNumber, $columnNumber); - } - - /** - * @param list $commentsBeforeRule - * - * @throws UnexpectedEOFException - * @throws UnexpectedTokenException - * - * @internal since V8.8.0 - */ - public static function parse(ParserState $parserState, array $commentsBeforeRule = []): Rule - { - $comments = $commentsBeforeRule; - $parserState->consumeWhiteSpace($comments); - $rule = new Rule( - $parserState->parseIdentifier(!$parserState->comes('--')), - $parserState->currentLine(), - $parserState->currentColumn() - ); - $parserState->consumeWhiteSpace($comments); - $rule->setComments($comments); - $parserState->consume(':'); - $value = Value::parseValue($parserState, self::getDelimitersForPropertyValue($rule->getRule())); - $rule->setValue($value); - $parserState->consumeWhiteSpace(); - if ($parserState->comes('!')) { - $parserState->consume('!'); - $parserState->consumeWhiteSpace(); - $parserState->consume('important'); - $rule->setIsImportant(true); - } - $parserState->consumeWhiteSpace(); - while ($parserState->comes(';')) { - $parserState->consume(';'); - } - - return $rule; - } - - /** - * Returns a list of delimiters (or separators). - * The first item is the innermost separator (or, put another way, the highest-precedence operator). - * The sequence continues to the outermost separator (or lowest-precedence operator). - * - * @param non-empty-string $propertyName - * - * @return list - */ - private static function getDelimitersForPropertyValue(string $propertyName): array - { - if (preg_match('/^font($|-)/', $propertyName) === 1) { - return [',', '/', ' ']; - } - - switch ($propertyName) { - case 'src': - return [' ', ',']; - default: - return [',', ' ', '/']; - } - } - - /** - * @param non-empty-string $propertyName - */ - public function setPropertyName(string $propertyName): void - { - $this->propertyName = $propertyName; - } - - /** - * @return non-empty-string - */ - public function getPropertyName(): string - { - return $this->propertyName; - } - - /** - * @param non-empty-string $propertyName - * - * @deprecated in v9.2, will be removed in v10.0; use `setPropertyName()` instead. - */ - public function setRule(string $propertyName): void - { - $this->propertyName = $propertyName; - } - - /** - * @return non-empty-string - * - * @deprecated in v9.2, will be removed in v10.0; use `getPropertyName()` instead. - */ - public function getRule(): string - { - return $this->propertyName; - } - - /** - * @return RuleValueList|string|null - */ - public function getValue() - { - return $this->value; - } - - /** - * @param RuleValueList|string|null $value - */ - public function setValue($value): void - { - $this->value = $value; - } - - /** - * Adds a value to the existing value. Value will be appended if a `RuleValueList` exists of the given type. - * Otherwise, the existing value will be wrapped by one. - * - * @param RuleValueList|array $value - */ - public function addValue($value, string $type = ' '): void - { - if (!\is_array($value)) { - $value = [$value]; - } - if (!($this->value instanceof RuleValueList) || $this->value->getListSeparator() !== $type) { - $currentValue = $this->value; - $this->value = new RuleValueList($type, $this->getLineNumber()); - if ($currentValue !== null && $currentValue !== '') { - $this->value->addListComponent($currentValue); - } - } - foreach ($value as $valueItem) { - $this->value->addListComponent($valueItem); - } - } - - public function setIsImportant(bool $isImportant): void - { - $this->isImportant = $isImportant; - } - - public function getIsImportant(): bool - { - return $this->isImportant; - } - - /** - * @return non-empty-string - */ - public function render(OutputFormat $outputFormat): string - { - $formatter = $outputFormat->getFormatter(); - $result = "{$formatter->comments($this)}{$this->propertyName}:{$formatter->spaceAfterRuleName()}"; - if ($this->value instanceof Value) { // Can also be a ValueList - $result .= $this->value->render($outputFormat); - } else { - $result .= $this->value; - } - if ($this->isImportant) { - $result .= ' !important'; - } - $result .= ';'; - return $result; - } - - /** - * @return array|null> - * - * @internal - */ - public function getArrayRepresentation(): array - { - throw new \BadMethodCallException('`getArrayRepresentation` is not yet implemented for `' . self::class . '`'); - } -} +class_alias(Declaration::class, Rule::class); diff --git a/tests/UnitDeprecated/Rule/RuleTest.php b/tests/UnitDeprecated/Rule/RuleTest.php new file mode 100644 index 000000000..efe00a968 --- /dev/null +++ b/tests/UnitDeprecated/Rule/RuleTest.php @@ -0,0 +1,25 @@ + Date: Thu, 12 Feb 2026 17:07:16 +0000 Subject: [PATCH 097/136] [TASK] Add changelog entry for #1508 (#1510) --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 106d3717b..76c5713fc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ Please also have a look at our ### Changed +- `Rule\Rule` class is renamed to `Property\Declaration` (#1508) - `Rule::setRule()` and `getRule()` are replaced with `setPropertyName()` and `getPropertyName()` (#1506) - `Selector` is now represented as a sequence of `Selector\Component` objects @@ -31,6 +32,8 @@ Please also have a look at our ### Deprecated +- `Rule\Rule` class is deprecated; `Property\Declaration` is a direct + replacement (#1508) - `Rule::setRule()` and `getRule()` are deprecated and replaced with `setPropertyName()` and `getPropertyName()` (#1506) From 79417446e0872371b9dd0e1cd9cf60a80487daf6 Mon Sep 17 00:00:00 2001 From: JakeQZ Date: Thu, 12 Feb 2026 17:10:40 +0000 Subject: [PATCH 098/136] [TASK] Move/rename Rule/RuleTest.php to Property/DeclarationTest.php (#1511) Test the new `Declaration` class instead of the deprecated `Rule` alias. Also rename some variables and parameters in the tests from `$rule` to `$declaration`. --- .../DeclarationTest.php} | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) rename tests/Unit/{Rule/RuleTest.php => Property/DeclarationTest.php} (72%) diff --git a/tests/Unit/Rule/RuleTest.php b/tests/Unit/Property/DeclarationTest.php similarity index 72% rename from tests/Unit/Rule/RuleTest.php rename to tests/Unit/Property/DeclarationTest.php index 3a00bd751..8344bb879 100644 --- a/tests/Unit/Rule/RuleTest.php +++ b/tests/Unit/Property/DeclarationTest.php @@ -2,28 +2,28 @@ declare(strict_types=1); -namespace Sabberworm\CSS\Tests\Unit\Rule; +namespace Sabberworm\CSS\Tests\Unit\Property; use PHPUnit\Framework\TestCase; use Sabberworm\CSS\CSSElement; use Sabberworm\CSS\Parsing\ParserState; -use Sabberworm\CSS\Rule\Rule; +use Sabberworm\CSS\Property\Declaration; use Sabberworm\CSS\Settings; use Sabberworm\CSS\Value\RuleValueList; use Sabberworm\CSS\Value\Value; use Sabberworm\CSS\Value\ValueList; /** - * @covers \Sabberworm\CSS\Rule\Rule + * @covers \Sabberworm\CSS\Property\Declaration */ -final class RuleTest extends TestCase +final class DeclarationTest extends TestCase { /** * @test */ public function implementsCSSElement(): void { - $subject = new Rule('beverage-container'); + $subject = new Declaration('beverage-container'); self::assertInstanceOf(CSSElement::class, $subject); } @@ -31,7 +31,7 @@ public function implementsCSSElement(): void /** * @return array}> */ - public static function provideRulesAndExpectedParsedValueListTypes(): array + public static function provideDeclarationsAndExpectedParsedValueListTypes(): array { return [ 'src (e.g. in @font-face)' => [ @@ -49,11 +49,11 @@ public static function provideRulesAndExpectedParsedValueListTypes(): array * * @param list $expectedTypeClassnames * - * @dataProvider provideRulesAndExpectedParsedValueListTypes + * @dataProvider provideDeclarationsAndExpectedParsedValueListTypes */ - public function parsesValuesIntoExpectedTypeList(string $rule, array $expectedTypeClassnames): void + public function parsesValuesIntoExpectedTypeList(string $declaration, array $expectedTypeClassnames): void { - $subject = Rule::parse(new ParserState($rule, Settings::create())); + $subject = Declaration::parse(new ParserState($declaration, Settings::create())); $value = $subject->getValue(); self::assertInstanceOf(ValueList::class, $value); @@ -78,7 +78,7 @@ public function getArrayRepresentationThrowsException(): void { $this->expectException(\BadMethodCallException::class); - $subject = new Rule('todo'); + $subject = new Declaration('todo'); $subject->getArrayRepresentation(); } From 27da0c8e6f11c512131f89fb85203c502ac603fb Mon Sep 17 00:00:00 2001 From: JakeQZ Date: Thu, 12 Feb 2026 19:04:15 +0000 Subject: [PATCH 099/136] [TASK] Use `Property\Declaration` in `CSSBlockList` (#1512) This replaces `Rule\Rule`, following #1508. --- CHANGELOG.md | 2 +- src/CSSList/CSSBlockList.php | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 76c5713fc..ab465af94 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,7 +18,7 @@ Please also have a look at our ### Changed -- `Rule\Rule` class is renamed to `Property\Declaration` (#1508) +- `Rule\Rule` class is renamed to `Property\Declaration` (#1508, #1512) - `Rule::setRule()` and `getRule()` are replaced with `setPropertyName()` and `getPropertyName()` (#1506) - `Selector` is now represented as a sequence of `Selector\Component` objects diff --git a/src/CSSList/CSSBlockList.php b/src/CSSList/CSSBlockList.php index 122ac5d1d..574bdaa38 100644 --- a/src/CSSList/CSSBlockList.php +++ b/src/CSSList/CSSBlockList.php @@ -5,8 +5,8 @@ namespace Sabberworm\CSS\CSSList; use Sabberworm\CSS\CSSElement; +use Sabberworm\CSS\Property\Declaration; use Sabberworm\CSS\Property\Selector; -use Sabberworm\CSS\Rule\Rule; use Sabberworm\CSS\RuleSet\DeclarationBlock; use Sabberworm\CSS\RuleSet\RuleContainer; use Sabberworm\CSS\RuleSet\RuleSet; @@ -65,7 +65,7 @@ public function getAllRuleSets(): array } /** - * Returns all `Value` objects found recursively in `Rule`s in the tree. + * Returns all `Value` objects found recursively in `Declaration`s in the tree. * * @param CSSElement|null $element * This is the `CSSList` or `RuleSet` to start the search from (defaults to the whole document). @@ -105,7 +105,7 @@ public function getAllValues( $this->getAllValues($rule, $ruleSearchPattern, $searchInFunctionArguments) ); } - } elseif ($element instanceof Rule) { + } elseif ($element instanceof Declaration) { $value = $element->getValue(); // `string` values are discarded. if ($value instanceof CSSElement) { From 399afab8718b72a28395f61e156d3d431cfff285 Mon Sep 17 00:00:00 2001 From: JakeQZ Date: Fri, 13 Feb 2026 18:47:49 +0000 Subject: [PATCH 100/136] [TASK] Use `Property\Declaration` in `RuleContainer` classes (#1513) This replaces `Rule\Rule`, following #1508. --- CHANGELOG.md | 2 +- src/RuleSet/DeclarationBlock.php | 20 ++--- src/RuleSet/RuleContainer.php | 14 +-- src/RuleSet/RuleSet.php | 146 ++++++++++++++++--------------- 4 files changed, 92 insertions(+), 90 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ab465af94..5f209a962 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,7 +18,7 @@ Please also have a look at our ### Changed -- `Rule\Rule` class is renamed to `Property\Declaration` (#1508, #1512) +- `Rule\Rule` class is renamed to `Property\Declaration` (#1508, #1512, #1513) - `Rule::setRule()` and `getRule()` are replaced with `setPropertyName()` and `getPropertyName()` (#1506) - `Selector` is now represented as a sequence of `Selector\Component` objects diff --git a/src/RuleSet/DeclarationBlock.php b/src/RuleSet/DeclarationBlock.php index 323ad6417..8ded5558b 100644 --- a/src/RuleSet/DeclarationBlock.php +++ b/src/RuleSet/DeclarationBlock.php @@ -17,9 +17,9 @@ use Sabberworm\CSS\Parsing\UnexpectedTokenException; use Sabberworm\CSS\Position\Position; use Sabberworm\CSS\Position\Positionable; +use Sabberworm\CSS\Property\Declaration; use Sabberworm\CSS\Property\KeyframeSelector; use Sabberworm\CSS\Property\Selector; -use Sabberworm\CSS\Rule\Rule; use Sabberworm\CSS\Settings; /** @@ -182,13 +182,13 @@ public function getRuleSet(): RuleSet /** * @see RuleSet::addRule() */ - public function addRule(Rule $ruleToAdd, ?Rule $sibling = null): void + public function addRule(Declaration $declarationToAdd, ?Declaration $sibling = null): void { - $this->ruleSet->addRule($ruleToAdd, $sibling); + $this->ruleSet->addRule($declarationToAdd, $sibling); } /** - * @return array, Rule> + * @return array, Declaration> * * @see RuleSet::getRules() */ @@ -198,17 +198,17 @@ public function getRules(?string $searchPattern = null): array } /** - * @param array $rules + * @param array $declarations * * @see RuleSet::setRules() */ - public function setRules(array $rules): void + public function setRules(array $declarations): void { - $this->ruleSet->setRules($rules); + $this->ruleSet->setRules($declarations); } /** - * @return array + * @return array * * @see RuleSet::getRulesAssoc() */ @@ -220,9 +220,9 @@ public function getRulesAssoc(?string $searchPattern = null): array /** * @see RuleSet::removeRule() */ - public function removeRule(Rule $ruleToRemove): void + public function removeRule(Declaration $declarationToRemove): void { - $this->ruleSet->removeRule($ruleToRemove); + $this->ruleSet->removeRule($declarationToRemove); } /** diff --git a/src/RuleSet/RuleContainer.php b/src/RuleSet/RuleContainer.php index 0c6c5936c..82ec76ef5 100644 --- a/src/RuleSet/RuleContainer.php +++ b/src/RuleSet/RuleContainer.php @@ -4,33 +4,33 @@ namespace Sabberworm\CSS\RuleSet; -use Sabberworm\CSS\Rule\Rule; +use Sabberworm\CSS\Property\Declaration; /** * Represents a CSS item that contains `Rules`, defining the methods to manipulate them. */ interface RuleContainer { - public function addRule(Rule $ruleToAdd, ?Rule $sibling = null): void; + public function addRule(Declaration $declarationToAdd, ?Declaration $sibling = null): void; - public function removeRule(Rule $ruleToRemove): void; + public function removeRule(Declaration $declarationToRemove): void; public function removeMatchingRules(string $searchPattern): void; public function removeAllRules(): void; /** - * @param array $rules + * @param array $declarations */ - public function setRules(array $rules): void; + public function setRules(array $declarations): void; /** - * @return array, Rule> + * @return array, Declaration> */ public function getRules(?string $searchPattern = null): array; /** - * @return array + * @return array */ public function getRulesAssoc(?string $searchPattern = null): array; } diff --git a/src/RuleSet/RuleSet.php b/src/RuleSet/RuleSet.php index e1086328c..2d6d93933 100644 --- a/src/RuleSet/RuleSet.php +++ b/src/RuleSet/RuleSet.php @@ -13,16 +13,16 @@ use Sabberworm\CSS\Parsing\UnexpectedTokenException; use Sabberworm\CSS\Position\Position; use Sabberworm\CSS\Position\Positionable; -use Sabberworm\CSS\Rule\Rule; +use Sabberworm\CSS\Property\Declaration; /** - * This class is a container for individual 'Rule's. + * This class is a container for individual `Declaration`s. * * The most common form of a rule set is one constrained by a selector, i.e., a `DeclarationBlock`. * However, unknown `AtRule`s (like `@font-face`) are rule sets as well. * - * If you want to manipulate a `RuleSet`, use the methods `addRule(Rule $rule)`, `getRules()` and `removeRule($rule)` - * (which accepts either a `Rule` or a rule name; optionally suffixed by a dash to remove all related rules). + * If you want to manipulate a `RuleSet`, use the methods `addRule()`, `getRules()` and `removeRule()` + * (which accepts either a `Declaration` or a rule name; optionally suffixed by a dash to remove all related rules). * * Note that `CSSListItem` extends both `Commentable` and `Renderable`, so those interfaces must also be implemented. */ @@ -35,9 +35,9 @@ class RuleSet implements CSSElement, CSSListItem, Positionable, RuleContainer * the rules in this rule set, using the property name as the key, * with potentially multiple rules per property name. * - * @var array, Rule>> + * @var array, Declaration>> */ - private $rules = []; + private $declarations = []; /** * @param int<1, max>|null $lineNumber @@ -64,10 +64,10 @@ public static function parseRuleSet(ParserState $parserState, RuleSet $ruleSet): if ($parserState->comes('}')) { break; } - $rule = null; + $declaration = null; if ($parserState->getSettings()->usesLenientParsing()) { try { - $rule = Rule::parse($parserState, $commentsBeforeRule); + $declaration = Declaration::parse($parserState, $commentsBeforeRule); } catch (UnexpectedTokenException $e) { try { $consumedText = $parserState->consumeUntil(["\n", ';', '}'], true); @@ -85,10 +85,10 @@ public static function parseRuleSet(ParserState $parserState, RuleSet $ruleSet): } } } else { - $rule = Rule::parse($parserState, $commentsBeforeRule); + $declaration = Declaration::parse($parserState, $commentsBeforeRule); } - if ($rule instanceof Rule) { - $ruleSet->addRule($rule); + if ($declaration instanceof Declaration) { + $ruleSet->addRule($declaration); } } $parserState->consume('}'); @@ -96,31 +96,31 @@ public static function parseRuleSet(ParserState $parserState, RuleSet $ruleSet): /** * @throws \UnexpectedValueException - * if the last `Rule` is needed as a basis for setting position, but does not have a valid position, + * if the last `Declaration` is needed as a basis for setting position, but does not have a valid position, * which should never happen */ - public function addRule(Rule $ruleToAdd, ?Rule $sibling = null): void + public function addRule(Declaration $declarationToAdd, ?Declaration $sibling = null): void { - $propertyName = $ruleToAdd->getRule(); - if (!isset($this->rules[$propertyName])) { - $this->rules[$propertyName] = []; + $propertyName = $declarationToAdd->getRule(); + if (!isset($this->declarations[$propertyName])) { + $this->declarations[$propertyName] = []; } - $position = \count($this->rules[$propertyName]); + $position = \count($this->declarations[$propertyName]); if ($sibling !== null) { $siblingIsInSet = false; - $siblingPosition = \array_search($sibling, $this->rules[$propertyName], true); + $siblingPosition = \array_search($sibling, $this->declarations[$propertyName], true); if ($siblingPosition !== false) { $siblingIsInSet = true; $position = $siblingPosition; } else { $siblingIsInSet = $this->hasRule($sibling); if ($siblingIsInSet) { - // Maintain ordering within `$this->rules[$propertyName]` - // by inserting before first `Rule` with a same-or-later position than the sibling. - foreach ($this->rules[$propertyName] as $index => $rule) { - if (self::comparePositionable($rule, $sibling) >= 0) { + // Maintain ordering within `$this->declarations[$propertyName]` + // by inserting before first `Declaration` with a same-or-later position than the sibling. + foreach ($this->declarations[$propertyName] as $index => $declaration) { + if (self::comparePositionable($declaration, $sibling) >= 0) { $position = $index; break; } @@ -131,49 +131,49 @@ public function addRule(Rule $ruleToAdd, ?Rule $sibling = null): void // Increment column number of all existing rules on same line, starting at sibling $siblingLineNumber = $sibling->getLineNumber(); $siblingColumnNumber = $sibling->getColumnNumber(); - foreach ($this->rules as $rulesForAProperty) { - foreach ($rulesForAProperty as $rule) { + foreach ($this->declarations as $declarationsForAProperty) { + foreach ($declarationsForAProperty as $declaration) { if ( - $rule->getLineNumber() === $siblingLineNumber && - $rule->getColumnNumber() >= $siblingColumnNumber + $declaration->getLineNumber() === $siblingLineNumber && + $declaration->getColumnNumber() >= $siblingColumnNumber ) { - $rule->setPosition($siblingLineNumber, $rule->getColumnNumber() + 1); + $declaration->setPosition($siblingLineNumber, $declaration->getColumnNumber() + 1); } } } - $ruleToAdd->setPosition($siblingLineNumber, $siblingColumnNumber); + $declarationToAdd->setPosition($siblingLineNumber, $siblingColumnNumber); } } - if ($ruleToAdd->getLineNumber() === null) { + if ($declarationToAdd->getLineNumber() === null) { //this node is added manually, give it the next best line - $columnNumber = $ruleToAdd->getColumnNumber() ?? 0; - $rules = $this->getRules(); - $rulesCount = \count($rules); - if ($rulesCount > 0) { - $last = $rules[$rulesCount - 1]; + $columnNumber = $declarationToAdd->getColumnNumber() ?? 0; + $declarations = $this->getRules(); + $declarationsCount = \count($declarations); + if ($declarationsCount > 0) { + $last = $declarations[$declarationsCount - 1]; $lastsLineNumber = $last->getLineNumber(); if (!\is_int($lastsLineNumber)) { throw new \UnexpectedValueException( - 'A Rule without a line number was found during addRule', + 'A Declaration without a line number was found during addRule', 1750718399 ); } - $ruleToAdd->setPosition($lastsLineNumber + 1, $columnNumber); + $declarationToAdd->setPosition($lastsLineNumber + 1, $columnNumber); } else { - $ruleToAdd->setPosition(1, $columnNumber); + $declarationToAdd->setPosition(1, $columnNumber); } - } elseif ($ruleToAdd->getColumnNumber() === null) { - $ruleToAdd->setPosition($ruleToAdd->getLineNumber(), 0); + } elseif ($declarationToAdd->getColumnNumber() === null) { + $declarationToAdd->setPosition($declarationToAdd->getLineNumber(), 0); } - \array_splice($this->rules[$propertyName], $position, 0, [$ruleToAdd]); + \array_splice($this->declarations[$propertyName], $position, 0, [$declarationToAdd]); } /** * Returns all rules matching the given rule name * - * @example $ruleSet->getRules('font') // returns array(0 => $rule, …) or array(). + * @example $ruleSet->getRules('font') // returns array(0 => $declaration, …) or array(). * * @example $ruleSet->getRules('font-') * //returns an array of all rules either beginning with font- or matching font. @@ -183,12 +183,12 @@ public function addRule(Rule $ruleToAdd, ?Rule $sibling = null): void * If the pattern ends with a dash, all rules starting with the pattern are returned * as well as one matching the pattern with the dash excluded. * - * @return array, Rule> + * @return array, Declaration> */ public function getRules(?string $searchPattern = null): array { $result = []; - foreach ($this->rules as $propertyName => $rules) { + foreach ($this->declarations as $propertyName => $declarations) { // Either no search rule is given or the search rule matches the found rule exactly // or the search rule ends in “-” and the found rule starts with the search rule. if ( @@ -199,7 +199,7 @@ public function getRules(?string $searchPattern = null): array || $propertyName === \substr($searchPattern, 0, -1)) ) ) { - $result = \array_merge($result, $rules); + $result = \array_merge($result, $declarations); } } \usort($result, [self::class, 'comparePositionable']); @@ -210,13 +210,13 @@ public function getRules(?string $searchPattern = null): array /** * Overrides all the rules of this set. * - * @param array $rules The rules to override with. + * @param array $declarations The rules to override with. */ - public function setRules(array $rules): void + public function setRules(array $declarations): void { - $this->rules = []; - foreach ($rules as $rule) { - $this->addRule($rule); + $this->declarations = []; + foreach ($declarations as $declaration) { + $this->addRule($declaration); } } @@ -233,31 +233,31 @@ public function setRules(array $rules): void * all rules starting with the pattern are returned as well as one matching the pattern with the dash * excluded. * - * @return array + * @return array */ public function getRulesAssoc(?string $searchPattern = null): array { - /** @var array $result */ + /** @var array $result */ $result = []; - foreach ($this->getRules($searchPattern) as $rule) { - $result[$rule->getRule()] = $rule; + foreach ($this->getRules($searchPattern) as $declaration) { + $result[$declaration->getRule()] = $declaration; } return $result; } /** - * Removes a `Rule` from this `RuleSet` by identity. + * Removes a `Declaration` from this `RuleSet` by identity. */ - public function removeRule(Rule $ruleToRemove): void + public function removeRule(Declaration $declarationToRemove): void { - $nameOfPropertyToRemove = $ruleToRemove->getRule(); - if (!isset($this->rules[$nameOfPropertyToRemove])) { + $nameOfPropertyToRemove = $declarationToRemove->getRule(); + if (!isset($this->declarations[$nameOfPropertyToRemove])) { return; } - foreach ($this->rules[$nameOfPropertyToRemove] as $key => $rule) { - if ($rule === $ruleToRemove) { - unset($this->rules[$nameOfPropertyToRemove][$key]); + foreach ($this->declarations[$nameOfPropertyToRemove] as $key => $declaration) { + if ($declaration === $declarationToRemove) { + unset($this->declarations[$nameOfPropertyToRemove][$key]); } } } @@ -273,7 +273,7 @@ public function removeRule(Rule $ruleToRemove): void */ public function removeMatchingRules(string $searchPattern): void { - foreach ($this->rules as $propertyName => $rules) { + foreach ($this->declarations as $propertyName => $declarations) { // Either the search rule matches the found rule exactly // or the search rule ends in “-” and the found rule starts with the search rule or equals it // (without the trailing dash). @@ -283,14 +283,14 @@ public function removeMatchingRules(string $searchPattern): void && (\strpos($propertyName, $searchPattern) === 0 || $propertyName === \substr($searchPattern, 0, -1))) ) { - unset($this->rules[$propertyName]); + unset($this->declarations[$propertyName]); } } } public function removeAllRules(): void { - $this->rules = []; + $this->declarations = []; } /** @@ -306,11 +306,13 @@ protected function renderRules(OutputFormat $outputFormat): string $result = ''; $isFirst = true; $nextLevelFormat = $outputFormat->nextLevel(); - foreach ($this->getRules() as $rule) { + foreach ($this->getRules() as $declaration) { $nextLevelFormatter = $nextLevelFormat->getFormatter(); - $renderedRule = $nextLevelFormatter->safely(static function () use ($rule, $nextLevelFormat): string { - return $rule->render($nextLevelFormat); - }); + $renderedRule = $nextLevelFormatter->safely( + static function () use ($declaration, $nextLevelFormat): string { + return $declaration->render($nextLevelFormat); + } + ); if ($renderedRule === null) { continue; } @@ -353,7 +355,7 @@ private static function comparePositionable(Positionable $first, Positionable $s $secondsLineNumber = $second->getLineNumber(); if (!\is_int($firstsLineNumber) || !\is_int($secondsLineNumber)) { throw new \UnexpectedValueException( - 'A Rule without a line number was passed to comparePositionable', + 'A Declaration without a line number was passed to comparePositionable', 1750637683 ); } @@ -363,7 +365,7 @@ private static function comparePositionable(Positionable $first, Positionable $s $secondsColumnNumber = $second->getColumnNumber(); if (!\is_int($firstsColumnNumber) || !\is_int($secondsColumnNumber)) { throw new \UnexpectedValueException( - 'A Rule without a column number was passed to comparePositionable', + 'A Declaration without a column number was passed to comparePositionable', 1750637761 ); } @@ -373,10 +375,10 @@ private static function comparePositionable(Positionable $first, Positionable $s return $firstsLineNumber - $secondsLineNumber; } - private function hasRule(Rule $rule): bool + private function hasRule(Declaration $declaration): bool { - foreach ($this->rules as $rulesForAProperty) { - if (\in_array($rule, $rulesForAProperty, true)) { + foreach ($this->declarations as $declarationsForAProperty) { + if (\in_array($declaration, $declarationsForAProperty, true)) { return true; } } From 567e3907856a0b5542e70746bdade422ee0081a4 Mon Sep 17 00:00:00 2001 From: JakeQZ Date: Fri, 13 Feb 2026 21:45:21 +0000 Subject: [PATCH 101/136] [TASK] Use `Property\Declaration` in `DeclarationBlockTest` (#1514) This replaces `Rule\Rule`, following #1508. Also use `getPropertyName()` rather than `getRule()` which was deprecated in #1506. This covers both the 'functional' and 'unit' flavours of the `TestCase`, and the trait that does most of the heavy lifting for the 'unit' flavour. --- .../RuleSet/DeclarationBlockTest.php | 22 +- tests/Unit/RuleSet/DeclarationBlockTest.php | 14 +- tests/Unit/RuleSet/RuleContainerTest.php | 520 +++++++++--------- 3 files changed, 278 insertions(+), 278 deletions(-) diff --git a/tests/Functional/RuleSet/DeclarationBlockTest.php b/tests/Functional/RuleSet/DeclarationBlockTest.php index fe6b9e51c..3265c007f 100644 --- a/tests/Functional/RuleSet/DeclarationBlockTest.php +++ b/tests/Functional/RuleSet/DeclarationBlockTest.php @@ -7,8 +7,8 @@ use PHPUnit\Framework\TestCase; use Sabberworm\CSS\OutputFormat; use Sabberworm\CSS\Parsing\ParserState; +use Sabberworm\CSS\Property\Declaration; use Sabberworm\CSS\Property\Selector; -use Sabberworm\CSS\Rule\Rule; use Sabberworm\CSS\RuleSet\DeclarationBlock; use Sabberworm\CSS\Settings; @@ -46,22 +46,22 @@ public function parseReturnsNullForInvalidDeclarationBlock(string $invalidDeclar /** * @test */ - public function rendersRulesInOrderProvided(): void + public function rendersDeclarationsInOrderProvided(): void { $declarationBlock = new DeclarationBlock(); $declarationBlock->setSelectors([new Selector('.test')]); - $rule1 = new Rule('background-color'); - $rule1->setValue('transparent'); - $declarationBlock->addRule($rule1); + $declaration1 = new Declaration('background-color'); + $declaration1->setValue('transparent'); + $declarationBlock->addRule($declaration1); - $rule2 = new Rule('background'); - $rule2->setValue('#222'); - $declarationBlock->addRule($rule2); + $declaration2 = new Declaration('background'); + $declaration2->setValue('#222'); + $declarationBlock->addRule($declaration2); - $rule3 = new Rule('background-color'); - $rule3->setValue('#fff'); - $declarationBlock->addRule($rule3); + $declaration3 = new Declaration('background-color'); + $declaration3->setValue('#fff'); + $declarationBlock->addRule($declaration3); $expectedRendering = 'background-color: transparent;background: #222;background-color: #fff'; self::assertStringContainsString($expectedRendering, $declarationBlock->render(new OutputFormat())); diff --git a/tests/Unit/RuleSet/DeclarationBlockTest.php b/tests/Unit/RuleSet/DeclarationBlockTest.php index b9c908b49..75213fc10 100644 --- a/tests/Unit/RuleSet/DeclarationBlockTest.php +++ b/tests/Unit/RuleSet/DeclarationBlockTest.php @@ -11,8 +11,8 @@ use Sabberworm\CSS\Parsing\ParserState; use Sabberworm\CSS\Parsing\UnexpectedTokenException; use Sabberworm\CSS\Position\Positionable; +use Sabberworm\CSS\Property\Declaration; use Sabberworm\CSS\Property\Selector; -use Sabberworm\CSS\Rule\Rule; use Sabberworm\CSS\RuleSet\DeclarationBlock; use Sabberworm\CSS\RuleSet\RuleSet; use Sabberworm\CSS\Settings; @@ -391,7 +391,7 @@ public function getRuleSetOnVirginReturnsARuleSet(): void */ public function getRuleSetAfterRulesSetReturnsARuleSet(): void { - $this->subject->setRules([new Rule('color')]); + $this->subject->setRules([new Declaration('color')]); $result = $this->subject->getRuleSet(); @@ -401,7 +401,7 @@ public function getRuleSetAfterRulesSetReturnsARuleSet(): void /** * @test */ - public function getRuleSetOnVirginReturnsObjectWithoutRules(): void + public function getRuleSetOnVirginReturnsObjectWithoutDeclarations(): void { $result = $this->subject->getRuleSet(); @@ -415,14 +415,14 @@ public function getRuleSetOnVirginReturnsObjectWithoutRules(): void * * @dataProvider providePropertyNames */ - public function getRuleSetReturnsObjectWithRulesSet(array $propertyNamesToSet): void + public function getRuleSetReturnsObjectWithDeclarationsSet(array $propertyNamesToSet): void { - $rules = self::createRulesFromPropertyNames($propertyNamesToSet); - $this->subject->setRules($rules); + $declarations = self::createDeclarationsFromPropertyNames($propertyNamesToSet); + $this->subject->setRules($declarations); $result = $this->subject->getRuleSet(); - self::assertSame($rules, $result->getRules()); + self::assertSame($declarations, $result->getRules()); } /** diff --git a/tests/Unit/RuleSet/RuleContainerTest.php b/tests/Unit/RuleSet/RuleContainerTest.php index bcb641a22..001505f55 100644 --- a/tests/Unit/RuleSet/RuleContainerTest.php +++ b/tests/Unit/RuleSet/RuleContainerTest.php @@ -5,7 +5,7 @@ namespace Sabberworm\CSS\Tests\Unit\RuleSet; use PHPUnit\Framework\TestCase; -use Sabberworm\CSS\Rule\Rule; +use Sabberworm\CSS\Property\Declaration; use Sabberworm\CSS\RuleSet\RuleContainer; use TRegx\PhpUnit\DataProviders\DataProvider; @@ -66,17 +66,17 @@ public static function provideInitialPropertyNamesAndAnotherPropertyName(): Data * * @dataProvider provideInitialPropertyNamesAndAnotherPropertyName */ - public function addRuleWithoutPositionWithoutSiblingAddsRuleAfterInitialRules( + public function addRuleWithoutPositionWithoutSiblingAddsDeclarationAfterInitialDeclarations( array $initialPropertyNames, string $propertyNameToAdd ): void { - $ruleToAdd = new Rule($propertyNameToAdd); - $this->setRulesFromPropertyNames($initialPropertyNames); + $declarationToAdd = new Declaration($propertyNameToAdd); + $this->setDeclarationsFromPropertyNames($initialPropertyNames); - $this->subject->addRule($ruleToAdd); + $this->subject->addRule($declarationToAdd); - $rules = $this->subject->getRules(); - self::assertSame($ruleToAdd, \end($rules)); + $declarations = $this->subject->getRules(); + self::assertSame($declarationToAdd, \end($declarations)); } /** @@ -90,13 +90,13 @@ public function addRuleWithoutPositionWithoutSiblingSetsValidLineNumber( array $initialPropertyNames, string $propertyNameToAdd ): void { - $ruleToAdd = new Rule($propertyNameToAdd); - $this->setRulesFromPropertyNames($initialPropertyNames); + $declarationToAdd = new Declaration($propertyNameToAdd); + $this->setDeclarationsFromPropertyNames($initialPropertyNames); - $this->subject->addRule($ruleToAdd); + $this->subject->addRule($declarationToAdd); - self::assertIsInt($ruleToAdd->getLineNumber(), 'line number not set'); - self::assertGreaterThanOrEqual(1, $ruleToAdd->getLineNumber(), 'line number not valid'); + self::assertIsInt($declarationToAdd->getLineNumber(), 'line number not set'); + self::assertGreaterThanOrEqual(1, $declarationToAdd->getLineNumber(), 'line number not valid'); } /** @@ -110,13 +110,13 @@ public function addRuleWithoutPositionWithoutSiblingSetsValidColumnNumber( array $initialPropertyNames, string $propertyNameToAdd ): void { - $ruleToAdd = new Rule($propertyNameToAdd); - $this->setRulesFromPropertyNames($initialPropertyNames); + $declarationToAdd = new Declaration($propertyNameToAdd); + $this->setDeclarationsFromPropertyNames($initialPropertyNames); - $this->subject->addRule($ruleToAdd); + $this->subject->addRule($declarationToAdd); - self::assertIsInt($ruleToAdd->getColumnNumber(), 'column number not set'); - self::assertGreaterThanOrEqual(0, $ruleToAdd->getColumnNumber(), 'column number not valid'); + self::assertIsInt($declarationToAdd->getColumnNumber(), 'column number not set'); + self::assertGreaterThanOrEqual(0, $declarationToAdd->getColumnNumber(), 'column number not valid'); } /** @@ -126,17 +126,17 @@ public function addRuleWithoutPositionWithoutSiblingSetsValidColumnNumber( * * @param list $initialPropertyNames */ - public function addRuleWithOnlyLineNumberWithoutSiblingAddsRule( + public function addRuleWithOnlyLineNumberWithoutSiblingAddsDeclaration( array $initialPropertyNames, string $propertyNameToAdd ): void { - $ruleToAdd = new Rule($propertyNameToAdd); - $ruleToAdd->setPosition(42); - $this->setRulesFromPropertyNames($initialPropertyNames); + $declarationToAdd = new Declaration($propertyNameToAdd); + $declarationToAdd->setPosition(42); + $this->setDeclarationsFromPropertyNames($initialPropertyNames); - $this->subject->addRule($ruleToAdd); + $this->subject->addRule($declarationToAdd); - self::assertContains($ruleToAdd, $this->subject->getRules()); + self::assertContains($declarationToAdd, $this->subject->getRules()); } /** @@ -150,14 +150,14 @@ public function addRuleWithOnlyLineNumberWithoutSiblingSetsColumnNumber( array $initialPropertyNames, string $propertyNameToAdd ): void { - $ruleToAdd = new Rule($propertyNameToAdd); - $ruleToAdd->setPosition(42); - $this->setRulesFromPropertyNames($initialPropertyNames); + $declarationToAdd = new Declaration($propertyNameToAdd); + $declarationToAdd->setPosition(42); + $this->setDeclarationsFromPropertyNames($initialPropertyNames); - $this->subject->addRule($ruleToAdd); + $this->subject->addRule($declarationToAdd); - self::assertIsInt($ruleToAdd->getColumnNumber(), 'column number not set'); - self::assertGreaterThanOrEqual(0, $ruleToAdd->getColumnNumber(), 'column number not valid'); + self::assertIsInt($declarationToAdd->getColumnNumber(), 'column number not set'); + self::assertGreaterThanOrEqual(0, $declarationToAdd->getColumnNumber(), 'column number not valid'); } /** @@ -171,13 +171,13 @@ public function addRuleWithOnlyLineNumberWithoutSiblingPreservesLineNumber( array $initialPropertyNames, string $propertyNameToAdd ): void { - $ruleToAdd = new Rule($propertyNameToAdd); - $ruleToAdd->setPosition(42); - $this->setRulesFromPropertyNames($initialPropertyNames); + $declarationToAdd = new Declaration($propertyNameToAdd); + $declarationToAdd->setPosition(42); + $this->setDeclarationsFromPropertyNames($initialPropertyNames); - $this->subject->addRule($ruleToAdd); + $this->subject->addRule($declarationToAdd); - self::assertSame(42, $ruleToAdd->getLineNumber(), 'line number not preserved'); + self::assertSame(42, $declarationToAdd->getLineNumber(), 'line number not preserved'); } /** @@ -187,18 +187,18 @@ public function addRuleWithOnlyLineNumberWithoutSiblingPreservesLineNumber( * * @param list $initialPropertyNames */ - public function addRuleWithOnlyColumnNumberWithoutSiblingAddsRuleAfterInitialRules( + public function addRuleWithOnlyColumnNumberWithoutSiblingAddsDeclarationAfterInitialDeclarations( array $initialPropertyNames, string $propertyNameToAdd ): void { - $ruleToAdd = new Rule($propertyNameToAdd); - $ruleToAdd->setPosition(null, 42); - $this->setRulesFromPropertyNames($initialPropertyNames); + $declarationToAdd = new Declaration($propertyNameToAdd); + $declarationToAdd->setPosition(null, 42); + $this->setDeclarationsFromPropertyNames($initialPropertyNames); - $this->subject->addRule($ruleToAdd); + $this->subject->addRule($declarationToAdd); - $rules = $this->subject->getRules(); - self::assertSame($ruleToAdd, \end($rules)); + $declarations = $this->subject->getRules(); + self::assertSame($declarationToAdd, \end($declarations)); } /** @@ -212,14 +212,14 @@ public function addRuleWithOnlyColumnNumberWithoutSiblingSetsLineNumber( array $initialPropertyNames, string $propertyNameToAdd ): void { - $ruleToAdd = new Rule($propertyNameToAdd); - $ruleToAdd->setPosition(null, 42); - $this->setRulesFromPropertyNames($initialPropertyNames); + $declarationToAdd = new Declaration($propertyNameToAdd); + $declarationToAdd->setPosition(null, 42); + $this->setDeclarationsFromPropertyNames($initialPropertyNames); - $this->subject->addRule($ruleToAdd); + $this->subject->addRule($declarationToAdd); - self::assertIsInt($ruleToAdd->getLineNumber(), 'line number not set'); - self::assertGreaterThanOrEqual(1, $ruleToAdd->getLineNumber(), 'line number not valid'); + self::assertIsInt($declarationToAdd->getLineNumber(), 'line number not set'); + self::assertGreaterThanOrEqual(1, $declarationToAdd->getLineNumber(), 'line number not valid'); } /** @@ -233,13 +233,13 @@ public function addRuleWithOnlyColumnNumberWithoutSiblingPreservesColumnNumber( array $initialPropertyNames, string $propertyNameToAdd ): void { - $ruleToAdd = new Rule($propertyNameToAdd); - $ruleToAdd->setPosition(null, 42); - $this->setRulesFromPropertyNames($initialPropertyNames); + $declarationToAdd = new Declaration($propertyNameToAdd); + $declarationToAdd->setPosition(null, 42); + $this->setDeclarationsFromPropertyNames($initialPropertyNames); - $this->subject->addRule($ruleToAdd); + $this->subject->addRule($declarationToAdd); - self::assertSame(42, $ruleToAdd->getColumnNumber(), 'column number not preserved'); + self::assertSame(42, $declarationToAdd->getColumnNumber(), 'column number not preserved'); } /** @@ -249,17 +249,17 @@ public function addRuleWithOnlyColumnNumberWithoutSiblingPreservesColumnNumber( * * @param list $initialPropertyNames */ - public function addRuleWithCompletePositionWithoutSiblingAddsRule( + public function addRuleWithCompletePositionWithoutSiblingAddsDeclaration( array $initialPropertyNames, string $propertyNameToAdd ): void { - $ruleToAdd = new Rule($propertyNameToAdd); - $ruleToAdd->setPosition(42, 64); - $this->setRulesFromPropertyNames($initialPropertyNames); + $declarationToAdd = new Declaration($propertyNameToAdd); + $declarationToAdd->setPosition(42, 64); + $this->setDeclarationsFromPropertyNames($initialPropertyNames); - $this->subject->addRule($ruleToAdd); + $this->subject->addRule($declarationToAdd); - self::assertContains($ruleToAdd, $this->subject->getRules()); + self::assertContains($declarationToAdd, $this->subject->getRules()); } /** @@ -273,14 +273,14 @@ public function addRuleWithCompletePositionWithoutSiblingPreservesPosition( array $initialPropertyNames, string $propertyNameToAdd ): void { - $ruleToAdd = new Rule($propertyNameToAdd); - $ruleToAdd->setPosition(42, 64); - $this->setRulesFromPropertyNames($initialPropertyNames); + $declarationToAdd = new Declaration($propertyNameToAdd); + $declarationToAdd->setPosition(42, 64); + $this->setDeclarationsFromPropertyNames($initialPropertyNames); - $this->subject->addRule($ruleToAdd); + $this->subject->addRule($declarationToAdd); - self::assertSame(42, $ruleToAdd->getLineNumber(), 'line number not preserved'); - self::assertSame(64, $ruleToAdd->getColumnNumber(), 'column number not preserved'); + self::assertSame(42, $declarationToAdd->getLineNumber(), 'line number not preserved'); + self::assertSame(64, $declarationToAdd->getColumnNumber(), 'column number not preserved'); } /** @@ -290,7 +290,7 @@ public static function provideInitialPropertyNamesAndIndexOfOne(): array { $initialPropertyNamesSets = self::providePropertyNames(); - // Provide sets with each possible index for the initially set `Rule`s. + // Provide sets with each possible index for the initially set `Declaration`s. $initialPropertyNamesAndIndexSets = []; foreach ($initialPropertyNamesSets as $setName => $data) { $initialPropertyNames = $data[0]; @@ -322,21 +322,21 @@ public static function provideInitialPropertyNamesAndSiblingIndexAndPropertyName * * @dataProvider provideInitialPropertyNamesAndSiblingIndexAndPropertyNameToAdd */ - public function addRuleWithSiblingInsertsRuleBeforeSibling( + public function addRuleWithSiblingInsertsDeclarationBeforeSibling( array $initialPropertyNames, int $siblingIndex, string $propertyNameToAdd ): void { - $ruleToAdd = new Rule($propertyNameToAdd); - $this->setRulesFromPropertyNames($initialPropertyNames); + $declarationToAdd = new Declaration($propertyNameToAdd); + $this->setDeclarationsFromPropertyNames($initialPropertyNames); $sibling = $this->subject->getRules()[$siblingIndex]; - $this->subject->addRule($ruleToAdd, $sibling); + $this->subject->addRule($declarationToAdd, $sibling); - $rules = $this->subject->getRules(); - $siblingPosition = \array_search($sibling, $rules, true); + $declarations = $this->subject->getRules(); + $siblingPosition = \array_search($sibling, $declarations, true); self::assertIsInt($siblingPosition); - self::assertSame($siblingPosition - 1, \array_search($ruleToAdd, $rules, true)); + self::assertSame($siblingPosition - 1, \array_search($declarationToAdd, $declarations, true)); } /** @@ -352,14 +352,14 @@ public function addRuleWithSiblingSetsValidLineNumber( int $siblingIndex, string $propertyNameToAdd ): void { - $ruleToAdd = new Rule($propertyNameToAdd); - $this->setRulesFromPropertyNames($initialPropertyNames); + $declarationToAdd = new Declaration($propertyNameToAdd); + $this->setDeclarationsFromPropertyNames($initialPropertyNames); $sibling = $this->subject->getRules()[$siblingIndex]; - $this->subject->addRule($ruleToAdd, $sibling); + $this->subject->addRule($declarationToAdd, $sibling); - self::assertIsInt($ruleToAdd->getLineNumber(), 'line number not set'); - self::assertGreaterThanOrEqual(1, $ruleToAdd->getLineNumber(), 'line number not valid'); + self::assertIsInt($declarationToAdd->getLineNumber(), 'line number not set'); + self::assertGreaterThanOrEqual(1, $declarationToAdd->getLineNumber(), 'line number not valid'); } /** @@ -375,14 +375,14 @@ public function addRuleWithSiblingSetsValidColumnNumber( int $siblingIndex, string $propertyNameToAdd ): void { - $ruleToAdd = new Rule($propertyNameToAdd); - $this->setRulesFromPropertyNames($initialPropertyNames); + $declarationToAdd = new Declaration($propertyNameToAdd); + $this->setDeclarationsFromPropertyNames($initialPropertyNames); $sibling = $this->subject->getRules()[$siblingIndex]; - $this->subject->addRule($ruleToAdd, $sibling); + $this->subject->addRule($declarationToAdd, $sibling); - self::assertIsInt($ruleToAdd->getColumnNumber(), 'column number not set'); - self::assertGreaterThanOrEqual(0, $ruleToAdd->getColumnNumber(), 'column number not valid'); + self::assertIsInt($declarationToAdd->getColumnNumber(), 'column number not set'); + self::assertGreaterThanOrEqual(0, $declarationToAdd->getColumnNumber(), 'column number not valid'); } /** @@ -392,19 +392,19 @@ public function addRuleWithSiblingSetsValidColumnNumber( * * @dataProvider provideInitialPropertyNamesAndAnotherPropertyName */ - public function addRuleWithSiblingNotInSetAddsRuleAfterInitialRules( + public function addRuleWithSiblingNotInSetAddsDeclarationAfterInitialDeclarations( array $initialPropertyNames, string $propertyNameToAdd ): void { - $ruleToAdd = new Rule($propertyNameToAdd); - $this->setRulesFromPropertyNames($initialPropertyNames); + $declarationToAdd = new Declaration($propertyNameToAdd); + $this->setDeclarationsFromPropertyNames($initialPropertyNames); // `display` is sometimes in `$initialPropertyNames` and sometimes the `$propertyNameToAdd`. // Choosing this for the bogus sibling allows testing all combinations of whether it is or isn't. - $this->subject->addRule($ruleToAdd, new Rule('display')); + $this->subject->addRule($declarationToAdd, new Declaration('display')); - $rules = $this->subject->getRules(); - self::assertSame($ruleToAdd, \end($rules)); + $declarations = $this->subject->getRules(); + self::assertSame($declarationToAdd, \end($declarations)); } /** @@ -418,15 +418,15 @@ public function addRuleWithSiblingNotInSetSetsValidLineNumber( array $initialPropertyNames, string $propertyNameToAdd ): void { - $ruleToAdd = new Rule($propertyNameToAdd); - $this->setRulesFromPropertyNames($initialPropertyNames); + $declarationToAdd = new Declaration($propertyNameToAdd); + $this->setDeclarationsFromPropertyNames($initialPropertyNames); // `display` is sometimes in `$initialPropertyNames` and sometimes the `$propertyNameToAdd`. // Choosing this for the bogus sibling allows testing all combinations of whether it is or isn't. - $this->subject->addRule($ruleToAdd, new Rule('display')); + $this->subject->addRule($declarationToAdd, new Declaration('display')); - self::assertIsInt($ruleToAdd->getLineNumber(), 'line number not set'); - self::assertGreaterThanOrEqual(1, $ruleToAdd->getLineNumber(), 'line number not valid'); + self::assertIsInt($declarationToAdd->getLineNumber(), 'line number not set'); + self::assertGreaterThanOrEqual(1, $declarationToAdd->getLineNumber(), 'line number not valid'); } /** @@ -440,15 +440,15 @@ public function addRuleWithSiblingNotInSetSetsValidColumnNumber( array $initialPropertyNames, string $propertyNameToAdd ): void { - $ruleToAdd = new Rule($propertyNameToAdd); - $this->setRulesFromPropertyNames($initialPropertyNames); + $declarationToAdd = new Declaration($propertyNameToAdd); + $this->setDeclarationsFromPropertyNames($initialPropertyNames); // `display` is sometimes in `$initialPropertyNames` and sometimes the `$propertyNameToAdd`. // Choosing this for the bogus sibling allows testing all combinations of whether it is or isn't. - $this->subject->addRule($ruleToAdd, new Rule('display')); + $this->subject->addRule($declarationToAdd, new Declaration('display')); - self::assertIsInt($ruleToAdd->getColumnNumber(), 'column number not set'); - self::assertGreaterThanOrEqual(0, $ruleToAdd->getColumnNumber(), 'column number not valid'); + self::assertIsInt($declarationToAdd->getColumnNumber(), 'column number not set'); + self::assertGreaterThanOrEqual(0, $declarationToAdd->getColumnNumber(), 'column number not valid'); } /** @@ -459,14 +459,14 @@ public function addRuleWithSiblingNotInSetSetsValidColumnNumber( * * @dataProvider provideInitialPropertyNamesAndIndexOfOne */ - public function removeRuleRemovesRuleInSet(array $initialPropertyNames, int $indexToRemove): void + public function removeRuleRemovesDeclarationInSet(array $initialPropertyNames, int $indexToRemove): void { - $this->setRulesFromPropertyNames($initialPropertyNames); - $ruleToRemove = $this->subject->getRules()[$indexToRemove]; + $this->setDeclarationsFromPropertyNames($initialPropertyNames); + $declarationToRemove = $this->subject->getRules()[$indexToRemove]; - $this->subject->removeRule($ruleToRemove); + $this->subject->removeRule($declarationToRemove); - self::assertNotContains($ruleToRemove, $this->subject->getRules()); + self::assertNotContains($declarationToRemove, $this->subject->getRules()); } /** @@ -477,12 +477,12 @@ public function removeRuleRemovesRuleInSet(array $initialPropertyNames, int $ind * * @dataProvider provideInitialPropertyNamesAndIndexOfOne */ - public function removeRuleRemovesExactlyOneRule(array $initialPropertyNames, int $indexToRemove): void + public function removeRuleRemovesExactlyOneDeclaration(array $initialPropertyNames, int $indexToRemove): void { - $this->setRulesFromPropertyNames($initialPropertyNames); - $ruleToRemove = $this->subject->getRules()[$indexToRemove]; + $this->setDeclarationsFromPropertyNames($initialPropertyNames); + $declarationToRemove = $this->subject->getRules()[$indexToRemove]; - $this->subject->removeRule($ruleToRemove); + $this->subject->removeRule($declarationToRemove); self::assertCount(\count($initialPropertyNames) - 1, $this->subject->getRules()); } @@ -494,17 +494,17 @@ public function removeRuleRemovesExactlyOneRule(array $initialPropertyNames, int * * @dataProvider provideInitialPropertyNamesAndAnotherPropertyName */ - public function removeRuleWithRuleNotInSetKeepsSetUnchanged( + public function removeRuleWithDeclarationNotInSetKeepsSetUnchanged( array $initialPropertyNames, string $propertyNameToRemove ): void { - $this->setRulesFromPropertyNames($initialPropertyNames); - $initialRules = $this->subject->getRules(); - $ruleToRemove = new Rule($propertyNameToRemove); + $this->setDeclarationsFromPropertyNames($initialPropertyNames); + $initialDeclarations = $this->subject->getRules(); + $declarationToRemove = new Declaration($propertyNameToRemove); - $this->subject->removeRule($ruleToRemove); + $this->subject->removeRule($declarationToRemove); - self::assertSame($initialRules, $this->subject->getRules()); + self::assertSame($initialDeclarations, $this->subject->getRules()); } /** @@ -513,42 +513,42 @@ public function removeRuleWithRuleNotInSetKeepsSetUnchanged( public static function providePropertyNamesAndPropertyNameToRemoveAndExpectedRemainingPropertyNames(): array { return [ - 'removing single rule' => [ + 'removing single declaration' => [ ['color'], 'color', [], ], - 'removing first rule' => [ + 'removing first declaration' => [ ['color', 'display'], 'color', ['display'], ], - 'removing last rule' => [ + 'removing last declaration' => [ ['color', 'display'], 'display', ['color'], ], - 'removing middle rule' => [ + 'removing middle declaration' => [ ['color', 'display', 'width'], 'display', ['color', 'width'], ], - 'removing multiple rules' => [ + 'removing multiple declarations' => [ ['color', 'color'], 'color', [], ], - 'removing multiple rules with another kept' => [ + 'removing multiple declarations with another kept' => [ ['color', 'color', 'display'], 'color', ['display'], ], - 'removing nonexistent rule from empty list' => [ + 'removing nonexistent declaration from empty list' => [ [], 'color', [], ], - 'removing nonexistent rule from nonempty list' => [ + 'removing nonexistent declaration from nonempty list' => [ ['color', 'display'], 'width', ['color', 'display'], @@ -563,11 +563,11 @@ public static function providePropertyNamesAndPropertyNameToRemoveAndExpectedRem * * @dataProvider providePropertyNamesAndPropertyNameToRemoveAndExpectedRemainingPropertyNames */ - public function removeMatchingRulesRemovesRulesWithPropertyName( + public function removeMatchingRulesRemovesDeclarationsWithPropertyName( array $initialPropertyNames, string $propertyNameToRemove ): void { - $this->setRulesFromPropertyNames($initialPropertyNames); + $this->setDeclarationsFromPropertyNames($initialPropertyNames); $this->subject->removeMatchingRules($propertyNameToRemove); @@ -582,21 +582,21 @@ public function removeMatchingRulesRemovesRulesWithPropertyName( * * @dataProvider providePropertyNamesAndPropertyNameToRemoveAndExpectedRemainingPropertyNames */ - public function removeMatchingRulesWithPropertyNameKeepsOtherRules( + public function removeMatchingRulesWithPropertyNameKeepsOtherDeclarations( array $initialPropertyNames, string $propertyNameToRemove, array $expectedRemainingPropertyNames ): void { - $this->setRulesFromPropertyNames($initialPropertyNames); + $this->setDeclarationsFromPropertyNames($initialPropertyNames); $this->subject->removeMatchingRules($propertyNameToRemove); - $remainingRules = $this->subject->getRulesAssoc(); + $remainingDeclarations = $this->subject->getRulesAssoc(); if ($expectedRemainingPropertyNames === []) { - self::assertSame([], $remainingRules); + self::assertSame([], $remainingDeclarations); } foreach ($expectedRemainingPropertyNames as $expectedPropertyName) { - self::assertArrayHasKey($expectedPropertyName, $remainingRules); + self::assertArrayHasKey($expectedPropertyName, $remainingDeclarations); } } @@ -606,32 +606,32 @@ public function removeMatchingRulesWithPropertyNameKeepsOtherRules( public static function providePropertyNamesAndPropertyNamePrefixToRemoveAndExpectedRemainingPropertyNames(): array { return [ - 'removing shorthand rule' => [ + 'removing shorthand declaration' => [ ['font'], 'font', [], ], - 'removing longhand rule' => [ + 'removing longhand declaration' => [ ['font-size'], 'font', [], ], - 'removing shorthand and longhand rule' => [ + 'removing shorthand and longhand declaration' => [ ['font', 'font-size'], 'font', [], ], - 'removing shorthand rule with another kept' => [ + 'removing shorthand declaration with another kept' => [ ['font', 'color'], 'font', ['color'], ], - 'removing longhand rule with another kept' => [ + 'removing longhand declaration with another kept' => [ ['font-size', 'color'], 'font', ['color'], ], - 'keeping other rules whose property names begin with the same characters' => [ + 'keeping other declarations whose property names begin with the same characters' => [ ['contain', 'container', 'container-type'], 'contain', ['container', 'container-type'], @@ -646,18 +646,18 @@ public static function providePropertyNamesAndPropertyNamePrefixToRemoveAndExpec * * @dataProvider providePropertyNamesAndPropertyNamePrefixToRemoveAndExpectedRemainingPropertyNames */ - public function removeMatchingRulesRemovesRulesWithPropertyNamePrefix( + public function removeMatchingRulesRemovesDeclarationsWithPropertyNamePrefix( array $initialPropertyNames, string $propertyNamePrefix ): void { $propertyNamePrefixWithHyphen = $propertyNamePrefix . '-'; - $this->setRulesFromPropertyNames($initialPropertyNames); + $this->setDeclarationsFromPropertyNames($initialPropertyNames); $this->subject->removeMatchingRules($propertyNamePrefixWithHyphen); - $remainingRules = $this->subject->getRulesAssoc(); - self::assertArrayNotHasKey($propertyNamePrefix, $remainingRules); - foreach (\array_keys($remainingRules) as $remainingPropertyName) { + $remainingDeclarations = $this->subject->getRulesAssoc(); + self::assertArrayNotHasKey($propertyNamePrefix, $remainingDeclarations); + foreach (\array_keys($remainingDeclarations) as $remainingPropertyName) { self::assertStringStartsNotWith($propertyNamePrefixWithHyphen, $remainingPropertyName); } } @@ -670,22 +670,22 @@ public function removeMatchingRulesRemovesRulesWithPropertyNamePrefix( * * @dataProvider providePropertyNamesAndPropertyNamePrefixToRemoveAndExpectedRemainingPropertyNames */ - public function removeMatchingRulesWithPropertyNamePrefixKeepsOtherRules( + public function removeMatchingRulesWithPropertyNamePrefixKeepsOtherDeclarations( array $initialPropertyNames, string $propertyNamePrefix, array $expectedRemainingPropertyNames ): void { $propertyNamePrefixWithHyphen = $propertyNamePrefix . '-'; - $this->setRulesFromPropertyNames($initialPropertyNames); + $this->setDeclarationsFromPropertyNames($initialPropertyNames); $this->subject->removeMatchingRules($propertyNamePrefixWithHyphen); - $remainingRules = $this->subject->getRulesAssoc(); + $remainingDeclarations = $this->subject->getRulesAssoc(); if ($expectedRemainingPropertyNames === []) { - self::assertSame([], $remainingRules); + self::assertSame([], $remainingDeclarations); } foreach ($expectedRemainingPropertyNames as $expectedPropertyName) { - self::assertArrayHasKey($expectedPropertyName, $remainingRules); + self::assertArrayHasKey($expectedPropertyName, $remainingDeclarations); } } @@ -696,9 +696,9 @@ public function removeMatchingRulesWithPropertyNamePrefixKeepsOtherRules( * * @dataProvider providePropertyNames */ - public function removeAllRulesRemovesAllRules(array $propertyNamesToRemove): void + public function removeAllRulesRemovesAllDeclarations(array $propertyNamesToRemove): void { - $this->setRulesFromPropertyNames($propertyNamesToRemove); + $this->setDeclarationsFromPropertyNames($propertyNamesToRemove); $this->subject->removeAllRules(); @@ -712,13 +712,13 @@ public function removeAllRulesRemovesAllRules(array $propertyNamesToRemove): voi * * @dataProvider providePropertyNames */ - public function setRulesOnVirginSetsRulesWithoutPositionInOrder(array $propertyNamesToSet): void + public function setRulesOnVirginSetsDeclarationsWithoutPositionInOrder(array $propertyNamesToSet): void { - $rulesToSet = self::createRulesFromPropertyNames($propertyNamesToSet); + $declarationsToSet = self::createDeclarationsFromPropertyNames($propertyNamesToSet); - $this->subject->setRules($rulesToSet); + $this->subject->setRules($declarationsToSet); - self::assertSame($rulesToSet, $this->subject->getRules()); + self::assertSame($declarationsToSet, $this->subject->getRules()); } /** @@ -737,108 +737,108 @@ public static function provideInitialPropertyNamesAndPropertyNamesToSet(): DataP * * @dataProvider provideInitialPropertyNamesAndPropertyNamesToSet */ - public function setRulesReplacesRules(array $initialPropertyNames, array $propertyNamesToSet): void + public function setRulesReplacesDeclarations(array $initialPropertyNames, array $propertyNamesToSet): void { - $rulesToSet = self::createRulesFromPropertyNames($propertyNamesToSet); - $this->setRulesFromPropertyNames($initialPropertyNames); + $declarationsToSet = self::createDeclarationsFromPropertyNames($propertyNamesToSet); + $this->setDeclarationsFromPropertyNames($initialPropertyNames); - $this->subject->setRules($rulesToSet); + $this->subject->setRules($declarationsToSet); - self::assertSame($rulesToSet, $this->subject->getRules()); + self::assertSame($declarationsToSet, $this->subject->getRules()); } /** * @test */ - public function setRulesWithRuleWithoutPositionSetsValidLineNumber(): void + public function setRulesWithDeclarationWithoutPositionSetsValidLineNumber(): void { - $ruleToSet = new Rule('color'); + $declarationToSet = new Declaration('color'); - $this->subject->setRules([$ruleToSet]); + $this->subject->setRules([$declarationToSet]); - self::assertIsInt($ruleToSet->getLineNumber(), 'line number not set'); - self::assertGreaterThanOrEqual(1, $ruleToSet->getLineNumber(), 'line number not valid'); + self::assertIsInt($declarationToSet->getLineNumber(), 'line number not set'); + self::assertGreaterThanOrEqual(1, $declarationToSet->getLineNumber(), 'line number not valid'); } /** * @test */ - public function setRulesWithRuleWithoutPositionSetsValidColumnNumber(): void + public function setRulesWithDeclarationWithoutPositionSetsValidColumnNumber(): void { - $ruleToSet = new Rule('color'); + $declarationToSet = new Declaration('color'); - $this->subject->setRules([$ruleToSet]); + $this->subject->setRules([$declarationToSet]); - self::assertIsInt($ruleToSet->getColumnNumber(), 'column number not set'); - self::assertGreaterThanOrEqual(0, $ruleToSet->getColumnNumber(), 'column number not valid'); + self::assertIsInt($declarationToSet->getColumnNumber(), 'column number not set'); + self::assertGreaterThanOrEqual(0, $declarationToSet->getColumnNumber(), 'column number not valid'); } /** * @test */ - public function setRulesWithRuleWithOnlyLineNumberSetsColumnNumber(): void + public function setRulesWithDeclarationWithOnlyLineNumberSetsColumnNumber(): void { - $ruleToSet = new Rule('color'); - $ruleToSet->setPosition(42); + $declarationToSet = new Declaration('color'); + $declarationToSet->setPosition(42); - $this->subject->setRules([$ruleToSet]); + $this->subject->setRules([$declarationToSet]); - self::assertIsInt($ruleToSet->getColumnNumber(), 'column number not set'); - self::assertGreaterThanOrEqual(0, $ruleToSet->getColumnNumber(), 'column number not valid'); + self::assertIsInt($declarationToSet->getColumnNumber(), 'column number not set'); + self::assertGreaterThanOrEqual(0, $declarationToSet->getColumnNumber(), 'column number not valid'); } /** * @test */ - public function setRulesWithRuleWithOnlyLineNumberPreservesLineNumber(): void + public function setRulesWithDeclarationWithOnlyLineNumberPreservesLineNumber(): void { - $ruleToSet = new Rule('color'); - $ruleToSet->setPosition(42); + $declarationToSet = new Declaration('color'); + $declarationToSet->setPosition(42); - $this->subject->setRules([$ruleToSet]); + $this->subject->setRules([$declarationToSet]); - self::assertSame(42, $ruleToSet->getLineNumber(), 'line number not preserved'); + self::assertSame(42, $declarationToSet->getLineNumber(), 'line number not preserved'); } /** * @test */ - public function setRulesWithRuleWithOnlyColumnNumberSetsLineNumber(): void + public function setRulesWithDeclarationWithOnlyColumnNumberSetsLineNumber(): void { - $ruleToSet = new Rule('color'); - $ruleToSet->setPosition(null, 42); + $declarationToSet = new Declaration('color'); + $declarationToSet->setPosition(null, 42); - $this->subject->setRules([$ruleToSet]); + $this->subject->setRules([$declarationToSet]); - self::assertIsInt($ruleToSet->getLineNumber(), 'line number not set'); - self::assertGreaterThanOrEqual(1, $ruleToSet->getLineNumber(), 'line number not valid'); + self::assertIsInt($declarationToSet->getLineNumber(), 'line number not set'); + self::assertGreaterThanOrEqual(1, $declarationToSet->getLineNumber(), 'line number not valid'); } /** * @test */ - public function setRulesWithRuleWithOnlyColumnNumberPreservesColumnNumber(): void + public function setRulesWithDeclarationWithOnlyColumnNumberPreservesColumnNumber(): void { - $ruleToSet = new Rule('color'); - $ruleToSet->setPosition(null, 42); + $declarationToSet = new Declaration('color'); + $declarationToSet->setPosition(null, 42); - $this->subject->setRules([$ruleToSet]); + $this->subject->setRules([$declarationToSet]); - self::assertSame(42, $ruleToSet->getColumnNumber(), 'column number not preserved'); + self::assertSame(42, $declarationToSet->getColumnNumber(), 'column number not preserved'); } /** * @test */ - public function setRulesWithRuleWithCompletePositionPreservesPosition(): void + public function setRulesWithDeclarationWithCompletePositionPreservesPosition(): void { - $ruleToSet = new Rule('color'); - $ruleToSet->setPosition(42, 64); + $declarationToSet = new Declaration('color'); + $declarationToSet->setPosition(42, 64); - $this->subject->setRules([$ruleToSet]); + $this->subject->setRules([$declarationToSet]); - self::assertSame(42, $ruleToSet->getLineNumber(), 'line number not preserved'); - self::assertSame(64, $ruleToSet->getColumnNumber(), 'column number not preserved'); + self::assertSame(42, $declarationToSet->getLineNumber(), 'line number not preserved'); + self::assertSame(64, $declarationToSet->getColumnNumber(), 'column number not preserved'); } /** @@ -848,14 +848,14 @@ public function setRulesWithRuleWithCompletePositionPreservesPosition(): void * * @dataProvider providePropertyNames */ - public function getRulesReturnsRulesSet(array $propertyNamesToSet): void + public function getRulesReturnsDeclarationsSet(array $propertyNamesToSet): void { - $rulesToSet = self::createRulesFromPropertyNames($propertyNamesToSet); - $this->subject->setRules($rulesToSet); + $declarationsToSet = self::createDeclarationsFromPropertyNames($propertyNamesToSet); + $this->subject->setRules($declarationsToSet); $result = $this->subject->getRules(); - self::assertSame($rulesToSet, $result); + self::assertSame($declarationsToSet, $result); } /** @@ -863,9 +863,9 @@ public function getRulesReturnsRulesSet(array $propertyNamesToSet): void */ public function getRulesOrdersByLineNumber(): void { - $first = (new Rule('color'))->setPosition(1, 64); - $second = (new Rule('display'))->setPosition(19, 42); - $third = (new Rule('color'))->setPosition(55, 11); + $first = (new Declaration('color'))->setPosition(1, 64); + $second = (new Declaration('display'))->setPosition(19, 42); + $third = (new Declaration('color'))->setPosition(55, 11); $this->subject->setRules([$third, $second, $first]); $result = $this->subject->getRules(); @@ -876,11 +876,11 @@ public function getRulesOrdersByLineNumber(): void /** * @test */ - public function getRulesOrdersRulesWithSameLineNumberByColumnNumber(): void + public function getRulesOrdersDeclarationsWithSameLineNumberByColumnNumber(): void { - $first = (new Rule('color'))->setPosition(1, 11); - $second = (new Rule('display'))->setPosition(1, 42); - $third = (new Rule('color'))->setPosition(1, 64); + $first = (new Declaration('color'))->setPosition(1, 11); + $second = (new Declaration('display'))->setPosition(1, 42); + $third = (new Declaration('color'))->setPosition(1, 64); $this->subject->setRules([$third, $second, $first]); $result = $this->subject->getRules(); @@ -894,67 +894,67 @@ public function getRulesOrdersRulesWithSameLineNumberByColumnNumber(): void public static function providePropertyNamesAndSearchPatternAndMatchingPropertyNames(): array { return [ - 'single rule matched' => [ + 'single declaration matched' => [ ['color'], 'color', ['color'], ], - 'first rule matched' => [ + 'first declaration matched' => [ ['color', 'display'], 'color', ['color'], ], - 'last rule matched' => [ + 'last declaration matched' => [ ['color', 'display'], 'display', ['display'], ], - 'middle rule matched' => [ + 'middle declaration matched' => [ ['color', 'display', 'width'], 'display', ['display'], ], - 'multiple rules for the same property matched' => [ + 'multiple declarations for the same property matched' => [ ['color', 'color'], 'color', ['color'], ], - 'multiple rules for the same property matched in haystack' => [ + 'multiple declarations for the same property matched in haystack' => [ ['color', 'display', 'color', 'width'], 'color', ['color'], ], - 'shorthand rule matched' => [ + 'shorthand declaration matched' => [ ['font'], 'font-', ['font'], ], - 'longhand rule matched' => [ + 'longhand declaration matched' => [ ['font-size'], 'font-', ['font-size'], ], - 'shorthand and longhand rule matched' => [ + 'shorthand and longhand declaration matched' => [ ['font', 'font-size'], 'font-', ['font', 'font-size'], ], - 'shorthand rule matched in haystack' => [ + 'shorthand declaration matched in haystack' => [ ['font', 'color'], 'font-', ['font'], ], - 'longhand rule matched in haystack' => [ + 'longhand declaration matched in haystack' => [ ['font-size', 'color'], 'font-', ['font-size'], ], - 'rules whose property names begin with the same characters not matched with pattern match' => [ + 'declarations whose property names begin with the same characters not matched with pattern match' => [ ['contain', 'container', 'container-type'], 'contain-', ['contain'], ], - 'rules whose property names begin with the same characters not matched with exact match' => [ + 'declarations whose property names begin with the same characters not matched with exact match' => [ ['contain', 'container', 'container-type'], 'contain', ['contain'], @@ -970,27 +970,27 @@ public static function providePropertyNamesAndSearchPatternAndMatchingPropertyNa * * @dataProvider providePropertyNamesAndSearchPatternAndMatchingPropertyNames */ - public function getRulesWithPatternReturnsAllMatchingRules( + public function getRulesWithPatternReturnsAllMatchingDeclarations( array $propertyNamesToSet, string $searchPattern, array $matchingPropertyNames ): void { - $rulesToSet = self::createRulesFromPropertyNames($propertyNamesToSet); + $declarationsToSet = self::createDeclarationsFromPropertyNames($propertyNamesToSet); // Use `array_values` to ensure canonical numeric array, since `array_filter` does not reindex. - $matchingRules = \array_values( + $matchingDeclarations = \array_values( \array_filter( - $rulesToSet, - static function (Rule $rule) use ($matchingPropertyNames): bool { - return \in_array($rule->getRule(), $matchingPropertyNames, true); + $declarationsToSet, + static function (Declaration $declaration) use ($matchingPropertyNames): bool { + return \in_array($declaration->getPropertyName(), $matchingPropertyNames, true); } ) ); - $this->subject->setRules($rulesToSet); + $this->subject->setRules($declarationsToSet); $result = $this->subject->getRules($searchPattern); - // `Rule`s without pre-set positions are returned in the order set. This is tested separately. - self::assertSame($matchingRules, $result); + // `Declaration`s without pre-set positions are returned in the order set. This is tested separately. + self::assertSame($matchingDeclarations, $result); } /** @@ -1025,7 +1025,7 @@ public function getRulesWithNonMatchingPatternReturnsEmptyArray( array $propertyNamesToSet, string $searchPattern ): void { - $this->setRulesFromPropertyNames($propertyNamesToSet); + $this->setDeclarationsFromPropertyNames($propertyNamesToSet); $result = $this->subject->getRules($searchPattern); @@ -1035,11 +1035,11 @@ public function getRulesWithNonMatchingPatternReturnsEmptyArray( /** * @test */ - public function getRulesWithPatternOrdersRulesByPosition(): void + public function getRulesWithPatternOrdersDeclarationsByPosition(): void { - $first = (new Rule('color'))->setPosition(1, 42); - $second = (new Rule('color'))->setPosition(1, 64); - $third = (new Rule('color'))->setPosition(55, 7); + $first = (new Declaration('color'))->setPosition(1, 42); + $second = (new Declaration('color'))->setPosition(1, 64); + $third = (new Declaration('color'))->setPosition(55, 7); $this->subject->setRules([$third, $second, $first]); $result = $this->subject->getRules('color'); @@ -1066,38 +1066,38 @@ public static function provideDistinctPropertyNames(): array * * @dataProvider provideDistinctPropertyNames */ - public function getRulesAssocReturnsAllRulesWithDistinctPropertyNames(array $propertyNamesToSet): void + public function getRulesAssocReturnsAllDeclarationsWithDistinctPropertyNames(array $propertyNamesToSet): void { - $rulesToSet = self::createRulesFromPropertyNames($propertyNamesToSet); - $this->subject->setRules($rulesToSet); + $declarationsToSet = self::createDeclarationsFromPropertyNames($propertyNamesToSet); + $this->subject->setRules($declarationsToSet); $result = $this->subject->getRulesAssoc(); - self::assertSame($rulesToSet, \array_values($result)); + self::assertSame($declarationsToSet, \array_values($result)); } /** * @test */ - public function getRulesAssocReturnsLastRuleWithSamePropertyName(): void + public function getRulesAssocReturnsLastDeclarationWithSamePropertyName(): void { - $firstRule = new Rule('color'); - $lastRule = new Rule('color'); - $this->subject->setRules([$firstRule, $lastRule]); + $firstDeclaration = new Declaration('color'); + $lastDeclaration = new Declaration('color'); + $this->subject->setRules([$firstDeclaration, $lastDeclaration]); $result = $this->subject->getRulesAssoc(); - self::assertSame([$lastRule], \array_values($result)); + self::assertSame([$lastDeclaration], \array_values($result)); } /** * @test */ - public function getRulesAssocOrdersRulesByPosition(): void + public function getRulesAssocOrdersDeclarationsByPosition(): void { - $first = (new Rule('color'))->setPosition(1, 42); - $second = (new Rule('display'))->setPosition(1, 64); - $third = (new Rule('width'))->setPosition(55, 7); + $first = (new Declaration('color'))->setPosition(1, 42); + $second = (new Declaration('display'))->setPosition(1, 64); + $third = (new Declaration('width'))->setPosition(55, 7); $this->subject->setRules([$third, $second, $first]); $result = $this->subject->getRulesAssoc(); @@ -1108,14 +1108,14 @@ public function getRulesAssocOrdersRulesByPosition(): void /** * @test */ - public function getRulesAssocKeysRulesByPropertyName(): void + public function getRulesAssocKeysDeclarationsByPropertyName(): void { - $this->subject->setRules([new Rule('color'), new Rule('display')]); + $this->subject->setRules([new Declaration('color'), new Declaration('display')]); $result = $this->subject->getRulesAssoc(); - foreach ($result as $key => $rule) { - self::assertSame($rule->getRule(), $key); + foreach ($result as $key => $declaration) { + self::assertSame($declaration->getPropertyName(), $key); } } @@ -1132,7 +1132,7 @@ public function getRulesAssocWithPatternReturnsAllMatchingPropertyNames( string $searchPattern, array $matchingPropertyNames ): void { - $this->setRulesFromPropertyNames($propertyNamesToSet); + $this->setDeclarationsFromPropertyNames($propertyNamesToSet); $result = $this->subject->getRulesAssoc($searchPattern); @@ -1153,7 +1153,7 @@ public function getRulesAssocWithNonMatchingPatternReturnsEmptyArray( array $propertyNamesToSet, string $searchPattern ): void { - $this->setRulesFromPropertyNames($propertyNamesToSet); + $this->setDeclarationsFromPropertyNames($propertyNamesToSet); $result = $this->subject->getRulesAssoc($searchPattern); @@ -1163,11 +1163,11 @@ public function getRulesAssocWithNonMatchingPatternReturnsEmptyArray( /** * @test */ - public function getRulesAssocWithPatternOrdersRulesByPosition(): void + public function getRulesAssocWithPatternOrdersDeclarationsByPosition(): void { - $first = (new Rule('font'))->setPosition(1, 42); - $second = (new Rule('font-family'))->setPosition(1, 64); - $third = (new Rule('font-weight'))->setPosition(55, 7); + $first = (new Declaration('font'))->setPosition(1, 42); + $second = (new Declaration('font-family'))->setPosition(1, 64); + $third = (new Declaration('font-weight'))->setPosition(55, 7); $this->subject->setRules([$third, $second, $first]); $result = $this->subject->getRules('font-'); @@ -1178,21 +1178,21 @@ public function getRulesAssocWithPatternOrdersRulesByPosition(): void /** * @param list $propertyNames */ - private function setRulesFromPropertyNames(array $propertyNames): void + private function setDeclarationsFromPropertyNames(array $propertyNames): void { - $this->subject->setRules(self::createRulesFromPropertyNames($propertyNames)); + $this->subject->setRules(self::createDeclarationsFromPropertyNames($propertyNames)); } /** * @param list $propertyNames * - * @return list + * @return list */ - private static function createRulesFromPropertyNames(array $propertyNames): array + private static function createDeclarationsFromPropertyNames(array $propertyNames): array { return \array_map( - function (string $propertyName): Rule { - return new Rule($propertyName); + function (string $propertyName): Declaration { + return new Declaration($propertyName); }, $propertyNames ); From 32dce4bf8690b5e1607620223f94aa6042efdded Mon Sep 17 00:00:00 2001 From: JakeQZ Date: Sat, 14 Feb 2026 09:15:22 +0000 Subject: [PATCH 102/136] [TASK] Use `Property\Declaration` in `ValueTest` DocBlock references (#1515) This replaces `Rule\Rule`, following #1508. --- tests/Functional/Value/ValueTest.php | 2 +- tests/Unit/Value/ValueTest.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/Functional/Value/ValueTest.php b/tests/Functional/Value/ValueTest.php index 287361a8e..aa9ab037b 100644 --- a/tests/Functional/Value/ValueTest.php +++ b/tests/Functional/Value/ValueTest.php @@ -19,7 +19,7 @@ final class ValueTest extends TestCase /** * the default set of delimiters for parsing most values * - * @see \Sabberworm\CSS\Rule\Rule::getDelimitersForPropertyValue + * @see \Sabberworm\CSS\Property\Declaration::getDelimitersForPropertyValue */ private const DEFAULT_DELIMITERS = [',', ' ', '/']; diff --git a/tests/Unit/Value/ValueTest.php b/tests/Unit/Value/ValueTest.php index e3ab12c70..e9b36eef6 100644 --- a/tests/Unit/Value/ValueTest.php +++ b/tests/Unit/Value/ValueTest.php @@ -21,7 +21,7 @@ final class ValueTest extends TestCase /** * the default set of delimiters for parsing most values * - * @see \Sabberworm\CSS\Rule\Rule::getDelimitersForPropertyValue + * @see \Sabberworm\CSS\Property\Declaration::getDelimitersForPropertyValue */ private const DEFAULT_DELIMITERS = [',', ' ', '/']; From b9b78b8c9b10d575a8de8e9db6a8b3c2a08c9f5b Mon Sep 17 00:00:00 2001 From: JakeQZ Date: Sat, 14 Feb 2026 09:16:13 +0000 Subject: [PATCH 103/136] [TASK] Use `Property\Declaration` in `RuleSetTest` (#1516) This replaces `Rule\Rule`, following #1508. --- tests/Functional/RuleSet/RuleSetTest.php | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/tests/Functional/RuleSet/RuleSetTest.php b/tests/Functional/RuleSet/RuleSetTest.php index b0d976055..c33a15193 100644 --- a/tests/Functional/RuleSet/RuleSetTest.php +++ b/tests/Functional/RuleSet/RuleSetTest.php @@ -6,7 +6,7 @@ use PHPUnit\Framework\TestCase; use Sabberworm\CSS\OutputFormat; -use Sabberworm\CSS\Rule\Rule; +use Sabberworm\CSS\Property\Declaration; use Sabberworm\CSS\RuleSet\RuleSet; /** @@ -59,9 +59,9 @@ public static function providePropertyNamesAndValuesAndExpectedCss(): array * * @dataProvider providePropertyNamesAndValuesAndExpectedCss */ - public function renderReturnsCssForRulesSet(array $propertyNamesAndValuesToSet, string $expectedCss): void + public function renderReturnsCssForDeclarationsSet(array $propertyNamesAndValuesToSet, string $expectedCss): void { - $this->setRulesFromPropertyNamesAndValues($propertyNamesAndValuesToSet); + $this->setDeclarationsFromPropertyNamesAndValues($propertyNamesAndValuesToSet); $result = $this->subject->render(OutputFormat::create()); @@ -73,7 +73,7 @@ public function renderReturnsCssForRulesSet(array $propertyNamesAndValuesToSet, */ public function renderWithCompactOutputFormatReturnsCssWithoutWhitespaceOrTrailingSemicolon(): void { - $this->setRulesFromPropertyNamesAndValues([ + $this->setDeclarationsFromPropertyNamesAndValues([ ['name' => 'color', 'value' => 'green'], ['name' => 'display', 'value' => 'block'], ]); @@ -88,7 +88,7 @@ public function renderWithCompactOutputFormatReturnsCssWithoutWhitespaceOrTraili */ public function renderWithPrettyOutputFormatReturnsCssWithNewlinesAroundIndentedDeclarations(): void { - $this->setRulesFromPropertyNamesAndValues([ + $this->setDeclarationsFromPropertyNamesAndValues([ ['name' => 'color', 'value' => 'green'], ['name' => 'display', 'value' => 'block'], ]); @@ -101,19 +101,19 @@ public function renderWithPrettyOutputFormatReturnsCssWithNewlinesAroundIndented /** * @param list $propertyNamesAndValues */ - private function setRulesFromPropertyNamesAndValues(array $propertyNamesAndValues): void + private function setDeclarationsFromPropertyNamesAndValues(array $propertyNamesAndValues): void { - $rulesToSet = \array_map( + $declarationsToSet = \array_map( /** * @param array{name: string, value: string} $nameAndValue */ - static function (array $nameAndValue): Rule { - $rule = new Rule($nameAndValue['name']); - $rule->setValue($nameAndValue['value']); - return $rule; + static function (array $nameAndValue): Declaration { + $declaration = new Declaration($nameAndValue['name']); + $declaration->setValue($nameAndValue['value']); + return $declaration; }, $propertyNamesAndValues ); - $this->subject->setRules($rulesToSet); + $this->subject->setRules($declarationsToSet); } } From cc48a61b9979371b43cfac7199f144d6b833a3fe Mon Sep 17 00:00:00 2001 From: JakeQZ Date: Sat, 14 Feb 2026 09:16:53 +0000 Subject: [PATCH 104/136] [TASK] Use `Property\Declaration` in `CSSBlockListTest` (#1517) This replaces `Rule\Rule`, following #1508. --- tests/Unit/CSSList/CSSBlockListTest.php | 76 ++++++++++++------------- 1 file changed, 38 insertions(+), 38 deletions(-) diff --git a/tests/Unit/CSSList/CSSBlockListTest.php b/tests/Unit/CSSList/CSSBlockListTest.php index 50b6420ef..b8f8c429d 100644 --- a/tests/Unit/CSSList/CSSBlockListTest.php +++ b/tests/Unit/CSSList/CSSBlockListTest.php @@ -9,9 +9,9 @@ use Sabberworm\CSS\CSSList\AtRuleBlockList; use Sabberworm\CSS\CSSList\CSSList; use Sabberworm\CSS\Property\Charset; +use Sabberworm\CSS\Property\Declaration; use Sabberworm\CSS\Property\Import; use Sabberworm\CSS\Renderable; -use Sabberworm\CSS\Rule\Rule; use Sabberworm\CSS\RuleSet\AtRuleSet; use Sabberworm\CSS\RuleSet\DeclarationBlock; use Sabberworm\CSS\Tests\Unit\CSSList\Fixtures\ConcreteCSSBlockList; @@ -300,9 +300,9 @@ public function getAllValuesReturnsOneValueDirectlySetAsContent(): void $value = new CSSString('Superfont'); $declarationBlock = new DeclarationBlock(); - $rule = new Rule('font-family'); - $rule->setValue($value); - $declarationBlock->addRule($rule); + $declaration = new Declaration('font-family'); + $declaration->setValue($value); + $declarationBlock->addRule($declaration); $subject->setContents([$declarationBlock]); $result = $subject->getAllValues(); @@ -321,12 +321,12 @@ public function getAllValuesReturnsMultipleValuesDirectlySetAsContentInOneDeclar $value2 = new CSSString('aquamarine'); $declarationBlock = new DeclarationBlock(); - $rule1 = new Rule('font-family'); - $rule1->setValue($value1); - $declarationBlock->addRule($rule1); - $rule2 = new Rule('color'); - $rule2->setValue($value2); - $declarationBlock->addRule($rule2); + $declaration1 = new Declaration('font-family'); + $declaration1->setValue($value1); + $declarationBlock->addRule($declaration1); + $declaration2 = new Declaration('color'); + $declaration2->setValue($value2); + $declarationBlock->addRule($declaration2); $subject->setContents([$declarationBlock]); $result = $subject->getAllValues(); @@ -345,13 +345,13 @@ public function getAllValuesReturnsMultipleValuesDirectlySetAsContentInMultipleD $value2 = new CSSString('aquamarine'); $declarationBlock1 = new DeclarationBlock(); - $rule1 = new Rule('font-family'); - $rule1->setValue($value1); - $declarationBlock1->addRule($rule1); + $declaration1 = new Declaration('font-family'); + $declaration1->setValue($value1); + $declarationBlock1->addRule($declaration1); $declarationBlock2 = new DeclarationBlock(); - $rule2 = new Rule('color'); - $rule2->setValue($value2); - $declarationBlock2->addRule($rule2); + $declaration2 = new Declaration('color'); + $declaration2->setValue($value2); + $declarationBlock2->addRule($declaration2); $subject->setContents([$declarationBlock1, $declarationBlock2]); $result = $subject->getAllValues(); @@ -369,9 +369,9 @@ public function getAllValuesReturnsValuesWithinAtRuleBlockList(): void $value = new CSSString('Superfont'); $declarationBlock = new DeclarationBlock(); - $rule = new Rule('font-family'); - $rule->setValue($value); - $declarationBlock->addRule($rule); + $declaration = new Declaration('font-family'); + $declaration->setValue($value); + $declarationBlock->addRule($declaration); $atRuleBlockList = new AtRuleBlockList('media'); $atRuleBlockList->setContents([$declarationBlock]); $subject->setContents([$atRuleBlockList]); @@ -392,13 +392,13 @@ public function getAllValuesWithElementProvidedReturnsOnlyValuesWithinThatElemen $value2 = new CSSString('aquamarine'); $declarationBlock1 = new DeclarationBlock(); - $rule1 = new Rule('font-family'); - $rule1->setValue($value1); - $declarationBlock1->addRule($rule1); + $declaration1 = new Declaration('font-family'); + $declaration1->setValue($value1); + $declarationBlock1->addRule($declaration1); $declarationBlock2 = new DeclarationBlock(); - $rule2 = new Rule('color'); - $rule2->setValue($value2); - $declarationBlock2->addRule($rule2); + $declaration2 = new Declaration('color'); + $declaration2->setValue($value2); + $declarationBlock2->addRule($declaration2); $subject->setContents([$declarationBlock1, $declarationBlock2]); $result = $subject->getAllValues($declarationBlock1); @@ -409,7 +409,7 @@ public function getAllValuesWithElementProvidedReturnsOnlyValuesWithinThatElemen /** * @test */ - public function getAllValuesWithSearchStringProvidedReturnsOnlyValuesFromMatchingRules(): void + public function getAllValuesWithSearchStringProvidedReturnsOnlyValuesFromMatchingDeclarations(): void { $subject = new ConcreteCSSBlockList(); @@ -417,12 +417,12 @@ public function getAllValuesWithSearchStringProvidedReturnsOnlyValuesFromMatchin $value2 = new CSSString('aquamarine'); $declarationBlock = new DeclarationBlock(); - $rule1 = new Rule('font-family'); - $rule1->setValue($value1); - $declarationBlock->addRule($rule1); - $rule2 = new Rule('color'); - $rule2->setValue($value2); - $declarationBlock->addRule($rule2); + $declaration1 = new Declaration('font-family'); + $declaration1->setValue($value1); + $declarationBlock->addRule($declaration1); + $declaration2 = new Declaration('color'); + $declaration2->setValue($value2); + $declarationBlock->addRule($declaration2); $subject->setContents([$declarationBlock]); $result = $subject->getAllValues(null, 'font-'); @@ -441,9 +441,9 @@ public function getAllValuesByDefaultDoesNotReturnValuesInFunctionArguments(): v $value2 = new Size(2, '%'); $declarationBlock = new DeclarationBlock(); - $rule = new Rule('margin'); - $rule->setValue(new CSSFunction('max', [$value1, $value2])); - $declarationBlock->addRule($rule); + $declaration = new Declaration('margin'); + $declaration->setValue(new CSSFunction('max', [$value1, $value2])); + $declarationBlock->addRule($declaration); $subject->setContents([$declarationBlock]); $result = $subject->getAllValues(); @@ -462,9 +462,9 @@ public function getAllValuesWithSearchInFunctionArgumentsReturnsValuesInFunction $value2 = new Size(2, '%'); $declarationBlock = new DeclarationBlock(); - $rule = new Rule('margin'); - $rule->setValue(new CSSFunction('max', [$value1, $value2])); - $declarationBlock->addRule($rule); + $declaration = new Declaration('margin'); + $declaration->setValue(new CSSFunction('max', [$value1, $value2])); + $declarationBlock->addRule($declaration); $subject->setContents([$declarationBlock]); $result = $subject->getAllValues(null, null, true); From fd15e78b4c6e6832398b42e1a213343e0d3573c8 Mon Sep 17 00:00:00 2001 From: JakeQZ Date: Sat, 14 Feb 2026 09:17:31 +0000 Subject: [PATCH 105/136] [TASK] Use `Property\Declaration` in generic `DeclarationBlockTest` (#1518) This replaces `Rule\Rule`, following #1508. --- tests/RuleSet/DeclarationBlockTest.php | 64 +++++++++++++------------- 1 file changed, 32 insertions(+), 32 deletions(-) diff --git a/tests/RuleSet/DeclarationBlockTest.php b/tests/RuleSet/DeclarationBlockTest.php index 63411a646..02bfe86f0 100644 --- a/tests/RuleSet/DeclarationBlockTest.php +++ b/tests/RuleSet/DeclarationBlockTest.php @@ -7,7 +7,7 @@ use PHPUnit\Framework\TestCase; use Sabberworm\CSS\OutputFormat; use Sabberworm\CSS\Parser; -use Sabberworm\CSS\Rule\Rule; +use Sabberworm\CSS\Property\Declaration; use Sabberworm\CSS\RuleSet\DeclarationBlock; use Sabberworm\CSS\Settings as ParserSettings; use Sabberworm\CSS\Value\Size; @@ -25,25 +25,25 @@ public function overrideRules(): void $css = '.wrapper { left: 10px; text-align: left; }'; $parser = new Parser($css); $document = $parser->parse(); - $rule = new Rule('right'); - $rule->setValue('-10px'); + $declaration = new Declaration('right'); + $declaration->setValue('-10px'); $contents = $document->getContents(); $wrapper = $contents[0]; self::assertInstanceOf(DeclarationBlock::class, $wrapper); self::assertCount(2, $wrapper->getRules()); - $wrapper->setRules([$rule]); + $wrapper->setRules([$declaration]); - $rules = $wrapper->getRules(); - self::assertCount(1, $rules); - self::assertSame('right', $rules[0]->getRule()); - self::assertSame('-10px', $rules[0]->getValue()); + $declarations = $wrapper->getRules(); + self::assertCount(1, $declarations); + self::assertSame('right', $declarations[0]->getRule()); + self::assertSame('-10px', $declarations[0]->getValue()); } /** * @test */ - public function ruleInsertion(): void + public function declarationInsertion(): void { $css = '.wrapper { left: 10px; text-align: left; }'; $parser = new Parser($css); @@ -53,34 +53,34 @@ public function ruleInsertion(): void self::assertInstanceOf(DeclarationBlock::class, $wrapper); - $leftRules = $wrapper->getRules('left'); - self::assertCount(1, $leftRules); - $firstLeftRule = $leftRules[0]; + $leftDeclarations = $wrapper->getRules('left'); + self::assertCount(1, $leftDeclarations); + $firstLeftDeclaration = $leftDeclarations[0]; - $textRules = $wrapper->getRules('text-'); - self::assertCount(1, $textRules); - $firstTextRule = $textRules[0]; + $textDeclarations = $wrapper->getRules('text-'); + self::assertCount(1, $textDeclarations); + $firstTextDeclaration = $textDeclarations[0]; - $leftPrefixRule = new Rule('left'); - $leftPrefixRule->setValue(new Size(16, 'em')); + $leftPrefixDeclaration = new Declaration('left'); + $leftPrefixDeclaration->setValue(new Size(16, 'em')); - $textAlignRule = new Rule('text-align'); - $textAlignRule->setValue(new Size(1)); + $textAlignDeclaration = new Declaration('text-align'); + $textAlignDeclaration->setValue(new Size(1)); - $borderBottomRule = new Rule('border-bottom-width'); - $borderBottomRule->setValue(new Size(1, 'px')); + $borderBottomDeclaration = new Declaration('border-bottom-width'); + $borderBottomDeclaration->setValue(new Size(1, 'px')); - $wrapper->addRule($borderBottomRule); - $wrapper->addRule($leftPrefixRule, $firstLeftRule); - $wrapper->addRule($textAlignRule, $firstTextRule); + $wrapper->addRule($borderBottomDeclaration); + $wrapper->addRule($leftPrefixDeclaration, $firstLeftDeclaration); + $wrapper->addRule($textAlignDeclaration, $firstTextDeclaration); - $rules = $wrapper->getRules(); + $declarations = $wrapper->getRules(); - self::assertSame($leftPrefixRule, $rules[0]); - self::assertSame($firstLeftRule, $rules[1]); - self::assertSame($textAlignRule, $rules[2]); - self::assertSame($firstTextRule, $rules[3]); - self::assertSame($borderBottomRule, $rules[4]); + self::assertSame($leftPrefixDeclaration, $declarations[0]); + self::assertSame($firstLeftDeclaration, $declarations[1]); + self::assertSame($textAlignDeclaration, $declarations[2]); + self::assertSame($firstTextDeclaration, $declarations[3]); + self::assertSame($borderBottomDeclaration, $declarations[4]); self::assertSame( '.wrapper {left: 16em;left: 10px;text-align: 1;text-align: left;border-bottom-width: 1px;}', @@ -103,7 +103,7 @@ public static function declarationBlocksWithCommentsProvider(): array * @test * @dataProvider declarationBlocksWithCommentsProvider */ - public function canRemoveCommentsFromRulesUsingLenientParsing( + public function canRemoveCommentsFromDeclarationsUsingLenientParsing( string $cssWithComments, string $cssWithoutComments ): void { @@ -120,7 +120,7 @@ public function canRemoveCommentsFromRulesUsingLenientParsing( * @test * @dataProvider declarationBlocksWithCommentsProvider */ - public function canRemoveCommentsFromRulesUsingStrictParsing( + public function canRemoveCommentsFromDeclarationsUsingStrictParsing( string $cssWithComments, string $cssWithoutComments ): void { From 0a37763b805dd1271406ffbe3dce4316994ac10b Mon Sep 17 00:00:00 2001 From: JakeQZ Date: Sat, 14 Feb 2026 09:18:05 +0000 Subject: [PATCH 106/136] [TASK] Use `Declaration::getPropertyName()` in `RuleSet` (#1519) This method replaces `getRule()`, which is deprecated as of #1506. --- CHANGELOG.md | 2 +- src/RuleSet/RuleSet.php | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5f209a962..a8f3b5999 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -35,7 +35,7 @@ Please also have a look at our - `Rule\Rule` class is deprecated; `Property\Declaration` is a direct replacement (#1508) - `Rule::setRule()` and `getRule()` are deprecated and replaced with - `setPropertyName()` and `getPropertyName()` (#1506) + `setPropertyName()` and `getPropertyName()` (#1506, #1519) ### Removed diff --git a/src/RuleSet/RuleSet.php b/src/RuleSet/RuleSet.php index 2d6d93933..6be349eaa 100644 --- a/src/RuleSet/RuleSet.php +++ b/src/RuleSet/RuleSet.php @@ -101,7 +101,7 @@ public static function parseRuleSet(ParserState $parserState, RuleSet $ruleSet): */ public function addRule(Declaration $declarationToAdd, ?Declaration $sibling = null): void { - $propertyName = $declarationToAdd->getRule(); + $propertyName = $declarationToAdd->getPropertyName(); if (!isset($this->declarations[$propertyName])) { $this->declarations[$propertyName] = []; } @@ -240,7 +240,7 @@ public function getRulesAssoc(?string $searchPattern = null): array /** @var array $result */ $result = []; foreach ($this->getRules($searchPattern) as $declaration) { - $result[$declaration->getRule()] = $declaration; + $result[$declaration->getPropertyName()] = $declaration; } return $result; @@ -251,7 +251,7 @@ public function getRulesAssoc(?string $searchPattern = null): array */ public function removeRule(Declaration $declarationToRemove): void { - $nameOfPropertyToRemove = $declarationToRemove->getRule(); + $nameOfPropertyToRemove = $declarationToRemove->getPropertyName(); if (!isset($this->declarations[$nameOfPropertyToRemove])) { return; } From f6ddd4847b2cbdd04aa2223d57369597d371b101 Mon Sep 17 00:00:00 2001 From: JakeQZ Date: Sat, 14 Feb 2026 14:02:07 +0000 Subject: [PATCH 107/136] [TASK] Use `Declaration::getPropertyName()` in tests (#1520) This method replaces `getRule()`, which is deprecated as of #1506. --- tests/ParserTest.php | 6 +++--- tests/RuleSet/DeclarationBlockTest.php | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/ParserTest.php b/tests/ParserTest.php index d05dc8a88..55aa36110 100644 --- a/tests/ParserTest.php +++ b/tests/ParserTest.php @@ -344,8 +344,8 @@ public function ruleGetters(): void $bodyBlock = $declarationBlocks[1]; $backgroundHeaderRules = $headerBlock->getRules('background-'); self::assertCount(2, $backgroundHeaderRules); - self::assertSame('background-color', $backgroundHeaderRules[0]->getRule()); - self::assertSame('background-color', $backgroundHeaderRules[1]->getRule()); + self::assertSame('background-color', $backgroundHeaderRules[0]->getPropertyName()); + self::assertSame('background-color', $backgroundHeaderRules[1]->getPropertyName()); $backgroundHeaderRules = $headerBlock->getRulesAssoc('background-'); self::assertCount(1, $backgroundHeaderRules); self::assertInstanceOf(Color::class, $backgroundHeaderRules['background-color']->getValue()); @@ -886,7 +886,7 @@ public function missingPropertyValueLenient(): void $rules = $block->getRules(); self::assertCount(1, $rules); $rule = $rules[0]; - self::assertSame('display', $rule->getRule()); + self::assertSame('display', $rule->getPropertyName()); self::assertSame('inline-block', $rule->getValue()); } diff --git a/tests/RuleSet/DeclarationBlockTest.php b/tests/RuleSet/DeclarationBlockTest.php index 02bfe86f0..e6631903c 100644 --- a/tests/RuleSet/DeclarationBlockTest.php +++ b/tests/RuleSet/DeclarationBlockTest.php @@ -36,7 +36,7 @@ public function overrideRules(): void $declarations = $wrapper->getRules(); self::assertCount(1, $declarations); - self::assertSame('right', $declarations[0]->getRule()); + self::assertSame('right', $declarations[0]->getPropertyName()); self::assertSame('-10px', $declarations[0]->getValue()); } From e4be5f41abfb3d318aad83cbf892be331d367f5f Mon Sep 17 00:00:00 2001 From: JakeQZ Date: Sun, 15 Feb 2026 10:22:20 +0000 Subject: [PATCH 108/136] [TASK] Update DocBlock for `RuleContainer` to refer to `Declaration` (#1522) `Rule\Rule` was replaced with `Property\Declaration` in #1508. --- CHANGELOG.md | 3 ++- src/RuleSet/RuleContainer.php | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a8f3b5999..72ad1588a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,7 +18,8 @@ Please also have a look at our ### Changed -- `Rule\Rule` class is renamed to `Property\Declaration` (#1508, #1512, #1513) +- `Rule\Rule` class is renamed to `Property\Declaration` + (#1508, #1512, #1513, #1522) - `Rule::setRule()` and `getRule()` are replaced with `setPropertyName()` and `getPropertyName()` (#1506) - `Selector` is now represented as a sequence of `Selector\Component` objects diff --git a/src/RuleSet/RuleContainer.php b/src/RuleSet/RuleContainer.php index 82ec76ef5..fcd212bf2 100644 --- a/src/RuleSet/RuleContainer.php +++ b/src/RuleSet/RuleContainer.php @@ -7,7 +7,7 @@ use Sabberworm\CSS\Property\Declaration; /** - * Represents a CSS item that contains `Rules`, defining the methods to manipulate them. + * Represents a CSS item that contains `Declaration`s, defining the methods to manipulate them. */ interface RuleContainer { From fa583d89ce2e71169bcda09430dc694ed9807fdf Mon Sep 17 00:00:00 2001 From: JakeQZ Date: Sun, 15 Feb 2026 10:22:52 +0000 Subject: [PATCH 109/136] [CLEANUP] Correct the DocBlock for `RuleSet` (#1523) `RemoveRule` was split up in #1249 to avoid parameter type overloading, but updating the class DocBlock was missed then. The class DocBlock should not be describing what individual methods do, so that additional duplicated information has been removed. It exists in the DocBlock for the method. --- src/RuleSet/RuleSet.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/RuleSet/RuleSet.php b/src/RuleSet/RuleSet.php index 6be349eaa..ead0b9c9e 100644 --- a/src/RuleSet/RuleSet.php +++ b/src/RuleSet/RuleSet.php @@ -21,8 +21,8 @@ * The most common form of a rule set is one constrained by a selector, i.e., a `DeclarationBlock`. * However, unknown `AtRule`s (like `@font-face`) are rule sets as well. * - * If you want to manipulate a `RuleSet`, use the methods `addRule()`, `getRules()` and `removeRule()` - * (which accepts either a `Declaration` or a rule name; optionally suffixed by a dash to remove all related rules). + * If you want to manipulate a `RuleSet`, + * use the methods `addRule()`, `getRules()`, `removeRule()`, `removeMatchingRules()`, etc. * * Note that `CSSListItem` extends both `Commentable` and `Renderable`, so those interfaces must also be implemented. */ From 81cc71efadeef04f48eea48236a484e5650fc97d Mon Sep 17 00:00:00 2001 From: JakeQZ Date: Sun, 15 Feb 2026 10:23:47 +0000 Subject: [PATCH 110/136] [CLEANUP] Tidy up comments in `RuleSet` (#1524) Both in DocBlock and inline code comments. Make them more precise, and refer to 'declaration' rather than 'rule' where applicable. --- src/RuleSet/RuleSet.php | 43 ++++++++++++++++++++++------------------- 1 file changed, 23 insertions(+), 20 deletions(-) diff --git a/src/RuleSet/RuleSet.php b/src/RuleSet/RuleSet.php index ead0b9c9e..b7b603e67 100644 --- a/src/RuleSet/RuleSet.php +++ b/src/RuleSet/RuleSet.php @@ -32,8 +32,8 @@ class RuleSet implements CSSElement, CSSListItem, Positionable, RuleContainer use Position; /** - * the rules in this rule set, using the property name as the key, - * with potentially multiple rules per property name. + * the declarations in this rule set, using the property name as the key, + * with potentially multiple declarations per property name. * * @var array, Declaration>> */ @@ -171,16 +171,16 @@ public function addRule(Declaration $declarationToAdd, ?Declaration $sibling = n } /** - * Returns all rules matching the given rule name + * Returns all declarations matching the given property name * * @example $ruleSet->getRules('font') // returns array(0 => $declaration, …) or array(). * * @example $ruleSet->getRules('font-') - * //returns an array of all rules either beginning with font- or matching font. + * //returns an array of all declarations either beginning with font- or matching font. * * @param string|null $searchPattern - * Pattern to search for. If null, returns all rules. - * If the pattern ends with a dash, all rules starting with the pattern are returned + * Pattern to search for. If null, returns all declarations. + * If the pattern ends with a dash, all declarations starting with the pattern are returned * as well as one matching the pattern with the dash excluded. * * @return array, Declaration> @@ -189,8 +189,10 @@ public function getRules(?string $searchPattern = null): array { $result = []; foreach ($this->declarations as $propertyName => $declarations) { - // Either no search rule is given or the search rule matches the found rule exactly - // or the search rule ends in “-” and the found rule starts with the search rule. + // Either no search pattern was given + // or the search pattern matches the found declaration's property name exactly + // or the search pattern ends in “-” + // ... and the found declaration's property name starts with the search pattern if ( $searchPattern === null || $propertyName === $searchPattern || ( @@ -208,9 +210,9 @@ public function getRules(?string $searchPattern = null): array } /** - * Overrides all the rules of this set. + * Overrides all the declarations of this set. * - * @param array $declarations The rules to override with. + * @param array $declarations */ public function setRules(array $declarations): void { @@ -221,16 +223,17 @@ public function setRules(array $declarations): void } /** - * Returns all rules matching the given pattern and returns them in an associative array with the rule’s name - * as keys. This method exists mainly for backwards-compatibility and is really only partially useful. + * Returns all declarations with property names matching the given pattern and returns them in an associative array + * with the property names as keys. + * This method exists mainly for backwards-compatibility and is really only partially useful. * * Note: This method loses some information: Calling this (with an argument of `background-`) on a declaration block * like `{ background-color: green; background-color; rgba(0, 127, 0, 0.7); }` will only yield an associative array - * containing the rgba-valued rule while `getRules()` would yield an indexed array containing both. + * containing the rgba-valued declaration while `getDeclarations()` would yield an indexed array containing both. * * @param string|null $searchPattern - * Pattern to search for. If null, returns all rules. If the pattern ends with a dash, - * all rules starting with the pattern are returned as well as one matching the pattern with the dash + * Pattern to search for. If null, returns all declarations. If the pattern ends with a dash, + * all declarations starting with the pattern are returned as well as one matching the pattern with the dash * excluded. * * @return array @@ -263,20 +266,20 @@ public function removeRule(Declaration $declarationToRemove): void } /** - * Removes rules by property name or search pattern. + * Removes declarations by property name or search pattern. * * @param string $searchPattern * pattern to remove. * If the pattern ends in a dash, - * all rules starting with the pattern are removed as well as one matching the pattern with the dash + * all declarations starting with the pattern are removed as well as one matching the pattern with the dash * excluded. */ public function removeMatchingRules(string $searchPattern): void { foreach ($this->declarations as $propertyName => $declarations) { - // Either the search rule matches the found rule exactly - // or the search rule ends in “-” and the found rule starts with the search rule or equals it - // (without the trailing dash). + // Either the search pattern matches the found declaration's property name exactly + // or the search pattern ends in “-” and the found declaration's property name starts with the search + // pattern or equals it (without the trailing dash). if ( $propertyName === $searchPattern || (\strrpos($searchPattern, '-') === \strlen($searchPattern) - \strlen('-') From f9424cd821267e6e29798f9435da3461ebce455d Mon Sep 17 00:00:00 2001 From: JakeQZ Date: Sun, 15 Feb 2026 10:24:19 +0000 Subject: [PATCH 111/136] [TASK] Use 'declaration' internally in `RuleSet` (#1525) ... in local variables and private method names, instead of the misnomered 'rule' --- src/RuleSet/RuleSet.php | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/RuleSet/RuleSet.php b/src/RuleSet/RuleSet.php index b7b603e67..b3c9cf0d0 100644 --- a/src/RuleSet/RuleSet.php +++ b/src/RuleSet/RuleSet.php @@ -59,15 +59,15 @@ public static function parseRuleSet(ParserState $parserState, RuleSet $ruleSet): $parserState->consume(';'); } while (true) { - $commentsBeforeRule = []; - $parserState->consumeWhiteSpace($commentsBeforeRule); + $commentsBeforeDeclaration = []; + $parserState->consumeWhiteSpace($commentsBeforeDeclaration); if ($parserState->comes('}')) { break; } $declaration = null; if ($parserState->getSettings()->usesLenientParsing()) { try { - $declaration = Declaration::parse($parserState, $commentsBeforeRule); + $declaration = Declaration::parse($parserState, $commentsBeforeDeclaration); } catch (UnexpectedTokenException $e) { try { $consumedText = $parserState->consumeUntil(["\n", ';', '}'], true); @@ -85,7 +85,7 @@ public static function parseRuleSet(ParserState $parserState, RuleSet $ruleSet): } } } else { - $declaration = Declaration::parse($parserState, $commentsBeforeRule); + $declaration = Declaration::parse($parserState, $commentsBeforeDeclaration); } if ($declaration instanceof Declaration) { $ruleSet->addRule($declaration); @@ -115,7 +115,7 @@ public function addRule(Declaration $declarationToAdd, ?Declaration $sibling = n $siblingIsInSet = true; $position = $siblingPosition; } else { - $siblingIsInSet = $this->hasRule($sibling); + $siblingIsInSet = $this->hasDeclaration($sibling); if ($siblingIsInSet) { // Maintain ordering within `$this->declarations[$propertyName]` // by inserting before first `Declaration` with a same-or-later position than the sibling. @@ -311,12 +311,12 @@ protected function renderRules(OutputFormat $outputFormat): string $nextLevelFormat = $outputFormat->nextLevel(); foreach ($this->getRules() as $declaration) { $nextLevelFormatter = $nextLevelFormat->getFormatter(); - $renderedRule = $nextLevelFormatter->safely( + $renderedDeclaration = $nextLevelFormatter->safely( static function () use ($declaration, $nextLevelFormat): string { return $declaration->render($nextLevelFormat); } ); - if ($renderedRule === null) { + if ($renderedDeclaration === null) { continue; } if ($isFirst) { @@ -325,7 +325,7 @@ static function () use ($declaration, $nextLevelFormat): string { } else { $result .= $nextLevelFormatter->spaceBetweenRules(); } - $result .= $renderedRule; + $result .= $renderedDeclaration; } $formatter = $outputFormat->getFormatter(); @@ -378,7 +378,7 @@ private static function comparePositionable(Positionable $first, Positionable $s return $firstsLineNumber - $secondsLineNumber; } - private function hasRule(Declaration $declaration): bool + private function hasDeclaration(Declaration $declaration): bool { foreach ($this->declarations as $declarationsForAProperty) { if (\in_array($declaration, $declarationsForAProperty, true)) { From b1d098879641962e07f6bb3017a213dc6dd26524 Mon Sep 17 00:00:00 2001 From: JakeQZ Date: Sun, 15 Feb 2026 15:21:02 +0000 Subject: [PATCH 112/136] [TASK] Update a code comment in `RuleSet` to use 'declaration' (#1526) Missed in #1524. --- src/RuleSet/RuleSet.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/RuleSet/RuleSet.php b/src/RuleSet/RuleSet.php index b3c9cf0d0..087ab30f0 100644 --- a/src/RuleSet/RuleSet.php +++ b/src/RuleSet/RuleSet.php @@ -128,7 +128,7 @@ public function addRule(Declaration $declarationToAdd, ?Declaration $sibling = n } } if ($siblingIsInSet) { - // Increment column number of all existing rules on same line, starting at sibling + // Increment column number of all existing declarations on same line, starting at sibling $siblingLineNumber = $sibling->getLineNumber(); $siblingColumnNumber = $sibling->getColumnNumber(); foreach ($this->declarations as $declarationsForAProperty) { From e8ef816713c48ad750684266bbf95d1e73a3a31d Mon Sep 17 00:00:00 2001 From: JakeQZ Date: Sun, 15 Feb 2026 16:13:48 +0000 Subject: [PATCH 113/136] [TASK] Rename methods in `RuleContainer` (#1521) E.g. `addRule()` is changed to `addDeclaration()`, with `addRule()` deprecated and changed to a wrapper method to call on to `addDeclaration()`. `RuleContainer` itself should be renamed to `DeclarationContainer` as a separate change. The tests will also be updated as a separate change. --- CHANGELOG.md | 4 ++ src/RuleSet/AtRuleSet.php | 2 +- src/RuleSet/DeclarationBlock.php | 43 +++++++------ src/RuleSet/LegacyRuleContainerMethods.php | 75 ++++++++++++++++++++++ src/RuleSet/RuleContainer.php | 41 ++++++++++++ src/RuleSet/RuleSet.php | 37 +++++------ 6 files changed, 162 insertions(+), 40 deletions(-) create mode 100644 src/RuleSet/LegacyRuleContainerMethods.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 72ad1588a..14e3ecb83 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,8 @@ Please also have a look at our ### Changed +- Methods like `setRule()` in `RuleSet` and `DeclarationBlock` have been renamed + to `setDeclaration()`, etc. (#1521) - `Rule\Rule` class is renamed to `Property\Declaration` (#1508, #1512, #1513, #1522) - `Rule::setRule()` and `getRule()` are replaced with `setPropertyName()` and @@ -33,6 +35,8 @@ Please also have a look at our ### Deprecated +- Methods like `setRule()` in `RuleSet` and `DeclarationBlock` are deprecated; + there are direct replacements such as `setDeclaration()` (#1521) - `Rule\Rule` class is deprecated; `Property\Declaration` is a direct replacement (#1508) - `Rule::setRule()` and `getRule()` are deprecated and replaced with diff --git a/src/RuleSet/AtRuleSet.php b/src/RuleSet/AtRuleSet.php index 4cd5acc2a..7307193d4 100644 --- a/src/RuleSet/AtRuleSet.php +++ b/src/RuleSet/AtRuleSet.php @@ -61,7 +61,7 @@ public function render(OutputFormat $outputFormat): string $arguments = ' ' . $arguments; } $result .= "@{$this->type}$arguments{$formatter->spaceBeforeOpeningBrace()}{"; - $result .= $this->renderRules($outputFormat); + $result .= $this->renderDeclarations($outputFormat); $result .= '}'; return $result; } diff --git a/src/RuleSet/DeclarationBlock.php b/src/RuleSet/DeclarationBlock.php index 8ded5558b..29265ca69 100644 --- a/src/RuleSet/DeclarationBlock.php +++ b/src/RuleSet/DeclarationBlock.php @@ -35,6 +35,7 @@ class DeclarationBlock implements CSSElement, CSSListItem, Positionable, RuleContainer { use CommentContainer; + use LegacyRuleContainerMethods; use Position; /** @@ -180,65 +181,65 @@ public function getRuleSet(): RuleSet } /** - * @see RuleSet::addRule() + * @see RuleSet::addDeclaration() */ - public function addRule(Declaration $declarationToAdd, ?Declaration $sibling = null): void + public function addDeclaration(Declaration $declarationToAdd, ?Declaration $sibling = null): void { - $this->ruleSet->addRule($declarationToAdd, $sibling); + $this->ruleSet->addDeclaration($declarationToAdd, $sibling); } /** * @return array, Declaration> * - * @see RuleSet::getRules() + * @see RuleSet::getDeclarations() */ - public function getRules(?string $searchPattern = null): array + public function getDeclarations(?string $searchPattern = null): array { - return $this->ruleSet->getRules($searchPattern); + return $this->ruleSet->getDeclarations($searchPattern); } /** * @param array $declarations * - * @see RuleSet::setRules() + * @see RuleSet::setDeclarations() */ - public function setRules(array $declarations): void + public function setDeclarations(array $declarations): void { - $this->ruleSet->setRules($declarations); + $this->ruleSet->setDeclarations($declarations); } /** * @return array * - * @see RuleSet::getRulesAssoc() + * @see RuleSet::getDeclarationsAssociative() */ - public function getRulesAssoc(?string $searchPattern = null): array + public function getDeclarationsAssociative(?string $searchPattern = null): array { - return $this->ruleSet->getRulesAssoc($searchPattern); + return $this->ruleSet->getDeclarationsAssociative($searchPattern); } /** - * @see RuleSet::removeRule() + * @see RuleSet::removeDeclaration() */ - public function removeRule(Declaration $declarationToRemove): void + public function removeDeclaration(Declaration $declarationToRemove): void { - $this->ruleSet->removeRule($declarationToRemove); + $this->ruleSet->removeDeclaration($declarationToRemove); } /** - * @see RuleSet::removeMatchingRules() + * @see RuleSet::removeMatchingDeclarations() */ - public function removeMatchingRules(string $searchPattern): void + public function removeMatchingDeclarations(string $searchPattern): void { - $this->ruleSet->removeMatchingRules($searchPattern); + $this->ruleSet->removeMatchingDeclarations($searchPattern); } /** - * @see RuleSet::removeAllRules() + * @see RuleSet::removeAllDeclarations() */ - public function removeAllRules(): void + public function removeAllDeclarations(): void { - $this->ruleSet->removeAllRules(); + $this->ruleSet->removeAllDeclarations(); } /** diff --git a/src/RuleSet/LegacyRuleContainerMethods.php b/src/RuleSet/LegacyRuleContainerMethods.php new file mode 100644 index 000000000..5ed6f7944 --- /dev/null +++ b/src/RuleSet/LegacyRuleContainerMethods.php @@ -0,0 +1,75 @@ +addDeclaration($declarationToAdd, $sibling); + } + + /** + * @deprecated in v9.2, will be removed in v10.0; use `removeDeclaration()` instead. + */ + public function removeRule(Declaration $declarationToRemove): void + { + $this->removeDeclaration($declarationToRemove); + } + + /** + * @deprecated in v9.2, will be removed in v10.0; use `removeMatchingDeclarations()` instead. + */ + public function removeMatchingRules(string $searchPattern): void + { + $this->removeMatchingDeclarations($searchPattern); + } + + /** + * @deprecated in v9.2, will be removed in v10.0; use `removeAllDeclarations()` instead. + */ + public function removeAllRules(): void + { + $this->removeAllDeclarations(); + } + + /** + * @param array $declarations + * + * @deprecated in v9.2, will be removed in v10.0; use `setDeclarations()` instead. + */ + public function setRules(array $declarations): void + { + $this->setDeclarations($declarations); + } + + /** + * @return array, Declaration> + * + * @deprecated in v9.2, will be removed in v10.0; use `getDeclarations()` instead. + */ + public function getRules(?string $searchPattern = null): array + { + return $this->getDeclarations($searchPattern); + } + + /** + * @return array + * + * @deprecated in v9.2, will be removed in v10.0; use `getDeclarationsAssociative()` instead. + */ + public function getRulesAssoc(?string $searchPattern = null): array + { + return $this->getDeclarationsAssociative($searchPattern); + } +} diff --git a/src/RuleSet/RuleContainer.php b/src/RuleSet/RuleContainer.php index fcd212bf2..2767d5057 100644 --- a/src/RuleSet/RuleContainer.php +++ b/src/RuleSet/RuleContainer.php @@ -11,26 +11,67 @@ */ interface RuleContainer { + public function addDeclaration(Declaration $declarationToAdd, ?Declaration $sibling = null): void; + + public function removeDeclaration(Declaration $declarationToRemove): void; + + public function removeMatchingDeclarations(string $searchPattern): void; + + public function removeAllDeclarations(): void; + + /** + * @param array $declarations + */ + public function setDeclarations(array $declarations): void; + + /** + * @return array, Declaration> + */ + public function getDeclarations(?string $searchPattern = null): array; + + /** + * @return array + */ + public function getDeclarationsAssociative(?string $searchPattern = null): array; + + /** + * @deprecated in v9.2, will be removed in v10.0; use `addDeclaration()` instead. + */ public function addRule(Declaration $declarationToAdd, ?Declaration $sibling = null): void; + /** + * @deprecated in v9.2, will be removed in v10.0; use `removeDeclaration()` instead. + */ public function removeRule(Declaration $declarationToRemove): void; + /** + * @deprecated in v9.2, will be removed in v10.0; use `removeMatchingDeclarations()` instead. + */ public function removeMatchingRules(string $searchPattern): void; + /** + * @deprecated in v9.2, will be removed in v10.0; use `removeAllDeclarations()` instead. + */ public function removeAllRules(): void; /** * @param array $declarations + * + * @deprecated in v9.2, will be removed in v10.0; use `setDeclarations()` instead. */ public function setRules(array $declarations): void; /** * @return array, Declaration> + * + * @deprecated in v9.2, will be removed in v10.0; use `getDeclarations()` instead. */ public function getRules(?string $searchPattern = null): array; /** * @return array + * + * @deprecated in v9.2, will be removed in v10.0; use `getDeclarationsAssociative()` instead. */ public function getRulesAssoc(?string $searchPattern = null): array; } diff --git a/src/RuleSet/RuleSet.php b/src/RuleSet/RuleSet.php index 087ab30f0..f124a2b16 100644 --- a/src/RuleSet/RuleSet.php +++ b/src/RuleSet/RuleSet.php @@ -22,13 +22,14 @@ * However, unknown `AtRule`s (like `@font-face`) are rule sets as well. * * If you want to manipulate a `RuleSet`, - * use the methods `addRule()`, `getRules()`, `removeRule()`, `removeMatchingRules()`, etc. + * use the methods `addDeclaration()`, `getDeclarations()`, `removeDeclaration()`, `removeMatchingDeclarations()`, etc. * * Note that `CSSListItem` extends both `Commentable` and `Renderable`, so those interfaces must also be implemented. */ class RuleSet implements CSSElement, CSSListItem, Positionable, RuleContainer { use CommentContainer; + use LegacyRuleContainerMethods; use Position; /** @@ -88,7 +89,7 @@ public static function parseRuleSet(ParserState $parserState, RuleSet $ruleSet): $declaration = Declaration::parse($parserState, $commentsBeforeDeclaration); } if ($declaration instanceof Declaration) { - $ruleSet->addRule($declaration); + $ruleSet->addDeclaration($declaration); } } $parserState->consume('}'); @@ -99,7 +100,7 @@ public static function parseRuleSet(ParserState $parserState, RuleSet $ruleSet): * if the last `Declaration` is needed as a basis for setting position, but does not have a valid position, * which should never happen */ - public function addRule(Declaration $declarationToAdd, ?Declaration $sibling = null): void + public function addDeclaration(Declaration $declarationToAdd, ?Declaration $sibling = null): void { $propertyName = $declarationToAdd->getPropertyName(); if (!isset($this->declarations[$propertyName])) { @@ -148,14 +149,14 @@ public function addRule(Declaration $declarationToAdd, ?Declaration $sibling = n if ($declarationToAdd->getLineNumber() === null) { //this node is added manually, give it the next best line $columnNumber = $declarationToAdd->getColumnNumber() ?? 0; - $declarations = $this->getRules(); + $declarations = $this->getDeclarations(); $declarationsCount = \count($declarations); if ($declarationsCount > 0) { $last = $declarations[$declarationsCount - 1]; $lastsLineNumber = $last->getLineNumber(); if (!\is_int($lastsLineNumber)) { throw new \UnexpectedValueException( - 'A Declaration without a line number was found during addRule', + 'A Declaration without a line number was found during addDeclaration', 1750718399 ); } @@ -173,9 +174,9 @@ public function addRule(Declaration $declarationToAdd, ?Declaration $sibling = n /** * Returns all declarations matching the given property name * - * @example $ruleSet->getRules('font') // returns array(0 => $declaration, …) or array(). + * @example $ruleSet->getDeclarations('font') // returns array(0 => $declaration, …) or array(). * - * @example $ruleSet->getRules('font-') + * @example $ruleSet->getDeclarations('font-') * //returns an array of all declarations either beginning with font- or matching font. * * @param string|null $searchPattern @@ -185,7 +186,7 @@ public function addRule(Declaration $declarationToAdd, ?Declaration $sibling = n * * @return array, Declaration> */ - public function getRules(?string $searchPattern = null): array + public function getDeclarations(?string $searchPattern = null): array { $result = []; foreach ($this->declarations as $propertyName => $declarations) { @@ -214,11 +215,11 @@ public function getRules(?string $searchPattern = null): array * * @param array $declarations */ - public function setRules(array $declarations): void + public function setDeclarations(array $declarations): void { $this->declarations = []; foreach ($declarations as $declaration) { - $this->addRule($declaration); + $this->addDeclaration($declaration); } } @@ -238,11 +239,11 @@ public function setRules(array $declarations): void * * @return array */ - public function getRulesAssoc(?string $searchPattern = null): array + public function getDeclarationsAssociative(?string $searchPattern = null): array { /** @var array $result */ $result = []; - foreach ($this->getRules($searchPattern) as $declaration) { + foreach ($this->getDeclarations($searchPattern) as $declaration) { $result[$declaration->getPropertyName()] = $declaration; } @@ -252,7 +253,7 @@ public function getRulesAssoc(?string $searchPattern = null): array /** * Removes a `Declaration` from this `RuleSet` by identity. */ - public function removeRule(Declaration $declarationToRemove): void + public function removeDeclaration(Declaration $declarationToRemove): void { $nameOfPropertyToRemove = $declarationToRemove->getPropertyName(); if (!isset($this->declarations[$nameOfPropertyToRemove])) { @@ -274,7 +275,7 @@ public function removeRule(Declaration $declarationToRemove): void * all declarations starting with the pattern are removed as well as one matching the pattern with the dash * excluded. */ - public function removeMatchingRules(string $searchPattern): void + public function removeMatchingDeclarations(string $searchPattern): void { foreach ($this->declarations as $propertyName => $declarations) { // Either the search pattern matches the found declaration's property name exactly @@ -291,7 +292,7 @@ public function removeMatchingRules(string $searchPattern): void } } - public function removeAllRules(): void + public function removeAllDeclarations(): void { $this->declarations = []; } @@ -301,15 +302,15 @@ public function removeAllRules(): void */ public function render(OutputFormat $outputFormat): string { - return $this->renderRules($outputFormat); + return $this->renderDeclarations($outputFormat); } - protected function renderRules(OutputFormat $outputFormat): string + protected function renderDeclarations(OutputFormat $outputFormat): string { $result = ''; $isFirst = true; $nextLevelFormat = $outputFormat->nextLevel(); - foreach ($this->getRules() as $declaration) { + foreach ($this->getDeclarations() as $declaration) { $nextLevelFormatter = $nextLevelFormat->getFormatter(); $renderedDeclaration = $nextLevelFormatter->safely( static function () use ($declaration, $nextLevelFormat): string { From 3d9baf9feef24a208ed37cb865179d416293b943 Mon Sep 17 00:00:00 2001 From: Oliver Klee Date: Sun, 15 Feb 2026 17:32:42 +0100 Subject: [PATCH 114/136] [TASK] Update to thecodingmachine/safe 3.4 (#1527) This version massively reduces the download size. https://github.com/thecodingmachine/safe/releases/tag/v3.4.0 --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index e037dcb53..b0fe1213e 100644 --- a/composer.json +++ b/composer.json @@ -25,7 +25,7 @@ "require": { "php": "^7.2.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0", "ext-iconv": "*", - "thecodingmachine/safe": "^1.3 || ^2.5 || ^3.3" + "thecodingmachine/safe": "^1.3 || ^2.5 || ^3.4" }, "require-dev": { "php-parallel-lint/php-parallel-lint": "1.4.0", From b38597e8eb7bb4f7fd41ad72b209dd7284833b10 Mon Sep 17 00:00:00 2001 From: JakeQZ Date: Mon, 16 Feb 2026 13:45:54 +0000 Subject: [PATCH 115/136] [TASK] Use `addDeclaration()` etc. in tests (#1528) The methods of the `RuleContainer` interface were renamed in #1521. --- .../RuleSet/DeclarationBlockTest.php | 6 +- tests/Functional/RuleSet/RuleSetTest.php | 2 +- tests/ParserTest.php | 48 ++-- tests/RuleSet/DeclarationBlockTest.php | 18 +- tests/Unit/CSSList/CSSBlockListTest.php | 24 +- tests/Unit/RuleSet/DeclarationBlockTest.php | 8 +- tests/Unit/RuleSet/RuleContainerTest.php | 254 +++++++++--------- 7 files changed, 180 insertions(+), 180 deletions(-) diff --git a/tests/Functional/RuleSet/DeclarationBlockTest.php b/tests/Functional/RuleSet/DeclarationBlockTest.php index 3265c007f..4960c4d8a 100644 --- a/tests/Functional/RuleSet/DeclarationBlockTest.php +++ b/tests/Functional/RuleSet/DeclarationBlockTest.php @@ -53,15 +53,15 @@ public function rendersDeclarationsInOrderProvided(): void $declaration1 = new Declaration('background-color'); $declaration1->setValue('transparent'); - $declarationBlock->addRule($declaration1); + $declarationBlock->addDeclaration($declaration1); $declaration2 = new Declaration('background'); $declaration2->setValue('#222'); - $declarationBlock->addRule($declaration2); + $declarationBlock->addDeclaration($declaration2); $declaration3 = new Declaration('background-color'); $declaration3->setValue('#fff'); - $declarationBlock->addRule($declaration3); + $declarationBlock->addDeclaration($declaration3); $expectedRendering = 'background-color: transparent;background: #222;background-color: #fff'; self::assertStringContainsString($expectedRendering, $declarationBlock->render(new OutputFormat())); diff --git a/tests/Functional/RuleSet/RuleSetTest.php b/tests/Functional/RuleSet/RuleSetTest.php index c33a15193..5d637b62f 100644 --- a/tests/Functional/RuleSet/RuleSetTest.php +++ b/tests/Functional/RuleSet/RuleSetTest.php @@ -114,6 +114,6 @@ static function (array $nameAndValue): Declaration { }, $propertyNamesAndValues ); - $this->subject->setRules($declarationsToSet); + $this->subject->setDeclarations($declarationsToSet); } } diff --git a/tests/ParserTest.php b/tests/ParserTest.php index 55aa36110..4b1fabcd8 100644 --- a/tests/ParserTest.php +++ b/tests/ParserTest.php @@ -95,10 +95,10 @@ public function colorParsing(): void $selectors = $declarationBlock->getSelectors(); $selector = $selectors[0]->getSelector(); if ($selector === '#mine') { - $colorRules = $declarationBlock->getRules('color'); + $colorRules = $declarationBlock->getDeclarations('color'); $colorRuleValue = $colorRules[0]->getValue(); self::assertSame('red', $colorRuleValue); - $colorRules = $declarationBlock->getRules('background-'); + $colorRules = $declarationBlock->getDeclarations('background-'); $colorRuleValue = $colorRules[0]->getValue(); self::assertInstanceOf(Color::class, $colorRuleValue); self::assertEquals([ @@ -106,7 +106,7 @@ public function colorParsing(): void 'g' => new Size(35.0, null, true, $colorRuleValue->getLineNumber()), 'b' => new Size(35.0, null, true, $colorRuleValue->getLineNumber()), ], $colorRuleValue->getColor()); - $colorRules = $declarationBlock->getRules('border-color'); + $colorRules = $declarationBlock->getDeclarations('border-color'); $colorRuleValue = $colorRules[0]->getValue(); self::assertInstanceOf(Color::class, $colorRuleValue); self::assertEquals([ @@ -122,7 +122,7 @@ public function colorParsing(): void 'b' => new Size(231.0, null, true, $colorRuleValue->getLineNumber()), 'a' => new Size('0000.3', null, true, $colorRuleValue->getLineNumber()), ], $colorRuleValue->getColor()); - $colorRules = $declarationBlock->getRules('outline-color'); + $colorRules = $declarationBlock->getDeclarations('outline-color'); $colorRuleValue = $colorRules[0]->getValue(); self::assertInstanceOf(Color::class, $colorRuleValue); self::assertEquals([ @@ -131,7 +131,7 @@ public function colorParsing(): void 'b' => new Size(34.0, null, true, $colorRuleValue->getLineNumber()), ], $colorRuleValue->getColor()); } elseif ($selector === '#yours') { - $colorRules = $declarationBlock->getRules('background-color'); + $colorRules = $declarationBlock->getDeclarations('background-color'); $colorRuleValue = $colorRules[0]->getValue(); self::assertInstanceOf(Color::class, $colorRuleValue); self::assertEquals([ @@ -147,7 +147,7 @@ public function colorParsing(): void 'l' => new Size(220.0, '%', true, $colorRuleValue->getLineNumber()), 'a' => new Size(0000.3, null, true, $colorRuleValue->getLineNumber()), ], $colorRuleValue->getColor()); - $colorRules = $declarationBlock->getRules('outline-color'); + $colorRules = $declarationBlock->getDeclarations('outline-color'); self::assertEmpty($colorRules); } } @@ -179,7 +179,7 @@ public function unicodeParsing(): void if (\substr($selector, 0, \strlen('.test-')) !== '.test-') { continue; } - $contentRules = $declarationBlock->getRules('content'); + $contentRules = $declarationBlock->getDeclarations('content'); $firstContentRuleAsString = $contentRules[0]->getValue()->render(OutputFormat::create()); if ($selector === '.test-1') { self::assertSame('" "', $firstContentRuleAsString); @@ -315,7 +315,7 @@ public function manipulation(): void $document->render() ); foreach ($document->getAllRuleSets() as $ruleSet) { - $ruleSet->removeMatchingRules('font-'); + $ruleSet->removeMatchingDeclarations('font-'); } self::assertSame( '#header {margin: 10px 2em 1cm 2%;color: red !important;background-color: green;' @@ -324,7 +324,7 @@ public function manipulation(): void $document->render() ); foreach ($document->getAllRuleSets() as $ruleSet) { - $ruleSet->removeMatchingRules('background-'); + $ruleSet->removeMatchingDeclarations('background-'); } self::assertSame( '#header {margin: 10px 2em 1cm 2%;color: red !important;frequency: 30Hz;transform: rotate(1turn);} @@ -342,16 +342,16 @@ public function ruleGetters(): void $declarationBlocks = $document->getAllDeclarationBlocks(); $headerBlock = $declarationBlocks[0]; $bodyBlock = $declarationBlocks[1]; - $backgroundHeaderRules = $headerBlock->getRules('background-'); + $backgroundHeaderRules = $headerBlock->getDeclarations('background-'); self::assertCount(2, $backgroundHeaderRules); self::assertSame('background-color', $backgroundHeaderRules[0]->getPropertyName()); self::assertSame('background-color', $backgroundHeaderRules[1]->getPropertyName()); - $backgroundHeaderRules = $headerBlock->getRulesAssoc('background-'); + $backgroundHeaderRules = $headerBlock->getDeclarationsAssociative('background-'); self::assertCount(1, $backgroundHeaderRules); self::assertInstanceOf(Color::class, $backgroundHeaderRules['background-color']->getValue()); self::assertSame('rgba', $backgroundHeaderRules['background-color']->getValue()->getColorDescription()); - $headerBlock->removeRule($backgroundHeaderRules['background-color']); - $backgroundHeaderRules = $headerBlock->getRules('background-'); + $headerBlock->removeDeclaration($backgroundHeaderRules['background-color']); + $backgroundHeaderRules = $headerBlock->getDeclarations('background-'); self::assertCount(1, $backgroundHeaderRules); self::assertSame('green', $backgroundHeaderRules[0]->getValue()); } @@ -372,7 +372,7 @@ public function slashedValues(): void } } foreach ($document->getAllDeclarationBlocks() as $declarationBlock) { - $fontRules = $declarationBlock->getRules('font'); + $fontRules = $declarationBlock->getDeclarations('font'); $fontRule = $fontRules[0]; $fontRuleValue = $fontRule->getValue(); self::assertSame(' ', $fontRuleValue->getListSeparator()); @@ -383,7 +383,7 @@ public function slashedValues(): void self::assertInstanceOf(ValueList::class, $slashList); self::assertSame(',', $commaList->getListSeparator()); self::assertSame('/', $slashList->getListSeparator()); - $borderRadiusRules = $declarationBlock->getRules('border-radius'); + $borderRadiusRules = $declarationBlock->getDeclarations('border-radius'); $borderRadiusRule = $borderRadiusRules[0]; $slashList = $borderRadiusRule->getValue(); self::assertSame('/', $slashList->getListSeparator()); @@ -883,7 +883,7 @@ public function missingPropertyValueLenient(): void $block = $declarationBlocks[0]; self::assertInstanceOf(DeclarationBlock::class, $block); self::assertEquals([new Selector('div')], $block->getSelectors()); - $rules = $block->getRules(); + $rules = $block->getDeclarations(); self::assertCount(1, $rules); $rule = $rules[0]; self::assertSame('display', $rule->getPropertyName()); @@ -948,7 +948,7 @@ public function lineNumbersParsing(): void $declarationBlocks = $document->getAllDeclarationBlocks(); // Choose the 2nd one $secondDeclarationBlock = $declarationBlocks[1]; - $rules = $secondDeclarationBlock->getRules(); + $rules = $secondDeclarationBlock->getDeclarations(); // Choose the 2nd one $valueOfSecondRule = $rules[1]->getValue(); self::assertInstanceOf(Color::class, $valueOfSecondRule); @@ -1007,7 +1007,7 @@ public function commentExtracting(): void // Declaration rules. self::assertInstanceOf(DeclarationBlock::class, $fooBarBlock); - $fooBarRules = $fooBarBlock->getRules(); + $fooBarRules = $fooBarBlock->getDeclarations(); $fooBarRule = $fooBarRules[0]; $fooBarRuleComments = $fooBarRule->getComments(); self::assertCount(1, $fooBarRuleComments); @@ -1028,7 +1028,7 @@ public function commentExtracting(): void // Media -> declaration -> rule. self::assertInstanceOf(DeclarationBlock::class, $mediaRules[0]); - $fooBarRules = $mediaRules[0]->getRules(); + $fooBarRules = $mediaRules[0]->getDeclarations(); $fooBarChildComments = $fooBarRules[0]->getComments(); self::assertCount(1, $fooBarChildComments); self::assertSame('* Number 10b *', $fooBarChildComments[0]->getComment()); @@ -1044,7 +1044,7 @@ public function flatCommentExtractingOneComment(): void $contents = $document->getContents(); self::assertInstanceOf(DeclarationBlock::class, $contents[0]); - $divRules = $contents[0]->getRules(); + $divRules = $contents[0]->getDeclarations(); $comments = $divRules[0]->getComments(); self::assertCount(1, $comments); @@ -1061,7 +1061,7 @@ public function flatCommentExtractingTwoConjoinedCommentsForOneRule(): void $contents = $document->getContents(); self::assertInstanceOf(DeclarationBlock::class, $contents[0]); - $divRules = $contents[0]->getRules(); + $divRules = $contents[0]->getDeclarations(); $comments = $divRules[0]->getComments(); self::assertCount(2, $comments); @@ -1079,7 +1079,7 @@ public function flatCommentExtractingTwoSpaceSeparatedCommentsForOneRule(): void $contents = $document->getContents(); self::assertInstanceOf(DeclarationBlock::class, $contents[0]); - $divRules = $contents[0]->getRules(); + $divRules = $contents[0]->getDeclarations(); $comments = $divRules[0]->getComments(); self::assertCount(2, $comments); @@ -1097,7 +1097,7 @@ public function flatCommentExtractingCommentsForTwoRules(): void $contents = $document->getContents(); self::assertInstanceOf(DeclarationBlock::class, $contents[0]); - $divRules = $contents[0]->getRules(); + $divRules = $contents[0]->getDeclarations(); $rule1Comments = $divRules[0]->getComments(); $rule2Comments = $divRules[1]->getComments(); @@ -1182,7 +1182,7 @@ public function escapedSpecialCaseTokens(): void $document = self::parsedStructureForFile('escaped-tokens'); $contents = $document->getContents(); self::assertInstanceOf(RuleSet::class, $contents[0]); - $rules = $contents[0]->getRules(); + $rules = $contents[0]->getDeclarations(); $urlRule = $rules[0]; $calcRule = $rules[1]; self::assertInstanceOf(URL::class, $urlRule->getValue()); diff --git a/tests/RuleSet/DeclarationBlockTest.php b/tests/RuleSet/DeclarationBlockTest.php index e6631903c..b1e16d513 100644 --- a/tests/RuleSet/DeclarationBlockTest.php +++ b/tests/RuleSet/DeclarationBlockTest.php @@ -31,10 +31,10 @@ public function overrideRules(): void $wrapper = $contents[0]; self::assertInstanceOf(DeclarationBlock::class, $wrapper); - self::assertCount(2, $wrapper->getRules()); - $wrapper->setRules([$declaration]); + self::assertCount(2, $wrapper->getDeclarations()); + $wrapper->setDeclarations([$declaration]); - $declarations = $wrapper->getRules(); + $declarations = $wrapper->getDeclarations(); self::assertCount(1, $declarations); self::assertSame('right', $declarations[0]->getPropertyName()); self::assertSame('-10px', $declarations[0]->getValue()); @@ -53,11 +53,11 @@ public function declarationInsertion(): void self::assertInstanceOf(DeclarationBlock::class, $wrapper); - $leftDeclarations = $wrapper->getRules('left'); + $leftDeclarations = $wrapper->getDeclarations('left'); self::assertCount(1, $leftDeclarations); $firstLeftDeclaration = $leftDeclarations[0]; - $textDeclarations = $wrapper->getRules('text-'); + $textDeclarations = $wrapper->getDeclarations('text-'); self::assertCount(1, $textDeclarations); $firstTextDeclaration = $textDeclarations[0]; @@ -70,11 +70,11 @@ public function declarationInsertion(): void $borderBottomDeclaration = new Declaration('border-bottom-width'); $borderBottomDeclaration->setValue(new Size(1, 'px')); - $wrapper->addRule($borderBottomDeclaration); - $wrapper->addRule($leftPrefixDeclaration, $firstLeftDeclaration); - $wrapper->addRule($textAlignDeclaration, $firstTextDeclaration); + $wrapper->addDeclaration($borderBottomDeclaration); + $wrapper->addDeclaration($leftPrefixDeclaration, $firstLeftDeclaration); + $wrapper->addDeclaration($textAlignDeclaration, $firstTextDeclaration); - $declarations = $wrapper->getRules(); + $declarations = $wrapper->getDeclarations(); self::assertSame($leftPrefixDeclaration, $declarations[0]); self::assertSame($firstLeftDeclaration, $declarations[1]); diff --git a/tests/Unit/CSSList/CSSBlockListTest.php b/tests/Unit/CSSList/CSSBlockListTest.php index b8f8c429d..fc584541b 100644 --- a/tests/Unit/CSSList/CSSBlockListTest.php +++ b/tests/Unit/CSSList/CSSBlockListTest.php @@ -302,7 +302,7 @@ public function getAllValuesReturnsOneValueDirectlySetAsContent(): void $declarationBlock = new DeclarationBlock(); $declaration = new Declaration('font-family'); $declaration->setValue($value); - $declarationBlock->addRule($declaration); + $declarationBlock->addDeclaration($declaration); $subject->setContents([$declarationBlock]); $result = $subject->getAllValues(); @@ -323,10 +323,10 @@ public function getAllValuesReturnsMultipleValuesDirectlySetAsContentInOneDeclar $declarationBlock = new DeclarationBlock(); $declaration1 = new Declaration('font-family'); $declaration1->setValue($value1); - $declarationBlock->addRule($declaration1); + $declarationBlock->addDeclaration($declaration1); $declaration2 = new Declaration('color'); $declaration2->setValue($value2); - $declarationBlock->addRule($declaration2); + $declarationBlock->addDeclaration($declaration2); $subject->setContents([$declarationBlock]); $result = $subject->getAllValues(); @@ -347,11 +347,11 @@ public function getAllValuesReturnsMultipleValuesDirectlySetAsContentInMultipleD $declarationBlock1 = new DeclarationBlock(); $declaration1 = new Declaration('font-family'); $declaration1->setValue($value1); - $declarationBlock1->addRule($declaration1); + $declarationBlock1->addDeclaration($declaration1); $declarationBlock2 = new DeclarationBlock(); $declaration2 = new Declaration('color'); $declaration2->setValue($value2); - $declarationBlock2->addRule($declaration2); + $declarationBlock2->addDeclaration($declaration2); $subject->setContents([$declarationBlock1, $declarationBlock2]); $result = $subject->getAllValues(); @@ -371,7 +371,7 @@ public function getAllValuesReturnsValuesWithinAtRuleBlockList(): void $declarationBlock = new DeclarationBlock(); $declaration = new Declaration('font-family'); $declaration->setValue($value); - $declarationBlock->addRule($declaration); + $declarationBlock->addDeclaration($declaration); $atRuleBlockList = new AtRuleBlockList('media'); $atRuleBlockList->setContents([$declarationBlock]); $subject->setContents([$atRuleBlockList]); @@ -394,11 +394,11 @@ public function getAllValuesWithElementProvidedReturnsOnlyValuesWithinThatElemen $declarationBlock1 = new DeclarationBlock(); $declaration1 = new Declaration('font-family'); $declaration1->setValue($value1); - $declarationBlock1->addRule($declaration1); + $declarationBlock1->addDeclaration($declaration1); $declarationBlock2 = new DeclarationBlock(); $declaration2 = new Declaration('color'); $declaration2->setValue($value2); - $declarationBlock2->addRule($declaration2); + $declarationBlock2->addDeclaration($declaration2); $subject->setContents([$declarationBlock1, $declarationBlock2]); $result = $subject->getAllValues($declarationBlock1); @@ -419,10 +419,10 @@ public function getAllValuesWithSearchStringProvidedReturnsOnlyValuesFromMatchin $declarationBlock = new DeclarationBlock(); $declaration1 = new Declaration('font-family'); $declaration1->setValue($value1); - $declarationBlock->addRule($declaration1); + $declarationBlock->addDeclaration($declaration1); $declaration2 = new Declaration('color'); $declaration2->setValue($value2); - $declarationBlock->addRule($declaration2); + $declarationBlock->addDeclaration($declaration2); $subject->setContents([$declarationBlock]); $result = $subject->getAllValues(null, 'font-'); @@ -443,7 +443,7 @@ public function getAllValuesByDefaultDoesNotReturnValuesInFunctionArguments(): v $declarationBlock = new DeclarationBlock(); $declaration = new Declaration('margin'); $declaration->setValue(new CSSFunction('max', [$value1, $value2])); - $declarationBlock->addRule($declaration); + $declarationBlock->addDeclaration($declaration); $subject->setContents([$declarationBlock]); $result = $subject->getAllValues(); @@ -464,7 +464,7 @@ public function getAllValuesWithSearchInFunctionArgumentsReturnsValuesInFunction $declarationBlock = new DeclarationBlock(); $declaration = new Declaration('margin'); $declaration->setValue(new CSSFunction('max', [$value1, $value2])); - $declarationBlock->addRule($declaration); + $declarationBlock->addDeclaration($declaration); $subject->setContents([$declarationBlock]); $result = $subject->getAllValues(null, null, true); diff --git a/tests/Unit/RuleSet/DeclarationBlockTest.php b/tests/Unit/RuleSet/DeclarationBlockTest.php index 75213fc10..6f39ea4b7 100644 --- a/tests/Unit/RuleSet/DeclarationBlockTest.php +++ b/tests/Unit/RuleSet/DeclarationBlockTest.php @@ -391,7 +391,7 @@ public function getRuleSetOnVirginReturnsARuleSet(): void */ public function getRuleSetAfterRulesSetReturnsARuleSet(): void { - $this->subject->setRules([new Declaration('color')]); + $this->subject->setDeclarations([new Declaration('color')]); $result = $this->subject->getRuleSet(); @@ -405,7 +405,7 @@ public function getRuleSetOnVirginReturnsObjectWithoutDeclarations(): void { $result = $this->subject->getRuleSet(); - self::assertSame([], $result->getRules()); + self::assertSame([], $result->getDeclarations()); } /** @@ -418,11 +418,11 @@ public function getRuleSetOnVirginReturnsObjectWithoutDeclarations(): void public function getRuleSetReturnsObjectWithDeclarationsSet(array $propertyNamesToSet): void { $declarations = self::createDeclarationsFromPropertyNames($propertyNamesToSet); - $this->subject->setRules($declarations); + $this->subject->setDeclarations($declarations); $result = $this->subject->getRuleSet(); - self::assertSame($declarations, $result->getRules()); + self::assertSame($declarations, $result->getDeclarations()); } /** diff --git a/tests/Unit/RuleSet/RuleContainerTest.php b/tests/Unit/RuleSet/RuleContainerTest.php index 001505f55..7bb2964cf 100644 --- a/tests/Unit/RuleSet/RuleContainerTest.php +++ b/tests/Unit/RuleSet/RuleContainerTest.php @@ -66,16 +66,16 @@ public static function provideInitialPropertyNamesAndAnotherPropertyName(): Data * * @dataProvider provideInitialPropertyNamesAndAnotherPropertyName */ - public function addRuleWithoutPositionWithoutSiblingAddsDeclarationAfterInitialDeclarations( + public function addDeclarationWithoutPositionWithoutSiblingAddsDeclarationAfterInitialDeclarations( array $initialPropertyNames, string $propertyNameToAdd ): void { $declarationToAdd = new Declaration($propertyNameToAdd); $this->setDeclarationsFromPropertyNames($initialPropertyNames); - $this->subject->addRule($declarationToAdd); + $this->subject->addDeclaration($declarationToAdd); - $declarations = $this->subject->getRules(); + $declarations = $this->subject->getDeclarations(); self::assertSame($declarationToAdd, \end($declarations)); } @@ -86,14 +86,14 @@ public function addRuleWithoutPositionWithoutSiblingAddsDeclarationAfterInitialD * * @dataProvider provideInitialPropertyNamesAndAnotherPropertyName */ - public function addRuleWithoutPositionWithoutSiblingSetsValidLineNumber( + public function addDeclarationWithoutPositionWithoutSiblingSetsValidLineNumber( array $initialPropertyNames, string $propertyNameToAdd ): void { $declarationToAdd = new Declaration($propertyNameToAdd); $this->setDeclarationsFromPropertyNames($initialPropertyNames); - $this->subject->addRule($declarationToAdd); + $this->subject->addDeclaration($declarationToAdd); self::assertIsInt($declarationToAdd->getLineNumber(), 'line number not set'); self::assertGreaterThanOrEqual(1, $declarationToAdd->getLineNumber(), 'line number not valid'); @@ -106,14 +106,14 @@ public function addRuleWithoutPositionWithoutSiblingSetsValidLineNumber( * * @dataProvider provideInitialPropertyNamesAndAnotherPropertyName */ - public function addRuleWithoutPositionWithoutSiblingSetsValidColumnNumber( + public function addDeclarationWithoutPositionWithoutSiblingSetsValidColumnNumber( array $initialPropertyNames, string $propertyNameToAdd ): void { $declarationToAdd = new Declaration($propertyNameToAdd); $this->setDeclarationsFromPropertyNames($initialPropertyNames); - $this->subject->addRule($declarationToAdd); + $this->subject->addDeclaration($declarationToAdd); self::assertIsInt($declarationToAdd->getColumnNumber(), 'column number not set'); self::assertGreaterThanOrEqual(0, $declarationToAdd->getColumnNumber(), 'column number not valid'); @@ -126,7 +126,7 @@ public function addRuleWithoutPositionWithoutSiblingSetsValidColumnNumber( * * @param list $initialPropertyNames */ - public function addRuleWithOnlyLineNumberWithoutSiblingAddsDeclaration( + public function addDeclarationWithOnlyLineNumberWithoutSiblingAddsDeclaration( array $initialPropertyNames, string $propertyNameToAdd ): void { @@ -134,9 +134,9 @@ public function addRuleWithOnlyLineNumberWithoutSiblingAddsDeclaration( $declarationToAdd->setPosition(42); $this->setDeclarationsFromPropertyNames($initialPropertyNames); - $this->subject->addRule($declarationToAdd); + $this->subject->addDeclaration($declarationToAdd); - self::assertContains($declarationToAdd, $this->subject->getRules()); + self::assertContains($declarationToAdd, $this->subject->getDeclarations()); } /** @@ -146,7 +146,7 @@ public function addRuleWithOnlyLineNumberWithoutSiblingAddsDeclaration( * * @param list $initialPropertyNames */ - public function addRuleWithOnlyLineNumberWithoutSiblingSetsColumnNumber( + public function addDeclarationWithOnlyLineNumberWithoutSiblingSetsColumnNumber( array $initialPropertyNames, string $propertyNameToAdd ): void { @@ -154,7 +154,7 @@ public function addRuleWithOnlyLineNumberWithoutSiblingSetsColumnNumber( $declarationToAdd->setPosition(42); $this->setDeclarationsFromPropertyNames($initialPropertyNames); - $this->subject->addRule($declarationToAdd); + $this->subject->addDeclaration($declarationToAdd); self::assertIsInt($declarationToAdd->getColumnNumber(), 'column number not set'); self::assertGreaterThanOrEqual(0, $declarationToAdd->getColumnNumber(), 'column number not valid'); @@ -167,7 +167,7 @@ public function addRuleWithOnlyLineNumberWithoutSiblingSetsColumnNumber( * * @param list $initialPropertyNames */ - public function addRuleWithOnlyLineNumberWithoutSiblingPreservesLineNumber( + public function addDeclarationWithOnlyLineNumberWithoutSiblingPreservesLineNumber( array $initialPropertyNames, string $propertyNameToAdd ): void { @@ -175,7 +175,7 @@ public function addRuleWithOnlyLineNumberWithoutSiblingPreservesLineNumber( $declarationToAdd->setPosition(42); $this->setDeclarationsFromPropertyNames($initialPropertyNames); - $this->subject->addRule($declarationToAdd); + $this->subject->addDeclaration($declarationToAdd); self::assertSame(42, $declarationToAdd->getLineNumber(), 'line number not preserved'); } @@ -187,7 +187,7 @@ public function addRuleWithOnlyLineNumberWithoutSiblingPreservesLineNumber( * * @param list $initialPropertyNames */ - public function addRuleWithOnlyColumnNumberWithoutSiblingAddsDeclarationAfterInitialDeclarations( + public function addDeclarationWithOnlyColumnNumberWithoutSiblingAddsDeclarationAfterInitialDeclarations( array $initialPropertyNames, string $propertyNameToAdd ): void { @@ -195,9 +195,9 @@ public function addRuleWithOnlyColumnNumberWithoutSiblingAddsDeclarationAfterIni $declarationToAdd->setPosition(null, 42); $this->setDeclarationsFromPropertyNames($initialPropertyNames); - $this->subject->addRule($declarationToAdd); + $this->subject->addDeclaration($declarationToAdd); - $declarations = $this->subject->getRules(); + $declarations = $this->subject->getDeclarations(); self::assertSame($declarationToAdd, \end($declarations)); } @@ -208,7 +208,7 @@ public function addRuleWithOnlyColumnNumberWithoutSiblingAddsDeclarationAfterIni * * @param list $initialPropertyNames */ - public function addRuleWithOnlyColumnNumberWithoutSiblingSetsLineNumber( + public function addDeclarationWithOnlyColumnNumberWithoutSiblingSetsLineNumber( array $initialPropertyNames, string $propertyNameToAdd ): void { @@ -216,7 +216,7 @@ public function addRuleWithOnlyColumnNumberWithoutSiblingSetsLineNumber( $declarationToAdd->setPosition(null, 42); $this->setDeclarationsFromPropertyNames($initialPropertyNames); - $this->subject->addRule($declarationToAdd); + $this->subject->addDeclaration($declarationToAdd); self::assertIsInt($declarationToAdd->getLineNumber(), 'line number not set'); self::assertGreaterThanOrEqual(1, $declarationToAdd->getLineNumber(), 'line number not valid'); @@ -229,7 +229,7 @@ public function addRuleWithOnlyColumnNumberWithoutSiblingSetsLineNumber( * * @param list $initialPropertyNames */ - public function addRuleWithOnlyColumnNumberWithoutSiblingPreservesColumnNumber( + public function addDeclarationWithOnlyColumnNumberWithoutSiblingPreservesColumnNumber( array $initialPropertyNames, string $propertyNameToAdd ): void { @@ -237,7 +237,7 @@ public function addRuleWithOnlyColumnNumberWithoutSiblingPreservesColumnNumber( $declarationToAdd->setPosition(null, 42); $this->setDeclarationsFromPropertyNames($initialPropertyNames); - $this->subject->addRule($declarationToAdd); + $this->subject->addDeclaration($declarationToAdd); self::assertSame(42, $declarationToAdd->getColumnNumber(), 'column number not preserved'); } @@ -249,7 +249,7 @@ public function addRuleWithOnlyColumnNumberWithoutSiblingPreservesColumnNumber( * * @param list $initialPropertyNames */ - public function addRuleWithCompletePositionWithoutSiblingAddsDeclaration( + public function addDeclarationWithCompletePositionWithoutSiblingAddsDeclaration( array $initialPropertyNames, string $propertyNameToAdd ): void { @@ -257,9 +257,9 @@ public function addRuleWithCompletePositionWithoutSiblingAddsDeclaration( $declarationToAdd->setPosition(42, 64); $this->setDeclarationsFromPropertyNames($initialPropertyNames); - $this->subject->addRule($declarationToAdd); + $this->subject->addDeclaration($declarationToAdd); - self::assertContains($declarationToAdd, $this->subject->getRules()); + self::assertContains($declarationToAdd, $this->subject->getDeclarations()); } /** @@ -269,7 +269,7 @@ public function addRuleWithCompletePositionWithoutSiblingAddsDeclaration( * * @param list $initialPropertyNames */ - public function addRuleWithCompletePositionWithoutSiblingPreservesPosition( + public function addDeclarationWithCompletePositionWithoutSiblingPreservesPosition( array $initialPropertyNames, string $propertyNameToAdd ): void { @@ -277,7 +277,7 @@ public function addRuleWithCompletePositionWithoutSiblingPreservesPosition( $declarationToAdd->setPosition(42, 64); $this->setDeclarationsFromPropertyNames($initialPropertyNames); - $this->subject->addRule($declarationToAdd); + $this->subject->addDeclaration($declarationToAdd); self::assertSame(42, $declarationToAdd->getLineNumber(), 'line number not preserved'); self::assertSame(64, $declarationToAdd->getColumnNumber(), 'column number not preserved'); @@ -322,18 +322,18 @@ public static function provideInitialPropertyNamesAndSiblingIndexAndPropertyName * * @dataProvider provideInitialPropertyNamesAndSiblingIndexAndPropertyNameToAdd */ - public function addRuleWithSiblingInsertsDeclarationBeforeSibling( + public function addDeclarationWithSiblingInsertsDeclarationBeforeSibling( array $initialPropertyNames, int $siblingIndex, string $propertyNameToAdd ): void { $declarationToAdd = new Declaration($propertyNameToAdd); $this->setDeclarationsFromPropertyNames($initialPropertyNames); - $sibling = $this->subject->getRules()[$siblingIndex]; + $sibling = $this->subject->getDeclarations()[$siblingIndex]; - $this->subject->addRule($declarationToAdd, $sibling); + $this->subject->addDeclaration($declarationToAdd, $sibling); - $declarations = $this->subject->getRules(); + $declarations = $this->subject->getDeclarations(); $siblingPosition = \array_search($sibling, $declarations, true); self::assertIsInt($siblingPosition); self::assertSame($siblingPosition - 1, \array_search($declarationToAdd, $declarations, true)); @@ -347,16 +347,16 @@ public function addRuleWithSiblingInsertsDeclarationBeforeSibling( * * @dataProvider provideInitialPropertyNamesAndSiblingIndexAndPropertyNameToAdd */ - public function addRuleWithSiblingSetsValidLineNumber( + public function addDeclarationWithSiblingSetsValidLineNumber( array $initialPropertyNames, int $siblingIndex, string $propertyNameToAdd ): void { $declarationToAdd = new Declaration($propertyNameToAdd); $this->setDeclarationsFromPropertyNames($initialPropertyNames); - $sibling = $this->subject->getRules()[$siblingIndex]; + $sibling = $this->subject->getDeclarations()[$siblingIndex]; - $this->subject->addRule($declarationToAdd, $sibling); + $this->subject->addDeclaration($declarationToAdd, $sibling); self::assertIsInt($declarationToAdd->getLineNumber(), 'line number not set'); self::assertGreaterThanOrEqual(1, $declarationToAdd->getLineNumber(), 'line number not valid'); @@ -370,16 +370,16 @@ public function addRuleWithSiblingSetsValidLineNumber( * * @dataProvider provideInitialPropertyNamesAndSiblingIndexAndPropertyNameToAdd */ - public function addRuleWithSiblingSetsValidColumnNumber( + public function addDeclarationWithSiblingSetsValidColumnNumber( array $initialPropertyNames, int $siblingIndex, string $propertyNameToAdd ): void { $declarationToAdd = new Declaration($propertyNameToAdd); $this->setDeclarationsFromPropertyNames($initialPropertyNames); - $sibling = $this->subject->getRules()[$siblingIndex]; + $sibling = $this->subject->getDeclarations()[$siblingIndex]; - $this->subject->addRule($declarationToAdd, $sibling); + $this->subject->addDeclaration($declarationToAdd, $sibling); self::assertIsInt($declarationToAdd->getColumnNumber(), 'column number not set'); self::assertGreaterThanOrEqual(0, $declarationToAdd->getColumnNumber(), 'column number not valid'); @@ -392,7 +392,7 @@ public function addRuleWithSiblingSetsValidColumnNumber( * * @dataProvider provideInitialPropertyNamesAndAnotherPropertyName */ - public function addRuleWithSiblingNotInSetAddsDeclarationAfterInitialDeclarations( + public function addDeclarationWithSiblingNotInSetAddsDeclarationAfterInitialDeclarations( array $initialPropertyNames, string $propertyNameToAdd ): void { @@ -401,9 +401,9 @@ public function addRuleWithSiblingNotInSetAddsDeclarationAfterInitialDeclaration // `display` is sometimes in `$initialPropertyNames` and sometimes the `$propertyNameToAdd`. // Choosing this for the bogus sibling allows testing all combinations of whether it is or isn't. - $this->subject->addRule($declarationToAdd, new Declaration('display')); + $this->subject->addDeclaration($declarationToAdd, new Declaration('display')); - $declarations = $this->subject->getRules(); + $declarations = $this->subject->getDeclarations(); self::assertSame($declarationToAdd, \end($declarations)); } @@ -414,7 +414,7 @@ public function addRuleWithSiblingNotInSetAddsDeclarationAfterInitialDeclaration * * @dataProvider provideInitialPropertyNamesAndAnotherPropertyName */ - public function addRuleWithSiblingNotInSetSetsValidLineNumber( + public function addDeclarationWithSiblingNotInSetSetsValidLineNumber( array $initialPropertyNames, string $propertyNameToAdd ): void { @@ -423,7 +423,7 @@ public function addRuleWithSiblingNotInSetSetsValidLineNumber( // `display` is sometimes in `$initialPropertyNames` and sometimes the `$propertyNameToAdd`. // Choosing this for the bogus sibling allows testing all combinations of whether it is or isn't. - $this->subject->addRule($declarationToAdd, new Declaration('display')); + $this->subject->addDeclaration($declarationToAdd, new Declaration('display')); self::assertIsInt($declarationToAdd->getLineNumber(), 'line number not set'); self::assertGreaterThanOrEqual(1, $declarationToAdd->getLineNumber(), 'line number not valid'); @@ -436,7 +436,7 @@ public function addRuleWithSiblingNotInSetSetsValidLineNumber( * * @dataProvider provideInitialPropertyNamesAndAnotherPropertyName */ - public function addRuleWithSiblingNotInSetSetsValidColumnNumber( + public function addDeclarationWithSiblingNotInSetSetsValidColumnNumber( array $initialPropertyNames, string $propertyNameToAdd ): void { @@ -445,7 +445,7 @@ public function addRuleWithSiblingNotInSetSetsValidColumnNumber( // `display` is sometimes in `$initialPropertyNames` and sometimes the `$propertyNameToAdd`. // Choosing this for the bogus sibling allows testing all combinations of whether it is or isn't. - $this->subject->addRule($declarationToAdd, new Declaration('display')); + $this->subject->addDeclaration($declarationToAdd, new Declaration('display')); self::assertIsInt($declarationToAdd->getColumnNumber(), 'column number not set'); self::assertGreaterThanOrEqual(0, $declarationToAdd->getColumnNumber(), 'column number not valid'); @@ -459,14 +459,14 @@ public function addRuleWithSiblingNotInSetSetsValidColumnNumber( * * @dataProvider provideInitialPropertyNamesAndIndexOfOne */ - public function removeRuleRemovesDeclarationInSet(array $initialPropertyNames, int $indexToRemove): void + public function removeDeclarationRemovesDeclarationInSet(array $initialPropertyNames, int $indexToRemove): void { $this->setDeclarationsFromPropertyNames($initialPropertyNames); - $declarationToRemove = $this->subject->getRules()[$indexToRemove]; + $declarationToRemove = $this->subject->getDeclarations()[$indexToRemove]; - $this->subject->removeRule($declarationToRemove); + $this->subject->removeDeclaration($declarationToRemove); - self::assertNotContains($declarationToRemove, $this->subject->getRules()); + self::assertNotContains($declarationToRemove, $this->subject->getDeclarations()); } /** @@ -477,14 +477,14 @@ public function removeRuleRemovesDeclarationInSet(array $initialPropertyNames, i * * @dataProvider provideInitialPropertyNamesAndIndexOfOne */ - public function removeRuleRemovesExactlyOneDeclaration(array $initialPropertyNames, int $indexToRemove): void + public function removeDeclarationRemovesExactlyOneDeclaration(array $initialPropertyNames, int $indexToRemove): void { $this->setDeclarationsFromPropertyNames($initialPropertyNames); - $declarationToRemove = $this->subject->getRules()[$indexToRemove]; + $declarationToRemove = $this->subject->getDeclarations()[$indexToRemove]; - $this->subject->removeRule($declarationToRemove); + $this->subject->removeDeclaration($declarationToRemove); - self::assertCount(\count($initialPropertyNames) - 1, $this->subject->getRules()); + self::assertCount(\count($initialPropertyNames) - 1, $this->subject->getDeclarations()); } /** @@ -494,17 +494,17 @@ public function removeRuleRemovesExactlyOneDeclaration(array $initialPropertyNam * * @dataProvider provideInitialPropertyNamesAndAnotherPropertyName */ - public function removeRuleWithDeclarationNotInSetKeepsSetUnchanged( + public function removeDeclarationWithDeclarationNotInSetKeepsSetUnchanged( array $initialPropertyNames, string $propertyNameToRemove ): void { $this->setDeclarationsFromPropertyNames($initialPropertyNames); - $initialDeclarations = $this->subject->getRules(); + $initialDeclarations = $this->subject->getDeclarations(); $declarationToRemove = new Declaration($propertyNameToRemove); - $this->subject->removeRule($declarationToRemove); + $this->subject->removeDeclaration($declarationToRemove); - self::assertSame($initialDeclarations, $this->subject->getRules()); + self::assertSame($initialDeclarations, $this->subject->getDeclarations()); } /** @@ -563,15 +563,15 @@ public static function providePropertyNamesAndPropertyNameToRemoveAndExpectedRem * * @dataProvider providePropertyNamesAndPropertyNameToRemoveAndExpectedRemainingPropertyNames */ - public function removeMatchingRulesRemovesDeclarationsWithPropertyName( + public function removeMatchingDeclarationsRemovesDeclarationsWithPropertyName( array $initialPropertyNames, string $propertyNameToRemove ): void { $this->setDeclarationsFromPropertyNames($initialPropertyNames); - $this->subject->removeMatchingRules($propertyNameToRemove); + $this->subject->removeMatchingDeclarations($propertyNameToRemove); - self::assertArrayNotHasKey($propertyNameToRemove, $this->subject->getRulesAssoc()); + self::assertArrayNotHasKey($propertyNameToRemove, $this->subject->getDeclarationsAssociative()); } /** @@ -582,16 +582,16 @@ public function removeMatchingRulesRemovesDeclarationsWithPropertyName( * * @dataProvider providePropertyNamesAndPropertyNameToRemoveAndExpectedRemainingPropertyNames */ - public function removeMatchingRulesWithPropertyNameKeepsOtherDeclarations( + public function removeMatchingDeclarationsWithPropertyNameKeepsOtherDeclarations( array $initialPropertyNames, string $propertyNameToRemove, array $expectedRemainingPropertyNames ): void { $this->setDeclarationsFromPropertyNames($initialPropertyNames); - $this->subject->removeMatchingRules($propertyNameToRemove); + $this->subject->removeMatchingDeclarations($propertyNameToRemove); - $remainingDeclarations = $this->subject->getRulesAssoc(); + $remainingDeclarations = $this->subject->getDeclarationsAssociative(); if ($expectedRemainingPropertyNames === []) { self::assertSame([], $remainingDeclarations); } @@ -646,16 +646,16 @@ public static function providePropertyNamesAndPropertyNamePrefixToRemoveAndExpec * * @dataProvider providePropertyNamesAndPropertyNamePrefixToRemoveAndExpectedRemainingPropertyNames */ - public function removeMatchingRulesRemovesDeclarationsWithPropertyNamePrefix( + public function removeMatchingDeclarationsRemovesDeclarationsWithPropertyNamePrefix( array $initialPropertyNames, string $propertyNamePrefix ): void { $propertyNamePrefixWithHyphen = $propertyNamePrefix . '-'; $this->setDeclarationsFromPropertyNames($initialPropertyNames); - $this->subject->removeMatchingRules($propertyNamePrefixWithHyphen); + $this->subject->removeMatchingDeclarations($propertyNamePrefixWithHyphen); - $remainingDeclarations = $this->subject->getRulesAssoc(); + $remainingDeclarations = $this->subject->getDeclarationsAssociative(); self::assertArrayNotHasKey($propertyNamePrefix, $remainingDeclarations); foreach (\array_keys($remainingDeclarations) as $remainingPropertyName) { self::assertStringStartsNotWith($propertyNamePrefixWithHyphen, $remainingPropertyName); @@ -670,7 +670,7 @@ public function removeMatchingRulesRemovesDeclarationsWithPropertyNamePrefix( * * @dataProvider providePropertyNamesAndPropertyNamePrefixToRemoveAndExpectedRemainingPropertyNames */ - public function removeMatchingRulesWithPropertyNamePrefixKeepsOtherDeclarations( + public function removeMatchingDeclarationsWithPropertyNamePrefixKeepsOtherDeclarations( array $initialPropertyNames, string $propertyNamePrefix, array $expectedRemainingPropertyNames @@ -678,9 +678,9 @@ public function removeMatchingRulesWithPropertyNamePrefixKeepsOtherDeclarations( $propertyNamePrefixWithHyphen = $propertyNamePrefix . '-'; $this->setDeclarationsFromPropertyNames($initialPropertyNames); - $this->subject->removeMatchingRules($propertyNamePrefixWithHyphen); + $this->subject->removeMatchingDeclarations($propertyNamePrefixWithHyphen); - $remainingDeclarations = $this->subject->getRulesAssoc(); + $remainingDeclarations = $this->subject->getDeclarationsAssociative(); if ($expectedRemainingPropertyNames === []) { self::assertSame([], $remainingDeclarations); } @@ -696,13 +696,13 @@ public function removeMatchingRulesWithPropertyNamePrefixKeepsOtherDeclarations( * * @dataProvider providePropertyNames */ - public function removeAllRulesRemovesAllDeclarations(array $propertyNamesToRemove): void + public function removeAllDeclarationsRemovesAllDeclarations(array $propertyNamesToRemove): void { $this->setDeclarationsFromPropertyNames($propertyNamesToRemove); - $this->subject->removeAllRules(); + $this->subject->removeAllDeclarations(); - self::assertSame([], $this->subject->getRules()); + self::assertSame([], $this->subject->getDeclarations()); } /** @@ -712,13 +712,13 @@ public function removeAllRulesRemovesAllDeclarations(array $propertyNamesToRemov * * @dataProvider providePropertyNames */ - public function setRulesOnVirginSetsDeclarationsWithoutPositionInOrder(array $propertyNamesToSet): void + public function setDeclarationsOnVirginSetsDeclarationsWithoutPositionInOrder(array $propertyNamesToSet): void { $declarationsToSet = self::createDeclarationsFromPropertyNames($propertyNamesToSet); - $this->subject->setRules($declarationsToSet); + $this->subject->setDeclarations($declarationsToSet); - self::assertSame($declarationsToSet, $this->subject->getRules()); + self::assertSame($declarationsToSet, $this->subject->getDeclarations()); } /** @@ -737,24 +737,24 @@ public static function provideInitialPropertyNamesAndPropertyNamesToSet(): DataP * * @dataProvider provideInitialPropertyNamesAndPropertyNamesToSet */ - public function setRulesReplacesDeclarations(array $initialPropertyNames, array $propertyNamesToSet): void + public function setDeclarationsReplacesDeclarations(array $initialPropertyNames, array $propertyNamesToSet): void { $declarationsToSet = self::createDeclarationsFromPropertyNames($propertyNamesToSet); $this->setDeclarationsFromPropertyNames($initialPropertyNames); - $this->subject->setRules($declarationsToSet); + $this->subject->setDeclarations($declarationsToSet); - self::assertSame($declarationsToSet, $this->subject->getRules()); + self::assertSame($declarationsToSet, $this->subject->getDeclarations()); } /** * @test */ - public function setRulesWithDeclarationWithoutPositionSetsValidLineNumber(): void + public function setDeclarationsWithDeclarationWithoutPositionSetsValidLineNumber(): void { $declarationToSet = new Declaration('color'); - $this->subject->setRules([$declarationToSet]); + $this->subject->setDeclarations([$declarationToSet]); self::assertIsInt($declarationToSet->getLineNumber(), 'line number not set'); self::assertGreaterThanOrEqual(1, $declarationToSet->getLineNumber(), 'line number not valid'); @@ -763,11 +763,11 @@ public function setRulesWithDeclarationWithoutPositionSetsValidLineNumber(): voi /** * @test */ - public function setRulesWithDeclarationWithoutPositionSetsValidColumnNumber(): void + public function setDeclarationsWithDeclarationWithoutPositionSetsValidColumnNumber(): void { $declarationToSet = new Declaration('color'); - $this->subject->setRules([$declarationToSet]); + $this->subject->setDeclarations([$declarationToSet]); self::assertIsInt($declarationToSet->getColumnNumber(), 'column number not set'); self::assertGreaterThanOrEqual(0, $declarationToSet->getColumnNumber(), 'column number not valid'); @@ -776,12 +776,12 @@ public function setRulesWithDeclarationWithoutPositionSetsValidColumnNumber(): v /** * @test */ - public function setRulesWithDeclarationWithOnlyLineNumberSetsColumnNumber(): void + public function setDeclarationsWithDeclarationWithOnlyLineNumberSetsColumnNumber(): void { $declarationToSet = new Declaration('color'); $declarationToSet->setPosition(42); - $this->subject->setRules([$declarationToSet]); + $this->subject->setDeclarations([$declarationToSet]); self::assertIsInt($declarationToSet->getColumnNumber(), 'column number not set'); self::assertGreaterThanOrEqual(0, $declarationToSet->getColumnNumber(), 'column number not valid'); @@ -790,12 +790,12 @@ public function setRulesWithDeclarationWithOnlyLineNumberSetsColumnNumber(): voi /** * @test */ - public function setRulesWithDeclarationWithOnlyLineNumberPreservesLineNumber(): void + public function setDeclarationsWithDeclarationWithOnlyLineNumberPreservesLineNumber(): void { $declarationToSet = new Declaration('color'); $declarationToSet->setPosition(42); - $this->subject->setRules([$declarationToSet]); + $this->subject->setDeclarations([$declarationToSet]); self::assertSame(42, $declarationToSet->getLineNumber(), 'line number not preserved'); } @@ -803,12 +803,12 @@ public function setRulesWithDeclarationWithOnlyLineNumberPreservesLineNumber(): /** * @test */ - public function setRulesWithDeclarationWithOnlyColumnNumberSetsLineNumber(): void + public function setDeclarationsWithDeclarationWithOnlyColumnNumberSetsLineNumber(): void { $declarationToSet = new Declaration('color'); $declarationToSet->setPosition(null, 42); - $this->subject->setRules([$declarationToSet]); + $this->subject->setDeclarations([$declarationToSet]); self::assertIsInt($declarationToSet->getLineNumber(), 'line number not set'); self::assertGreaterThanOrEqual(1, $declarationToSet->getLineNumber(), 'line number not valid'); @@ -817,12 +817,12 @@ public function setRulesWithDeclarationWithOnlyColumnNumberSetsLineNumber(): voi /** * @test */ - public function setRulesWithDeclarationWithOnlyColumnNumberPreservesColumnNumber(): void + public function setDeclarationsWithDeclarationWithOnlyColumnNumberPreservesColumnNumber(): void { $declarationToSet = new Declaration('color'); $declarationToSet->setPosition(null, 42); - $this->subject->setRules([$declarationToSet]); + $this->subject->setDeclarations([$declarationToSet]); self::assertSame(42, $declarationToSet->getColumnNumber(), 'column number not preserved'); } @@ -830,12 +830,12 @@ public function setRulesWithDeclarationWithOnlyColumnNumberPreservesColumnNumber /** * @test */ - public function setRulesWithDeclarationWithCompletePositionPreservesPosition(): void + public function setDeclarationsWithDeclarationWithCompletePositionPreservesPosition(): void { $declarationToSet = new Declaration('color'); $declarationToSet->setPosition(42, 64); - $this->subject->setRules([$declarationToSet]); + $this->subject->setDeclarations([$declarationToSet]); self::assertSame(42, $declarationToSet->getLineNumber(), 'line number not preserved'); self::assertSame(64, $declarationToSet->getColumnNumber(), 'column number not preserved'); @@ -848,12 +848,12 @@ public function setRulesWithDeclarationWithCompletePositionPreservesPosition(): * * @dataProvider providePropertyNames */ - public function getRulesReturnsDeclarationsSet(array $propertyNamesToSet): void + public function getDeclarationsReturnsDeclarationsSet(array $propertyNamesToSet): void { $declarationsToSet = self::createDeclarationsFromPropertyNames($propertyNamesToSet); - $this->subject->setRules($declarationsToSet); + $this->subject->setDeclarations($declarationsToSet); - $result = $this->subject->getRules(); + $result = $this->subject->getDeclarations(); self::assertSame($declarationsToSet, $result); } @@ -861,14 +861,14 @@ public function getRulesReturnsDeclarationsSet(array $propertyNamesToSet): void /** * @test */ - public function getRulesOrdersByLineNumber(): void + public function getDeclarationsOrdersByLineNumber(): void { $first = (new Declaration('color'))->setPosition(1, 64); $second = (new Declaration('display'))->setPosition(19, 42); $third = (new Declaration('color'))->setPosition(55, 11); - $this->subject->setRules([$third, $second, $first]); + $this->subject->setDeclarations([$third, $second, $first]); - $result = $this->subject->getRules(); + $result = $this->subject->getDeclarations(); self::assertSame([$first, $second, $third], $result); } @@ -876,14 +876,14 @@ public function getRulesOrdersByLineNumber(): void /** * @test */ - public function getRulesOrdersDeclarationsWithSameLineNumberByColumnNumber(): void + public function getDeclarationsOrdersDeclarationsWithSameLineNumberByColumnNumber(): void { $first = (new Declaration('color'))->setPosition(1, 11); $second = (new Declaration('display'))->setPosition(1, 42); $third = (new Declaration('color'))->setPosition(1, 64); - $this->subject->setRules([$third, $second, $first]); + $this->subject->setDeclarations([$third, $second, $first]); - $result = $this->subject->getRules(); + $result = $this->subject->getDeclarations(); self::assertSame([$first, $second, $third], $result); } @@ -970,7 +970,7 @@ public static function providePropertyNamesAndSearchPatternAndMatchingPropertyNa * * @dataProvider providePropertyNamesAndSearchPatternAndMatchingPropertyNames */ - public function getRulesWithPatternReturnsAllMatchingDeclarations( + public function getDeclarationsWithPatternReturnsAllMatchingDeclarations( array $propertyNamesToSet, string $searchPattern, array $matchingPropertyNames @@ -985,9 +985,9 @@ static function (Declaration $declaration) use ($matchingPropertyNames): bool { } ) ); - $this->subject->setRules($declarationsToSet); + $this->subject->setDeclarations($declarationsToSet); - $result = $this->subject->getRules($searchPattern); + $result = $this->subject->getDeclarations($searchPattern); // `Declaration`s without pre-set positions are returned in the order set. This is tested separately. self::assertSame($matchingDeclarations, $result); @@ -1021,13 +1021,13 @@ public static function providePropertyNamesAndNonMatchingSearchPattern(): array * * @dataProvider providePropertyNamesAndNonMatchingSearchPattern */ - public function getRulesWithNonMatchingPatternReturnsEmptyArray( + public function getDeclarationsWithNonMatchingPatternReturnsEmptyArray( array $propertyNamesToSet, string $searchPattern ): void { $this->setDeclarationsFromPropertyNames($propertyNamesToSet); - $result = $this->subject->getRules($searchPattern); + $result = $this->subject->getDeclarations($searchPattern); self::assertSame([], $result); } @@ -1035,14 +1035,14 @@ public function getRulesWithNonMatchingPatternReturnsEmptyArray( /** * @test */ - public function getRulesWithPatternOrdersDeclarationsByPosition(): void + public function getDeclarationsWithPatternOrdersDeclarationsByPosition(): void { $first = (new Declaration('color'))->setPosition(1, 42); $second = (new Declaration('color'))->setPosition(1, 64); $third = (new Declaration('color'))->setPosition(55, 7); - $this->subject->setRules([$third, $second, $first]); + $this->subject->setDeclarations([$third, $second, $first]); - $result = $this->subject->getRules('color'); + $result = $this->subject->getDeclarations('color'); self::assertSame([$first, $second, $third], $result); } @@ -1066,12 +1066,12 @@ public static function provideDistinctPropertyNames(): array * * @dataProvider provideDistinctPropertyNames */ - public function getRulesAssocReturnsAllDeclarationsWithDistinctPropertyNames(array $propertyNamesToSet): void + public function getDeclarationsAssocReturnsAllDeclarationsWithDistinctPropertyNames(array $propertyNamesToSet): void { $declarationsToSet = self::createDeclarationsFromPropertyNames($propertyNamesToSet); - $this->subject->setRules($declarationsToSet); + $this->subject->setDeclarations($declarationsToSet); - $result = $this->subject->getRulesAssoc(); + $result = $this->subject->getDeclarationsAssociative(); self::assertSame($declarationsToSet, \array_values($result)); } @@ -1079,13 +1079,13 @@ public function getRulesAssocReturnsAllDeclarationsWithDistinctPropertyNames(arr /** * @test */ - public function getRulesAssocReturnsLastDeclarationWithSamePropertyName(): void + public function getDeclarationsAssocReturnsLastDeclarationWithSamePropertyName(): void { $firstDeclaration = new Declaration('color'); $lastDeclaration = new Declaration('color'); - $this->subject->setRules([$firstDeclaration, $lastDeclaration]); + $this->subject->setDeclarations([$firstDeclaration, $lastDeclaration]); - $result = $this->subject->getRulesAssoc(); + $result = $this->subject->getDeclarationsAssociative(); self::assertSame([$lastDeclaration], \array_values($result)); } @@ -1093,14 +1093,14 @@ public function getRulesAssocReturnsLastDeclarationWithSamePropertyName(): void /** * @test */ - public function getRulesAssocOrdersDeclarationsByPosition(): void + public function getDeclarationsAssocOrdersDeclarationsByPosition(): void { $first = (new Declaration('color'))->setPosition(1, 42); $second = (new Declaration('display'))->setPosition(1, 64); $third = (new Declaration('width'))->setPosition(55, 7); - $this->subject->setRules([$third, $second, $first]); + $this->subject->setDeclarations([$third, $second, $first]); - $result = $this->subject->getRulesAssoc(); + $result = $this->subject->getDeclarationsAssociative(); self::assertSame([$first, $second, $third], \array_values($result)); } @@ -1108,11 +1108,11 @@ public function getRulesAssocOrdersDeclarationsByPosition(): void /** * @test */ - public function getRulesAssocKeysDeclarationsByPropertyName(): void + public function getDeclarationsAssocKeysDeclarationsByPropertyName(): void { - $this->subject->setRules([new Declaration('color'), new Declaration('display')]); + $this->subject->setDeclarations([new Declaration('color'), new Declaration('display')]); - $result = $this->subject->getRulesAssoc(); + $result = $this->subject->getDeclarationsAssociative(); foreach ($result as $key => $declaration) { self::assertSame($declaration->getPropertyName(), $key); @@ -1127,14 +1127,14 @@ public function getRulesAssocKeysDeclarationsByPropertyName(): void * * @dataProvider providePropertyNamesAndSearchPatternAndMatchingPropertyNames */ - public function getRulesAssocWithPatternReturnsAllMatchingPropertyNames( + public function getDeclarationsAssocWithPatternReturnsAllMatchingPropertyNames( array $propertyNamesToSet, string $searchPattern, array $matchingPropertyNames ): void { $this->setDeclarationsFromPropertyNames($propertyNamesToSet); - $result = $this->subject->getRulesAssoc($searchPattern); + $result = $this->subject->getDeclarationsAssociative($searchPattern); $resultPropertyNames = \array_keys($result); \sort($matchingPropertyNames); @@ -1149,13 +1149,13 @@ public function getRulesAssocWithPatternReturnsAllMatchingPropertyNames( * * @dataProvider providePropertyNamesAndNonMatchingSearchPattern */ - public function getRulesAssocWithNonMatchingPatternReturnsEmptyArray( + public function getDeclarationsAssocWithNonMatchingPatternReturnsEmptyArray( array $propertyNamesToSet, string $searchPattern ): void { $this->setDeclarationsFromPropertyNames($propertyNamesToSet); - $result = $this->subject->getRulesAssoc($searchPattern); + $result = $this->subject->getDeclarationsAssociative($searchPattern); self::assertSame([], $result); } @@ -1163,14 +1163,14 @@ public function getRulesAssocWithNonMatchingPatternReturnsEmptyArray( /** * @test */ - public function getRulesAssocWithPatternOrdersDeclarationsByPosition(): void + public function getDeclarationsAssocWithPatternOrdersDeclarationsByPosition(): void { $first = (new Declaration('font'))->setPosition(1, 42); $second = (new Declaration('font-family'))->setPosition(1, 64); $third = (new Declaration('font-weight'))->setPosition(55, 7); - $this->subject->setRules([$third, $second, $first]); + $this->subject->setDeclarations([$third, $second, $first]); - $result = $this->subject->getRules('font-'); + $result = $this->subject->getDeclarations('font-'); self::assertSame([$first, $second, $third], \array_values($result)); } @@ -1180,7 +1180,7 @@ public function getRulesAssocWithPatternOrdersDeclarationsByPosition(): void */ private function setDeclarationsFromPropertyNames(array $propertyNames): void { - $this->subject->setRules(self::createDeclarationsFromPropertyNames($propertyNames)); + $this->subject->setDeclarations(self::createDeclarationsFromPropertyNames($propertyNames)); } /** From 015a17bf6931287b30eba5d6683fb1789faab068 Mon Sep 17 00:00:00 2001 From: JakeQZ Date: Mon, 16 Feb 2026 18:26:52 +0000 Subject: [PATCH 116/136] [TASK] Rename `Rule` to `Declatation` in test variables etc. (#1529) - [x] Local variables - [x] Test method names - [x] Code comments --- tests/ParserTest.php | 250 ++++++++++++------------- tests/RuleSet/DeclarationBlockTest.php | 2 +- 2 files changed, 126 insertions(+), 126 deletions(-) diff --git a/tests/ParserTest.php b/tests/ParserTest.php index 4b1fabcd8..af3b1ae4f 100644 --- a/tests/ParserTest.php +++ b/tests/ParserTest.php @@ -95,60 +95,60 @@ public function colorParsing(): void $selectors = $declarationBlock->getSelectors(); $selector = $selectors[0]->getSelector(); if ($selector === '#mine') { - $colorRules = $declarationBlock->getDeclarations('color'); - $colorRuleValue = $colorRules[0]->getValue(); - self::assertSame('red', $colorRuleValue); - $colorRules = $declarationBlock->getDeclarations('background-'); - $colorRuleValue = $colorRules[0]->getValue(); - self::assertInstanceOf(Color::class, $colorRuleValue); + $colorDeclarations = $declarationBlock->getDeclarations('color'); + $colorDeclarationValue = $colorDeclarations[0]->getValue(); + self::assertSame('red', $colorDeclarationValue); + $colorDeclarations = $declarationBlock->getDeclarations('background-'); + $colorDeclarationValue = $colorDeclarations[0]->getValue(); + self::assertInstanceOf(Color::class, $colorDeclarationValue); self::assertEquals([ - 'r' => new Size(35.0, null, true, $colorRuleValue->getLineNumber()), - 'g' => new Size(35.0, null, true, $colorRuleValue->getLineNumber()), - 'b' => new Size(35.0, null, true, $colorRuleValue->getLineNumber()), - ], $colorRuleValue->getColor()); - $colorRules = $declarationBlock->getDeclarations('border-color'); - $colorRuleValue = $colorRules[0]->getValue(); - self::assertInstanceOf(Color::class, $colorRuleValue); + 'r' => new Size(35.0, null, true, $colorDeclarationValue->getLineNumber()), + 'g' => new Size(35.0, null, true, $colorDeclarationValue->getLineNumber()), + 'b' => new Size(35.0, null, true, $colorDeclarationValue->getLineNumber()), + ], $colorDeclarationValue->getColor()); + $colorDeclarations = $declarationBlock->getDeclarations('border-color'); + $colorDeclarationValue = $colorDeclarations[0]->getValue(); + self::assertInstanceOf(Color::class, $colorDeclarationValue); self::assertEquals([ - 'r' => new Size(10.0, null, true, $colorRuleValue->getLineNumber()), - 'g' => new Size(100.0, null, true, $colorRuleValue->getLineNumber()), - 'b' => new Size(230.0, null, true, $colorRuleValue->getLineNumber()), - ], $colorRuleValue->getColor()); - $colorRuleValue = $colorRules[1]->getValue(); - self::assertInstanceOf(Color::class, $colorRuleValue); + 'r' => new Size(10.0, null, true, $colorDeclarationValue->getLineNumber()), + 'g' => new Size(100.0, null, true, $colorDeclarationValue->getLineNumber()), + 'b' => new Size(230.0, null, true, $colorDeclarationValue->getLineNumber()), + ], $colorDeclarationValue->getColor()); + $colorDeclarationValue = $colorDeclarations[1]->getValue(); + self::assertInstanceOf(Color::class, $colorDeclarationValue); self::assertEquals([ - 'r' => new Size(10.0, null, true, $colorRuleValue->getLineNumber()), - 'g' => new Size(100.0, null, true, $colorRuleValue->getLineNumber()), - 'b' => new Size(231.0, null, true, $colorRuleValue->getLineNumber()), - 'a' => new Size('0000.3', null, true, $colorRuleValue->getLineNumber()), - ], $colorRuleValue->getColor()); - $colorRules = $declarationBlock->getDeclarations('outline-color'); - $colorRuleValue = $colorRules[0]->getValue(); - self::assertInstanceOf(Color::class, $colorRuleValue); + 'r' => new Size(10.0, null, true, $colorDeclarationValue->getLineNumber()), + 'g' => new Size(100.0, null, true, $colorDeclarationValue->getLineNumber()), + 'b' => new Size(231.0, null, true, $colorDeclarationValue->getLineNumber()), + 'a' => new Size('0000.3', null, true, $colorDeclarationValue->getLineNumber()), + ], $colorDeclarationValue->getColor()); + $colorDeclarations = $declarationBlock->getDeclarations('outline-color'); + $colorDeclarationValue = $colorDeclarations[0]->getValue(); + self::assertInstanceOf(Color::class, $colorDeclarationValue); self::assertEquals([ - 'r' => new Size(34.0, null, true, $colorRuleValue->getLineNumber()), - 'g' => new Size(34.0, null, true, $colorRuleValue->getLineNumber()), - 'b' => new Size(34.0, null, true, $colorRuleValue->getLineNumber()), - ], $colorRuleValue->getColor()); + 'r' => new Size(34.0, null, true, $colorDeclarationValue->getLineNumber()), + 'g' => new Size(34.0, null, true, $colorDeclarationValue->getLineNumber()), + 'b' => new Size(34.0, null, true, $colorDeclarationValue->getLineNumber()), + ], $colorDeclarationValue->getColor()); } elseif ($selector === '#yours') { - $colorRules = $declarationBlock->getDeclarations('background-color'); - $colorRuleValue = $colorRules[0]->getValue(); - self::assertInstanceOf(Color::class, $colorRuleValue); + $colorDeclarations = $declarationBlock->getDeclarations('background-color'); + $colorDeclarationValue = $colorDeclarations[0]->getValue(); + self::assertInstanceOf(Color::class, $colorDeclarationValue); self::assertEquals([ - 'h' => new Size(220.0, null, true, $colorRuleValue->getLineNumber()), - 's' => new Size(10.0, '%', true, $colorRuleValue->getLineNumber()), - 'l' => new Size(220.0, '%', true, $colorRuleValue->getLineNumber()), - ], $colorRuleValue->getColor()); - $colorRuleValue = $colorRules[1]->getValue(); - self::assertInstanceOf(Color::class, $colorRuleValue); + 'h' => new Size(220.0, null, true, $colorDeclarationValue->getLineNumber()), + 's' => new Size(10.0, '%', true, $colorDeclarationValue->getLineNumber()), + 'l' => new Size(220.0, '%', true, $colorDeclarationValue->getLineNumber()), + ], $colorDeclarationValue->getColor()); + $colorDeclarationValue = $colorDeclarations[1]->getValue(); + self::assertInstanceOf(Color::class, $colorDeclarationValue); self::assertEquals([ - 'h' => new Size(220.0, null, true, $colorRuleValue->getLineNumber()), - 's' => new Size(10.0, '%', true, $colorRuleValue->getLineNumber()), - 'l' => new Size(220.0, '%', true, $colorRuleValue->getLineNumber()), - 'a' => new Size(0000.3, null, true, $colorRuleValue->getLineNumber()), - ], $colorRuleValue->getColor()); - $colorRules = $declarationBlock->getDeclarations('outline-color'); - self::assertEmpty($colorRules); + 'h' => new Size(220.0, null, true, $colorDeclarationValue->getLineNumber()), + 's' => new Size(10.0, '%', true, $colorDeclarationValue->getLineNumber()), + 'l' => new Size(220.0, '%', true, $colorDeclarationValue->getLineNumber()), + 'a' => new Size(0000.3, null, true, $colorDeclarationValue->getLineNumber()), + ], $colorDeclarationValue->getColor()); + $colorDeclarations = $declarationBlock->getDeclarations('outline-color'); + self::assertEmpty($colorDeclarations); } } foreach ($document->getAllValues(null, 'color') as $colorValue) { @@ -179,40 +179,40 @@ public function unicodeParsing(): void if (\substr($selector, 0, \strlen('.test-')) !== '.test-') { continue; } - $contentRules = $declarationBlock->getDeclarations('content'); - $firstContentRuleAsString = $contentRules[0]->getValue()->render(OutputFormat::create()); + $contentDeclarations = $declarationBlock->getDeclarations('content'); + $firstContentDeclarationAsString = $contentDeclarations[0]->getValue()->render(OutputFormat::create()); if ($selector === '.test-1') { - self::assertSame('" "', $firstContentRuleAsString); + self::assertSame('" "', $firstContentDeclarationAsString); } if ($selector === '.test-2') { - self::assertSame('"é"', $firstContentRuleAsString); + self::assertSame('"é"', $firstContentDeclarationAsString); } if ($selector === '.test-3') { - self::assertSame('" "', $firstContentRuleAsString); + self::assertSame('" "', $firstContentDeclarationAsString); } if ($selector === '.test-4') { - self::assertSame('"𝄞"', $firstContentRuleAsString); + self::assertSame('"𝄞"', $firstContentDeclarationAsString); } if ($selector === '.test-5') { - self::assertSame('"水"', $firstContentRuleAsString); + self::assertSame('"水"', $firstContentDeclarationAsString); } if ($selector === '.test-6') { - self::assertSame('"¥"', $firstContentRuleAsString); + self::assertSame('"¥"', $firstContentDeclarationAsString); } if ($selector === '.test-7') { - self::assertSame('"\\A"', $firstContentRuleAsString); + self::assertSame('"\\A"', $firstContentDeclarationAsString); } if ($selector === '.test-8') { - self::assertSame('"\\"\\""', $firstContentRuleAsString); + self::assertSame('"\\"\\""', $firstContentDeclarationAsString); } if ($selector === '.test-9') { - self::assertSame('"\\"\'"', $firstContentRuleAsString); + self::assertSame('"\\"\'"', $firstContentDeclarationAsString); } if ($selector === '.test-10') { - self::assertSame('"\'\\\\"', $firstContentRuleAsString); + self::assertSame('"\'\\\\"', $firstContentDeclarationAsString); } if ($selector === '.test-11') { - self::assertSame('"test"', $firstContentRuleAsString); + self::assertSame('"test"', $firstContentDeclarationAsString); } } } @@ -336,24 +336,24 @@ public function manipulation(): void /** * @test */ - public function ruleGetters(): void + public function declarationGetters(): void { $document = self::parsedStructureForFile('values'); $declarationBlocks = $document->getAllDeclarationBlocks(); $headerBlock = $declarationBlocks[0]; $bodyBlock = $declarationBlocks[1]; - $backgroundHeaderRules = $headerBlock->getDeclarations('background-'); - self::assertCount(2, $backgroundHeaderRules); - self::assertSame('background-color', $backgroundHeaderRules[0]->getPropertyName()); - self::assertSame('background-color', $backgroundHeaderRules[1]->getPropertyName()); - $backgroundHeaderRules = $headerBlock->getDeclarationsAssociative('background-'); - self::assertCount(1, $backgroundHeaderRules); - self::assertInstanceOf(Color::class, $backgroundHeaderRules['background-color']->getValue()); - self::assertSame('rgba', $backgroundHeaderRules['background-color']->getValue()->getColorDescription()); - $headerBlock->removeDeclaration($backgroundHeaderRules['background-color']); - $backgroundHeaderRules = $headerBlock->getDeclarations('background-'); - self::assertCount(1, $backgroundHeaderRules); - self::assertSame('green', $backgroundHeaderRules[0]->getValue()); + $backgroundHeaderDeclarations = $headerBlock->getDeclarations('background-'); + self::assertCount(2, $backgroundHeaderDeclarations); + self::assertSame('background-color', $backgroundHeaderDeclarations[0]->getPropertyName()); + self::assertSame('background-color', $backgroundHeaderDeclarations[1]->getPropertyName()); + $backgroundHeaderDeclarations = $headerBlock->getDeclarationsAssociative('background-'); + self::assertCount(1, $backgroundHeaderDeclarations); + self::assertInstanceOf(Color::class, $backgroundHeaderDeclarations['background-color']->getValue()); + self::assertSame('rgba', $backgroundHeaderDeclarations['background-color']->getValue()->getColorDescription()); + $headerBlock->removeDeclaration($backgroundHeaderDeclarations['background-color']); + $backgroundHeaderDeclarations = $headerBlock->getDeclarations('background-'); + self::assertCount(1, $backgroundHeaderDeclarations); + self::assertSame('green', $backgroundHeaderDeclarations[0]->getValue()); } /** @@ -372,20 +372,20 @@ public function slashedValues(): void } } foreach ($document->getAllDeclarationBlocks() as $declarationBlock) { - $fontRules = $declarationBlock->getDeclarations('font'); - $fontRule = $fontRules[0]; - $fontRuleValue = $fontRule->getValue(); - self::assertSame(' ', $fontRuleValue->getListSeparator()); - $fontRuleValueComponents = $fontRuleValue->getListComponents(); - $commaList = $fontRuleValueComponents[1]; + $fontDeclarations = $declarationBlock->getDeclarations('font'); + $fontDeclaration = $fontDeclarations[0]; + $fontDeclarationValue = $fontDeclaration->getValue(); + self::assertSame(' ', $fontDeclarationValue->getListSeparator()); + $fontDeclarationValueComponents = $fontDeclarationValue->getListComponents(); + $commaList = $fontDeclarationValueComponents[1]; self::assertInstanceOf(ValueList::class, $commaList); - $slashList = $fontRuleValueComponents[0]; + $slashList = $fontDeclarationValueComponents[0]; self::assertInstanceOf(ValueList::class, $slashList); self::assertSame(',', $commaList->getListSeparator()); self::assertSame('/', $slashList->getListSeparator()); - $borderRadiusRules = $declarationBlock->getDeclarations('border-radius'); - $borderRadiusRule = $borderRadiusRules[0]; - $slashList = $borderRadiusRule->getValue(); + $borderRadiusDeclarations = $declarationBlock->getDeclarations('border-radius'); + $borderRadiusDeclaration = $borderRadiusDeclarations[0]; + $slashList = $borderRadiusDeclaration->getValue(); self::assertSame('/', $slashList->getListSeparator()); $slashListComponents = $slashList->getListComponents(); $secondSlashListComponent = $slashListComponents[1]; @@ -883,11 +883,11 @@ public function missingPropertyValueLenient(): void $block = $declarationBlocks[0]; self::assertInstanceOf(DeclarationBlock::class, $block); self::assertEquals([new Selector('div')], $block->getSelectors()); - $rules = $block->getDeclarations(); - self::assertCount(1, $rules); - $rule = $rules[0]; - self::assertSame('display', $rule->getPropertyName()); - self::assertSame('inline-block', $rule->getValue()); + $declarations = $block->getDeclarations(); + self::assertCount(1, $declarations); + $declaration = $declarations[0]; + self::assertSame('display', $declaration->getPropertyName()); + self::assertSame('inline-block', $declaration->getValue()); } /** @@ -943,19 +943,19 @@ public function lineNumbersParsing(): void } } - // Checking for the multiline color rule lines 27-31 + // Checking for the multiline color declaration lines 27-31 $expectedColorLineNumbers = [28, 29, 30]; $declarationBlocks = $document->getAllDeclarationBlocks(); // Choose the 2nd one $secondDeclarationBlock = $declarationBlocks[1]; - $rules = $secondDeclarationBlock->getDeclarations(); + $declarations = $secondDeclarationBlock->getDeclarations(); // Choose the 2nd one - $valueOfSecondRule = $rules[1]->getValue(); - self::assertInstanceOf(Color::class, $valueOfSecondRule); - self::assertSame(27, $rules[1]->getLineNumber()); + $valueOfSecondDeclaration = $declarations[1]->getValue(); + self::assertInstanceOf(Color::class, $valueOfSecondDeclaration); + self::assertSame(27, $declarations[1]->getLineNumber()); $actualColorLineNumbers = []; - foreach ($valueOfSecondRule->getColor() as $size) { + foreach ($valueOfSecondDeclaration->getColor() as $size) { $actualColorLineNumbers[] = $size->getLineNumber(); } @@ -1005,13 +1005,13 @@ public function commentExtracting(): void self::assertSame(' Number 4 ', $fooBarBlockComments[0]->getComment()); self::assertSame(' Number 5 ', $fooBarBlockComments[1]->getComment()); - // Declaration rules. + // Declarations. self::assertInstanceOf(DeclarationBlock::class, $fooBarBlock); - $fooBarRules = $fooBarBlock->getDeclarations(); - $fooBarRule = $fooBarRules[0]; - $fooBarRuleComments = $fooBarRule->getComments(); - self::assertCount(1, $fooBarRuleComments); - self::assertSame(' Number 6 ', $fooBarRuleComments[0]->getComment()); + $fooBarDeclarations = $fooBarBlock->getDeclarations(); + $fooBarDeclaration = $fooBarDeclarations[0]; + $fooBarDeclarationComments = $fooBarDeclaration->getComments(); + self::assertCount(1, $fooBarDeclarationComments); + self::assertSame(' Number 6 ', $fooBarDeclarationComments[0]->getComment()); // Media property. self::assertInstanceOf(Commentable::class, $nodes[2]); @@ -1026,10 +1026,10 @@ public function commentExtracting(): void self::assertCount(1, $fooBarComments); self::assertSame('* Number 10 *', $fooBarComments[0]->getComment()); - // Media -> declaration -> rule. + // Media -> rule -> declaration. self::assertInstanceOf(DeclarationBlock::class, $mediaRules[0]); - $fooBarRules = $mediaRules[0]->getDeclarations(); - $fooBarChildComments = $fooBarRules[0]->getComments(); + $fooBarDeclarations = $mediaRules[0]->getDeclarations(); + $fooBarChildComments = $fooBarDeclarations[0]->getComments(); self::assertCount(1, $fooBarChildComments); self::assertSame('* Number 10b *', $fooBarChildComments[0]->getComment()); } @@ -1044,8 +1044,8 @@ public function flatCommentExtractingOneComment(): void $contents = $document->getContents(); self::assertInstanceOf(DeclarationBlock::class, $contents[0]); - $divRules = $contents[0]->getDeclarations(); - $comments = $divRules[0]->getComments(); + $divDeclarations = $contents[0]->getDeclarations(); + $comments = $divDeclarations[0]->getComments(); self::assertCount(1, $comments); self::assertSame('Find Me!', $comments[0]->getComment()); @@ -1054,15 +1054,15 @@ public function flatCommentExtractingOneComment(): void /** * @test */ - public function flatCommentExtractingTwoConjoinedCommentsForOneRule(): void + public function flatCommentExtractingTwoConjoinedCommentsForOneDeclaration(): void { $parser = new Parser('div {/*Find Me!*//*Find Me Too!*/left:10px; text-align:left;}'); $document = $parser->parse(); $contents = $document->getContents(); self::assertInstanceOf(DeclarationBlock::class, $contents[0]); - $divRules = $contents[0]->getDeclarations(); - $comments = $divRules[0]->getComments(); + $divDeclarations = $contents[0]->getDeclarations(); + $comments = $divDeclarations[0]->getComments(); self::assertCount(2, $comments); self::assertSame('Find Me!', $comments[0]->getComment()); @@ -1072,15 +1072,15 @@ public function flatCommentExtractingTwoConjoinedCommentsForOneRule(): void /** * @test */ - public function flatCommentExtractingTwoSpaceSeparatedCommentsForOneRule(): void + public function flatCommentExtractingTwoSpaceSeparatedCommentsForOneDeclaration(): void { $parser = new Parser('div { /*Find Me!*/ /*Find Me Too!*/ left:10px; text-align:left;}'); $document = $parser->parse(); $contents = $document->getContents(); self::assertInstanceOf(DeclarationBlock::class, $contents[0]); - $divRules = $contents[0]->getDeclarations(); - $comments = $divRules[0]->getComments(); + $divDeclarations = $contents[0]->getDeclarations(); + $comments = $divDeclarations[0]->getComments(); self::assertCount(2, $comments); self::assertSame('Find Me!', $comments[0]->getComment()); @@ -1090,21 +1090,21 @@ public function flatCommentExtractingTwoSpaceSeparatedCommentsForOneRule(): void /** * @test */ - public function flatCommentExtractingCommentsForTwoRules(): void + public function flatCommentExtractingCommentsForTwoDeclarations(): void { $parser = new Parser('div {/*Find Me!*/left:10px; /*Find Me Too!*/text-align:left;}'); $document = $parser->parse(); $contents = $document->getContents(); self::assertInstanceOf(DeclarationBlock::class, $contents[0]); - $divRules = $contents[0]->getDeclarations(); - $rule1Comments = $divRules[0]->getComments(); - $rule2Comments = $divRules[1]->getComments(); - - self::assertCount(1, $rule1Comments); - self::assertCount(1, $rule2Comments); - self::assertSame('Find Me!', $rule1Comments[0]->getComment()); - self::assertSame('Find Me Too!', $rule2Comments[0]->getComment()); + $divDeclarations = $contents[0]->getDeclarations(); + $declaration1Comments = $divDeclarations[0]->getComments(); + $declaration2Comments = $divDeclarations[1]->getComments(); + + self::assertCount(1, $declaration1Comments); + self::assertCount(1, $declaration2Comments); + self::assertSame('Find Me!', $declaration1Comments[0]->getComment()); + self::assertSame('Find Me Too!', $declaration2Comments[0]->getComment()); } /** @@ -1182,10 +1182,10 @@ public function escapedSpecialCaseTokens(): void $document = self::parsedStructureForFile('escaped-tokens'); $contents = $document->getContents(); self::assertInstanceOf(RuleSet::class, $contents[0]); - $rules = $contents[0]->getDeclarations(); - $urlRule = $rules[0]; - $calcRule = $rules[1]; - self::assertInstanceOf(URL::class, $urlRule->getValue()); - self::assertInstanceOf(CalcFunction::class, $calcRule->getValue()); + $declarations = $contents[0]->getDeclarations(); + $urlDeclaration = $declarations[0]; + $calcDeclaration = $declarations[1]; + self::assertInstanceOf(URL::class, $urlDeclaration->getValue()); + self::assertInstanceOf(CalcFunction::class, $calcDeclaration->getValue()); } } diff --git a/tests/RuleSet/DeclarationBlockTest.php b/tests/RuleSet/DeclarationBlockTest.php index b1e16d513..f3e4a9bf3 100644 --- a/tests/RuleSet/DeclarationBlockTest.php +++ b/tests/RuleSet/DeclarationBlockTest.php @@ -20,7 +20,7 @@ final class DeclarationBlockTest extends TestCase /** * @test */ - public function overrideRules(): void + public function overrideDeclarations(): void { $css = '.wrapper { left: 10px; text-align: left; }'; $parser = new Parser($css); From a3cc1673aa22c55db334cf8c8343f73dec56bf3f Mon Sep 17 00:00:00 2001 From: JakeQZ Date: Tue, 17 Feb 2026 17:26:12 +0000 Subject: [PATCH 117/136] [TASK] Rename `RuleContainer` to `DeclarationList` (#1530) It contains property declarations not rules, and describing it as a 'list' rather than a 'container' seems more apt, as the declarations are ordered. Changes to use the new interface name in the source code and tests will be submitted as follow-up PRs. --- CHANGELOG.md | 3 ++ config/phpstan.neon | 1 + src/RuleSet/DeclarationList.php | 77 +++++++++++++++++++++++++++++++++ src/RuleSet/RuleContainer.php | 71 ++---------------------------- 4 files changed, 84 insertions(+), 68 deletions(-) create mode 100644 src/RuleSet/DeclarationList.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 14e3ecb83..224916a79 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ Please also have a look at our ### Changed +- `RuleSet\RuleContainer` is renamed to `RuleSet\DeclarationList` (#1530) - Methods like `setRule()` in `RuleSet` and `DeclarationBlock` have been renamed to `setDeclaration()`, etc. (#1521) - `Rule\Rule` class is renamed to `Property\Declaration` @@ -35,6 +36,8 @@ Please also have a look at our ### Deprecated +- `RuleSet\RuleContainer` is deprecated; use `RuleSet\DeclarationList` instead + (#1530) - Methods like `setRule()` in `RuleSet` and `DeclarationBlock` are deprecated; there are direct replacements such as `setDeclaration()` (#1521) - `Rule\Rule` class is deprecated; `Property\Declaration` is a direct diff --git a/config/phpstan.neon b/config/phpstan.neon index cfdfefe1d..84416ade1 100644 --- a/config/phpstan.neon +++ b/config/phpstan.neon @@ -17,6 +17,7 @@ parameters: bootstrapFiles: - %currentWorkingDirectory%/src/Rule/Rule.php + - %currentWorkingDirectory%/src/RuleSet/RuleContainer.php type_perfect: no_mixed_property: true diff --git a/src/RuleSet/DeclarationList.php b/src/RuleSet/DeclarationList.php new file mode 100644 index 000000000..b1e67915f --- /dev/null +++ b/src/RuleSet/DeclarationList.php @@ -0,0 +1,77 @@ + $declarations + */ + public function setDeclarations(array $declarations): void; + + /** + * @return array, Declaration> + */ + public function getDeclarations(?string $searchPattern = null): array; + + /** + * @return array + */ + public function getDeclarationsAssociative(?string $searchPattern = null): array; + + /** + * @deprecated in v9.2, will be removed in v10.0; use `addDeclaration()` instead. + */ + public function addRule(Declaration $declarationToAdd, ?Declaration $sibling = null): void; + + /** + * @deprecated in v9.2, will be removed in v10.0; use `removeDeclaration()` instead. + */ + public function removeRule(Declaration $declarationToRemove): void; + + /** + * @deprecated in v9.2, will be removed in v10.0; use `removeMatchingDeclarations()` instead. + */ + public function removeMatchingRules(string $searchPattern): void; + + /** + * @deprecated in v9.2, will be removed in v10.0; use `removeAllDeclarations()` instead. + */ + public function removeAllRules(): void; + + /** + * @param array $declarations + * + * @deprecated in v9.2, will be removed in v10.0; use `setDeclarations()` instead. + */ + public function setRules(array $declarations): void; + + /** + * @return array, Declaration> + * + * @deprecated in v9.2, will be removed in v10.0; use `getDeclarations()` instead. + */ + public function getRules(?string $searchPattern = null): array; + + /** + * @return array + * + * @deprecated in v9.2, will be removed in v10.0; use `getDeclarationsAssociative()` instead. + */ + public function getRulesAssoc(?string $searchPattern = null): array; +} diff --git a/src/RuleSet/RuleContainer.php b/src/RuleSet/RuleContainer.php index 2767d5057..daaf13090 100644 --- a/src/RuleSet/RuleContainer.php +++ b/src/RuleSet/RuleContainer.php @@ -4,74 +4,9 @@ namespace Sabberworm\CSS\RuleSet; -use Sabberworm\CSS\Property\Declaration; +use function Safe\class_alias; /** - * Represents a CSS item that contains `Declaration`s, defining the methods to manipulate them. + * @deprecated in v9.2, will be removed in v10.0. Use `DeclarationList` instead, which is a direct replacement. */ -interface RuleContainer -{ - public function addDeclaration(Declaration $declarationToAdd, ?Declaration $sibling = null): void; - - public function removeDeclaration(Declaration $declarationToRemove): void; - - public function removeMatchingDeclarations(string $searchPattern): void; - - public function removeAllDeclarations(): void; - - /** - * @param array $declarations - */ - public function setDeclarations(array $declarations): void; - - /** - * @return array, Declaration> - */ - public function getDeclarations(?string $searchPattern = null): array; - - /** - * @return array - */ - public function getDeclarationsAssociative(?string $searchPattern = null): array; - - /** - * @deprecated in v9.2, will be removed in v10.0; use `addDeclaration()` instead. - */ - public function addRule(Declaration $declarationToAdd, ?Declaration $sibling = null): void; - - /** - * @deprecated in v9.2, will be removed in v10.0; use `removeDeclaration()` instead. - */ - public function removeRule(Declaration $declarationToRemove): void; - - /** - * @deprecated in v9.2, will be removed in v10.0; use `removeMatchingDeclarations()` instead. - */ - public function removeMatchingRules(string $searchPattern): void; - - /** - * @deprecated in v9.2, will be removed in v10.0; use `removeAllDeclarations()` instead. - */ - public function removeAllRules(): void; - - /** - * @param array $declarations - * - * @deprecated in v9.2, will be removed in v10.0; use `setDeclarations()` instead. - */ - public function setRules(array $declarations): void; - - /** - * @return array, Declaration> - * - * @deprecated in v9.2, will be removed in v10.0; use `getDeclarations()` instead. - */ - public function getRules(?string $searchPattern = null): array; - - /** - * @return array - * - * @deprecated in v9.2, will be removed in v10.0; use `getDeclarationsAssociative()` instead. - */ - public function getRulesAssoc(?string $searchPattern = null): array; -} +class_alias(DeclarationList::class, RuleContainer::class); From 447fcb85612c84d820ee2f15cbe818ab4c5137d3 Mon Sep 17 00:00:00 2001 From: Oliver Klee Date: Wed, 18 Feb 2026 00:48:24 +0100 Subject: [PATCH 118/136] [TASK] Streamline the code checker and fixer configuration (#1535) - always check and fix the `bin/`, `config/`, `src/` and `test/` directories - sort the directories when provided to a tool - make the rector command explicit --- composer.json | 14 +++++++------- config/php-cs-fixer.php | 4 +++- config/rector.php | 2 ++ 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/composer.json b/composer.json index b0fe1213e..c871f23b7 100644 --- a/composer.json +++ b/composer.json @@ -76,10 +76,10 @@ "check:dynamic": [ "@check:tests" ], - "check:php:codesniffer": "phpcs --standard=config/phpcs.xml config src tests", - "check:php:fixer": "\"./.phive/php-cs-fixer\" --config=config/php-cs-fixer.php fix --dry-run -v --show-progress=dots --diff bin src tests config", - "check:php:lint": "parallel-lint src tests config bin", - "check:php:rector": "rector --no-progress-bar --dry-run --config=config/rector.php", + "check:php:codesniffer": "phpcs --standard=config/phpcs.xml bin config src tests", + "check:php:fixer": "\"./.phive/php-cs-fixer\" --config=config/php-cs-fixer.php fix --dry-run -v --show-progress=dots --diff bin config src tests", + "check:php:lint": "parallel-lint bin config src tests", + "check:php:rector": "rector process --no-progress-bar --dry-run --config=config/rector.php", "check:php:stan": "phpstan --no-progress --configuration=config/phpstan.neon", "check:static": [ "@check:composer:normalize", @@ -105,9 +105,9 @@ "@fix:php:codesniffer", "@fix:php:fixer" ], - "fix:php:codesniffer": "phpcbf --standard=config/phpcs.xml config src tests", - "fix:php:fixer": "\"./.phive/php-cs-fixer\" --config=config/php-cs-fixer.php fix bin src tests", - "fix:php:rector": "rector --config=config/rector.php", + "fix:php:codesniffer": "phpcbf --standard=config/phpcs.xml bin config src tests", + "fix:php:fixer": "\"./.phive/php-cs-fixer\" --config=config/php-cs-fixer.php fix bin config src tests", + "fix:php:rector": "rector process --config=config/rector.php", "phpstan:baseline": "phpstan --configuration=config/phpstan.neon --generate-baseline=config/phpstan-baseline.neon --allow-empty-baseline" }, "scripts-descriptions": { diff --git a/config/php-cs-fixer.php b/config/php-cs-fixer.php index 326105a0b..807b376fd 100644 --- a/config/php-cs-fixer.php +++ b/config/php-cs-fixer.php @@ -2,7 +2,9 @@ declare(strict_types=1); -return (new \PhpCsFixer\Config()) +use PhpCsFixer\Config; + +return (new Config()) ->setRiskyAllowed(true) ->setRules( [ diff --git a/config/rector.php b/config/rector.php index 61df84182..cf501b19c 100644 --- a/config/rector.php +++ b/config/rector.php @@ -10,6 +10,8 @@ return RectorConfig::configure() ->withPaths( [ + __DIR__ . '/../bin', + __DIR__ . '/../config', __DIR__ . '/../src', __DIR__ . '/../tests', ] From 34ba5d4b2101cbaaf819a3cdd5377fdedad8aa6f Mon Sep 17 00:00:00 2001 From: Oliver Klee Date: Wed, 18 Feb 2026 17:31:41 +0100 Subject: [PATCH 119/136] [BUGFIX] Make class aliases available for consumers (#1534) Fixes #1532 --- composer.json | 6 +++++- config/phpstan.neon | 4 ---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/composer.json b/composer.json index c871f23b7..6fbb7909f 100644 --- a/composer.json +++ b/composer.json @@ -46,7 +46,11 @@ "autoload": { "psr-4": { "Sabberworm\\CSS\\": "src/" - } + }, + "files": [ + "src/Rule/Rule.php", + "src/RuleSet/RuleContainer.php" + ] }, "autoload-dev": { "psr-4": { diff --git a/config/phpstan.neon b/config/phpstan.neon index 84416ade1..55ba682af 100644 --- a/config/phpstan.neon +++ b/config/phpstan.neon @@ -15,10 +15,6 @@ parameters: - %currentWorkingDirectory%/src/ - %currentWorkingDirectory%/tests/ - bootstrapFiles: - - %currentWorkingDirectory%/src/Rule/Rule.php - - %currentWorkingDirectory%/src/RuleSet/RuleContainer.php - type_perfect: no_mixed_property: true no_mixed_caller: true From 17dd54a45816fd4583a57e919011144e8ba8d182 Mon Sep 17 00:00:00 2001 From: JakeQZ Date: Wed, 18 Feb 2026 19:29:58 +0000 Subject: [PATCH 120/136] [BUGFIX] Parse attribute selectors (#1536) Don't treat spaces or characters like `~` within them as a stop character representing a combinator. The bug was introduced by #1496 and related changes that split selector representation and parsing into compound selectors and combinators. Addresses some of the issues in #1533. --- CHANGELOG.md | 2 +- src/Property/Selector/CompoundSelector.php | 33 +++++++++++++++++-- .../Selector/CompoundSelectorTest.php | 13 ++++++++ 3 files changed, 45 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 224916a79..97c6683a9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,7 +27,7 @@ Please also have a look at our `getPropertyName()` (#1506) - `Selector` is now represented as a sequence of `Selector\Component` objects which can be accessed via `getComponents()`, manipulated individually, or set - via `setComponents()` (#1478, #1486, #1487, #1488, #1494, #1496) + via `setComponents()` (#1478, #1486, #1487, #1488, #1494, #1496, #1536) - `Selector::setSelector()` and `Selector` constructor will now throw exception upon provision of an invalid selectior (#1498, #1502) - Clean up extra whitespace in CSS selector (#1398) diff --git a/src/Property/Selector/CompoundSelector.php b/src/Property/Selector/CompoundSelector.php index 4e0b85210..1e6fa5419 100644 --- a/src/Property/Selector/CompoundSelector.php +++ b/src/Property/Selector/CompoundSelector.php @@ -27,6 +27,8 @@ class CompoundSelector implements Component '"', '(', ')', + '[', + ']', ',', ' ', "\t", @@ -46,7 +48,7 @@ class CompoundSelector implements Component (?: (?: # any sequence of valid unescaped characters, except quotes - [a-zA-Z0-9\\x{00A0}-\\x{FFFF}_^$|*=\\[\\]()\\-\\.:#,\\s]++ + [a-zA-Z0-9\\x{00A0}-\\x{FFFF}_^$|*~=\\[\\]()\\-\\.:#,\\s]++ | # one or more escaped characters (?:\\\\.)++ @@ -100,6 +102,7 @@ public static function parse(ParserState $parserState, array &$comments = []): s $selectorParts = []; $stringWrapperCharacter = null; $functionNestingLevel = 0; + $isWithinAttribute = false; while (true) { $selectorParts[] = $parserState->consumeUntil(self::PARSER_STOP_CHARACTERS, false, false, $comments); @@ -140,6 +143,32 @@ public static function parse(ParserState $parserState, array &$comments = []): s --$functionNestingLevel; } break; + case '[': + if (!\is_string($stringWrapperCharacter)) { + if ($isWithinAttribute) { + throw new UnexpectedTokenException( + 'anything but', + '[', + 'literal', + $parserState->currentLine() + ); + } + $isWithinAttribute = true; + } + break; + case ']': + if (!\is_string($stringWrapperCharacter)) { + if (!$isWithinAttribute) { + throw new UnexpectedTokenException( + 'anything but', + ']', + 'literal', + $parserState->currentLine() + ); + } + $isWithinAttribute = false; + } + break; case '{': // The fallthrough is intentional. case '}': @@ -162,7 +191,7 @@ public static function parse(ParserState $parserState, array &$comments = []): s case '+': // The fallthrough is intentional. case '~': - if (!\is_string($stringWrapperCharacter) && $functionNestingLevel === 0) { + if (!\is_string($stringWrapperCharacter) && $functionNestingLevel === 0 && !$isWithinAttribute) { break 2; } break; diff --git a/tests/Unit/Property/Selector/CompoundSelectorTest.php b/tests/Unit/Property/Selector/CompoundSelectorTest.php index 1e3f6bd6a..e8e659e85 100644 --- a/tests/Unit/Property/Selector/CompoundSelectorTest.php +++ b/tests/Unit/Property/Selector/CompoundSelectorTest.php @@ -57,6 +57,17 @@ public static function provideCompoundSelectorAndSpecificity(): array '`not` with multiple arguments' => [':not(#your-mug, .their-mug)', 110], 'attribute with `"`' => ['[alt="{}()[]\\"\',"]', 10], 'attribute with `\'`' => ['[alt=\'{}()[]"\\\',\']', 10], + // TODO, broken: specificity should be 11, but the calculator doesn't realize the `#` is in a string. + 'attribute with `^=`' => ['a[href^="#"]', 111], + 'attribute with `*=`' => ['a[href*="example"]', 11], + // TODO, broken: specificity should be 11, but the calculator doesn't realize the `.` is in a string. + 'attribute with `$=`' => ['a[href$=".org"]', 21], + 'attribute with `~=`' => ['span[title~="bonjour"]', 11], + 'attribute with `|=`' => ['[lang|="en"]', 10], + // TODO, broken: specificity should be 11, but the calculator doesn't realize the `i` is in an attribute. + 'attribute with case insensitive modifier' => ['a[href*="insensitive" i]', 12], + // TODO, broken: specificity should be 21, but the calculator doesn't realize the `.` is in a string. + 'multiple attributes' => ['a[href^="https://"][href$=".org"]', 31], ]; } @@ -173,6 +184,8 @@ public static function provideInvalidCompoundSelectorForParse(): array 'attribute value missing closing double quote' => ['a[href="#top]'], 'attribute value with mismatched quotes, single quote opening' => ['a[href=\'#top"]'], 'attribute value with mismatched quotes, double quote opening' => ['a[href="#top\']'], + 'attribute value with extra `[`' => ['a[[href="#top"]'], + 'attribute value with extra `]`' => ['a[href="#top"]]'], ]; } From a32cddcda4e5f664418ffd40bd8fd1738b7f45e2 Mon Sep 17 00:00:00 2001 From: JakeQZ Date: Wed, 18 Feb 2026 20:12:19 +0000 Subject: [PATCH 121/136] [BUGFIX] Allow selector combinators in pseudo-class functions (#1537) The bug was introduced by #1496 and related changes that split selector representation and parsing into compound selectors and combinators. The regex for `CompoundSelector::isValid()` now matches that for `Selector::isValid()`. Fixes #1533 --- CHANGELOG.md | 2 +- src/Property/Selector/CompoundSelector.php | 2 +- tests/Unit/Property/Selector/CompoundSelectorTest.php | 10 ++++++++++ 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 97c6683a9..97620c163 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,7 +27,7 @@ Please also have a look at our `getPropertyName()` (#1506) - `Selector` is now represented as a sequence of `Selector\Component` objects which can be accessed via `getComponents()`, manipulated individually, or set - via `setComponents()` (#1478, #1486, #1487, #1488, #1494, #1496, #1536) + via `setComponents()` (#1478, #1486, #1487, #1488, #1494, #1496, #1536, #1537) - `Selector::setSelector()` and `Selector` constructor will now throw exception upon provision of an invalid selectior (#1498, #1502) - Clean up extra whitespace in CSS selector (#1398) diff --git a/src/Property/Selector/CompoundSelector.php b/src/Property/Selector/CompoundSelector.php index 1e6fa5419..80b1670cd 100644 --- a/src/Property/Selector/CompoundSelector.php +++ b/src/Property/Selector/CompoundSelector.php @@ -48,7 +48,7 @@ class CompoundSelector implements Component (?: (?: # any sequence of valid unescaped characters, except quotes - [a-zA-Z0-9\\x{00A0}-\\x{FFFF}_^$|*~=\\[\\]()\\-\\.:#,\\s]++ + [a-zA-Z0-9\\x{00A0}-\\x{FFFF}_^$|*=~\\[\\]()\\-\\s\\.:#+>,]++ | # one or more escaped characters (?:\\\\.)++ diff --git a/tests/Unit/Property/Selector/CompoundSelectorTest.php b/tests/Unit/Property/Selector/CompoundSelectorTest.php index e8e659e85..b42c76f15 100644 --- a/tests/Unit/Property/Selector/CompoundSelectorTest.php +++ b/tests/Unit/Property/Selector/CompoundSelectorTest.php @@ -68,6 +68,16 @@ public static function provideCompoundSelectorAndSpecificity(): array 'attribute with case insensitive modifier' => ['a[href*="insensitive" i]', 12], // TODO, broken: specificity should be 21, but the calculator doesn't realize the `.` is in a string. 'multiple attributes' => ['a[href^="https://"][href$=".org"]', 31], + // TODO, broken: specificity should be 11, but the calculator is treating the `n` as a type selector. + 'nth-last-child' => ['li:nth-last-child(2n+3)', 12], + // TODO, maybe broken: specificity should probably be 2 (1 for `p` and 1 for `span`). + '`not` with descendent combinator' => [':not(p span)', 1], + // TODO, maybe broken: specificity should probably be 2 (1 for `p` and 1 for `span`). + '`not` with child combinator' => [':not(p > span)', 1], + // TODO, maybe broken: specificity should probably be 2 (1 for `h1` and 1 for `p`). + '`not` with next-sibling combinator' => [':not(h1 + p)', 1], + // TODO, maybe broken: specificity should probably be 2 (1 for `h1` and 1 for `p`). + '`not` with subsequent-sibling combinator' => [':not(h1 ~ p)', 1], ]; } From 69f4ed8c3fa1fd084aa1f7be209dfae4f9afa4e8 Mon Sep 17 00:00:00 2001 From: JakeQZ Date: Thu, 19 Feb 2026 11:14:13 +0000 Subject: [PATCH 122/136] [FEATURE] Add `phpstan:clearcache` Composer script (#1538) --- composer.json | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index 6fbb7909f..59044eb8e 100644 --- a/composer.json +++ b/composer.json @@ -112,7 +112,8 @@ "fix:php:codesniffer": "phpcbf --standard=config/phpcs.xml bin config src tests", "fix:php:fixer": "\"./.phive/php-cs-fixer\" --config=config/php-cs-fixer.php fix bin config src tests", "fix:php:rector": "rector process --config=config/rector.php", - "phpstan:baseline": "phpstan --configuration=config/phpstan.neon --generate-baseline=config/phpstan-baseline.neon --allow-empty-baseline" + "phpstan:baseline": "phpstan --configuration=config/phpstan.neon --generate-baseline=config/phpstan-baseline.neon --allow-empty-baseline", + "phpstan:clearcache": "phpstan clear-result-cache" }, "scripts-descriptions": { "check": "Runs all dynamic and static code checks.", @@ -134,6 +135,7 @@ "fix:php:codesniffer": "Reformats the code with PHP_CodeSniffer.", "fix:php:fixer": "Fixes autofixable issues found by PHP CS Fixer.", "fix:php:rector": "Fixes autofixable issues found by Rector.", - "phpstan:baseline": "Updates the PHPStan baseline file to match the code." + "phpstan:baseline": "Updates the PHPStan baseline file to match the code.", + "phpstan:clearcache": "Clears the PHPStan cache." } } From eaa13f3b2bc8c378022a5cb2952446e01a5f7b80 Mon Sep 17 00:00:00 2001 From: JakeQZ Date: Fri, 20 Feb 2026 11:09:43 +0000 Subject: [PATCH 123/136] [TASK] Use `DeclarationList` in the source code (#1539) The interface was renamed from `RuleContainer` in #1530. Also rename the trait that transitionally provides a mapping for the methods deprecated in #1521 for the implementing classes. --- CHANGELOG.md | 2 +- src/CSSList/CSSBlockList.php | 4 ++-- src/RuleSet/DeclarationBlock.php | 4 ++-- ...eContainerMethods.php => LegacyDeclarationListMethods.php} | 4 ++-- src/RuleSet/RuleSet.php | 4 ++-- 5 files changed, 9 insertions(+), 9 deletions(-) rename src/RuleSet/{LegacyRuleContainerMethods.php => LegacyDeclarationListMethods.php} (93%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 97620c163..61e278938 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,7 +18,7 @@ Please also have a look at our ### Changed -- `RuleSet\RuleContainer` is renamed to `RuleSet\DeclarationList` (#1530) +- `RuleSet\RuleContainer` is renamed to `RuleSet\DeclarationList` (#1530, #1539) - Methods like `setRule()` in `RuleSet` and `DeclarationBlock` have been renamed to `setDeclaration()`, etc. (#1521) - `Rule\Rule` class is renamed to `Property\Declaration` diff --git a/src/CSSList/CSSBlockList.php b/src/CSSList/CSSBlockList.php index 574bdaa38..b54a61d83 100644 --- a/src/CSSList/CSSBlockList.php +++ b/src/CSSList/CSSBlockList.php @@ -8,7 +8,7 @@ use Sabberworm\CSS\Property\Declaration; use Sabberworm\CSS\Property\Selector; use Sabberworm\CSS\RuleSet\DeclarationBlock; -use Sabberworm\CSS\RuleSet\RuleContainer; +use Sabberworm\CSS\RuleSet\DeclarationList; use Sabberworm\CSS\RuleSet\RuleSet; use Sabberworm\CSS\Value\CSSFunction; use Sabberworm\CSS\Value\Value; @@ -98,7 +98,7 @@ public function getAllValues( ); } } - } elseif ($element instanceof RuleContainer) { + } elseif ($element instanceof DeclarationList) { foreach ($element->getRules($ruleSearchPattern) as $rule) { $result = \array_merge( $result, diff --git a/src/RuleSet/DeclarationBlock.php b/src/RuleSet/DeclarationBlock.php index 29265ca69..6812cb487 100644 --- a/src/RuleSet/DeclarationBlock.php +++ b/src/RuleSet/DeclarationBlock.php @@ -32,10 +32,10 @@ * * Note that `CSSListItem` extends both `Commentable` and `Renderable`, so those interfaces must also be implemented. */ -class DeclarationBlock implements CSSElement, CSSListItem, Positionable, RuleContainer +class DeclarationBlock implements CSSElement, CSSListItem, Positionable, DeclarationList { use CommentContainer; - use LegacyRuleContainerMethods; + use LegacyDeclarationListMethods; use Position; /** diff --git a/src/RuleSet/LegacyRuleContainerMethods.php b/src/RuleSet/LegacyDeclarationListMethods.php similarity index 93% rename from src/RuleSet/LegacyRuleContainerMethods.php rename to src/RuleSet/LegacyDeclarationListMethods.php index 5ed6f7944..2eb82692b 100644 --- a/src/RuleSet/LegacyRuleContainerMethods.php +++ b/src/RuleSet/LegacyDeclarationListMethods.php @@ -7,9 +7,9 @@ use Sabberworm\CSS\Property\Declaration; /** - * Provides a mapping of the deprecated methods in a `RuleContainer` to their renamed replacements. + * Provides a mapping of the deprecated methods in a `DeclarationList` to their renamed replacements. */ -trait LegacyRuleContainerMethods +trait LegacyDeclarationListMethods { /** * @deprecated in v9.2, will be removed in v10.0; use `addDeclaration()` instead. diff --git a/src/RuleSet/RuleSet.php b/src/RuleSet/RuleSet.php index f124a2b16..137e8fd45 100644 --- a/src/RuleSet/RuleSet.php +++ b/src/RuleSet/RuleSet.php @@ -26,10 +26,10 @@ * * Note that `CSSListItem` extends both `Commentable` and `Renderable`, so those interfaces must also be implemented. */ -class RuleSet implements CSSElement, CSSListItem, Positionable, RuleContainer +class RuleSet implements CSSElement, CSSListItem, Positionable, DeclarationList { use CommentContainer; - use LegacyRuleContainerMethods; + use LegacyDeclarationListMethods; use Position; /** From cb56b23dcdf1a94aaa057fd38521cb5aef888f33 Mon Sep 17 00:00:00 2001 From: JakeQZ Date: Fri, 20 Feb 2026 23:00:14 +0000 Subject: [PATCH 124/136] [TASK] Use `DeclarationList` in the tests (#1541) The interface was renamed from `RuleContainer` in #1530. Also accordingly rename the trait that provides test methods for implementing classes. --- tests/Unit/RuleSet/DeclarationBlockTest.php | 2 +- ...{RuleContainerTest.php => DeclarationListTests.php} | 10 +++++----- tests/Unit/RuleSet/RuleSetTest.php | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) rename tests/Unit/RuleSet/{RuleContainerTest.php => DeclarationListTests.php} (99%) diff --git a/tests/Unit/RuleSet/DeclarationBlockTest.php b/tests/Unit/RuleSet/DeclarationBlockTest.php index 6f39ea4b7..39c40592d 100644 --- a/tests/Unit/RuleSet/DeclarationBlockTest.php +++ b/tests/Unit/RuleSet/DeclarationBlockTest.php @@ -23,7 +23,7 @@ */ final class DeclarationBlockTest extends TestCase { - use RuleContainerTest; + use DeclarationListTests; /** * @var DeclarationBlock diff --git a/tests/Unit/RuleSet/RuleContainerTest.php b/tests/Unit/RuleSet/DeclarationListTests.php similarity index 99% rename from tests/Unit/RuleSet/RuleContainerTest.php rename to tests/Unit/RuleSet/DeclarationListTests.php index 7bb2964cf..f831e6181 100644 --- a/tests/Unit/RuleSet/RuleContainerTest.php +++ b/tests/Unit/RuleSet/DeclarationListTests.php @@ -6,24 +6,24 @@ use PHPUnit\Framework\TestCase; use Sabberworm\CSS\Property\Declaration; -use Sabberworm\CSS\RuleSet\RuleContainer; +use Sabberworm\CSS\RuleSet\DeclarationList; use TRegx\PhpUnit\DataProviders\DataProvider; /** - * This trait provides test methods for unit-testing classes that implement `RuleContainer`. + * This trait provides test methods for unit-testing classes that implement `DeclarationList`. * It can be `use`d in a `TestCase` which has a `$subject` property that is an instance of the implementing class * (the class under test), `setUp()` with default values. * * @phpstan-require-extends TestCase */ -trait RuleContainerTest +trait DeclarationListTests { /** * @test */ - public function implementsRuleContainer(): void + public function implementsDeclarationList(): void { - self::assertInstanceOf(RuleContainer::class, $this->subject); + self::assertInstanceOf(DeclarationList::class, $this->subject); } /** diff --git a/tests/Unit/RuleSet/RuleSetTest.php b/tests/Unit/RuleSet/RuleSetTest.php index 76cf7b2e6..6ba574de2 100644 --- a/tests/Unit/RuleSet/RuleSetTest.php +++ b/tests/Unit/RuleSet/RuleSetTest.php @@ -14,7 +14,7 @@ */ final class RuleSetTest extends TestCase { - use RuleContainerTest; + use DeclarationListTests; /** * @var RuleSet From 59373045e11ad47b5c18fc615feee0219e42f6d3 Mon Sep 17 00:00:00 2001 From: Oliver Klee Date: Sat, 21 Feb 2026 18:12:03 +0100 Subject: [PATCH 125/136] [TASK] Prepare release of version 9.2.0 (#1543) --- .github/dependabot.yml | 4 ++-- CHANGELOG.md | 18 ++++++++++++++---- composer.json | 2 +- 3 files changed, 17 insertions(+), 7 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 76a72c265..877b9029a 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -8,7 +8,7 @@ updates: interval: "daily" commit-message: prefix: "[Dependabot] " - milestone: 10 + milestone: 11 - package-ecosystem: "composer" directory: "/" @@ -26,4 +26,4 @@ updates: versioning-strategy: "increase" commit-message: prefix: "[Dependabot] " - milestone: 10 + milestone: 11 diff --git a/CHANGELOG.md b/CHANGELOG.md index 61e278938..8463e2118 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,20 @@ Please also have a look at our ### Added +### Changed + +### Deprecated + +### Removed + +### Fixed + +### Documentation + +## 9.2.0: New features and deprecations + +### Added + - Add `OutputFormat::setSpaceAroundSelectorCombinator()` (#1504) - Add support for escaped quotes in the selectors (#1485, #1489) - Provide line number in exception message for mismatched parentheses in @@ -45,8 +59,6 @@ Please also have a look at our - `Rule::setRule()` and `getRule()` are deprecated and replaced with `setPropertyName()` and `getPropertyName()` (#1506, #1519) -### Removed - ### Fixed - Do not escape characters that do not need escaping in CSS string (#1444) @@ -61,8 +73,6 @@ Please also have a look at our follows a `+` or `-` operator (#1399) - Use typesafe versions of PHP functions (#1379, #1380, #1382, #1383, #1384) -### Documentation - ## 9.1.0: Add support for PHP 8.5 ### Added diff --git a/composer.json b/composer.json index 59044eb8e..4d5285eab 100644 --- a/composer.json +++ b/composer.json @@ -68,7 +68,7 @@ }, "extra": { "branch-alias": { - "dev-main": "9.2.x-dev" + "dev-main": "9.3.x-dev" } }, "scripts": { From 7f85aa83d2b31cdede74421bfcecd1fe471c7e11 Mon Sep 17 00:00:00 2001 From: Oliver Klee Date: Sun, 22 Feb 2026 18:46:25 +0100 Subject: [PATCH 126/136] [TASK] Implement `CSSNamespace::getArrayRepresentation()` (#1544) Part of #1440. --- src/Property/CSSNamespace.php | 9 ++- tests/Unit/Property/CSSNamespaceTest.php | 76 +++++++++++++++++++++++- 2 files changed, 81 insertions(+), 4 deletions(-) diff --git a/src/Property/CSSNamespace.php b/src/Property/CSSNamespace.php index 1801904a3..6046bdf95 100644 --- a/src/Property/CSSNamespace.php +++ b/src/Property/CSSNamespace.php @@ -8,6 +8,7 @@ use Sabberworm\CSS\OutputFormat; use Sabberworm\CSS\Position\Position; use Sabberworm\CSS\Position\Positionable; +use Sabberworm\CSS\ShortClassNameProvider; use Sabberworm\CSS\Value\CSSString; use Sabberworm\CSS\Value\URL; @@ -18,6 +19,7 @@ class CSSNamespace implements AtRule, Positionable { use CommentContainer; use Position; + use ShortClassNameProvider; /** * @var CSSString|URL @@ -102,6 +104,11 @@ public function atRuleArgs(): array */ public function getArrayRepresentation(): array { - throw new \BadMethodCallException('`getArrayRepresentation` is not yet implemented for `' . self::class . '`'); + return [ + 'class' => $this->getShortClassName(), + // We're using `uri` here instead of `url` to better match the spec. + 'uri' => $this->url->getArrayRepresentation(), + 'prefix' => $this->prefix, + ]; } } diff --git a/tests/Unit/Property/CSSNamespaceTest.php b/tests/Unit/Property/CSSNamespaceTest.php index 44f5c7d1f..b00f48489 100644 --- a/tests/Unit/Property/CSSNamespaceTest.php +++ b/tests/Unit/Property/CSSNamespaceTest.php @@ -8,6 +8,7 @@ use Sabberworm\CSS\CSSList\CSSListItem; use Sabberworm\CSS\Property\CSSNamespace; use Sabberworm\CSS\Value\CSSString; +use Sabberworm\CSS\Value\URL; /** * @covers \Sabberworm\CSS\Property\CSSNamespace @@ -35,10 +36,79 @@ public function implementsCSSListItem(): void /** * @test */ - public function getArrayRepresentationThrowsException(): void + public function getArrayRepresentationIncludesClassName(): void { - $this->expectException(\BadMethodCallException::class); + $subject = new CSSNamespace(new CSSString('http://www.w3.org/2000/svg')); - $this->subject->getArrayRepresentation(); + $result = $subject->getArrayRepresentation(); + + self::assertSame('CSSNamespace', $result['class']); + } + + /** + * @test + */ + public function getArrayRepresentationIncludesUrlProvidedAsCssString(): void + { + $uri = 'http://www.w3.org/2000/svg'; + $subject = new CSSNamespace(new CSSString($uri)); + + $result = $subject->getArrayRepresentation(); + + self::assertSame( + [ + 'class' => 'CSSString', + 'contents' => $uri, + ], + $result['uri'] + ); + } + + /** + * @test + */ + public function getArrayRepresentationIncludesUrlProvidedAsUrl(): void + { + $uri = 'http://www.w3.org/2000/svg'; + $subject = new CSSNamespace(new URL(new CSSString($uri))); + + $result = $subject->getArrayRepresentation(); + + self::assertSame( + [ + 'class' => 'URL', + 'uri' => [ + 'class' => 'CSSString', + 'contents' => $uri, + ], + ], + $result['uri'] + ); + } + + /** + * @test + */ + public function getArrayRepresentationIncludesPrefixForNullPrefix(): void + { + $subject = new CSSNamespace(new CSSString('http://www.w3.org/2000/svg'), null); + + $result = $subject->getArrayRepresentation(); + + self::assertNull($result['prefix']); + } + + /** + * @test + */ + public function getArrayRepresentationIncludesPrefixForStringPrefix(): void + { + $prefix = 'hello'; + + $subject = new CSSNamespace(new CSSString('http://www.w3.org/2000/svg'), $prefix); + + $result = $subject->getArrayRepresentation(); + + self::assertSame($prefix, $result['prefix']); } } From 707c04e6ee530bdc3e6b34f6ebea8756bb69dc2e Mon Sep 17 00:00:00 2001 From: Anna Reiter Date: Thu, 26 Feb 2026 16:28:12 +0100 Subject: [PATCH 127/136] [TASK] Set code coverage job to not block workflow (#1551) --- .github/workflows/codecoverage.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/codecoverage.yml b/.github/workflows/codecoverage.yml index 15b1c0546..a23d17eef 100644 --- a/.github/workflows/codecoverage.yml +++ b/.github/workflows/codecoverage.yml @@ -59,6 +59,8 @@ jobs: - name: Upload coverage results to Coveralls uses: coverallsapp/github-action@v2 + with: + fail-on-error: false env: github-token: ${{ secrets.GITHUB_TOKEN }} file: coverage.xml From c4f46fe7d23b4429394aabfe8815c5a9894bf890 Mon Sep 17 00:00:00 2001 From: Oliver Klee Date: Thu, 26 Feb 2026 18:58:11 +0100 Subject: [PATCH 128/136] [TASK] Allow PHPStan to use unlimited processes (#1550) This helps speed up local development and CI. --- config/phpstan.neon | 4 ---- 1 file changed, 4 deletions(-) diff --git a/config/phpstan.neon b/config/phpstan.neon index 55ba682af..ec625a44a 100644 --- a/config/phpstan.neon +++ b/config/phpstan.neon @@ -2,10 +2,6 @@ includes: - phpstan-baseline.neon parameters: - parallel: - # Don't be overly greedy on machines with more CPU's to be a good neighbor especially on CI - maximumNumberOfProcesses: 5 - phpVersion: 70200 level: 6 From 418627201a9ff4e67ec9ca7da16c6b96ae603d2b Mon Sep 17 00:00:00 2001 From: Oliver Klee Date: Sat, 28 Feb 2026 19:01:14 +0100 Subject: [PATCH 129/136] [TASK] Implement `Import::getArrayRepresentation()` (#1554) Part of #1440. --- src/Property/Import.php | 9 ++++++++- tests/Unit/Property/ImportTest.php | 32 +++++++++++++++++++++++++++--- 2 files changed, 37 insertions(+), 4 deletions(-) diff --git a/src/Property/Import.php b/src/Property/Import.php index d7c24d7af..b9075504f 100644 --- a/src/Property/Import.php +++ b/src/Property/Import.php @@ -8,6 +8,7 @@ use Sabberworm\CSS\OutputFormat; use Sabberworm\CSS\Position\Position; use Sabberworm\CSS\Position\Positionable; +use Sabberworm\CSS\ShortClassNameProvider; use Sabberworm\CSS\Value\URL; /** @@ -17,6 +18,7 @@ class Import implements AtRule, Positionable { use CommentContainer; use Position; + use ShortClassNameProvider; /** * @var URL @@ -90,6 +92,11 @@ public function getMediaQuery(): ?string */ public function getArrayRepresentation(): array { - throw new \BadMethodCallException('`getArrayRepresentation` is not yet implemented for `' . self::class . '`'); + return [ + 'class' => $this->getShortClassName(), + // We're using the term "uri" here to match the wording used in the specs: + // https://www.w3.org/TR/CSS22/cascade.html#at-import + 'uri' => $this->location->getArrayRepresentation(), + ]; } } diff --git a/tests/Unit/Property/ImportTest.php b/tests/Unit/Property/ImportTest.php index 3164bc201..77a4f1362 100644 --- a/tests/Unit/Property/ImportTest.php +++ b/tests/Unit/Property/ImportTest.php @@ -36,10 +36,36 @@ public function implementsCSSListItem(): void /** * @test */ - public function getArrayRepresentationThrowsException(): void + public function getArrayRepresentationIncludesClassName(): void { - $this->expectException(\BadMethodCallException::class); + $subject = new Import(new URL(new CSSString('https://example.org/')), null); - $this->subject->getArrayRepresentation(); + $result = $subject->getArrayRepresentation(); + + self::assertSame('Import', $result['class']); + } + + /** + * @test + */ + public function getArrayRepresentationIncludesUri(): void + { + $uri = 'https://example.com'; + $url = new URL(new CSSString($uri)); + + $subject = new Import($url, null); + + $result = $subject->getArrayRepresentation(); + + self::assertSame( + [ + 'class' => 'URL', + 'uri' => [ + 'class' => 'CSSString', + 'contents' => $uri, + ], + ], + $result['uri'] + ); } } From fa5eba4a87363a7b50553061eb08ae333baca09d Mon Sep 17 00:00:00 2001 From: samuil-banti-wpenigne Date: Sat, 28 Feb 2026 20:07:15 +0200 Subject: [PATCH 130/136] Add support for modern CSS at-rules `@layer`, `@scope`, and `@starting-style` (#1549) --- CHANGELOG.md | 2 + src/Property/AtRule.php | 3 +- tests/CSSList/AtRuleBlockListTest.php | 124 ++++++++++++++++++++++++++ tests/Unit/Property/AtRuleTest.php | 12 ++- 4 files changed, 139 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8463e2118..151d0ac19 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,8 @@ Please also have a look at our ### Added +- Add support for modern CSS at-rules: `@layer`, `@scope`, and `@starting-style` (#1549) + ### Changed ### Deprecated diff --git a/src/Property/AtRule.php b/src/Property/AtRule.php index f5ae475ea..3ba6236ab 100644 --- a/src/Property/AtRule.php +++ b/src/Property/AtRule.php @@ -18,7 +18,8 @@ interface AtRule extends CSSListItem * * @internal since 8.5.2 */ - public const BLOCK_RULES = 'media/document/supports/region-style/font-feature-values/container'; + public const BLOCK_RULES = + 'media/document/supports/region-style/font-feature-values/container/layer/scope/starting-style'; /** * @return non-empty-string diff --git a/tests/CSSList/AtRuleBlockListTest.php b/tests/CSSList/AtRuleBlockListTest.php index 3539463bf..e30b25bc3 100644 --- a/tests/CSSList/AtRuleBlockListTest.php +++ b/tests/CSSList/AtRuleBlockListTest.php @@ -51,6 +51,24 @@ public static function provideSyntacticallyCorrectAtRule(): array 'container' => [ '@container (min-width: 60rem) { .items { background: blue; } }', ], + 'layer named' => [ + '@layer theme { .button { color: blue; } }', + ], + 'layer anonymous' => [ + '@layer { .card { padding: 1rem; } }', + ], + 'scope with selector' => [ + '@scope (.card) { .title { font-size: 2rem; } }', + ], + 'scope root only' => [ + '@scope { .content { margin: 0; } }', + ], + 'scope with limit' => [ + '@scope (.article-body) to (figure) { h2 { color: red; } }', + ], + 'starting-style' => [ + '@starting-style { .dialog { opacity: 0; transform: translateY(-10px); } }', + ], ]; } @@ -94,4 +112,110 @@ public function parsesSyntacticallyCorrectAtRuleInStrictMode(string $css): void self::assertNotEmpty($contents, 'Failing CSS: `' . $css . '`'); } + + /** + * @return array + */ + public static function provideAtRuleParsingData(): array + { + return [ + 'layer with named argument' => [ + '@layer theme { .button { color: blue; } }', + 'layer', + 'theme', + 1, + ], + 'layer without arguments' => [ + '@layer { .card { padding: 1rem; } }', + 'layer', + '', + 1, + ], + 'scope with selector' => [ + '@scope (.card) { .title { font-size: 2rem; } }', + 'scope', + '(.card)', + 1, + ], + 'scope without selector' => [ + '@scope { .content { margin: 0; } }', + 'scope', + '', + 1, + ], + 'scope with limit' => [ + '@scope (.article-body) to (figure) { h2 { color: red; } }', + 'scope', + '(.article-body) to (figure)', + 1, + ], + 'starting-style' => [ + '@starting-style { .dialog { opacity: 0; transform: translateY(-10px); } }', + 'starting-style', + '', + 1, + ], + ]; + } + + /** + * @return array}> + */ + public static function provideAtRuleRenderingData(): array + { + return [ + 'layer with named argument' => [ + '@layer theme { .button { color: blue; } }', + ['@layer theme', '.button', 'color: blue'], + ], + 'scope with selector' => [ + '@scope (.card) { .title { font-size: 2rem; } }', + ['@scope (.card)', '.title', 'font-size: 2rem'], + ], + 'scope with limit' => [ + '@scope (.article-body) to (figure) { h2 { color: red; } }', + ['@scope (.article-body) to (figure)', 'h2', 'color: red'], + ], + 'starting-style' => [ + '@starting-style { .dialog { opacity: 0; } }', + ['@starting-style', '.dialog', 'opacity: 0'], + ], + ]; + } + + /** + * @test + * + * @dataProvider provideAtRuleParsingData + */ + public function parsesAtRuleBlockList( + string $css, + string $expectedName, + string $expectedArgs, + int $expectedContentCount + ): void { + $contents = (new Parser($css))->parse()->getContents(); + $atRuleBlockList = $contents[0]; + + self::assertInstanceOf(AtRuleBlockList::class, $atRuleBlockList); + self::assertSame($expectedName, $atRuleBlockList->atRuleName()); + self::assertSame($expectedArgs, $atRuleBlockList->atRuleArgs()); + self::assertCount($expectedContentCount, $atRuleBlockList->getContents()); + } + + /** + * @test + * + * @dataProvider provideAtRuleRenderingData + * + * @param list $expectedSubstrings + */ + public function rendersAtRuleBlockListCorrectly(string $css, array $expectedSubstrings): void + { + $rendered = (new Parser($css))->parse()->render(); + + foreach ($expectedSubstrings as $expected) { + self::assertStringContainsString($expected, $rendered); + } + } } diff --git a/tests/Unit/Property/AtRuleTest.php b/tests/Unit/Property/AtRuleTest.php index 6094fbf83..526bb3121 100644 --- a/tests/Unit/Property/AtRuleTest.php +++ b/tests/Unit/Property/AtRuleTest.php @@ -18,7 +18,17 @@ final class AtRuleTest extends TestCase public function blockRulesConstantIsCorrect(): void { self::assertEqualsCanonicalizing( - ['media', 'document', 'supports', 'region-style', 'font-feature-values', 'container'], + [ + 'media', + 'document', + 'supports', + 'region-style', + 'font-feature-values', + 'container', + 'layer', + 'scope', + 'starting-style', + ], explode('/', AtRule::BLOCK_RULES) ); } From 5d342d30fd4678e9d02bde03b216ae0c98a0b046 Mon Sep 17 00:00:00 2001 From: JakeQZ Date: Sat, 28 Feb 2026 19:58:59 +0000 Subject: [PATCH 131/136] [CLEANUP] Tighten type annotations in `AtRuleBlockListTest` (#1555) --- tests/CSSList/AtRuleBlockListTest.php | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/tests/CSSList/AtRuleBlockListTest.php b/tests/CSSList/AtRuleBlockListTest.php index e30b25bc3..10f8b2556 100644 --- a/tests/CSSList/AtRuleBlockListTest.php +++ b/tests/CSSList/AtRuleBlockListTest.php @@ -17,7 +17,7 @@ final class AtRuleBlockListTest extends TestCase { /** - * @return array + * @return array */ public static function provideMinWidthMediaRule(): array { @@ -28,7 +28,7 @@ public static function provideMinWidthMediaRule(): array } /** - * @return array + * @return array */ public static function provideSyntacticallyCorrectAtRule(): array { @@ -75,6 +75,8 @@ public static function provideSyntacticallyCorrectAtRule(): array /** * @test * + * @param non-empty-string $css + * * @dataProvider provideMinWidthMediaRule */ public function parsesRuleNameOfMediaQueries(string $css): void @@ -89,6 +91,8 @@ public function parsesRuleNameOfMediaQueries(string $css): void /** * @test * + * @param non-empty-string $css + * * @dataProvider provideMinWidthMediaRule */ public function parsesArgumentsOfMediaQueries(string $css): void @@ -103,6 +107,8 @@ public function parsesArgumentsOfMediaQueries(string $css): void /** * @test * + * @param non-empty-string $css + * * @dataProvider provideMinWidthMediaRule * @dataProvider provideSyntacticallyCorrectAtRule */ @@ -114,7 +120,7 @@ public function parsesSyntacticallyCorrectAtRuleInStrictMode(string $css): void } /** - * @return array + * @return array}> */ public static function provideAtRuleParsingData(): array { @@ -159,7 +165,7 @@ public static function provideAtRuleParsingData(): array } /** - * @return array}> + * @return array}> */ public static function provideAtRuleRenderingData(): array { @@ -186,6 +192,10 @@ public static function provideAtRuleRenderingData(): array /** * @test * + * @param non-empty-string $css + * @param non-empty-string $expectedName + * @param int<0, max> $expectedContentCount + * * @dataProvider provideAtRuleParsingData */ public function parsesAtRuleBlockList( @@ -208,7 +218,8 @@ public function parsesAtRuleBlockList( * * @dataProvider provideAtRuleRenderingData * - * @param list $expectedSubstrings + * @param non-empty-string $css + * @param non-empty-list $expectedSubstrings */ public function rendersAtRuleBlockListCorrectly(string $css, array $expectedSubstrings): void { From dfdf1834ca91d549206697220caf82b88c6bf338 Mon Sep 17 00:00:00 2001 From: JakeQZ Date: Sat, 28 Feb 2026 19:59:30 +0000 Subject: [PATCH 132/136] [CLEANUP] Change `AtRule::BLOCK_RULES` to an array (#1556) --- src/CSSList/CSSList.php | 2 +- src/Property/AtRule.php | 13 +++++++++++-- tests/Unit/Property/AtRuleTest.php | 2 +- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/src/CSSList/CSSList.php b/src/CSSList/CSSList.php index 62f7fe654..77af0c81b 100644 --- a/src/CSSList/CSSList.php +++ b/src/CSSList/CSSList.php @@ -222,7 +222,7 @@ private static function parseAtRule(ParserState $parserState): ?CSSListItem } } $useRuleSet = true; - foreach (\explode('/', AtRule::BLOCK_RULES) as $blockRuleName) { + foreach (AtRule::BLOCK_RULES as $blockRuleName) { if (self::identifierIs($identifier, $blockRuleName)) { $useRuleSet = false; break; diff --git a/src/Property/AtRule.php b/src/Property/AtRule.php index 3ba6236ab..808df79f8 100644 --- a/src/Property/AtRule.php +++ b/src/Property/AtRule.php @@ -18,8 +18,17 @@ interface AtRule extends CSSListItem * * @internal since 8.5.2 */ - public const BLOCK_RULES = - 'media/document/supports/region-style/font-feature-values/container/layer/scope/starting-style'; + public const BLOCK_RULES = [ + 'media', + 'document', + 'supports', + 'region-style', + 'font-feature-values', + 'container', + 'layer', + 'scope', + 'starting-style', + ]; /** * @return non-empty-string diff --git a/tests/Unit/Property/AtRuleTest.php b/tests/Unit/Property/AtRuleTest.php index 526bb3121..85407745f 100644 --- a/tests/Unit/Property/AtRuleTest.php +++ b/tests/Unit/Property/AtRuleTest.php @@ -29,7 +29,7 @@ public function blockRulesConstantIsCorrect(): void 'scope', 'starting-style', ], - explode('/', AtRule::BLOCK_RULES) + AtRule::BLOCK_RULES ); } } From 1b395cbe259d84d454638d97a89c18a377012d52 Mon Sep 17 00:00:00 2001 From: Oliver Klee Date: Tue, 3 Mar 2026 18:14:07 +0100 Subject: [PATCH 133/136] [BUGFIX] Avoid double autoloading of class aliases (#1552) --- CHANGELOG.md | 2 ++ src/Rule/Rule.php | 11 +++++++---- src/RuleSet/RuleContainer.php | 10 ++++++---- 3 files changed, 15 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 151d0ac19..befaf3c0d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,8 @@ Please also have a look at our ### Fixed +- Avoid double autoloading of class aliases (#1552) + ### Documentation ## 9.2.0: New features and deprecations diff --git a/src/Rule/Rule.php b/src/Rule/Rule.php index bed19cf2a..ee97121df 100644 --- a/src/Rule/Rule.php +++ b/src/Rule/Rule.php @@ -8,7 +8,10 @@ use function Safe\class_alias; -/** - * @deprecated in v9.2, will be removed in v10.0. Use `Property\Declaration` instead, which is a direct replacement. - */ -class_alias(Declaration::class, Rule::class); +if (!\class_exists(Rule::class, false) && !\interface_exists(Rule::class, false)) { + /** + * @deprecated in v9.2, will be removed in v10.0. Use `Property\Declaration` instead, which is a direct + * replacement. + */ + class_alias(Declaration::class, Rule::class); +} diff --git a/src/RuleSet/RuleContainer.php b/src/RuleSet/RuleContainer.php index daaf13090..fa82b4929 100644 --- a/src/RuleSet/RuleContainer.php +++ b/src/RuleSet/RuleContainer.php @@ -6,7 +6,9 @@ use function Safe\class_alias; -/** - * @deprecated in v9.2, will be removed in v10.0. Use `DeclarationList` instead, which is a direct replacement. - */ -class_alias(DeclarationList::class, RuleContainer::class); +if (!\class_exists(RuleContainer::class, false) && !\interface_exists(RuleContainer::class, false)) { + /** + * @deprecated in v9.2, will be removed in v10.0. Use `DeclarationList` instead, which is a direct replacement. + */ + class_alias(DeclarationList::class, RuleContainer::class); +} From 88dbd0f7f91abbfe4402d0a3071e9ff4d81ed949 Mon Sep 17 00:00:00 2001 From: Oliver Klee Date: Tue, 3 Mar 2026 18:31:43 +0100 Subject: [PATCH 134/136] Prepare release of version 9.3.0 (#1558) --- .github/dependabot.yml | 4 ++-- CHANGELOG.md | 12 ++++++++++-- composer.json | 2 +- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 877b9029a..aaceaa779 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -8,7 +8,7 @@ updates: interval: "daily" commit-message: prefix: "[Dependabot] " - milestone: 11 + milestone: 12 - package-ecosystem: "composer" directory: "/" @@ -26,4 +26,4 @@ updates: versioning-strategy: "increase" commit-message: prefix: "[Dependabot] " - milestone: 11 + milestone: 12 diff --git a/CHANGELOG.md b/CHANGELOG.md index befaf3c0d..00dc246d9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,8 +10,6 @@ Please also have a look at our ### Added -- Add support for modern CSS at-rules: `@layer`, `@scope`, and `@starting-style` (#1549) - ### Changed ### Deprecated @@ -20,6 +18,16 @@ Please also have a look at our ### Fixed +### Documentation + +## 9.3.0: Support for modern CSS at-rules and autoloading bugfix + +### Added + +- Add support for modern CSS at-rules: `@layer`, `@scope`, and `@starting-style` (#1549) + +### Fixed + - Avoid double autoloading of class aliases (#1552) ### Documentation diff --git a/composer.json b/composer.json index 4d5285eab..44733d37c 100644 --- a/composer.json +++ b/composer.json @@ -68,7 +68,7 @@ }, "extra": { "branch-alias": { - "dev-main": "9.3.x-dev" + "dev-main": "9.4.x-dev" } }, "scripts": { From 44130cf285ff0b5d5120d8cb2b8ac13bf0ad280b Mon Sep 17 00:00:00 2001 From: JakeQZ Date: Wed, 4 Mar 2026 16:41:11 +0000 Subject: [PATCH 135/136] [TASK] Make aliased class deprecation notices visible to tools (#1559) Resolves #1553 --- src/Rule/Rule.php | 14 ++++++++++---- src/RuleSet/RuleContainer.php | 12 +++++++++--- 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/src/Rule/Rule.php b/src/Rule/Rule.php index ee97121df..525114f0d 100644 --- a/src/Rule/Rule.php +++ b/src/Rule/Rule.php @@ -9,9 +9,15 @@ use function Safe\class_alias; if (!\class_exists(Rule::class, false) && !\interface_exists(Rule::class, false)) { - /** - * @deprecated in v9.2, will be removed in v10.0. Use `Property\Declaration` instead, which is a direct - * replacement. - */ class_alias(Declaration::class, Rule::class); + // The test is expected to evaluate to false, + // but allows for the deprecation notice to be picked up by IDEs like PHPStorm. + // @phpstan-ignore-next-line booleanAnd.alwaysTrue + if (!\class_exists(Rule::class, false) && !\interface_exists(Rule::class, false)) { + /** + * @deprecated in v9.2, will be removed in v10.0. Use `Property\Declaration` instead, which is a direct + * replacement. + */ + class Rule {} + } } diff --git a/src/RuleSet/RuleContainer.php b/src/RuleSet/RuleContainer.php index fa82b4929..eaaed9a7f 100644 --- a/src/RuleSet/RuleContainer.php +++ b/src/RuleSet/RuleContainer.php @@ -7,8 +7,14 @@ use function Safe\class_alias; if (!\class_exists(RuleContainer::class, false) && !\interface_exists(RuleContainer::class, false)) { - /** - * @deprecated in v9.2, will be removed in v10.0. Use `DeclarationList` instead, which is a direct replacement. - */ class_alias(DeclarationList::class, RuleContainer::class); + // The test is expected to evaluate to false, + // but allows for the deprecation notice to be picked up by IDEs like PHPStorm. + // @phpstan-ignore-next-line booleanAnd.alwaysTrue + if (!\class_exists(RuleContainer::class, false) && !\interface_exists(RuleContainer::class, false)) { + /** + * @deprecated in v9.2, will be removed in v10.0. Use `DeclarationList` instead, which is a direct replacement. + */ + interface RuleContainer {} + } } From 97f2d1f23210e350780b16574532467b70fae5d1 Mon Sep 17 00:00:00 2001 From: JakeQZ Date: Wed, 4 Mar 2026 22:30:26 +0000 Subject: [PATCH 136/136] [TASK] Specify the PHPStan errors to ignore for deprecation notices (#1560) Follow-up from #1559. `phpstan-ignore-next-line` does not have parameters - it ignores all warnings on the next line regardless (see https://github.com/phpstan/phpstan/issues/11340#issuecomment-3943559262). `phpstan-ignore` applies to the next line, and has parameters to specify which warnings to ignore. Because the `if` statement generates two `booleanNot.alwaysTrue` warnings, this needs to be specified twice in the argument list. --- src/Rule/Rule.php | 2 +- src/RuleSet/RuleContainer.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Rule/Rule.php b/src/Rule/Rule.php index 525114f0d..82f837dc8 100644 --- a/src/Rule/Rule.php +++ b/src/Rule/Rule.php @@ -12,7 +12,7 @@ class_alias(Declaration::class, Rule::class); // The test is expected to evaluate to false, // but allows for the deprecation notice to be picked up by IDEs like PHPStorm. - // @phpstan-ignore-next-line booleanAnd.alwaysTrue + // @phpstan-ignore booleanNot.alwaysTrue, booleanNot.alwaysTrue, booleanAnd.alwaysTrue if (!\class_exists(Rule::class, false) && !\interface_exists(Rule::class, false)) { /** * @deprecated in v9.2, will be removed in v10.0. Use `Property\Declaration` instead, which is a direct diff --git a/src/RuleSet/RuleContainer.php b/src/RuleSet/RuleContainer.php index eaaed9a7f..01da5d3ce 100644 --- a/src/RuleSet/RuleContainer.php +++ b/src/RuleSet/RuleContainer.php @@ -10,7 +10,7 @@ class_alias(DeclarationList::class, RuleContainer::class); // The test is expected to evaluate to false, // but allows for the deprecation notice to be picked up by IDEs like PHPStorm. - // @phpstan-ignore-next-line booleanAnd.alwaysTrue + // @phpstan-ignore booleanNot.alwaysTrue, booleanNot.alwaysTrue, booleanAnd.alwaysTrue if (!\class_exists(RuleContainer::class, false) && !\interface_exists(RuleContainer::class, false)) { /** * @deprecated in v9.2, will be removed in v10.0. Use `DeclarationList` instead, which is a direct replacement.