diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index 76a72c265..aaceaa779 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -8,7 +8,7 @@ updates:
interval: "daily"
commit-message:
prefix: "[Dependabot] "
- milestone: 10
+ milestone: 12
- package-ecosystem: "composer"
directory: "/"
@@ -26,4 +26,4 @@ updates:
versioning-strategy: "increase"
commit-message:
prefix: "[Dependabot] "
- milestone: 10
+ milestone: 12
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 251e8fc4d..72f25f4aa 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
@@ -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') }}
@@ -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
@@ -63,7 +63,7 @@ jobs:
steps:
- name: Checkout
- uses: actions/checkout@v5
+ uses: actions/checkout@v6
- name: Install PHP
uses: shivammathur/setup-php@v2
@@ -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') }}
@@ -105,6 +105,7 @@ jobs:
command:
- composer:normalize
- php:fixer
+ - php:codesniffer
- php:stan
- php:rector
php-version:
@@ -112,7 +113,7 @@ jobs:
steps:
- name: Checkout
- uses: actions/checkout@v5
+ uses: actions/checkout@v6
- name: Install PHP
uses: shivammathur/setup-php@v2
@@ -126,7 +127,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') }}
@@ -145,4 +146,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..a23d17eef 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
@@ -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') }}
@@ -52,13 +52,15 @@ jobs:
composer show;
- name: Run Tests
- run: composer ci:tests:coverage
+ run: composer check:tests:coverage
- name: Show generated coverage files
run: ls -lah
- 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
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/CHANGELOG.md b/CHANGELOG.md
index 8978fbb71..00dc246d9 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -18,10 +18,73 @@ Please also have a look at our
### Fixed
-- Use typesafe versions of PHP functions (#1379, #1380, #1382, #1383, #1384)
+### 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
+## 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
+ selector (#1435)
+- Add support for CSS container queries (#1400)
+
+### Changed
+
+- `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`
+ (#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
+ which can be accessed via `getComponents()`, manipulated individually, or set
+ 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)
+- The array keys passed to `DeclarationBlock::setSelectors()` are no longer
+ preserved (#1407)
+
+### 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
+ replacement (#1508)
+- `Rule::setRule()` and `getRule()` are deprecated and replaced with
+ `setPropertyName()` and `getPropertyName()` (#1506, #1519)
+
+### 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)
+- Parse consecutive comments (#1421)
+- 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)
+- Use typesafe versions of PHP functions (#1379, #1380, #1382, #1383, #1384)
+
## 9.1.0: Add support for PHP 8.5
### Added
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..44733d37c 100644
--- a/composer.json
+++ b/composer.json
@@ -25,18 +25,19 @@
"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",
"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",
- "phpunit/phpunit": "8.5.48",
+ "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.52",
"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"
},
"suggest": {
@@ -45,7 +46,11 @@
"autoload": {
"psr-4": {
"Sabberworm\\CSS\\": "src/"
- }
+ },
+ "files": [
+ "src/Rule/Rule.php",
+ "src/RuleSet/RuleContainer.php"
+ ]
},
"autoload-dev": {
"psr-4": {
@@ -63,35 +68,37 @@
},
"extra": {
"branch-alias": {
- "dev-main": "9.2.x-dev"
+ "dev-main": "9.4.x-dev"
}
},
"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: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",
+ "@check:php:fixer",
+ "@check:php:codesniffer",
+ "@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"
],
@@ -99,30 +106,36 @@
"fix:php": [
"@fix:composer:normalize",
"@fix:php:rector",
+ "@fix:php:codesniffer",
"@fix:php:fixer"
],
- "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"
+ "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:clearcache": "phpstan clear-result-cache"
},
"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: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.",
+ "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.",
+ "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."
}
}
diff --git a/config/php-cs-fixer.php b/config/php-cs-fixer.php
index c10bf59ae..807b376fd 100644
--- a/config/php-cs-fixer.php
+++ b/config/php-cs-fixer.php
@@ -2,22 +2,24 @@
declare(strict_types=1);
-return (new \PhpCsFixer\Config())
+use PhpCsFixer\Config;
+
+return (new Config())
->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']],
diff --git a/config/phpcs.xml b/config/phpcs.xml
new file mode 100644
index 000000000..f4a05b783
--- /dev/null
+++ b/config/phpcs.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/config/phpstan-baseline.neon b/config/phpstan-baseline.neon
index 6205096ae..ea84804b7 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
@@ -85,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
@@ -97,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..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
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',
]
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
+```
diff --git a/src/CSSList/CSSBlockList.php b/src/CSSList/CSSBlockList.php
index 122ac5d1d..b54a61d83 100644
--- a/src/CSSList/CSSBlockList.php
+++ b/src/CSSList/CSSBlockList.php
@@ -5,10 +5,10 @@
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\DeclarationList;
use Sabberworm\CSS\RuleSet\RuleSet;
use Sabberworm\CSS\Value\CSSFunction;
use Sabberworm\CSS\Value\Value;
@@ -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).
@@ -98,14 +98,14 @@ public function getAllValues(
);
}
}
- } elseif ($element instanceof RuleContainer) {
+ } elseif ($element instanceof DeclarationList) {
foreach ($element->getRules($ruleSearchPattern) as $rule) {
$result = \array_merge(
$result,
$this->getAllValues($rule, $ruleSearchPattern, $searchInFunctionArguments)
);
}
- } elseif ($element instanceof Rule) {
+ } elseif ($element instanceof Declaration) {
$value = $element->getValue();
// `string` values are discarded.
if ($value instanceof CSSElement) {
diff --git a/src/CSSList/CSSList.php b/src/CSSList/CSSList.php
index e09a03a98..77af0c81b 100644
--- a/src/CSSList/CSSList.php
+++ b/src/CSSList/CSSList.php
@@ -68,13 +68,19 @@ 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 {
+ $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);
@@ -87,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) {
@@ -133,7 +140,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());
}
@@ -214,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;
@@ -370,7 +378,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 +435,44 @@ public function getContents(): array
{
return $this->contents;
}
+
+ /**
+ * @return array|null>
+ *
+ * @internal
+ */
+ public function getArrayRepresentation(): array
+ {
+ throw new \BadMethodCallException('`getArrayRepresentation` is not yet implemented for `' . self::class . '`');
+ }
+
+ /**
+ * @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/src/Comment/Comment.php b/src/Comment/Comment.php
index 33188988f..cba49d925 100644
--- a/src/Comment/Comment.php
+++ b/src/Comment/Comment.php
@@ -5,13 +5,15 @@
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;
+use Sabberworm\CSS\ShortClassNameProvider;
class Comment implements Positionable, Renderable
{
use Position;
+ use ShortClassNameProvider;
/**
* @var string
@@ -46,4 +48,19 @@ public function render(OutputFormat $outputFormat): string
{
return '/*' . $this->commentText . '*/';
}
+
+ /**
+ * @return array|null>
+ *
+ * @internal
+ */
+ public function getArrayRepresentation(): array
+ {
+ 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/src/OutputFormat.php b/src/OutputFormat.php
index 5c493865a..4f7ba0b19 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 = '"';
@@ -94,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.
*
@@ -181,7 +184,7 @@ final class OutputFormat
private $indentationLevel = 0;
/**
- * @return non-empty-string
+ * @return '"'|"'"
*
* @internal
*/
@@ -191,7 +194,7 @@ public function getStringQuotingType(): string
}
/**
- * @param non-empty-string $quotingType
+ * @param '"'|"'" $quotingType
*
* @return $this fluent interface
*/
@@ -436,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
*/
@@ -726,6 +750,7 @@ public static function createCompact(): self
->setSpaceAfterRuleName('')
->setSpaceBeforeOpeningBrace('')
->setSpaceAfterSelectorSeparator('')
+ ->setSpaceAroundSelectorCombinator('')
->setSemicolonAfterLastRule(false)
->setRenderComments(false);
diff --git a/src/Parsing/ParserState.php b/src/Parsing/ParserState.php
index cc69ed974..3451fe0c4 100644
--- a/src/Parsing/ParserState.php
+++ b/src/Parsing/ParserState.php
@@ -16,9 +16,6 @@
*/
class ParserState
{
- /**
- * @var null
- */
public const EOF = null;
/**
@@ -191,17 +188,25 @@ 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
+ *
+ * @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
+ 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 {
@@ -218,7 +223,7 @@ public function consumeWhiteSpace(): array
}
} while ($comment instanceof Comment);
- return $comments;
+ return $consumed;
}
/**
@@ -282,6 +287,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
@@ -331,7 +357,7 @@ public function isEnd(): bool
/**
* @param list|string|self::EOF $stopCharacters
- * @param array $comments
+ * @param list $comments
*
* @throws UnexpectedEOFException
* @throws UnexpectedTokenException
@@ -346,6 +372,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)) {
@@ -357,10 +384,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)) {
@@ -458,4 +482,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/src/Property/AtRule.php b/src/Property/AtRule.php
index 49a160a1a..808df79f8 100644
--- a/src/Property/AtRule.php
+++ b/src/Property/AtRule.php
@@ -16,11 +16,19 @@ 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';
+ public const BLOCK_RULES = [
+ 'media',
+ 'document',
+ 'supports',
+ 'region-style',
+ 'font-feature-values',
+ 'container',
+ 'layer',
+ 'scope',
+ 'starting-style',
+ ];
/**
* @return non-empty-string
diff --git a/src/Property/CSSNamespace.php b/src/Property/CSSNamespace.php
index 66164a318..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
@@ -94,4 +96,19 @@ public function atRuleArgs(): array
}
return $result;
}
+
+ /**
+ * @return array|null>
+ *
+ * @internal
+ */
+ public function getArrayRepresentation(): array
+ {
+ 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/src/Property/Charset.php b/src/Property/Charset.php
index c9488ad1e..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
@@ -71,4 +73,17 @@ public function atRuleArgs(): CSSString
{
return $this->charset;
}
+
+ /**
+ * @return array|null>
+ *
+ * @internal
+ */
+ public function getArrayRepresentation(): array
+ {
+ return [
+ 'class' => $this->getShortClassName(),
+ 'charset' => $this->charset->getArrayRepresentation(),
+ ];
+ }
}
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/Property/Import.php b/src/Property/Import.php
index a9dabe979..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
@@ -82,4 +84,19 @@ public function getMediaQuery(): ?string
{
return $this->mediaQuery;
}
+
+ /**
+ * @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/cascade.html#at-import
+ 'uri' => $this->location->getArrayRepresentation(),
+ ];
+ }
}
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..66c061ada 100644
--- a/src/Property/Selector.php
+++ b/src/Property/Selector.php
@@ -4,9 +4,16 @@
namespace Sabberworm\CSS\Property;
+use Sabberworm\CSS\Comment\Comment;
use Sabberworm\CSS\OutputFormat;
-use Sabberworm\CSS\Property\Selector\SpecificityCalculator;
+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\Renderable;
+use Sabberworm\CSS\Settings;
+use Sabberworm\CSS\ShortClassNameProvider;
use function Safe\preg_match;
@@ -16,13 +23,15 @@
*/
class Selector implements Renderable
{
+ use ShortClassNameProvider;
+
/**
- * @var non-empty-string
- *
* @internal since 8.5.2
*/
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\\.:#+>,]++
@@ -49,9 +58,9 @@ class Selector implements Renderable
/ux';
/**
- * @var string
+ * @var non-empty-list
*/
- private $selector;
+ private $components;
/**
* @internal since V8.8.0
@@ -64,19 +73,137 @@ public static function isValid(string $selector): bool
return $numberOfMatches === 1;
}
- public function __construct(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 UnexpectedTokenException if the selector is not valid
+ */
+ final public function __construct($selector)
+ {
+ if (\is_string($selector)) {
+ $this->setSelector($selector);
+ } else {
+ $this->setComponents($selector);
+ }
+ }
+
+ /**
+ * @param list $comments
+ *
+ * @return non-empty-list
+ *
+ * @throws UnexpectedTokenException
+ */
+ 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()`.)
+ // Gobble any up now to get a clean start.
+ $parserState->consumeWhiteSpace($comments);
+
+ $selectorParts = [];
+ while (true) {
+ 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;
+ } else {
+ throw $e;
+ }
+ }
+ try {
+ $selectorParts[] = Combinator::parse($parserState, $comments);
+ } catch (UnexpectedTokenException $e) {
+ // End of selector has been reached.
+ break;
+ }
+ }
+
+ 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(
+ '`,`, `{`, `}` or EOF',
+ $parserState->peek(5),
+ 'literal',
+ $parserState->currentLine()
+ );
+ }
+
+ return new static($selectorParts);
+ }
+
+ /**
+ * @return non-empty-list
+ */
+ public function getComponents(): array
{
- $this->setSelector($selector);
+ 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 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
{
- $this->selector = \trim($selector);
+ $parserState = new ParserState($selector, Settings::create());
+
+ $components = self::parseComponents($parserState);
+
+ // Check that the selector has been fully parsed:
+ if (!$parserState->isEnd()) {
+ throw new UnexpectedTokenException(
+ 'EOF',
+ $parserState->peek(5),
+ 'literal'
+ );
+ }
+
+ $this->components = $components;
}
/**
@@ -84,11 +211,39 @@ 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
+ ));
+ }
+
+ /**
+ * @return array|null>
+ *
+ * @internal
+ */
+ public function getArrayRepresentation(): array
+ {
+ return [
+ 'class' => $this->getShortClassName(),
+ 'components' => \array_map(
+ static function (Component $component): array {
+ return $component->getArrayRepresentation();
+ },
+ $this->components
+ ),
+ ];
}
}
diff --git a/src/Property/Selector/Combinator.php b/src/Property/Selector/Combinator.php
new file mode 100644
index 000000000..15f63e2a7
--- /dev/null
+++ b/src/Property/Selector/Combinator.php
@@ -0,0 +1,119 @@
+`, `+`, or `~`).
+ *
+ * @phpstan-type ValidCombinatorValue ' '|'>'|'+'|'~'
+ */
+class Combinator implements Component
+{
+ 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
+ {
+ $spacing = $outputFormat->getSpaceAroundSelectorCombinator();
+ if ($this->value === ' ') {
+ $rendering = $spacing !== '' ? $spacing : ' ';
+ } else {
+ $rendering = $spacing . $this->value . $spacing;
+ }
+
+ return $rendering;
+ }
+
+ /**
+ * @return array|null>
+ *
+ * @internal
+ */
+ public function getArrayRepresentation(): array
+ {
+ return [
+ 'class' => $this->getShortClassName(),
+ 'value' => $this->value,
+ ];
+ }
+}
diff --git a/src/Property/Selector/Component.php b/src/Property/Selector/Component.php
new file mode 100644
index 000000000..faab5b9a2
--- /dev/null
+++ b/src/Property/Selector/Component.php
@@ -0,0 +1,41 @@
+
+ */
+ public function getSpecificity(): int;
+}
diff --git a/src/Property/Selector/CompoundSelector.php b/src/Property/Selector/CompoundSelector.php
new file mode 100644
index 000000000..80b1670cd
--- /dev/null
+++ b/src/Property/Selector/CompoundSelector.php
@@ -0,0 +1,284 @@
+`, `+`, `~`) before being passed to this class.
+ */
+class CompoundSelector implements 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;
+ $isWithinAttribute = false;
+
+ 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 '[':
+ 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 '}':
+ 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 && !$isWithinAttribute) {
+ 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/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/Renderable.php b/src/Renderable.php
index 9ebf9a9b9..2c3cf0635 100644
--- a/src/Renderable.php
+++ b/src/Renderable.php
@@ -7,4 +7,11 @@
interface Renderable
{
public function render(OutputFormat $outputFormat): string;
+
+ /**
+ * @return array|null>
+ *
+ * @internal
+ */
+ public function getArrayRepresentation(): array;
}
diff --git a/src/Rule/Rule.php b/src/Rule/Rule.php
index 96ef1b4da..82f837dc8 100644
--- a/src/Rule/Rule.php
+++ b/src/Rule/Rule.php
@@ -4,197 +4,20 @@
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 function Safe\preg_match;
-
-/**
- * `Rule`s just have a string key (the rule) and a 'Value'.
- *
- * In CSS, `Rule`s are expressed as follows: “key: value[0][0] value[0][1], value[1][0] value[1][1];”
- */
-class Rule implements Commentable, CSSElement, Positionable
-{
- use CommentContainer;
- use Position;
-
- /**
- * @var non-empty-string
- */
- private $rule;
-
- /**
- * @var RuleValueList|string|null
- */
- private $value;
-
- /**
- * @var bool
- */
- private $isImportant = false;
-
- /**
- * @param non-empty-string $rule
- * @param int<1, max>|null $lineNumber
- * @param int<0, max>|null $columnNumber
- */
- public function __construct(string $rule, ?int $lineNumber = null, ?int $columnNumber = null)
- {
- $this->rule = $rule;
- $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 = \array_merge($commentsBeforeRule, $parserState->consumeWhiteSpace());
- $rule = new Rule(
- $parserState->parseIdentifier(!$parserState->comes('--')),
- $parserState->currentLine(),
- $parserState->currentColumn()
- );
- $rule->setComments($comments);
- $rule->addComments($parserState->consumeWhiteSpace());
- $parserState->consume(':');
- $value = Value::parseValue($parserState, self::listDelimiterForRule($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 $rule
- *
- * @return list
- */
- private static function listDelimiterForRule(string $rule): array
- {
- if (preg_match('/^font($|-)/', $rule) === 1) {
- return [',', '/', ' '];
- }
-
- switch ($rule) {
- case 'src':
- return [' ', ','];
- default:
- return [',', ' ', '/'];
- }
- }
-
- /**
- * @param non-empty-string $rule
- */
- public function setRule(string $rule): void
- {
- $this->rule = $rule;
- }
-
- /**
- * @return non-empty-string
- */
- public function getRule(): string
- {
- return $this->rule;
- }
-
- /**
- * @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->rule}:{$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;
+use Sabberworm\CSS\Property\Declaration;
+
+use function Safe\class_alias;
+
+if (!\class_exists(Rule::class, false) && !\interface_exists(Rule::class, false)) {
+ 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 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
+ * replacement.
+ */
+ class Rule {}
}
}
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 4d0775460..6812cb487 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;
@@ -16,9 +17,10 @@
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;
/**
* This class represents a `RuleSet` constrained by a `Selector`.
@@ -30,13 +32,14 @@
*
* 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 LegacyDeclarationListMethods;
use Position;
/**
- * @var array
+ * @var list
*/
private $selectors = [];
@@ -65,66 +68,15 @@ 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, $list, $comments);
$result->setSelectors($selectors, $list);
if ($parserState->comes('{')) {
$parserState->consume(1);
}
} catch (UnexpectedTokenException $e) {
if ($parserState->getSettings()->usesLenientParsing()) {
- if (!$parserState->comes('}')) {
- $parserState->consumeUntil('}', false, true);
+ if (!$parserState->consumeIfComes('}')) {
+ $parserState->consumeUntil(['}', ParserState::EOF], false, true);
}
return null;
} else {
@@ -146,11 +98,29 @@ 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);
+ // A string of comma-separated selectors requires parsing.
+ try {
+ $parserState = new ParserState($selectors, Settings::create());
+ $selectorsToSet = self::parseSelectors($parserState, $list);
+ 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'
+ );
+ }
}
- 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 +130,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 +139,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 +168,7 @@ public function removeSelector($selectorToRemove): bool
}
/**
- * @return array
+ * @return list
*/
public function getSelectors(): array
{
@@ -208,65 +181,65 @@ public function getRuleSet(): RuleSet
}
/**
- * @see RuleSet::addRule()
+ * @see RuleSet::addDeclaration()
*/
- public function addRule(Rule $ruleToAdd, ?Rule $sibling = null): void
+ public function addDeclaration(Declaration $declarationToAdd, ?Declaration $sibling = null): void
{
- $this->ruleSet->addRule($ruleToAdd, $sibling);
+ $this->ruleSet->addDeclaration($declarationToAdd, $sibling);
}
/**
- * @see RuleSet::getRules()
+ * @return array, Declaration>
*
- * @return array, Rule>
+ * @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);
}
/**
- * @see RuleSet::setRules()
+ * @param array $declarations
*
- * @param array $rules
+ * @see RuleSet::setDeclarations()
*/
- public function setRules(array $rules): void
+ public function setDeclarations(array $declarations): void
{
- $this->ruleSet->setRules($rules);
+ $this->ruleSet->setDeclarations($declarations);
}
/**
- * @see RuleSet::getRulesAssoc()
+ * @return array
*
- * @return array
+ * @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(Rule $ruleToRemove): void
+ public function removeDeclaration(Declaration $declarationToRemove): void
{
- $this->ruleSet->removeRule($ruleToRemove);
+ $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();
}
/**
@@ -298,4 +271,36 @@ public function render(OutputFormat $outputFormat): string
return $result;
}
+
+ /**
+ * @return array|null>
+ *
+ * @internal
+ */
+ public function getArrayRepresentation(): array
+ {
+ throw new \BadMethodCallException('`getArrayRepresentation` is not yet implemented for `' . self::class . '`');
+ }
+
+ /**
+ * @param list $comments
+ *
+ * @return list
+ *
+ * @throws UnexpectedTokenException
+ */
+ private static function parseSelectors(ParserState $parserState, ?CSSList $list, array &$comments = []): array
+ {
+ $selectorClass = $list instanceof KeyFrame ? KeyFrameSelector::class : Selector::class;
+ $selectors = [];
+
+ while (true) {
+ $selectors[] = $selectorClass::parse($parserState, $comments);
+ if (!$parserState->consumeIfComes(',')) {
+ break;
+ }
+ }
+
+ return $selectors;
+ }
}
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/LegacyDeclarationListMethods.php b/src/RuleSet/LegacyDeclarationListMethods.php
new file mode 100644
index 000000000..2eb82692b
--- /dev/null
+++ b/src/RuleSet/LegacyDeclarationListMethods.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 0c6c5936c..01da5d3ce 100644
--- a/src/RuleSet/RuleContainer.php
+++ b/src/RuleSet/RuleContainer.php
@@ -4,33 +4,17 @@
namespace Sabberworm\CSS\RuleSet;
-use Sabberworm\CSS\Rule\Rule;
-
-/**
- * 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 removeRule(Rule $ruleToRemove): void;
-
- public function removeMatchingRules(string $searchPattern): void;
-
- public function removeAllRules(): void;
-
- /**
- * @param array $rules
- */
- public function setRules(array $rules): void;
-
- /**
- * @return array, Rule>
- */
- public function getRules(?string $searchPattern = null): array;
-
- /**
- * @return array
- */
- public function getRulesAssoc(?string $searchPattern = null): array;
+use function Safe\class_alias;
+
+if (!\class_exists(RuleContainer::class, false) && !\interface_exists(RuleContainer::class, false)) {
+ 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 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.
+ */
+ interface RuleContainer {}
+ }
}
diff --git a/src/RuleSet/RuleSet.php b/src/RuleSet/RuleSet.php
index 521a6ae9b..137e8fd45 100644
--- a/src/RuleSet/RuleSet.php
+++ b/src/RuleSet/RuleSet.php
@@ -13,31 +13,32 @@
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 `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
+class RuleSet implements CSSElement, CSSListItem, Positionable, DeclarationList
{
use CommentContainer;
+ use LegacyDeclarationListMethods;
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, Rule>>
+ * @var array, Declaration>>
*/
- private $rules = [];
+ private $declarations = [];
/**
* @param int<1, max>|null $lineNumber
@@ -59,14 +60,15 @@ public static function parseRuleSet(ParserState $parserState, RuleSet $ruleSet):
$parserState->consume(';');
}
while (true) {
- $commentsBeforeRule = $parserState->consumeWhiteSpace();
+ $commentsBeforeDeclaration = [];
+ $parserState->consumeWhiteSpace($commentsBeforeDeclaration);
if ($parserState->comes('}')) {
break;
}
- $rule = null;
+ $declaration = null;
if ($parserState->getSettings()->usesLenientParsing()) {
try {
- $rule = Rule::parse($parserState, $commentsBeforeRule);
+ $declaration = Declaration::parse($parserState, $commentsBeforeDeclaration);
} catch (UnexpectedTokenException $e) {
try {
$consumedText = $parserState->consumeUntil(["\n", ';', '}'], true);
@@ -84,10 +86,10 @@ public static function parseRuleSet(ParserState $parserState, RuleSet $ruleSet):
}
}
} else {
- $rule = Rule::parse($parserState, $commentsBeforeRule);
+ $declaration = Declaration::parse($parserState, $commentsBeforeDeclaration);
}
- if ($rule instanceof Rule) {
- $ruleSet->addRule($rule);
+ if ($declaration instanceof Declaration) {
+ $ruleSet->addDeclaration($declaration);
}
}
$parserState->consume('}');
@@ -95,31 +97,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 addDeclaration(Declaration $declarationToAdd, ?Declaration $sibling = null): void
{
- $propertyName = $ruleToAdd->getRule();
- if (!isset($this->rules[$propertyName])) {
- $this->rules[$propertyName] = [];
+ $propertyName = $declarationToAdd->getPropertyName();
+ 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);
+ $siblingIsInSet = $this->hasDeclaration($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;
}
@@ -127,69 +129,71 @@ public function addRule(Rule $ruleToAdd, ?Rule $sibling = null): void
}
}
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->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->getDeclarations();
+ $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 addDeclaration',
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
+ * Returns all declarations matching the given property name
*
- * @example $ruleSet->getRules('font') // returns array(0 => $rule, …) or array().
+ * @example $ruleSet->getDeclarations('font') // returns array(0 => $declaration, …) or array().
*
- * @example $ruleSet->getRules('font-')
- * //returns an array of all rules either beginning with font- or matching font.
+ * @example $ruleSet->getDeclarations('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, Rule>
+ * @return array, Declaration>
*/
- public function getRules(?string $searchPattern = null): array
+ public function getDeclarations(?string $searchPattern = null): array
{
$result = [];
- foreach ($this->rules as $propertyName => $rules) {
- // 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.
+ foreach ($this->declarations as $propertyName => $declarations) {
+ // 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
|| (
@@ -198,7 +202,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']);
@@ -207,89 +211,90 @@ public function getRules(?string $searchPattern = null): array
}
/**
- * Overrides all the rules of this set.
+ * Overrides all the declarations of this set.
*
- * @param array $rules The rules to override with.
+ * @param array $declarations
*/
- public function setRules(array $rules): void
+ public function setDeclarations(array $declarations): void
{
- $this->rules = [];
- foreach ($rules as $rule) {
- $this->addRule($rule);
+ $this->declarations = [];
+ foreach ($declarations as $declaration) {
+ $this->addDeclaration($declaration);
}
}
/**
- * 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
+ * @return array
*/
- public function getRulesAssoc(?string $searchPattern = null): array
+ public function getDeclarationsAssociative(?string $searchPattern = null): array
{
- /** @var array $result */
+ /** @var array $result */
$result = [];
- foreach ($this->getRules($searchPattern) as $rule) {
- $result[$rule->getRule()] = $rule;
+ foreach ($this->getDeclarations($searchPattern) as $declaration) {
+ $result[$declaration->getPropertyName()] = $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 removeDeclaration(Declaration $declarationToRemove): void
{
- $nameOfPropertyToRemove = $ruleToRemove->getRule();
- if (!isset($this->rules[$nameOfPropertyToRemove])) {
+ $nameOfPropertyToRemove = $declarationToRemove->getPropertyName();
+ 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]);
}
}
}
/**
- * 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
+ public function removeMatchingDeclarations(string $searchPattern): void
{
- foreach ($this->rules as $propertyName => $rules) {
- // 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).
+ foreach ($this->declarations as $propertyName => $declarations) {
+ // 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('-')
&& (\strpos($propertyName, $searchPattern) === 0
|| $propertyName === \substr($searchPattern, 0, -1)))
) {
- unset($this->rules[$propertyName]);
+ unset($this->declarations[$propertyName]);
}
}
}
- public function removeAllRules(): void
+ public function removeAllDeclarations(): void
{
- $this->rules = [];
+ $this->declarations = [];
}
/**
@@ -297,20 +302,22 @@ 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 $rule) {
+ foreach ($this->getDeclarations() as $declaration) {
$nextLevelFormatter = $nextLevelFormat->getFormatter();
- $renderedRule = $nextLevelFormatter->safely(static function () use ($rule, $nextLevelFormat): string {
- return $rule->render($nextLevelFormat);
- });
- if ($renderedRule === null) {
+ $renderedDeclaration = $nextLevelFormatter->safely(
+ static function () use ($declaration, $nextLevelFormat): string {
+ return $declaration->render($nextLevelFormat);
+ }
+ );
+ if ($renderedDeclaration === null) {
continue;
}
if ($isFirst) {
@@ -319,7 +326,7 @@ protected function renderRules(OutputFormat $outputFormat): string
} else {
$result .= $nextLevelFormatter->spaceBetweenRules();
}
- $result .= $renderedRule;
+ $result .= $renderedDeclaration;
}
$formatter = $outputFormat->getFormatter();
@@ -331,6 +338,16 @@ protected function renderRules(OutputFormat $outputFormat): string
return $formatter->removeLastSemicolon($result);
}
+ /**
+ * @return array|null>
+ *
+ * @internal
+ */
+ 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
*
@@ -342,7 +359,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
);
}
@@ -352,7 +369,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
);
}
@@ -362,10 +379,10 @@ private static function comparePositionable(Positionable $first, Positionable $s
return $firstsLineNumber - $secondsLineNumber;
}
- private function hasRule(Rule $rule): bool
+ private function hasDeclaration(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;
}
}
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/src/Value/CSSFunction.php b/src/Value/CSSFunction.php
index 86b56d9b1..30228340a 100644
--- a/src/Value/CSSFunction.php
+++ b/src/Value/CSSFunction.php
@@ -113,4 +113,20 @@ public function render(OutputFormat $outputFormat): string
$arguments = parent::render($outputFormat);
return "{$this->name}({$arguments})";
}
+
+ /**
+ * @return array|null>
+ *
+ * @internal
+ */
+ public function getArrayRepresentation(): array
+ {
+ return \array_merge(
+ [
+ 'class' => 'placeholder',
+ 'name' => $this->name,
+ ],
+ parent::getArrayRepresentation()
+ );
+ }
}
diff --git a/src/Value/CSSString.php b/src/Value/CSSString.php
index 569311d77..8271671f2 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
*/
@@ -90,8 +93,33 @@ public function getString(): string
*/
public function render(OutputFormat $outputFormat): string
{
- $string = \addslashes($this->string);
- $string = \str_replace("\n", '\\A', $string);
- return $outputFormat->getStringQuotingType() . $string . $outputFormat->getStringQuotingType();
+ return $outputFormat->getStringQuotingType()
+ . $this->escape($this->string, $outputFormat)
+ . $outputFormat->getStringQuotingType();
+ }
+
+ /**
+ * @return array|null>
+ *
+ * @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,
+ ];
+ }
+
+ private function escape(string $string, OutputFormat $outputFormat): string
+ {
+ $charactersToEscape = '\\';
+ $charactersToEscape .= ($outputFormat->getStringQuotingType() === '"' ? '"' : "'");
+ $withEscapedQuotes = \addcslashes($string, $charactersToEscape);
+
+ $withNewlineEncoded = \str_replace("\n", '\\A', $withEscapedQuotes);
+
+ return $withNewlineEncoded;
}
}
diff --git a/src/Value/CalcFunction.php b/src/Value/CalcFunction.php
index dba6e1dd9..3d85b22a7 100644
--- a/src/Value/CalcFunction.php
+++ b/src/Value/CalcFunction.php
@@ -8,16 +8,11 @@
use Sabberworm\CSS\Parsing\UnexpectedEOFException;
use Sabberworm\CSS\Parsing\UnexpectedTokenException;
+use function Safe\preg_match;
+
class CalcFunction extends CSSFunction
{
- /**
- * @var int
- */
private const T_OPERAND = 1;
-
- /**
- * @var int
- */
private const T_OPERATOR = 2;
/**
@@ -67,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()} ",
@@ -102,4 +96,14 @@ public static function parse(ParserState $parserState, bool $ignoreCase = false)
}
return new CalcFunction($function, $list, ',', $parserState->currentLine());
}
+
+ /**
+ * @return array|null>
+ *
+ * @internal
+ */
+ public function getArrayRepresentation(): array
+ {
+ throw new \BadMethodCallException('`getArrayRepresentation` is not yet implemented for `' . self::class . '`');
+ }
}
diff --git a/src/Value/CalcRuleValueList.php b/src/Value/CalcRuleValueList.php
index f904f12c7..5396dfdd5 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|null>
+ *
+ * @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..df491692c 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|null>
+ *
+ * @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/src/Value/LineName.php b/src/Value/LineName.php
index 763cc48ea..7b7690f88 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|null>
+ *
+ * @internal
+ */
+ public function getArrayRepresentation(): array
+ {
+ throw new \BadMethodCallException('`getArrayRepresentation` is not yet implemented for `' . self::class . '`');
+ }
}
diff --git a/src/Value/Size.php b/src/Value/Size.php
index eac736d79..ad6d794cc 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,10 +18,10 @@
*/
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)
- *
- * @var list
*/
private const ABSOLUTE_SIZE_UNITS = [
'px',
@@ -40,14 +41,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'];
/**
@@ -210,4 +205,19 @@ public function render(OutputFormat $outputFormat): string
return preg_replace(["/$decimalPoint/", '/^(-?)0\\./'], ['.', '$1.'], $size) . ($this->unit ?? '');
}
+
+ /**
+ * @return array|null>
+ *
+ * @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/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/src/Value/Value.php b/src/Value/Value.php
index 263c420da..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
@@ -181,6 +183,18 @@ public static function parsePrimitiveValue(ParserState $parserState)
return $value;
}
+ /**
+ * @return array|null>
+ *
+ * @internal
+ */
+ public function getArrayRepresentation(): array
+ {
+ return [
+ 'class' => $this->getShortClassName(),
+ ];
+ }
+
/**
* @throws UnexpectedEOFException
* @throws UnexpectedTokenException
diff --git a/src/Value/ValueList.php b/src/Value/ValueList.php
index 6f85f895e..9490a8bd1 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|null>
+ *
+ * @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/CSSList/AtRuleBlockListTest.php b/tests/CSSList/AtRuleBlockListTest.php
index 9a725b212..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
{
@@ -48,12 +48,35 @@ 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); } }',
+ ],
];
}
/**
* @test
*
+ * @param non-empty-string $css
+ *
* @dataProvider provideMinWidthMediaRule
*/
public function parsesRuleNameOfMediaQueries(string $css): void
@@ -68,6 +91,8 @@ public function parsesRuleNameOfMediaQueries(string $css): void
/**
* @test
*
+ * @param non-empty-string $css
+ *
* @dataProvider provideMinWidthMediaRule
*/
public function parsesArgumentsOfMediaQueries(string $css): void
@@ -82,6 +107,8 @@ public function parsesArgumentsOfMediaQueries(string $css): void
/**
* @test
*
+ * @param non-empty-string $css
+ *
* @dataProvider provideMinWidthMediaRule
* @dataProvider provideSyntacticallyCorrectAtRule
*/
@@ -91,4 +118,115 @@ 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
+ *
+ * @param non-empty-string $css
+ * @param non-empty-string $expectedName
+ * @param int<0, max> $expectedContentCount
+ *
+ * @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 non-empty-string $css
+ * @param non-empty-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/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()));
}
}
diff --git a/tests/Functional/RuleSet/DeclarationBlockTest.php b/tests/Functional/RuleSet/DeclarationBlockTest.php
index fe6b9e51c..4960c4d8a 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->addDeclaration($declaration1);
- $rule2 = new Rule('background');
- $rule2->setValue('#222');
- $declarationBlock->addRule($rule2);
+ $declaration2 = new Declaration('background');
+ $declaration2->setValue('#222');
+ $declarationBlock->addDeclaration($declaration2);
- $rule3 = new Rule('background-color');
- $rule3->setValue('#fff');
- $declarationBlock->addRule($rule3);
+ $declaration3 = new Declaration('background-color');
+ $declaration3->setValue('#fff');
+ $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 b0d976055..5d637b62f 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->setDeclarations($declarationsToSet);
}
}
diff --git a/tests/Functional/Value/ValueTest.php b/tests/Functional/Value/ValueTest.php
index 4d65ee2a8..aa9ab037b 100644
--- a/tests/Functional/Value/ValueTest.php
+++ b/tests/Functional/Value/ValueTest.php
@@ -19,9 +19,7 @@ final class ValueTest extends TestCase
/**
* the default set of delimiters for parsing most values
*
- * @see \Sabberworm\CSS\Rule\Rule::listDelimiterForRule
- *
- * @var list
+ * @see \Sabberworm\CSS\Property\Declaration::getDelimitersForPropertyValue
*/
private const DEFAULT_DELIMITERS = [',', ' ', '/'];
diff --git a/tests/OutputFormatTest.php b/tests/OutputFormatTest.php
index 3a8deb30e..dce67db92 100644
--- a/tests/OutputFormatTest.php
+++ b/tests/OutputFormatTest.php
@@ -15,25 +15,18 @@
*/
final class OutputFormatTest extends TestCase
{
- /**
- * @var string
- */
- private const TEST_CSS = <<document->render()
);
}
@@ -90,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(' '))
);
}
@@ -102,8 +95,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(' ')
@@ -122,9 +115,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"))
);
}
@@ -135,8 +128,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("'"))
);
}
@@ -147,8 +140,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))
);
}
@@ -159,8 +152,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))
);
}
@@ -171,8 +164,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"))
);
}
@@ -187,15 +180,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)
+ );
}
/**
@@ -208,12 +204,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)
+ );
}
/**
@@ -229,19 +227,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)
+ );
}
/**
@@ -273,19 +273,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)
+ );
}
/**
@@ -297,8 +299,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)
);
}
@@ -316,8 +318,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');
diff --git a/tests/ParserTest.php b/tests/ParserTest.php
index b80280a77..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->getRules('color');
- $colorRuleValue = $colorRules[0]->getValue();
- self::assertSame('red', $colorRuleValue);
- $colorRules = $declarationBlock->getRules('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->getRules('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->getRules('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->getRules('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->getRules('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) {
@@ -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()
@@ -182,40 +179,40 @@ public function unicodeParsing(): void
if (\substr($selector, 0, \strlen('.test-')) !== '.test-') {
continue;
}
- $contentRules = $declarationBlock->getRules('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);
}
}
}
@@ -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))
);
@@ -346,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;'
@@ -355,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);}
@@ -367,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->getRules('background-');
- self::assertCount(2, $backgroundHeaderRules);
- self::assertSame('background-color', $backgroundHeaderRules[0]->getRule());
- self::assertSame('background-color', $backgroundHeaderRules[1]->getRule());
- $backgroundHeaderRules = $headerBlock->getRulesAssoc('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-');
- 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());
}
/**
@@ -403,20 +372,20 @@ public function slashedValues(): void
}
}
foreach ($document->getAllDeclarationBlocks() as $declarationBlock) {
- $fontRules = $declarationBlock->getRules('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->getRules('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];
@@ -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());
@@ -570,11 +535,10 @@ public function selectorRemoval(): void
public function comments(): void
{
$document = self::parsedStructureForFile('comments');
- $expected = <<render());
}
@@ -719,6 +683,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());
@@ -728,6 +693,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());
}
@@ -741,15 +707,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());
}
/**
@@ -769,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());
}
@@ -786,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());
}
@@ -820,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());
}
@@ -931,11 +883,11 @@ public function missingPropertyValueLenient(): void
$block = $declarationBlocks[0];
self::assertInstanceOf(DeclarationBlock::class, $block);
self::assertEquals([new Selector('div')], $block->getSelectors());
- $rules = $block->getRules();
- self::assertCount(1, $rules);
- $rule = $rules[0];
- self::assertSame('display', $rule->getRule());
- 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());
}
/**
@@ -991,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->getRules();
+ $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();
}
@@ -1049,18 +1001,17 @@ 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.
+ // Declarations.
self::assertInstanceOf(DeclarationBlock::class, $fooBarBlock);
- $fooBarRules = $fooBarBlock->getRules();
- $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]);
@@ -1075,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]->getRules();
- $fooBarChildComments = $fooBarRules[0]->getComments();
+ $fooBarDeclarations = $mediaRules[0]->getDeclarations();
+ $fooBarChildComments = $fooBarDeclarations[0]->getComments();
self::assertCount(1, $fooBarChildComments);
self::assertSame('* Number 10b *', $fooBarChildComments[0]->getComment());
}
@@ -1093,8 +1044,8 @@ public function flatCommentExtractingOneComment(): void
$contents = $document->getContents();
self::assertInstanceOf(DeclarationBlock::class, $contents[0]);
- $divRules = $contents[0]->getRules();
- $comments = $divRules[0]->getComments();
+ $divDeclarations = $contents[0]->getDeclarations();
+ $comments = $divDeclarations[0]->getComments();
self::assertCount(1, $comments);
self::assertSame('Find Me!', $comments[0]->getComment());
@@ -1103,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]->getRules();
- $comments = $divRules[0]->getComments();
+ $divDeclarations = $contents[0]->getDeclarations();
+ $comments = $divDeclarations[0]->getComments();
self::assertCount(2, $comments);
self::assertSame('Find Me!', $comments[0]->getComment());
@@ -1121,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]->getRules();
- $comments = $divRules[0]->getComments();
+ $divDeclarations = $contents[0]->getDeclarations();
+ $comments = $divDeclarations[0]->getComments();
self::assertCount(2, $comments);
self::assertSame('Find Me!', $comments[0]->getComment());
@@ -1139,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]->getRules();
- $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());
}
/**
@@ -1231,10 +1182,10 @@ public function escapedSpecialCaseTokens(): void
$document = self::parsedStructureForFile('escaped-tokens');
$contents = $document->getContents();
self::assertInstanceOf(RuleSet::class, $contents[0]);
- $rules = $contents[0]->getRules();
- $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 63411a646..f3e4a9bf3 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;
@@ -20,30 +20,30 @@ 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);
$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]);
+ self::assertCount(2, $wrapper->getDeclarations());
+ $wrapper->setDeclarations([$declaration]);
- $rules = $wrapper->getRules();
- self::assertCount(1, $rules);
- self::assertSame('right', $rules[0]->getRule());
- self::assertSame('-10px', $rules[0]->getValue());
+ $declarations = $wrapper->getDeclarations();
+ self::assertCount(1, $declarations);
+ self::assertSame('right', $declarations[0]->getPropertyName());
+ 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->getDeclarations('left');
+ self::assertCount(1, $leftDeclarations);
+ $firstLeftDeclaration = $leftDeclarations[0];
- $textRules = $wrapper->getRules('text-');
- self::assertCount(1, $textRules);
- $firstTextRule = $textRules[0];
+ $textDeclarations = $wrapper->getDeclarations('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->addDeclaration($borderBottomDeclaration);
+ $wrapper->addDeclaration($leftPrefixDeclaration, $firstLeftDeclaration);
+ $wrapper->addDeclaration($textAlignDeclaration, $firstTextDeclaration);
- $rules = $wrapper->getRules();
+ $declarations = $wrapper->getDeclarations();
- 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 {
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..fc584541b 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->addDeclaration($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->addDeclaration($declaration1);
+ $declaration2 = new Declaration('color');
+ $declaration2->setValue($value2);
+ $declarationBlock->addDeclaration($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->addDeclaration($declaration1);
$declarationBlock2 = new DeclarationBlock();
- $rule2 = new Rule('color');
- $rule2->setValue($value2);
- $declarationBlock2->addRule($rule2);
+ $declaration2 = new Declaration('color');
+ $declaration2->setValue($value2);
+ $declarationBlock2->addDeclaration($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->addDeclaration($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->addDeclaration($declaration1);
$declarationBlock2 = new DeclarationBlock();
- $rule2 = new Rule('color');
- $rule2->setValue($value2);
- $declarationBlock2->addRule($rule2);
+ $declaration2 = new Declaration('color');
+ $declaration2->setValue($value2);
+ $declarationBlock2->addDeclaration($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->addDeclaration($declaration1);
+ $declaration2 = new Declaration('color');
+ $declaration2->setValue($value2);
+ $declarationBlock->addDeclaration($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->addDeclaration($declaration);
$subject->setContents([$declarationBlock]);
$result = $subject->getAllValues();
@@ -462,13 +462,25 @@ 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->addDeclaration($declaration);
$subject->setContents([$declarationBlock]);
$result = $subject->getAllValues(null, null, true);
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/CSSListTest.php b/tests/Unit/CSSList/CSSListTest.php
index ada176e9a..3ab8d19f1 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;
/**
@@ -239,6 +244,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
*/
@@ -326,4 +347,49 @@ 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()));
+ }
+
+ /**
+ * @test
+ */
+ public function getArrayRepresentationThrowsException(): void
+ {
+ $this->expectException(\BadMethodCallException::class);
+
+ $subject = new ConcreteCSSList();
+
+ $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/Comment/CommentTest.php b/tests/Unit/Comment/CommentTest.php
index 69572e903..9c2d1e9a1 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();
@@ -77,4 +77,29 @@ public function getLineNumberReturnsLineNumberProvidedToConstructor(): void
self::assertSame($lineNumber, $subject->getLineNumber());
}
+
+ /**
+ * @test
+ */
+ public function getArrayRepresentationIncludesClassName(): void
+ {
+ $subject = new Comment();
+
+ $result = $subject->getArrayRepresentation();
+
+ self::assertSame('Comment', $result['class']);
+ }
+
+ /**
+ * @test
+ */
+ public function getArrayRepresentationIncludesContents(): void
+ {
+ $contents = 'War. War never changes.';
+ $subject = new Comment($contents);
+
+ $result = $subject->getArrayRepresentation();
+
+ self::assertSame($contents, $result['contents']);
+ }
}
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/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/Parsing/ParserStateTest.php b/tests/Unit/Parsing/ParserStateTest.php
new file mode 100644
index 000000000..ebc3c7fa7
--- /dev/null
+++ b/tests/Unit/Parsing/ParserStateTest.php
@@ -0,0 +1,306 @@
+
+ * }
+ * >
+ */
+ 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);
+ }
+
+ /**
+ * @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());
+ }
+
+ /**
+ * @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));
+ }
+}
diff --git a/tests/Unit/Property/AtRuleTest.php b/tests/Unit/Property/AtRuleTest.php
new file mode 100644
index 000000000..85407745f
--- /dev/null
+++ b/tests/Unit/Property/AtRuleTest.php
@@ -0,0 +1,35 @@
+subject);
}
+
+ /**
+ * @test
+ */
+ public function getArrayRepresentationIncludesClassName(): void
+ {
+ $subject = new CSSNamespace(new CSSString('http://www.w3.org/2000/svg'));
+
+ $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']);
+ }
}
diff --git a/tests/Unit/Property/CharsetTest.php b/tests/Unit/Property/CharsetTest.php
index e0645f5ef..bdbc1994a 100644
--- a/tests/Unit/Property/CharsetTest.php
+++ b/tests/Unit/Property/CharsetTest.php
@@ -31,4 +31,33 @@ public function implementsCSSListItem(): void
{
self::assertInstanceOf(CSSListItem::class, $this->subject);
}
+
+ /**
+ * @test
+ */
+ public function getArrayRepresentationIncludesClassName(): void
+ {
+ $result = $this->subject->getArrayRepresentation();
+
+ self::assertSame('Charset', $result['class']);
+ }
+
+ /**
+ * @test
+ */
+ public function getArrayRepresentationIncludesCharset(): void
+ {
+ $charset = 'iso-8859-15';
+ $subject = new Charset(new CSSString($charset));
+
+ $result = $subject->getArrayRepresentation();
+
+ self::assertSame(
+ [
+ 'class' => 'CSSString',
+ 'contents' => $charset,
+ ],
+ $result['charset']
+ );
+ }
}
diff --git a/tests/Unit/Rule/RuleTest.php b/tests/Unit/Property/DeclarationTest.php
similarity index 63%
rename from tests/Unit/Rule/RuleTest.php
rename to tests/Unit/Property/DeclarationTest.php
index 008bcfc18..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);
@@ -70,4 +70,16 @@ static function ($component): string {
self::assertSame($expectedTypeClassnames, $actualClassnames);
}
+
+ /**
+ * @test
+ */
+ public function getArrayRepresentationThrowsException(): void
+ {
+ $this->expectException(\BadMethodCallException::class);
+
+ $subject = new Declaration('todo');
+
+ $subject->getArrayRepresentation();
+ }
}
diff --git a/tests/Unit/Property/ImportTest.php b/tests/Unit/Property/ImportTest.php
index 4ec028e3f..77a4f1362 100644
--- a/tests/Unit/Property/ImportTest.php
+++ b/tests/Unit/Property/ImportTest.php
@@ -32,4 +32,40 @@ public function implementsCSSListItem(): void
{
self::assertInstanceOf(CSSListItem::class, $this->subject);
}
+
+ /**
+ * @test
+ */
+ public function getArrayRepresentationIncludesClassName(): void
+ {
+ $subject = new Import(new URL(new CSSString('https://example.org/')), null);
+
+ $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']
+ );
+ }
}
diff --git a/tests/Unit/Property/KeyframeSelectorTest.php b/tests/Unit/Property/KeyframeSelectorTest.php
new file mode 100644
index 000000000..e73bf2323
--- /dev/null
+++ b/tests/Unit/Property/KeyframeSelectorTest.php
@@ -0,0 +1,40 @@
+getArrayRepresentation();
+
+ 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/Selector/CombinatorTest.php b/tests/Unit/Property/Selector/CombinatorTest.php
new file mode 100644
index 000000000..e416678d1
--- /dev/null
+++ b/tests/Unit/Property/Selector/CombinatorTest.php
@@ -0,0 +1,347 @@
+'));
+ }
+
+ /**
+ * @test
+ */
+ public function implementsSelectorComponent(): void
+ {
+ self::assertInstanceOf(Component::class, new Combinator('>'));
+ }
+
+ /**
+ * @return array
+ */
+ public static function provideValidValueAndDefaultRendering(): 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 provideValidValueAndDefaultRendering
+ */
+ 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 provideValidValueAndDefaultRendering
+ */
+ 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 provideValidValueAndDefaultRendering
+ */
+ 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 provideValidValueAndDefaultRendering
+ */
+ 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 provideValidValueAndDefaultRendering
+ */
+ 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 provideValidValueAndDefaultRendering
+ */
+ 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 provideValidValueAndDefaultRendering
+ */
+ 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 provideValidValueAndDefaultRendering
+ */
+ public function getValueReturnsValueProvidedToConstructor(string $value): void
+ {
+ $subject = new Combinator($value);
+
+ $result = $subject->getValue();
+
+ self::assertSame($value, $result);
+ }
+
+ /**
+ * @test
+ *
+ * @param ValidCombinatorValue $value
+ *
+ * @dataProvider provideValidValueAndDefaultRendering
+ */
+ public function getValueReturnsValueProvidedToSetValue(string $value): void
+ {
+ $subject = new Combinator('>');
+ $subject->setValue($value);
+
+ $result = $subject->getValue();
+
+ self::assertSame($value, $result);
+ }
+
+ /**
+ * @test
+ *
+ * @param ValidCombinatorValue $value
+ *
+ * @dataProvider provideValidValueAndDefaultRendering
+ */
+ public function hasNoSpecificity(string $value): void
+ {
+ $subject = new Combinator($value);
+
+ self::assertSame(0, $subject->getSpecificity());
+ }
+
+ /**
+ * @test
+ *
+ * @param ValidCombinatorValue $value
+ * @param non-empty-string $expectedRendering
+ *
+ * @dataProvider provideValidValueAndDefaultRendering
+ */
+ public function renderReturnsDefaultRenderingForValueProvided(string $value, string $expectedRendering): void
+ {
+ $subject = new Combinator($value);
+
+ 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
+ */
+ public function getArrayRepresentationIncludesClassName(): void
+ {
+ $subject = new Combinator('>');
+
+ $result = $subject->getArrayRepresentation();
+
+ self::assertSame('Combinator', $result['class']);
+ }
+
+ /**
+ * @test
+ *
+ * @param ValidCombinatorValue $value
+ *
+ * @dataProvider provideValidValueAndDefaultRendering
+ */
+ public function getArrayRepresentationIncludesValue(string $value): void
+ {
+ $subject = new Combinator($value);
+
+ $result = $subject->getArrayRepresentation();
+
+ self::assertSame($value, $result['value']);
+ }
+}
diff --git a/tests/Unit/Property/Selector/CompoundSelectorTest.php b/tests/Unit/Property/Selector/CompoundSelectorTest.php
new file mode 100644
index 000000000..b42c76f15
--- /dev/null
+++ b/tests/Unit/Property/Selector/CompoundSelectorTest.php
@@ -0,0 +1,517 @@
+}>
+ */
+ 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],
+ // 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],
+ // 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],
+ ];
+ }
+
+ /**
+ * @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\']'],
+ 'attribute value with extra `[`' => ['a[[href="#top"]'],
+ 'attribute value with extra `]`' => ['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']);
+ }
+}
diff --git a/tests/Unit/Property/SelectorTest.php b/tests/Unit/Property/SelectorTest.php
index e53e8274d..d22190da7 100644
--- a/tests/Unit/Property/SelectorTest.php
+++ b/tests/Unit/Property/SelectorTest.php
@@ -5,8 +5,16 @@
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\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;
/**
* @covers \Sabberworm\CSS\Property\Selector
@@ -48,24 +56,416 @@ public function setSelectorOverwritesSelectorProvidedToConstructor(): void
}
/**
- * @return array}>
+ * @return array}>
*/
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],
+ ];
+ }
+
+ /**
+ * @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
+ *
+ * @param non-empty-string $selector
+ *
+ * @dataProvider provideSelectorsAndSpecificities
+ * @dataProvider provideSelectorsWithEscapedQuotes
+ */
+ public function parsesValidSelector(string $selector): void
+ {
+ $result = Selector::parse(new ParserState($selector, Settings::create()));
+
+ self::assertInstanceOf(Selector::class, $result);
+ 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
+ */
+ public static function provideInvalidSelectors(): 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' => ['<'],
+ ];
+ }
+
+ /**
+ * @return array
+ */
+ public static function provideInvalidSelectorsForParse(): array
+ {
+ return [
+ '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
+ *
+ * @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
+ *
+ * @dataProvider provideInvalidSelectors
+ * @dataProvider provideInvalidSelectorsForParse
+ */
+ public function constructorThrowsExceptionWithInvalidSelector(string $selector): void
+ {
+ $this->expectException(UnexpectedTokenException::class);
+
+ new Selector($selector);
+ }
+
+ /**
+ * @test
+ *
+ * @dataProvider provideInvalidSelectors
+ * @dataProvider provideInvalidSelectorsForParse
+ */
+ public function setSelectorThrowsExceptionWithInvalidSelector(string $selector): void
+ {
+ $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
*
@@ -106,6 +506,7 @@ public function getSpecificityReturnsSpecificityOfSelectorLastProvidedViaSetSele
* @test
*
* @dataProvider provideSelectorsAndSpecificities
+ * @dataProvider provideSelectorsWithEscapedQuotes
*/
public function isValidForValidSelectorReturnsTrue(string $selector): void
{
@@ -113,32 +514,83 @@ public function isValidForValidSelectorReturnsTrue(string $selector): void
}
/**
- * @return array
+ * @test
+ *
+ * @dataProvider provideInvalidSelectors
*/
- public static function provideInvalidSelectors(): array
+ public function isValidForInvalidSelectorReturnsFalse(string $selector): void
{
- 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"],
- ];
+ self::assertFalse(Selector::isValid($selector));
}
/**
* @test
- *
- * @dataProvider provideInvalidSelectors
*/
- public function isValidForInvalidSelectorReturnsFalse(string $selector): void
+ public function cleansUpSpacesWithinSelector(): void
{
- self::assertFalse(Selector::isValid($selector));
+ $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());
+ }
+
+ /**
+ * @test
+ */
+ public function getArrayRepresentationIncludesClassName(): void
+ {
+ $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']);
}
}
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/RuleSet/DeclarationBlockTest.php b/tests/Unit/RuleSet/DeclarationBlockTest.php
index 4b20e9fc8..39c40592d 100644
--- a/tests/Unit/RuleSet/DeclarationBlockTest.php
+++ b/tests/Unit/RuleSet/DeclarationBlockTest.php
@@ -5,12 +5,14 @@
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;
+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;
@@ -21,7 +23,7 @@
*/
final class DeclarationBlockTest extends TestCase
{
- use RuleContainerTest;
+ use DeclarationListTests;
/**
* @var DeclarationBlock
@@ -158,25 +160,110 @@ public function parsesTwoCommaSeparatedSelectors(string $firstSelector, string $
}
/**
- * @return array
+ * @return array
*/
- public static function provideInvalidSelector(): array
+ public static function provideSelectorWithAndWithoutComment(): array
{
- // TODO: the `parse` method consumes the first character without inspection,
- // so the 'lone' test strings are prefixed with a space.
return [
- 'lone `(`' => [' ('],
- 'lone `)`' => [' )'],
- 'unclosed `(`' => [':not(#your-mug'],
- 'extra `)`' => [':not(#your-mug))'],
+ '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
+ {
+ return [
+ 'no selector' => ['', 'Token “selector” (literal) not found. Got “{”. [line no: 1]'],
+ '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 “{”. [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]'],
+ ];
+ }
+
+ /**
+ * @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
+ *
* @dataProvider provideInvalidSelector
*/
public function parseSkipsBlockWithInvalidSelector(string $selector): void
@@ -192,16 +279,101 @@ public function parseSkipsBlockWithInvalidSelector(string $selector): void
}
/**
- * @return array
+ * @test
+ *
+ * @param non-empty-string $expectedExceptionMessage
+ *
+ * @dataProvider provideInvalidSelectorAndExpectedExceptionMessage
*/
- private static function getSelectorsAsStrings(DeclarationBlock $declarationBlock): array
+ 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
+ */
+ public static function provideClosingBrace(): array
{
- return \array_map(
- static function (Selector $selectorObject): string {
- return $selectorObject->getSelector();
- },
- $declarationBlock->getSelectors()
- );
+ 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
+ */
+ 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());
}
/**
@@ -219,7 +391,7 @@ public function getRuleSetOnVirginReturnsARuleSet(): void
*/
public function getRuleSetAfterRulesSetReturnsARuleSet(): void
{
- $this->subject->setRules([new Rule('color')]);
+ $this->subject->setDeclarations([new Declaration('color')]);
$result = $this->subject->getRuleSet();
@@ -229,11 +401,11 @@ public function getRuleSetAfterRulesSetReturnsARuleSet(): void
/**
* @test
*/
- public function getRuleSetOnVirginReturnsObjectWithoutRules(): void
+ public function getRuleSetOnVirginReturnsObjectWithoutDeclarations(): void
{
$result = $this->subject->getRuleSet();
- self::assertSame([], $result->getRules());
+ self::assertSame([], $result->getDeclarations());
}
/**
@@ -243,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->setDeclarations($declarations);
$result = $this->subject->getRuleSet();
- self::assertSame($rules, $result->getRules());
+ self::assertSame($declarations, $result->getDeclarations());
}
/**
@@ -278,4 +450,124 @@ 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));
+ }
+
+ /**
+ * @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);
+ }
+
+ /**
+ * @test
+ */
+ public function getArrayRepresentationThrowsException(): void
+ {
+ $this->expectException(\BadMethodCallException::class);
+
+ $this->subject->getArrayRepresentation();
+ }
+
+ /**
+ * @return list
+ */
+ 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()
+ );
+ }
}
diff --git a/tests/Unit/RuleSet/DeclarationListTests.php b/tests/Unit/RuleSet/DeclarationListTests.php
new file mode 100644
index 000000000..f831e6181
--- /dev/null
+++ b/tests/Unit/RuleSet/DeclarationListTests.php
@@ -0,0 +1,1200 @@
+subject);
+ }
+
+ /**
+ * @return array}>
+ */
+ public static function providePropertyNames(): array
+ {
+ return [
+ 'no properties' => [[]],
+ 'one property' => [['color']],
+ 'two different properties' => [['color', 'display']],
+ 'two of the same property' => [['color', 'color']],
+ ];
+ }
+
+ /**
+ * @return array
+ */
+ public static function provideAnotherPropertyName(): array
+ {
+ return [
+ 'property name `color` maybe matching that of existing declaration' => ['color'],
+ 'property name `display` maybe matching that of existing declaration' => ['display'],
+ 'property name `width` not matching that of existing declaration' => ['width'],
+ ];
+ }
+
+ /**
+ * @return DataProvider, 1: string}>
+ */
+ public static function provideInitialPropertyNamesAndAnotherPropertyName(): DataProvider
+ {
+ return DataProvider::cross(self::providePropertyNames(), self::provideAnotherPropertyName());
+ }
+
+ /**
+ * @test
+ *
+ * @param list $initialPropertyNames
+ *
+ * @dataProvider provideInitialPropertyNamesAndAnotherPropertyName
+ */
+ public function addDeclarationWithoutPositionWithoutSiblingAddsDeclarationAfterInitialDeclarations(
+ array $initialPropertyNames,
+ string $propertyNameToAdd
+ ): void {
+ $declarationToAdd = new Declaration($propertyNameToAdd);
+ $this->setDeclarationsFromPropertyNames($initialPropertyNames);
+
+ $this->subject->addDeclaration($declarationToAdd);
+
+ $declarations = $this->subject->getDeclarations();
+ self::assertSame($declarationToAdd, \end($declarations));
+ }
+
+ /**
+ * @test
+ *
+ * @param list $initialPropertyNames
+ *
+ * @dataProvider provideInitialPropertyNamesAndAnotherPropertyName
+ */
+ public function addDeclarationWithoutPositionWithoutSiblingSetsValidLineNumber(
+ array $initialPropertyNames,
+ string $propertyNameToAdd
+ ): void {
+ $declarationToAdd = new Declaration($propertyNameToAdd);
+ $this->setDeclarationsFromPropertyNames($initialPropertyNames);
+
+ $this->subject->addDeclaration($declarationToAdd);
+
+ self::assertIsInt($declarationToAdd->getLineNumber(), 'line number not set');
+ self::assertGreaterThanOrEqual(1, $declarationToAdd->getLineNumber(), 'line number not valid');
+ }
+
+ /**
+ * @test
+ *
+ * @param list $initialPropertyNames
+ *
+ * @dataProvider provideInitialPropertyNamesAndAnotherPropertyName
+ */
+ public function addDeclarationWithoutPositionWithoutSiblingSetsValidColumnNumber(
+ array $initialPropertyNames,
+ string $propertyNameToAdd
+ ): void {
+ $declarationToAdd = new Declaration($propertyNameToAdd);
+ $this->setDeclarationsFromPropertyNames($initialPropertyNames);
+
+ $this->subject->addDeclaration($declarationToAdd);
+
+ self::assertIsInt($declarationToAdd->getColumnNumber(), 'column number not set');
+ self::assertGreaterThanOrEqual(0, $declarationToAdd->getColumnNumber(), 'column number not valid');
+ }
+
+ /**
+ * @test
+ *
+ * @dataProvider provideInitialPropertyNamesAndAnotherPropertyName
+ *
+ * @param list $initialPropertyNames
+ */
+ public function addDeclarationWithOnlyLineNumberWithoutSiblingAddsDeclaration(
+ array $initialPropertyNames,
+ string $propertyNameToAdd
+ ): void {
+ $declarationToAdd = new Declaration($propertyNameToAdd);
+ $declarationToAdd->setPosition(42);
+ $this->setDeclarationsFromPropertyNames($initialPropertyNames);
+
+ $this->subject->addDeclaration($declarationToAdd);
+
+ self::assertContains($declarationToAdd, $this->subject->getDeclarations());
+ }
+
+ /**
+ * @test
+ *
+ * @dataProvider provideInitialPropertyNamesAndAnotherPropertyName
+ *
+ * @param list $initialPropertyNames
+ */
+ public function addDeclarationWithOnlyLineNumberWithoutSiblingSetsColumnNumber(
+ array $initialPropertyNames,
+ string $propertyNameToAdd
+ ): void {
+ $declarationToAdd = new Declaration($propertyNameToAdd);
+ $declarationToAdd->setPosition(42);
+ $this->setDeclarationsFromPropertyNames($initialPropertyNames);
+
+ $this->subject->addDeclaration($declarationToAdd);
+
+ self::assertIsInt($declarationToAdd->getColumnNumber(), 'column number not set');
+ self::assertGreaterThanOrEqual(0, $declarationToAdd->getColumnNumber(), 'column number not valid');
+ }
+
+ /**
+ * @test
+ *
+ * @dataProvider provideInitialPropertyNamesAndAnotherPropertyName
+ *
+ * @param list $initialPropertyNames
+ */
+ public function addDeclarationWithOnlyLineNumberWithoutSiblingPreservesLineNumber(
+ array $initialPropertyNames,
+ string $propertyNameToAdd
+ ): void {
+ $declarationToAdd = new Declaration($propertyNameToAdd);
+ $declarationToAdd->setPosition(42);
+ $this->setDeclarationsFromPropertyNames($initialPropertyNames);
+
+ $this->subject->addDeclaration($declarationToAdd);
+
+ self::assertSame(42, $declarationToAdd->getLineNumber(), 'line number not preserved');
+ }
+
+ /**
+ * @test
+ *
+ * @dataProvider provideInitialPropertyNamesAndAnotherPropertyName
+ *
+ * @param list $initialPropertyNames
+ */
+ public function addDeclarationWithOnlyColumnNumberWithoutSiblingAddsDeclarationAfterInitialDeclarations(
+ array $initialPropertyNames,
+ string $propertyNameToAdd
+ ): void {
+ $declarationToAdd = new Declaration($propertyNameToAdd);
+ $declarationToAdd->setPosition(null, 42);
+ $this->setDeclarationsFromPropertyNames($initialPropertyNames);
+
+ $this->subject->addDeclaration($declarationToAdd);
+
+ $declarations = $this->subject->getDeclarations();
+ self::assertSame($declarationToAdd, \end($declarations));
+ }
+
+ /**
+ * @test
+ *
+ * @dataProvider provideInitialPropertyNamesAndAnotherPropertyName
+ *
+ * @param list $initialPropertyNames
+ */
+ public function addDeclarationWithOnlyColumnNumberWithoutSiblingSetsLineNumber(
+ array $initialPropertyNames,
+ string $propertyNameToAdd
+ ): void {
+ $declarationToAdd = new Declaration($propertyNameToAdd);
+ $declarationToAdd->setPosition(null, 42);
+ $this->setDeclarationsFromPropertyNames($initialPropertyNames);
+
+ $this->subject->addDeclaration($declarationToAdd);
+
+ self::assertIsInt($declarationToAdd->getLineNumber(), 'line number not set');
+ self::assertGreaterThanOrEqual(1, $declarationToAdd->getLineNumber(), 'line number not valid');
+ }
+
+ /**
+ * @test
+ *
+ * @dataProvider provideInitialPropertyNamesAndAnotherPropertyName
+ *
+ * @param list $initialPropertyNames
+ */
+ public function addDeclarationWithOnlyColumnNumberWithoutSiblingPreservesColumnNumber(
+ array $initialPropertyNames,
+ string $propertyNameToAdd
+ ): void {
+ $declarationToAdd = new Declaration($propertyNameToAdd);
+ $declarationToAdd->setPosition(null, 42);
+ $this->setDeclarationsFromPropertyNames($initialPropertyNames);
+
+ $this->subject->addDeclaration($declarationToAdd);
+
+ self::assertSame(42, $declarationToAdd->getColumnNumber(), 'column number not preserved');
+ }
+
+ /**
+ * @test
+ *
+ * @dataProvider provideInitialPropertyNamesAndAnotherPropertyName
+ *
+ * @param list $initialPropertyNames
+ */
+ public function addDeclarationWithCompletePositionWithoutSiblingAddsDeclaration(
+ array $initialPropertyNames,
+ string $propertyNameToAdd
+ ): void {
+ $declarationToAdd = new Declaration($propertyNameToAdd);
+ $declarationToAdd->setPosition(42, 64);
+ $this->setDeclarationsFromPropertyNames($initialPropertyNames);
+
+ $this->subject->addDeclaration($declarationToAdd);
+
+ self::assertContains($declarationToAdd, $this->subject->getDeclarations());
+ }
+
+ /**
+ * @test
+ *
+ * @dataProvider provideInitialPropertyNamesAndAnotherPropertyName
+ *
+ * @param list $initialPropertyNames
+ */
+ public function addDeclarationWithCompletePositionWithoutSiblingPreservesPosition(
+ array $initialPropertyNames,
+ string $propertyNameToAdd
+ ): void {
+ $declarationToAdd = new Declaration($propertyNameToAdd);
+ $declarationToAdd->setPosition(42, 64);
+ $this->setDeclarationsFromPropertyNames($initialPropertyNames);
+
+ $this->subject->addDeclaration($declarationToAdd);
+
+ self::assertSame(42, $declarationToAdd->getLineNumber(), 'line number not preserved');
+ self::assertSame(64, $declarationToAdd->getColumnNumber(), 'column number not preserved');
+ }
+
+ /**
+ * @return array, 1: int<0, max>}>
+ */
+ public static function provideInitialPropertyNamesAndIndexOfOne(): array
+ {
+ $initialPropertyNamesSets = self::providePropertyNames();
+
+ // Provide sets with each possible index for the initially set `Declaration`s.
+ $initialPropertyNamesAndIndexSets = [];
+ foreach ($initialPropertyNamesSets as $setName => $data) {
+ $initialPropertyNames = $data[0];
+ for ($index = 0; $index < \count($initialPropertyNames); ++$index) {
+ $initialPropertyNamesAndIndexSets[$setName . ', index ' . $index] =
+ [$initialPropertyNames, $index];
+ }
+ }
+
+ return $initialPropertyNamesAndIndexSets;
+ }
+
+ /**
+ * @return DataProvider, 1: int<0, max>, 2: string}>
+ */
+ public static function provideInitialPropertyNamesAndSiblingIndexAndPropertyNameToAdd(): DataProvider
+ {
+ return DataProvider::cross(
+ self::provideInitialPropertyNamesAndIndexOfOne(),
+ self::provideAnotherPropertyName()
+ );
+ }
+
+ /**
+ * @test
+ *
+ * @param non-empty-list $initialPropertyNames
+ * @param int<0, max> $siblingIndex
+ *
+ * @dataProvider provideInitialPropertyNamesAndSiblingIndexAndPropertyNameToAdd
+ */
+ public function addDeclarationWithSiblingInsertsDeclarationBeforeSibling(
+ array $initialPropertyNames,
+ int $siblingIndex,
+ string $propertyNameToAdd
+ ): void {
+ $declarationToAdd = new Declaration($propertyNameToAdd);
+ $this->setDeclarationsFromPropertyNames($initialPropertyNames);
+ $sibling = $this->subject->getDeclarations()[$siblingIndex];
+
+ $this->subject->addDeclaration($declarationToAdd, $sibling);
+
+ $declarations = $this->subject->getDeclarations();
+ $siblingPosition = \array_search($sibling, $declarations, true);
+ self::assertIsInt($siblingPosition);
+ self::assertSame($siblingPosition - 1, \array_search($declarationToAdd, $declarations, true));
+ }
+
+ /**
+ * @test
+ *
+ * @param non-empty-list $initialPropertyNames
+ * @param int<0, max> $siblingIndex
+ *
+ * @dataProvider provideInitialPropertyNamesAndSiblingIndexAndPropertyNameToAdd
+ */
+ public function addDeclarationWithSiblingSetsValidLineNumber(
+ array $initialPropertyNames,
+ int $siblingIndex,
+ string $propertyNameToAdd
+ ): void {
+ $declarationToAdd = new Declaration($propertyNameToAdd);
+ $this->setDeclarationsFromPropertyNames($initialPropertyNames);
+ $sibling = $this->subject->getDeclarations()[$siblingIndex];
+
+ $this->subject->addDeclaration($declarationToAdd, $sibling);
+
+ self::assertIsInt($declarationToAdd->getLineNumber(), 'line number not set');
+ self::assertGreaterThanOrEqual(1, $declarationToAdd->getLineNumber(), 'line number not valid');
+ }
+
+ /**
+ * @test
+ *
+ * @param non-empty-list $initialPropertyNames
+ * @param int<0, max> $siblingIndex
+ *
+ * @dataProvider provideInitialPropertyNamesAndSiblingIndexAndPropertyNameToAdd
+ */
+ public function addDeclarationWithSiblingSetsValidColumnNumber(
+ array $initialPropertyNames,
+ int $siblingIndex,
+ string $propertyNameToAdd
+ ): void {
+ $declarationToAdd = new Declaration($propertyNameToAdd);
+ $this->setDeclarationsFromPropertyNames($initialPropertyNames);
+ $sibling = $this->subject->getDeclarations()[$siblingIndex];
+
+ $this->subject->addDeclaration($declarationToAdd, $sibling);
+
+ self::assertIsInt($declarationToAdd->getColumnNumber(), 'column number not set');
+ self::assertGreaterThanOrEqual(0, $declarationToAdd->getColumnNumber(), 'column number not valid');
+ }
+
+ /**
+ * @test
+ *
+ * @param list $initialPropertyNames
+ *
+ * @dataProvider provideInitialPropertyNamesAndAnotherPropertyName
+ */
+ public function addDeclarationWithSiblingNotInSetAddsDeclarationAfterInitialDeclarations(
+ array $initialPropertyNames,
+ string $propertyNameToAdd
+ ): void {
+ $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->addDeclaration($declarationToAdd, new Declaration('display'));
+
+ $declarations = $this->subject->getDeclarations();
+ self::assertSame($declarationToAdd, \end($declarations));
+ }
+
+ /**
+ * @test
+ *
+ * @param list $initialPropertyNames
+ *
+ * @dataProvider provideInitialPropertyNamesAndAnotherPropertyName
+ */
+ public function addDeclarationWithSiblingNotInSetSetsValidLineNumber(
+ array $initialPropertyNames,
+ string $propertyNameToAdd
+ ): void {
+ $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->addDeclaration($declarationToAdd, new Declaration('display'));
+
+ self::assertIsInt($declarationToAdd->getLineNumber(), 'line number not set');
+ self::assertGreaterThanOrEqual(1, $declarationToAdd->getLineNumber(), 'line number not valid');
+ }
+
+ /**
+ * @test
+ *
+ * @param list $initialPropertyNames
+ *
+ * @dataProvider provideInitialPropertyNamesAndAnotherPropertyName
+ */
+ public function addDeclarationWithSiblingNotInSetSetsValidColumnNumber(
+ array $initialPropertyNames,
+ string $propertyNameToAdd
+ ): void {
+ $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->addDeclaration($declarationToAdd, new Declaration('display'));
+
+ self::assertIsInt($declarationToAdd->getColumnNumber(), 'column number not set');
+ self::assertGreaterThanOrEqual(0, $declarationToAdd->getColumnNumber(), 'column number not valid');
+ }
+
+ /**
+ * @test
+ *
+ * @param non-empty-list $initialPropertyNames
+ * @param int<0, max> $indexToRemove
+ *
+ * @dataProvider provideInitialPropertyNamesAndIndexOfOne
+ */
+ public function removeDeclarationRemovesDeclarationInSet(array $initialPropertyNames, int $indexToRemove): void
+ {
+ $this->setDeclarationsFromPropertyNames($initialPropertyNames);
+ $declarationToRemove = $this->subject->getDeclarations()[$indexToRemove];
+
+ $this->subject->removeDeclaration($declarationToRemove);
+
+ self::assertNotContains($declarationToRemove, $this->subject->getDeclarations());
+ }
+
+ /**
+ * @test
+ *
+ * @param non-empty-list $initialPropertyNames
+ * @param int<0, max> $indexToRemove
+ *
+ * @dataProvider provideInitialPropertyNamesAndIndexOfOne
+ */
+ public function removeDeclarationRemovesExactlyOneDeclaration(array $initialPropertyNames, int $indexToRemove): void
+ {
+ $this->setDeclarationsFromPropertyNames($initialPropertyNames);
+ $declarationToRemove = $this->subject->getDeclarations()[$indexToRemove];
+
+ $this->subject->removeDeclaration($declarationToRemove);
+
+ self::assertCount(\count($initialPropertyNames) - 1, $this->subject->getDeclarations());
+ }
+
+ /**
+ * @test
+ *
+ * @param list $initialPropertyNames
+ *
+ * @dataProvider provideInitialPropertyNamesAndAnotherPropertyName
+ */
+ public function removeDeclarationWithDeclarationNotInSetKeepsSetUnchanged(
+ array $initialPropertyNames,
+ string $propertyNameToRemove
+ ): void {
+ $this->setDeclarationsFromPropertyNames($initialPropertyNames);
+ $initialDeclarations = $this->subject->getDeclarations();
+ $declarationToRemove = new Declaration($propertyNameToRemove);
+
+ $this->subject->removeDeclaration($declarationToRemove);
+
+ self::assertSame($initialDeclarations, $this->subject->getDeclarations());
+ }
+
+ /**
+ * @return array, 1: string, 2: list}>
+ */
+ public static function providePropertyNamesAndPropertyNameToRemoveAndExpectedRemainingPropertyNames(): array
+ {
+ return [
+ 'removing single declaration' => [
+ ['color'],
+ 'color',
+ [],
+ ],
+ 'removing first declaration' => [
+ ['color', 'display'],
+ 'color',
+ ['display'],
+ ],
+ 'removing last declaration' => [
+ ['color', 'display'],
+ 'display',
+ ['color'],
+ ],
+ 'removing middle declaration' => [
+ ['color', 'display', 'width'],
+ 'display',
+ ['color', 'width'],
+ ],
+ 'removing multiple declarations' => [
+ ['color', 'color'],
+ 'color',
+ [],
+ ],
+ 'removing multiple declarations with another kept' => [
+ ['color', 'color', 'display'],
+ 'color',
+ ['display'],
+ ],
+ 'removing nonexistent declaration from empty list' => [
+ [],
+ 'color',
+ [],
+ ],
+ 'removing nonexistent declaration from nonempty list' => [
+ ['color', 'display'],
+ 'width',
+ ['color', 'display'],
+ ],
+ ];
+ }
+
+ /**
+ * @test
+ *
+ * @param list $initialPropertyNames
+ *
+ * @dataProvider providePropertyNamesAndPropertyNameToRemoveAndExpectedRemainingPropertyNames
+ */
+ public function removeMatchingDeclarationsRemovesDeclarationsWithPropertyName(
+ array $initialPropertyNames,
+ string $propertyNameToRemove
+ ): void {
+ $this->setDeclarationsFromPropertyNames($initialPropertyNames);
+
+ $this->subject->removeMatchingDeclarations($propertyNameToRemove);
+
+ self::assertArrayNotHasKey($propertyNameToRemove, $this->subject->getDeclarationsAssociative());
+ }
+
+ /**
+ * @test
+ *
+ * @param list $initialPropertyNames
+ * @param list $expectedRemainingPropertyNames
+ *
+ * @dataProvider providePropertyNamesAndPropertyNameToRemoveAndExpectedRemainingPropertyNames
+ */
+ public function removeMatchingDeclarationsWithPropertyNameKeepsOtherDeclarations(
+ array $initialPropertyNames,
+ string $propertyNameToRemove,
+ array $expectedRemainingPropertyNames
+ ): void {
+ $this->setDeclarationsFromPropertyNames($initialPropertyNames);
+
+ $this->subject->removeMatchingDeclarations($propertyNameToRemove);
+
+ $remainingDeclarations = $this->subject->getDeclarationsAssociative();
+ if ($expectedRemainingPropertyNames === []) {
+ self::assertSame([], $remainingDeclarations);
+ }
+ foreach ($expectedRemainingPropertyNames as $expectedPropertyName) {
+ self::assertArrayHasKey($expectedPropertyName, $remainingDeclarations);
+ }
+ }
+
+ /**
+ * @return array, 1: string, 2: list}>
+ */
+ public static function providePropertyNamesAndPropertyNamePrefixToRemoveAndExpectedRemainingPropertyNames(): array
+ {
+ return [
+ 'removing shorthand declaration' => [
+ ['font'],
+ 'font',
+ [],
+ ],
+ 'removing longhand declaration' => [
+ ['font-size'],
+ 'font',
+ [],
+ ],
+ 'removing shorthand and longhand declaration' => [
+ ['font', 'font-size'],
+ 'font',
+ [],
+ ],
+ 'removing shorthand declaration with another kept' => [
+ ['font', 'color'],
+ 'font',
+ ['color'],
+ ],
+ 'removing longhand declaration with another kept' => [
+ ['font-size', 'color'],
+ 'font',
+ ['color'],
+ ],
+ 'keeping other declarations whose property names begin with the same characters' => [
+ ['contain', 'container', 'container-type'],
+ 'contain',
+ ['container', 'container-type'],
+ ],
+ ];
+ }
+
+ /**
+ * @test
+ *
+ * @param list $initialPropertyNames
+ *
+ * @dataProvider providePropertyNamesAndPropertyNamePrefixToRemoveAndExpectedRemainingPropertyNames
+ */
+ public function removeMatchingDeclarationsRemovesDeclarationsWithPropertyNamePrefix(
+ array $initialPropertyNames,
+ string $propertyNamePrefix
+ ): void {
+ $propertyNamePrefixWithHyphen = $propertyNamePrefix . '-';
+ $this->setDeclarationsFromPropertyNames($initialPropertyNames);
+
+ $this->subject->removeMatchingDeclarations($propertyNamePrefixWithHyphen);
+
+ $remainingDeclarations = $this->subject->getDeclarationsAssociative();
+ self::assertArrayNotHasKey($propertyNamePrefix, $remainingDeclarations);
+ foreach (\array_keys($remainingDeclarations) as $remainingPropertyName) {
+ self::assertStringStartsNotWith($propertyNamePrefixWithHyphen, $remainingPropertyName);
+ }
+ }
+
+ /**
+ * @test
+ *
+ * @param list $initialPropertyNames
+ * @param list $expectedRemainingPropertyNames
+ *
+ * @dataProvider providePropertyNamesAndPropertyNamePrefixToRemoveAndExpectedRemainingPropertyNames
+ */
+ public function removeMatchingDeclarationsWithPropertyNamePrefixKeepsOtherDeclarations(
+ array $initialPropertyNames,
+ string $propertyNamePrefix,
+ array $expectedRemainingPropertyNames
+ ): void {
+ $propertyNamePrefixWithHyphen = $propertyNamePrefix . '-';
+ $this->setDeclarationsFromPropertyNames($initialPropertyNames);
+
+ $this->subject->removeMatchingDeclarations($propertyNamePrefixWithHyphen);
+
+ $remainingDeclarations = $this->subject->getDeclarationsAssociative();
+ if ($expectedRemainingPropertyNames === []) {
+ self::assertSame([], $remainingDeclarations);
+ }
+ foreach ($expectedRemainingPropertyNames as $expectedPropertyName) {
+ self::assertArrayHasKey($expectedPropertyName, $remainingDeclarations);
+ }
+ }
+
+ /**
+ * @test
+ *
+ * @param list $propertyNamesToRemove
+ *
+ * @dataProvider providePropertyNames
+ */
+ public function removeAllDeclarationsRemovesAllDeclarations(array $propertyNamesToRemove): void
+ {
+ $this->setDeclarationsFromPropertyNames($propertyNamesToRemove);
+
+ $this->subject->removeAllDeclarations();
+
+ self::assertSame([], $this->subject->getDeclarations());
+ }
+
+ /**
+ * @test
+ *
+ * @param list $propertyNamesToSet
+ *
+ * @dataProvider providePropertyNames
+ */
+ public function setDeclarationsOnVirginSetsDeclarationsWithoutPositionInOrder(array $propertyNamesToSet): void
+ {
+ $declarationsToSet = self::createDeclarationsFromPropertyNames($propertyNamesToSet);
+
+ $this->subject->setDeclarations($declarationsToSet);
+
+ self::assertSame($declarationsToSet, $this->subject->getDeclarations());
+ }
+
+ /**
+ * @return DataProvider, 1: list}>
+ */
+ public static function provideInitialPropertyNamesAndPropertyNamesToSet(): DataProvider
+ {
+ return DataProvider::cross(self::providePropertyNames(), self::providePropertyNames());
+ }
+
+ /**
+ * @test
+ *
+ * @param list $initialPropertyNames
+ * @param list $propertyNamesToSet
+ *
+ * @dataProvider provideInitialPropertyNamesAndPropertyNamesToSet
+ */
+ public function setDeclarationsReplacesDeclarations(array $initialPropertyNames, array $propertyNamesToSet): void
+ {
+ $declarationsToSet = self::createDeclarationsFromPropertyNames($propertyNamesToSet);
+ $this->setDeclarationsFromPropertyNames($initialPropertyNames);
+
+ $this->subject->setDeclarations($declarationsToSet);
+
+ self::assertSame($declarationsToSet, $this->subject->getDeclarations());
+ }
+
+ /**
+ * @test
+ */
+ public function setDeclarationsWithDeclarationWithoutPositionSetsValidLineNumber(): void
+ {
+ $declarationToSet = new Declaration('color');
+
+ $this->subject->setDeclarations([$declarationToSet]);
+
+ self::assertIsInt($declarationToSet->getLineNumber(), 'line number not set');
+ self::assertGreaterThanOrEqual(1, $declarationToSet->getLineNumber(), 'line number not valid');
+ }
+
+ /**
+ * @test
+ */
+ public function setDeclarationsWithDeclarationWithoutPositionSetsValidColumnNumber(): void
+ {
+ $declarationToSet = new Declaration('color');
+
+ $this->subject->setDeclarations([$declarationToSet]);
+
+ self::assertIsInt($declarationToSet->getColumnNumber(), 'column number not set');
+ self::assertGreaterThanOrEqual(0, $declarationToSet->getColumnNumber(), 'column number not valid');
+ }
+
+ /**
+ * @test
+ */
+ public function setDeclarationsWithDeclarationWithOnlyLineNumberSetsColumnNumber(): void
+ {
+ $declarationToSet = new Declaration('color');
+ $declarationToSet->setPosition(42);
+
+ $this->subject->setDeclarations([$declarationToSet]);
+
+ self::assertIsInt($declarationToSet->getColumnNumber(), 'column number not set');
+ self::assertGreaterThanOrEqual(0, $declarationToSet->getColumnNumber(), 'column number not valid');
+ }
+
+ /**
+ * @test
+ */
+ public function setDeclarationsWithDeclarationWithOnlyLineNumberPreservesLineNumber(): void
+ {
+ $declarationToSet = new Declaration('color');
+ $declarationToSet->setPosition(42);
+
+ $this->subject->setDeclarations([$declarationToSet]);
+
+ self::assertSame(42, $declarationToSet->getLineNumber(), 'line number not preserved');
+ }
+
+ /**
+ * @test
+ */
+ public function setDeclarationsWithDeclarationWithOnlyColumnNumberSetsLineNumber(): void
+ {
+ $declarationToSet = new Declaration('color');
+ $declarationToSet->setPosition(null, 42);
+
+ $this->subject->setDeclarations([$declarationToSet]);
+
+ self::assertIsInt($declarationToSet->getLineNumber(), 'line number not set');
+ self::assertGreaterThanOrEqual(1, $declarationToSet->getLineNumber(), 'line number not valid');
+ }
+
+ /**
+ * @test
+ */
+ public function setDeclarationsWithDeclarationWithOnlyColumnNumberPreservesColumnNumber(): void
+ {
+ $declarationToSet = new Declaration('color');
+ $declarationToSet->setPosition(null, 42);
+
+ $this->subject->setDeclarations([$declarationToSet]);
+
+ self::assertSame(42, $declarationToSet->getColumnNumber(), 'column number not preserved');
+ }
+
+ /**
+ * @test
+ */
+ public function setDeclarationsWithDeclarationWithCompletePositionPreservesPosition(): void
+ {
+ $declarationToSet = new Declaration('color');
+ $declarationToSet->setPosition(42, 64);
+
+ $this->subject->setDeclarations([$declarationToSet]);
+
+ self::assertSame(42, $declarationToSet->getLineNumber(), 'line number not preserved');
+ self::assertSame(64, $declarationToSet->getColumnNumber(), 'column number not preserved');
+ }
+
+ /**
+ * @test
+ *
+ * @param list $propertyNamesToSet
+ *
+ * @dataProvider providePropertyNames
+ */
+ public function getDeclarationsReturnsDeclarationsSet(array $propertyNamesToSet): void
+ {
+ $declarationsToSet = self::createDeclarationsFromPropertyNames($propertyNamesToSet);
+ $this->subject->setDeclarations($declarationsToSet);
+
+ $result = $this->subject->getDeclarations();
+
+ self::assertSame($declarationsToSet, $result);
+ }
+
+ /**
+ * @test
+ */
+ 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->setDeclarations([$third, $second, $first]);
+
+ $result = $this->subject->getDeclarations();
+
+ self::assertSame([$first, $second, $third], $result);
+ }
+
+ /**
+ * @test
+ */
+ 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->setDeclarations([$third, $second, $first]);
+
+ $result = $this->subject->getDeclarations();
+
+ self::assertSame([$first, $second, $third], $result);
+ }
+
+ /**
+ * @return array, 1: string, 2: list}>
+ */
+ public static function providePropertyNamesAndSearchPatternAndMatchingPropertyNames(): array
+ {
+ return [
+ 'single declaration matched' => [
+ ['color'],
+ 'color',
+ ['color'],
+ ],
+ 'first declaration matched' => [
+ ['color', 'display'],
+ 'color',
+ ['color'],
+ ],
+ 'last declaration matched' => [
+ ['color', 'display'],
+ 'display',
+ ['display'],
+ ],
+ 'middle declaration matched' => [
+ ['color', 'display', 'width'],
+ 'display',
+ ['display'],
+ ],
+ 'multiple declarations for the same property matched' => [
+ ['color', 'color'],
+ 'color',
+ ['color'],
+ ],
+ 'multiple declarations for the same property matched in haystack' => [
+ ['color', 'display', 'color', 'width'],
+ 'color',
+ ['color'],
+ ],
+ 'shorthand declaration matched' => [
+ ['font'],
+ 'font-',
+ ['font'],
+ ],
+ 'longhand declaration matched' => [
+ ['font-size'],
+ 'font-',
+ ['font-size'],
+ ],
+ 'shorthand and longhand declaration matched' => [
+ ['font', 'font-size'],
+ 'font-',
+ ['font', 'font-size'],
+ ],
+ 'shorthand declaration matched in haystack' => [
+ ['font', 'color'],
+ 'font-',
+ ['font'],
+ ],
+ 'longhand declaration matched in haystack' => [
+ ['font-size', 'color'],
+ 'font-',
+ ['font-size'],
+ ],
+ 'declarations whose property names begin with the same characters not matched with pattern match' => [
+ ['contain', 'container', 'container-type'],
+ 'contain-',
+ ['contain'],
+ ],
+ 'declarations whose property names begin with the same characters not matched with exact match' => [
+ ['contain', 'container', 'container-type'],
+ 'contain',
+ ['contain'],
+ ],
+ ];
+ }
+
+ /**
+ * @test
+ *
+ * @param list $propertyNamesToSet
+ * @param list $matchingPropertyNames
+ *
+ * @dataProvider providePropertyNamesAndSearchPatternAndMatchingPropertyNames
+ */
+ public function getDeclarationsWithPatternReturnsAllMatchingDeclarations(
+ array $propertyNamesToSet,
+ string $searchPattern,
+ array $matchingPropertyNames
+ ): void {
+ $declarationsToSet = self::createDeclarationsFromPropertyNames($propertyNamesToSet);
+ // Use `array_values` to ensure canonical numeric array, since `array_filter` does not reindex.
+ $matchingDeclarations = \array_values(
+ \array_filter(
+ $declarationsToSet,
+ static function (Declaration $declaration) use ($matchingPropertyNames): bool {
+ return \in_array($declaration->getPropertyName(), $matchingPropertyNames, true);
+ }
+ )
+ );
+ $this->subject->setDeclarations($declarationsToSet);
+
+ $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);
+ }
+
+ /**
+ * @return array, 1: string}>
+ */
+ public static function providePropertyNamesAndNonMatchingSearchPattern(): array
+ {
+ return [
+ 'no match in empty list' => [
+ [],
+ 'color',
+ ],
+ 'no match for different property' => [
+ ['color'],
+ 'display',
+ ],
+ 'no match for property not in list' => [
+ ['color', 'display'],
+ 'width',
+ ],
+ ];
+ }
+
+ /**
+ * @test
+ *
+ * @param list $propertyNamesToSet
+ *
+ * @dataProvider providePropertyNamesAndNonMatchingSearchPattern
+ */
+ public function getDeclarationsWithNonMatchingPatternReturnsEmptyArray(
+ array $propertyNamesToSet,
+ string $searchPattern
+ ): void {
+ $this->setDeclarationsFromPropertyNames($propertyNamesToSet);
+
+ $result = $this->subject->getDeclarations($searchPattern);
+
+ self::assertSame([], $result);
+ }
+
+ /**
+ * @test
+ */
+ 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->setDeclarations([$third, $second, $first]);
+
+ $result = $this->subject->getDeclarations('color');
+
+ self::assertSame([$first, $second, $third], $result);
+ }
+
+ /**
+ * @return array}>
+ */
+ public static function provideDistinctPropertyNames(): array
+ {
+ return [
+ 'no properties' => [[]],
+ 'one property' => [['color']],
+ 'two properties' => [['color', 'display']],
+ ];
+ }
+
+ /**
+ * @test
+ *
+ * @param list $propertyNamesToSet
+ *
+ * @dataProvider provideDistinctPropertyNames
+ */
+ public function getDeclarationsAssocReturnsAllDeclarationsWithDistinctPropertyNames(array $propertyNamesToSet): void
+ {
+ $declarationsToSet = self::createDeclarationsFromPropertyNames($propertyNamesToSet);
+ $this->subject->setDeclarations($declarationsToSet);
+
+ $result = $this->subject->getDeclarationsAssociative();
+
+ self::assertSame($declarationsToSet, \array_values($result));
+ }
+
+ /**
+ * @test
+ */
+ public function getDeclarationsAssocReturnsLastDeclarationWithSamePropertyName(): void
+ {
+ $firstDeclaration = new Declaration('color');
+ $lastDeclaration = new Declaration('color');
+ $this->subject->setDeclarations([$firstDeclaration, $lastDeclaration]);
+
+ $result = $this->subject->getDeclarationsAssociative();
+
+ self::assertSame([$lastDeclaration], \array_values($result));
+ }
+
+ /**
+ * @test
+ */
+ 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->setDeclarations([$third, $second, $first]);
+
+ $result = $this->subject->getDeclarationsAssociative();
+
+ self::assertSame([$first, $second, $third], \array_values($result));
+ }
+
+ /**
+ * @test
+ */
+ public function getDeclarationsAssocKeysDeclarationsByPropertyName(): void
+ {
+ $this->subject->setDeclarations([new Declaration('color'), new Declaration('display')]);
+
+ $result = $this->subject->getDeclarationsAssociative();
+
+ foreach ($result as $key => $declaration) {
+ self::assertSame($declaration->getPropertyName(), $key);
+ }
+ }
+
+ /**
+ * @test
+ *
+ * @param list $propertyNamesToSet
+ * @param list $matchingPropertyNames
+ *
+ * @dataProvider providePropertyNamesAndSearchPatternAndMatchingPropertyNames
+ */
+ public function getDeclarationsAssocWithPatternReturnsAllMatchingPropertyNames(
+ array $propertyNamesToSet,
+ string $searchPattern,
+ array $matchingPropertyNames
+ ): void {
+ $this->setDeclarationsFromPropertyNames($propertyNamesToSet);
+
+ $result = $this->subject->getDeclarationsAssociative($searchPattern);
+
+ $resultPropertyNames = \array_keys($result);
+ \sort($matchingPropertyNames);
+ \sort($resultPropertyNames);
+ self::assertSame($matchingPropertyNames, $resultPropertyNames);
+ }
+
+ /**
+ * @test
+ *
+ * @param list $propertyNamesToSet
+ *
+ * @dataProvider providePropertyNamesAndNonMatchingSearchPattern
+ */
+ public function getDeclarationsAssocWithNonMatchingPatternReturnsEmptyArray(
+ array $propertyNamesToSet,
+ string $searchPattern
+ ): void {
+ $this->setDeclarationsFromPropertyNames($propertyNamesToSet);
+
+ $result = $this->subject->getDeclarationsAssociative($searchPattern);
+
+ self::assertSame([], $result);
+ }
+
+ /**
+ * @test
+ */
+ 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->setDeclarations([$third, $second, $first]);
+
+ $result = $this->subject->getDeclarations('font-');
+
+ self::assertSame([$first, $second, $third], \array_values($result));
+ }
+
+ /**
+ * @param list $propertyNames
+ */
+ private function setDeclarationsFromPropertyNames(array $propertyNames): void
+ {
+ $this->subject->setDeclarations(self::createDeclarationsFromPropertyNames($propertyNames));
+ }
+
+ /**
+ * @param list $propertyNames
+ *
+ * @return list
+ */
+ private static function createDeclarationsFromPropertyNames(array $propertyNames): array
+ {
+ return \array_map(
+ function (string $propertyName): Declaration {
+ return new Declaration($propertyName);
+ },
+ $propertyNames
+ );
+ }
+}
diff --git a/tests/Unit/RuleSet/RuleContainerTest.php b/tests/Unit/RuleSet/RuleContainerTest.php
deleted file mode 100644
index bcb641a22..000000000
--- a/tests/Unit/RuleSet/RuleContainerTest.php
+++ /dev/null
@@ -1,1200 +0,0 @@
-subject);
- }
-
- /**
- * @return array}>
- */
- public static function providePropertyNames(): array
- {
- return [
- 'no properties' => [[]],
- 'one property' => [['color']],
- 'two different properties' => [['color', 'display']],
- 'two of the same property' => [['color', 'color']],
- ];
- }
-
- /**
- * @return array
- */
- public static function provideAnotherPropertyName(): array
- {
- return [
- 'property name `color` maybe matching that of existing declaration' => ['color'],
- 'property name `display` maybe matching that of existing declaration' => ['display'],
- 'property name `width` not matching that of existing declaration' => ['width'],
- ];
- }
-
- /**
- * @return DataProvider, 1: string}>
- */
- public static function provideInitialPropertyNamesAndAnotherPropertyName(): DataProvider
- {
- return DataProvider::cross(self::providePropertyNames(), self::provideAnotherPropertyName());
- }
-
- /**
- * @test
- *
- * @param list $initialPropertyNames
- *
- * @dataProvider provideInitialPropertyNamesAndAnotherPropertyName
- */
- public function addRuleWithoutPositionWithoutSiblingAddsRuleAfterInitialRules(
- array $initialPropertyNames,
- string $propertyNameToAdd
- ): void {
- $ruleToAdd = new Rule($propertyNameToAdd);
- $this->setRulesFromPropertyNames($initialPropertyNames);
-
- $this->subject->addRule($ruleToAdd);
-
- $rules = $this->subject->getRules();
- self::assertSame($ruleToAdd, \end($rules));
- }
-
- /**
- * @test
- *
- * @param list $initialPropertyNames
- *
- * @dataProvider provideInitialPropertyNamesAndAnotherPropertyName
- */
- public function addRuleWithoutPositionWithoutSiblingSetsValidLineNumber(
- array $initialPropertyNames,
- string $propertyNameToAdd
- ): void {
- $ruleToAdd = new Rule($propertyNameToAdd);
- $this->setRulesFromPropertyNames($initialPropertyNames);
-
- $this->subject->addRule($ruleToAdd);
-
- self::assertIsInt($ruleToAdd->getLineNumber(), 'line number not set');
- self::assertGreaterThanOrEqual(1, $ruleToAdd->getLineNumber(), 'line number not valid');
- }
-
- /**
- * @test
- *
- * @param list $initialPropertyNames
- *
- * @dataProvider provideInitialPropertyNamesAndAnotherPropertyName
- */
- public function addRuleWithoutPositionWithoutSiblingSetsValidColumnNumber(
- array $initialPropertyNames,
- string $propertyNameToAdd
- ): void {
- $ruleToAdd = new Rule($propertyNameToAdd);
- $this->setRulesFromPropertyNames($initialPropertyNames);
-
- $this->subject->addRule($ruleToAdd);
-
- self::assertIsInt($ruleToAdd->getColumnNumber(), 'column number not set');
- self::assertGreaterThanOrEqual(0, $ruleToAdd->getColumnNumber(), 'column number not valid');
- }
-
- /**
- * @test
- *
- * @dataProvider provideInitialPropertyNamesAndAnotherPropertyName
- *
- * @param list $initialPropertyNames
- */
- public function addRuleWithOnlyLineNumberWithoutSiblingAddsRule(
- array $initialPropertyNames,
- string $propertyNameToAdd
- ): void {
- $ruleToAdd = new Rule($propertyNameToAdd);
- $ruleToAdd->setPosition(42);
- $this->setRulesFromPropertyNames($initialPropertyNames);
-
- $this->subject->addRule($ruleToAdd);
-
- self::assertContains($ruleToAdd, $this->subject->getRules());
- }
-
- /**
- * @test
- *
- * @dataProvider provideInitialPropertyNamesAndAnotherPropertyName
- *
- * @param list $initialPropertyNames
- */
- public function addRuleWithOnlyLineNumberWithoutSiblingSetsColumnNumber(
- array $initialPropertyNames,
- string $propertyNameToAdd
- ): void {
- $ruleToAdd = new Rule($propertyNameToAdd);
- $ruleToAdd->setPosition(42);
- $this->setRulesFromPropertyNames($initialPropertyNames);
-
- $this->subject->addRule($ruleToAdd);
-
- self::assertIsInt($ruleToAdd->getColumnNumber(), 'column number not set');
- self::assertGreaterThanOrEqual(0, $ruleToAdd->getColumnNumber(), 'column number not valid');
- }
-
- /**
- * @test
- *
- * @dataProvider provideInitialPropertyNamesAndAnotherPropertyName
- *
- * @param list $initialPropertyNames
- */
- public function addRuleWithOnlyLineNumberWithoutSiblingPreservesLineNumber(
- array $initialPropertyNames,
- string $propertyNameToAdd
- ): void {
- $ruleToAdd = new Rule($propertyNameToAdd);
- $ruleToAdd->setPosition(42);
- $this->setRulesFromPropertyNames($initialPropertyNames);
-
- $this->subject->addRule($ruleToAdd);
-
- self::assertSame(42, $ruleToAdd->getLineNumber(), 'line number not preserved');
- }
-
- /**
- * @test
- *
- * @dataProvider provideInitialPropertyNamesAndAnotherPropertyName
- *
- * @param list $initialPropertyNames
- */
- public function addRuleWithOnlyColumnNumberWithoutSiblingAddsRuleAfterInitialRules(
- array $initialPropertyNames,
- string $propertyNameToAdd
- ): void {
- $ruleToAdd = new Rule($propertyNameToAdd);
- $ruleToAdd->setPosition(null, 42);
- $this->setRulesFromPropertyNames($initialPropertyNames);
-
- $this->subject->addRule($ruleToAdd);
-
- $rules = $this->subject->getRules();
- self::assertSame($ruleToAdd, \end($rules));
- }
-
- /**
- * @test
- *
- * @dataProvider provideInitialPropertyNamesAndAnotherPropertyName
- *
- * @param list $initialPropertyNames
- */
- public function addRuleWithOnlyColumnNumberWithoutSiblingSetsLineNumber(
- array $initialPropertyNames,
- string $propertyNameToAdd
- ): void {
- $ruleToAdd = new Rule($propertyNameToAdd);
- $ruleToAdd->setPosition(null, 42);
- $this->setRulesFromPropertyNames($initialPropertyNames);
-
- $this->subject->addRule($ruleToAdd);
-
- self::assertIsInt($ruleToAdd->getLineNumber(), 'line number not set');
- self::assertGreaterThanOrEqual(1, $ruleToAdd->getLineNumber(), 'line number not valid');
- }
-
- /**
- * @test
- *
- * @dataProvider provideInitialPropertyNamesAndAnotherPropertyName
- *
- * @param list $initialPropertyNames
- */
- public function addRuleWithOnlyColumnNumberWithoutSiblingPreservesColumnNumber(
- array $initialPropertyNames,
- string $propertyNameToAdd
- ): void {
- $ruleToAdd = new Rule($propertyNameToAdd);
- $ruleToAdd->setPosition(null, 42);
- $this->setRulesFromPropertyNames($initialPropertyNames);
-
- $this->subject->addRule($ruleToAdd);
-
- self::assertSame(42, $ruleToAdd->getColumnNumber(), 'column number not preserved');
- }
-
- /**
- * @test
- *
- * @dataProvider provideInitialPropertyNamesAndAnotherPropertyName
- *
- * @param list $initialPropertyNames
- */
- public function addRuleWithCompletePositionWithoutSiblingAddsRule(
- array $initialPropertyNames,
- string $propertyNameToAdd
- ): void {
- $ruleToAdd = new Rule($propertyNameToAdd);
- $ruleToAdd->setPosition(42, 64);
- $this->setRulesFromPropertyNames($initialPropertyNames);
-
- $this->subject->addRule($ruleToAdd);
-
- self::assertContains($ruleToAdd, $this->subject->getRules());
- }
-
- /**
- * @test
- *
- * @dataProvider provideInitialPropertyNamesAndAnotherPropertyName
- *
- * @param list $initialPropertyNames
- */
- public function addRuleWithCompletePositionWithoutSiblingPreservesPosition(
- array $initialPropertyNames,
- string $propertyNameToAdd
- ): void {
- $ruleToAdd = new Rule($propertyNameToAdd);
- $ruleToAdd->setPosition(42, 64);
- $this->setRulesFromPropertyNames($initialPropertyNames);
-
- $this->subject->addRule($ruleToAdd);
-
- self::assertSame(42, $ruleToAdd->getLineNumber(), 'line number not preserved');
- self::assertSame(64, $ruleToAdd->getColumnNumber(), 'column number not preserved');
- }
-
- /**
- * @return array, 1: int<0, max>}>
- */
- public static function provideInitialPropertyNamesAndIndexOfOne(): array
- {
- $initialPropertyNamesSets = self::providePropertyNames();
-
- // Provide sets with each possible index for the initially set `Rule`s.
- $initialPropertyNamesAndIndexSets = [];
- foreach ($initialPropertyNamesSets as $setName => $data) {
- $initialPropertyNames = $data[0];
- for ($index = 0; $index < \count($initialPropertyNames); ++$index) {
- $initialPropertyNamesAndIndexSets[$setName . ', index ' . $index] =
- [$initialPropertyNames, $index];
- }
- }
-
- return $initialPropertyNamesAndIndexSets;
- }
-
- /**
- * @return DataProvider, 1: int<0, max>, 2: string}>
- */
- public static function provideInitialPropertyNamesAndSiblingIndexAndPropertyNameToAdd(): DataProvider
- {
- return DataProvider::cross(
- self::provideInitialPropertyNamesAndIndexOfOne(),
- self::provideAnotherPropertyName()
- );
- }
-
- /**
- * @test
- *
- * @param non-empty-list $initialPropertyNames
- * @param int<0, max> $siblingIndex
- *
- * @dataProvider provideInitialPropertyNamesAndSiblingIndexAndPropertyNameToAdd
- */
- public function addRuleWithSiblingInsertsRuleBeforeSibling(
- array $initialPropertyNames,
- int $siblingIndex,
- string $propertyNameToAdd
- ): void {
- $ruleToAdd = new Rule($propertyNameToAdd);
- $this->setRulesFromPropertyNames($initialPropertyNames);
- $sibling = $this->subject->getRules()[$siblingIndex];
-
- $this->subject->addRule($ruleToAdd, $sibling);
-
- $rules = $this->subject->getRules();
- $siblingPosition = \array_search($sibling, $rules, true);
- self::assertIsInt($siblingPosition);
- self::assertSame($siblingPosition - 1, \array_search($ruleToAdd, $rules, true));
- }
-
- /**
- * @test
- *
- * @param non-empty-list $initialPropertyNames
- * @param int<0, max> $siblingIndex
- *
- * @dataProvider provideInitialPropertyNamesAndSiblingIndexAndPropertyNameToAdd
- */
- public function addRuleWithSiblingSetsValidLineNumber(
- array $initialPropertyNames,
- int $siblingIndex,
- string $propertyNameToAdd
- ): void {
- $ruleToAdd = new Rule($propertyNameToAdd);
- $this->setRulesFromPropertyNames($initialPropertyNames);
- $sibling = $this->subject->getRules()[$siblingIndex];
-
- $this->subject->addRule($ruleToAdd, $sibling);
-
- self::assertIsInt($ruleToAdd->getLineNumber(), 'line number not set');
- self::assertGreaterThanOrEqual(1, $ruleToAdd->getLineNumber(), 'line number not valid');
- }
-
- /**
- * @test
- *
- * @param non-empty-list $initialPropertyNames
- * @param int<0, max> $siblingIndex
- *
- * @dataProvider provideInitialPropertyNamesAndSiblingIndexAndPropertyNameToAdd
- */
- public function addRuleWithSiblingSetsValidColumnNumber(
- array $initialPropertyNames,
- int $siblingIndex,
- string $propertyNameToAdd
- ): void {
- $ruleToAdd = new Rule($propertyNameToAdd);
- $this->setRulesFromPropertyNames($initialPropertyNames);
- $sibling = $this->subject->getRules()[$siblingIndex];
-
- $this->subject->addRule($ruleToAdd, $sibling);
-
- self::assertIsInt($ruleToAdd->getColumnNumber(), 'column number not set');
- self::assertGreaterThanOrEqual(0, $ruleToAdd->getColumnNumber(), 'column number not valid');
- }
-
- /**
- * @test
- *
- * @param list $initialPropertyNames
- *
- * @dataProvider provideInitialPropertyNamesAndAnotherPropertyName
- */
- public function addRuleWithSiblingNotInSetAddsRuleAfterInitialRules(
- array $initialPropertyNames,
- string $propertyNameToAdd
- ): void {
- $ruleToAdd = new Rule($propertyNameToAdd);
- $this->setRulesFromPropertyNames($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'));
-
- $rules = $this->subject->getRules();
- self::assertSame($ruleToAdd, \end($rules));
- }
-
- /**
- * @test
- *
- * @param list $initialPropertyNames
- *
- * @dataProvider provideInitialPropertyNamesAndAnotherPropertyName
- */
- public function addRuleWithSiblingNotInSetSetsValidLineNumber(
- array $initialPropertyNames,
- string $propertyNameToAdd
- ): void {
- $ruleToAdd = new Rule($propertyNameToAdd);
- $this->setRulesFromPropertyNames($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'));
-
- self::assertIsInt($ruleToAdd->getLineNumber(), 'line number not set');
- self::assertGreaterThanOrEqual(1, $ruleToAdd->getLineNumber(), 'line number not valid');
- }
-
- /**
- * @test
- *
- * @param list $initialPropertyNames
- *
- * @dataProvider provideInitialPropertyNamesAndAnotherPropertyName
- */
- public function addRuleWithSiblingNotInSetSetsValidColumnNumber(
- array $initialPropertyNames,
- string $propertyNameToAdd
- ): void {
- $ruleToAdd = new Rule($propertyNameToAdd);
- $this->setRulesFromPropertyNames($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'));
-
- self::assertIsInt($ruleToAdd->getColumnNumber(), 'column number not set');
- self::assertGreaterThanOrEqual(0, $ruleToAdd->getColumnNumber(), 'column number not valid');
- }
-
- /**
- * @test
- *
- * @param non-empty-list $initialPropertyNames
- * @param int<0, max> $indexToRemove
- *
- * @dataProvider provideInitialPropertyNamesAndIndexOfOne
- */
- public function removeRuleRemovesRuleInSet(array $initialPropertyNames, int $indexToRemove): void
- {
- $this->setRulesFromPropertyNames($initialPropertyNames);
- $ruleToRemove = $this->subject->getRules()[$indexToRemove];
-
- $this->subject->removeRule($ruleToRemove);
-
- self::assertNotContains($ruleToRemove, $this->subject->getRules());
- }
-
- /**
- * @test
- *
- * @param non-empty-list $initialPropertyNames
- * @param int<0, max> $indexToRemove
- *
- * @dataProvider provideInitialPropertyNamesAndIndexOfOne
- */
- public function removeRuleRemovesExactlyOneRule(array $initialPropertyNames, int $indexToRemove): void
- {
- $this->setRulesFromPropertyNames($initialPropertyNames);
- $ruleToRemove = $this->subject->getRules()[$indexToRemove];
-
- $this->subject->removeRule($ruleToRemove);
-
- self::assertCount(\count($initialPropertyNames) - 1, $this->subject->getRules());
- }
-
- /**
- * @test
- *
- * @param list $initialPropertyNames
- *
- * @dataProvider provideInitialPropertyNamesAndAnotherPropertyName
- */
- public function removeRuleWithRuleNotInSetKeepsSetUnchanged(
- array $initialPropertyNames,
- string $propertyNameToRemove
- ): void {
- $this->setRulesFromPropertyNames($initialPropertyNames);
- $initialRules = $this->subject->getRules();
- $ruleToRemove = new Rule($propertyNameToRemove);
-
- $this->subject->removeRule($ruleToRemove);
-
- self::assertSame($initialRules, $this->subject->getRules());
- }
-
- /**
- * @return array, 1: string, 2: list}>
- */
- public static function providePropertyNamesAndPropertyNameToRemoveAndExpectedRemainingPropertyNames(): array
- {
- return [
- 'removing single rule' => [
- ['color'],
- 'color',
- [],
- ],
- 'removing first rule' => [
- ['color', 'display'],
- 'color',
- ['display'],
- ],
- 'removing last rule' => [
- ['color', 'display'],
- 'display',
- ['color'],
- ],
- 'removing middle rule' => [
- ['color', 'display', 'width'],
- 'display',
- ['color', 'width'],
- ],
- 'removing multiple rules' => [
- ['color', 'color'],
- 'color',
- [],
- ],
- 'removing multiple rules with another kept' => [
- ['color', 'color', 'display'],
- 'color',
- ['display'],
- ],
- 'removing nonexistent rule from empty list' => [
- [],
- 'color',
- [],
- ],
- 'removing nonexistent rule from nonempty list' => [
- ['color', 'display'],
- 'width',
- ['color', 'display'],
- ],
- ];
- }
-
- /**
- * @test
- *
- * @param list $initialPropertyNames
- *
- * @dataProvider providePropertyNamesAndPropertyNameToRemoveAndExpectedRemainingPropertyNames
- */
- public function removeMatchingRulesRemovesRulesWithPropertyName(
- array $initialPropertyNames,
- string $propertyNameToRemove
- ): void {
- $this->setRulesFromPropertyNames($initialPropertyNames);
-
- $this->subject->removeMatchingRules($propertyNameToRemove);
-
- self::assertArrayNotHasKey($propertyNameToRemove, $this->subject->getRulesAssoc());
- }
-
- /**
- * @test
- *
- * @param list $initialPropertyNames
- * @param list $expectedRemainingPropertyNames
- *
- * @dataProvider providePropertyNamesAndPropertyNameToRemoveAndExpectedRemainingPropertyNames
- */
- public function removeMatchingRulesWithPropertyNameKeepsOtherRules(
- array $initialPropertyNames,
- string $propertyNameToRemove,
- array $expectedRemainingPropertyNames
- ): void {
- $this->setRulesFromPropertyNames($initialPropertyNames);
-
- $this->subject->removeMatchingRules($propertyNameToRemove);
-
- $remainingRules = $this->subject->getRulesAssoc();
- if ($expectedRemainingPropertyNames === []) {
- self::assertSame([], $remainingRules);
- }
- foreach ($expectedRemainingPropertyNames as $expectedPropertyName) {
- self::assertArrayHasKey($expectedPropertyName, $remainingRules);
- }
- }
-
- /**
- * @return array, 1: string, 2: list}>
- */
- public static function providePropertyNamesAndPropertyNamePrefixToRemoveAndExpectedRemainingPropertyNames(): array
- {
- return [
- 'removing shorthand rule' => [
- ['font'],
- 'font',
- [],
- ],
- 'removing longhand rule' => [
- ['font-size'],
- 'font',
- [],
- ],
- 'removing shorthand and longhand rule' => [
- ['font', 'font-size'],
- 'font',
- [],
- ],
- 'removing shorthand rule with another kept' => [
- ['font', 'color'],
- 'font',
- ['color'],
- ],
- 'removing longhand rule with another kept' => [
- ['font-size', 'color'],
- 'font',
- ['color'],
- ],
- 'keeping other rules whose property names begin with the same characters' => [
- ['contain', 'container', 'container-type'],
- 'contain',
- ['container', 'container-type'],
- ],
- ];
- }
-
- /**
- * @test
- *
- * @param list $initialPropertyNames
- *
- * @dataProvider providePropertyNamesAndPropertyNamePrefixToRemoveAndExpectedRemainingPropertyNames
- */
- public function removeMatchingRulesRemovesRulesWithPropertyNamePrefix(
- array $initialPropertyNames,
- string $propertyNamePrefix
- ): void {
- $propertyNamePrefixWithHyphen = $propertyNamePrefix . '-';
- $this->setRulesFromPropertyNames($initialPropertyNames);
-
- $this->subject->removeMatchingRules($propertyNamePrefixWithHyphen);
-
- $remainingRules = $this->subject->getRulesAssoc();
- self::assertArrayNotHasKey($propertyNamePrefix, $remainingRules);
- foreach (\array_keys($remainingRules) as $remainingPropertyName) {
- self::assertStringStartsNotWith($propertyNamePrefixWithHyphen, $remainingPropertyName);
- }
- }
-
- /**
- * @test
- *
- * @param list $initialPropertyNames
- * @param list $expectedRemainingPropertyNames
- *
- * @dataProvider providePropertyNamesAndPropertyNamePrefixToRemoveAndExpectedRemainingPropertyNames
- */
- public function removeMatchingRulesWithPropertyNamePrefixKeepsOtherRules(
- array $initialPropertyNames,
- string $propertyNamePrefix,
- array $expectedRemainingPropertyNames
- ): void {
- $propertyNamePrefixWithHyphen = $propertyNamePrefix . '-';
- $this->setRulesFromPropertyNames($initialPropertyNames);
-
- $this->subject->removeMatchingRules($propertyNamePrefixWithHyphen);
-
- $remainingRules = $this->subject->getRulesAssoc();
- if ($expectedRemainingPropertyNames === []) {
- self::assertSame([], $remainingRules);
- }
- foreach ($expectedRemainingPropertyNames as $expectedPropertyName) {
- self::assertArrayHasKey($expectedPropertyName, $remainingRules);
- }
- }
-
- /**
- * @test
- *
- * @param list $propertyNamesToRemove
- *
- * @dataProvider providePropertyNames
- */
- public function removeAllRulesRemovesAllRules(array $propertyNamesToRemove): void
- {
- $this->setRulesFromPropertyNames($propertyNamesToRemove);
-
- $this->subject->removeAllRules();
-
- self::assertSame([], $this->subject->getRules());
- }
-
- /**
- * @test
- *
- * @param list $propertyNamesToSet
- *
- * @dataProvider providePropertyNames
- */
- public function setRulesOnVirginSetsRulesWithoutPositionInOrder(array $propertyNamesToSet): void
- {
- $rulesToSet = self::createRulesFromPropertyNames($propertyNamesToSet);
-
- $this->subject->setRules($rulesToSet);
-
- self::assertSame($rulesToSet, $this->subject->getRules());
- }
-
- /**
- * @return DataProvider, 1: list}>
- */
- public static function provideInitialPropertyNamesAndPropertyNamesToSet(): DataProvider
- {
- return DataProvider::cross(self::providePropertyNames(), self::providePropertyNames());
- }
-
- /**
- * @test
- *
- * @param list $initialPropertyNames
- * @param list $propertyNamesToSet
- *
- * @dataProvider provideInitialPropertyNamesAndPropertyNamesToSet
- */
- public function setRulesReplacesRules(array $initialPropertyNames, array $propertyNamesToSet): void
- {
- $rulesToSet = self::createRulesFromPropertyNames($propertyNamesToSet);
- $this->setRulesFromPropertyNames($initialPropertyNames);
-
- $this->subject->setRules($rulesToSet);
-
- self::assertSame($rulesToSet, $this->subject->getRules());
- }
-
- /**
- * @test
- */
- public function setRulesWithRuleWithoutPositionSetsValidLineNumber(): void
- {
- $ruleToSet = new Rule('color');
-
- $this->subject->setRules([$ruleToSet]);
-
- self::assertIsInt($ruleToSet->getLineNumber(), 'line number not set');
- self::assertGreaterThanOrEqual(1, $ruleToSet->getLineNumber(), 'line number not valid');
- }
-
- /**
- * @test
- */
- public function setRulesWithRuleWithoutPositionSetsValidColumnNumber(): void
- {
- $ruleToSet = new Rule('color');
-
- $this->subject->setRules([$ruleToSet]);
-
- self::assertIsInt($ruleToSet->getColumnNumber(), 'column number not set');
- self::assertGreaterThanOrEqual(0, $ruleToSet->getColumnNumber(), 'column number not valid');
- }
-
- /**
- * @test
- */
- public function setRulesWithRuleWithOnlyLineNumberSetsColumnNumber(): void
- {
- $ruleToSet = new Rule('color');
- $ruleToSet->setPosition(42);
-
- $this->subject->setRules([$ruleToSet]);
-
- self::assertIsInt($ruleToSet->getColumnNumber(), 'column number not set');
- self::assertGreaterThanOrEqual(0, $ruleToSet->getColumnNumber(), 'column number not valid');
- }
-
- /**
- * @test
- */
- public function setRulesWithRuleWithOnlyLineNumberPreservesLineNumber(): void
- {
- $ruleToSet = new Rule('color');
- $ruleToSet->setPosition(42);
-
- $this->subject->setRules([$ruleToSet]);
-
- self::assertSame(42, $ruleToSet->getLineNumber(), 'line number not preserved');
- }
-
- /**
- * @test
- */
- public function setRulesWithRuleWithOnlyColumnNumberSetsLineNumber(): void
- {
- $ruleToSet = new Rule('color');
- $ruleToSet->setPosition(null, 42);
-
- $this->subject->setRules([$ruleToSet]);
-
- self::assertIsInt($ruleToSet->getLineNumber(), 'line number not set');
- self::assertGreaterThanOrEqual(1, $ruleToSet->getLineNumber(), 'line number not valid');
- }
-
- /**
- * @test
- */
- public function setRulesWithRuleWithOnlyColumnNumberPreservesColumnNumber(): void
- {
- $ruleToSet = new Rule('color');
- $ruleToSet->setPosition(null, 42);
-
- $this->subject->setRules([$ruleToSet]);
-
- self::assertSame(42, $ruleToSet->getColumnNumber(), 'column number not preserved');
- }
-
- /**
- * @test
- */
- public function setRulesWithRuleWithCompletePositionPreservesPosition(): void
- {
- $ruleToSet = new Rule('color');
- $ruleToSet->setPosition(42, 64);
-
- $this->subject->setRules([$ruleToSet]);
-
- self::assertSame(42, $ruleToSet->getLineNumber(), 'line number not preserved');
- self::assertSame(64, $ruleToSet->getColumnNumber(), 'column number not preserved');
- }
-
- /**
- * @test
- *
- * @param list $propertyNamesToSet
- *
- * @dataProvider providePropertyNames
- */
- public function getRulesReturnsRulesSet(array $propertyNamesToSet): void
- {
- $rulesToSet = self::createRulesFromPropertyNames($propertyNamesToSet);
- $this->subject->setRules($rulesToSet);
-
- $result = $this->subject->getRules();
-
- self::assertSame($rulesToSet, $result);
- }
-
- /**
- * @test
- */
- 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);
- $this->subject->setRules([$third, $second, $first]);
-
- $result = $this->subject->getRules();
-
- self::assertSame([$first, $second, $third], $result);
- }
-
- /**
- * @test
- */
- public function getRulesOrdersRulesWithSameLineNumberByColumnNumber(): void
- {
- $first = (new Rule('color'))->setPosition(1, 11);
- $second = (new Rule('display'))->setPosition(1, 42);
- $third = (new Rule('color'))->setPosition(1, 64);
- $this->subject->setRules([$third, $second, $first]);
-
- $result = $this->subject->getRules();
-
- self::assertSame([$first, $second, $third], $result);
- }
-
- /**
- * @return array, 1: string, 2: list}>
- */
- public static function providePropertyNamesAndSearchPatternAndMatchingPropertyNames(): array
- {
- return [
- 'single rule matched' => [
- ['color'],
- 'color',
- ['color'],
- ],
- 'first rule matched' => [
- ['color', 'display'],
- 'color',
- ['color'],
- ],
- 'last rule matched' => [
- ['color', 'display'],
- 'display',
- ['display'],
- ],
- 'middle rule matched' => [
- ['color', 'display', 'width'],
- 'display',
- ['display'],
- ],
- 'multiple rules for the same property matched' => [
- ['color', 'color'],
- 'color',
- ['color'],
- ],
- 'multiple rules for the same property matched in haystack' => [
- ['color', 'display', 'color', 'width'],
- 'color',
- ['color'],
- ],
- 'shorthand rule matched' => [
- ['font'],
- 'font-',
- ['font'],
- ],
- 'longhand rule matched' => [
- ['font-size'],
- 'font-',
- ['font-size'],
- ],
- 'shorthand and longhand rule matched' => [
- ['font', 'font-size'],
- 'font-',
- ['font', 'font-size'],
- ],
- 'shorthand rule matched in haystack' => [
- ['font', 'color'],
- 'font-',
- ['font'],
- ],
- 'longhand rule matched in haystack' => [
- ['font-size', 'color'],
- 'font-',
- ['font-size'],
- ],
- 'rules 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' => [
- ['contain', 'container', 'container-type'],
- 'contain',
- ['contain'],
- ],
- ];
- }
-
- /**
- * @test
- *
- * @param list $propertyNamesToSet
- * @param list $matchingPropertyNames
- *
- * @dataProvider providePropertyNamesAndSearchPatternAndMatchingPropertyNames
- */
- public function getRulesWithPatternReturnsAllMatchingRules(
- array $propertyNamesToSet,
- string $searchPattern,
- array $matchingPropertyNames
- ): void {
- $rulesToSet = self::createRulesFromPropertyNames($propertyNamesToSet);
- // Use `array_values` to ensure canonical numeric array, since `array_filter` does not reindex.
- $matchingRules = \array_values(
- \array_filter(
- $rulesToSet,
- static function (Rule $rule) use ($matchingPropertyNames): bool {
- return \in_array($rule->getRule(), $matchingPropertyNames, true);
- }
- )
- );
- $this->subject->setRules($rulesToSet);
-
- $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);
- }
-
- /**
- * @return array, 1: string}>
- */
- public static function providePropertyNamesAndNonMatchingSearchPattern(): array
- {
- return [
- 'no match in empty list' => [
- [],
- 'color',
- ],
- 'no match for different property' => [
- ['color'],
- 'display',
- ],
- 'no match for property not in list' => [
- ['color', 'display'],
- 'width',
- ],
- ];
- }
-
- /**
- * @test
- *
- * @param list $propertyNamesToSet
- *
- * @dataProvider providePropertyNamesAndNonMatchingSearchPattern
- */
- public function getRulesWithNonMatchingPatternReturnsEmptyArray(
- array $propertyNamesToSet,
- string $searchPattern
- ): void {
- $this->setRulesFromPropertyNames($propertyNamesToSet);
-
- $result = $this->subject->getRules($searchPattern);
-
- self::assertSame([], $result);
- }
-
- /**
- * @test
- */
- public function getRulesWithPatternOrdersRulesByPosition(): void
- {
- $first = (new Rule('color'))->setPosition(1, 42);
- $second = (new Rule('color'))->setPosition(1, 64);
- $third = (new Rule('color'))->setPosition(55, 7);
- $this->subject->setRules([$third, $second, $first]);
-
- $result = $this->subject->getRules('color');
-
- self::assertSame([$first, $second, $third], $result);
- }
-
- /**
- * @return array}>
- */
- public static function provideDistinctPropertyNames(): array
- {
- return [
- 'no properties' => [[]],
- 'one property' => [['color']],
- 'two properties' => [['color', 'display']],
- ];
- }
-
- /**
- * @test
- *
- * @param list $propertyNamesToSet
- *
- * @dataProvider provideDistinctPropertyNames
- */
- public function getRulesAssocReturnsAllRulesWithDistinctPropertyNames(array $propertyNamesToSet): void
- {
- $rulesToSet = self::createRulesFromPropertyNames($propertyNamesToSet);
- $this->subject->setRules($rulesToSet);
-
- $result = $this->subject->getRulesAssoc();
-
- self::assertSame($rulesToSet, \array_values($result));
- }
-
- /**
- * @test
- */
- public function getRulesAssocReturnsLastRuleWithSamePropertyName(): void
- {
- $firstRule = new Rule('color');
- $lastRule = new Rule('color');
- $this->subject->setRules([$firstRule, $lastRule]);
-
- $result = $this->subject->getRulesAssoc();
-
- self::assertSame([$lastRule], \array_values($result));
- }
-
- /**
- * @test
- */
- public function getRulesAssocOrdersRulesByPosition(): void
- {
- $first = (new Rule('color'))->setPosition(1, 42);
- $second = (new Rule('display'))->setPosition(1, 64);
- $third = (new Rule('width'))->setPosition(55, 7);
- $this->subject->setRules([$third, $second, $first]);
-
- $result = $this->subject->getRulesAssoc();
-
- self::assertSame([$first, $second, $third], \array_values($result));
- }
-
- /**
- * @test
- */
- public function getRulesAssocKeysRulesByPropertyName(): void
- {
- $this->subject->setRules([new Rule('color'), new Rule('display')]);
-
- $result = $this->subject->getRulesAssoc();
-
- foreach ($result as $key => $rule) {
- self::assertSame($rule->getRule(), $key);
- }
- }
-
- /**
- * @test
- *
- * @param list $propertyNamesToSet
- * @param list $matchingPropertyNames
- *
- * @dataProvider providePropertyNamesAndSearchPatternAndMatchingPropertyNames
- */
- public function getRulesAssocWithPatternReturnsAllMatchingPropertyNames(
- array $propertyNamesToSet,
- string $searchPattern,
- array $matchingPropertyNames
- ): void {
- $this->setRulesFromPropertyNames($propertyNamesToSet);
-
- $result = $this->subject->getRulesAssoc($searchPattern);
-
- $resultPropertyNames = \array_keys($result);
- \sort($matchingPropertyNames);
- \sort($resultPropertyNames);
- self::assertSame($matchingPropertyNames, $resultPropertyNames);
- }
-
- /**
- * @test
- *
- * @param list $propertyNamesToSet
- *
- * @dataProvider providePropertyNamesAndNonMatchingSearchPattern
- */
- public function getRulesAssocWithNonMatchingPatternReturnsEmptyArray(
- array $propertyNamesToSet,
- string $searchPattern
- ): void {
- $this->setRulesFromPropertyNames($propertyNamesToSet);
-
- $result = $this->subject->getRulesAssoc($searchPattern);
-
- self::assertSame([], $result);
- }
-
- /**
- * @test
- */
- public function getRulesAssocWithPatternOrdersRulesByPosition(): void
- {
- $first = (new Rule('font'))->setPosition(1, 42);
- $second = (new Rule('font-family'))->setPosition(1, 64);
- $third = (new Rule('font-weight'))->setPosition(55, 7);
- $this->subject->setRules([$third, $second, $first]);
-
- $result = $this->subject->getRules('font-');
-
- self::assertSame([$first, $second, $third], \array_values($result));
- }
-
- /**
- * @param list $propertyNames
- */
- private function setRulesFromPropertyNames(array $propertyNames): void
- {
- $this->subject->setRules(self::createRulesFromPropertyNames($propertyNames));
- }
-
- /**
- * @param list $propertyNames
- *
- * @return list
- */
- private static function createRulesFromPropertyNames(array $propertyNames): array
- {
- return \array_map(
- function (string $propertyName): Rule {
- return new Rule($propertyName);
- },
- $propertyNames
- );
- }
-}
diff --git a/tests/Unit/RuleSet/RuleSetTest.php b/tests/Unit/RuleSet/RuleSetTest.php
index 92c056ca0..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
@@ -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/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);
+ }
+}
diff --git a/tests/Unit/Value/CSSFunctionTest.php b/tests/Unit/Value/CSSFunctionTest.php
new file mode 100644
index 000000000..4ec56afd2
--- /dev/null
+++ b/tests/Unit/Value/CSSFunctionTest.php
@@ -0,0 +1,40 @@
+getArrayRepresentation();
+
+ self::assertSame('CSSFunction', $result['class']);
+ }
+
+ /**
+ * @test
+ */
+ public function getArrayRepresentationIncludesFunctionName(): void
+ {
+ $subject = new CSSFunction('filter', []);
+
+ $result = $subject->getArrayRepresentation();
+
+ self::assertSame('filter', $result['name']);
+ }
+}
diff --git a/tests/Unit/Value/CSSStringTest.php b/tests/Unit/Value/CSSStringTest.php
index c54ecdd75..6f6b5018f 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;
@@ -80,4 +81,67 @@ public function getLineNumberReturnsLineNumberProvidedToConstructor(): void
self::assertSame($lineNumber, $subject->getLineNumber());
}
+
+ /**
+ * @test
+ */
+ public function getArrayRepresentationIncludesClassName(): void
+ {
+ $subject = new CSSString('');
+
+ $result = $subject->getArrayRepresentation();
+
+ self::assertSame('CSSString', $result['class']);
+ }
+
+ /**
+ * @test
+ */
+ public function getArrayRepresentationIncludesContents(): void
+ {
+ $contents = 'What is love?';
+ $subject = new CSSString($contents);
+
+ $result = $subject->getArrayRepresentation();
+
+ 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/Unit/Value/CalcFunctionTest.php b/tests/Unit/Value/CalcFunctionTest.php
new file mode 100644
index 000000000..330c43b72
--- /dev/null
+++ b/tests/Unit/Value/CalcFunctionTest.php
@@ -0,0 +1,213 @@
+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]);
+
+ $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]);
+
+ $nestedCalc = $components[2];
+ self::assertInstanceOf(CalcFunction::class, $nestedCalc);
+
+ $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