diff --git a/.gitattributes b/.gitattributes
index 4bf8ac39d..82a4f0b95 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -4,7 +4,9 @@
/.gitignore export-ignore
/.phive/ export-ignore
/CODE_OF_CONDUCT.md export-ignore
+/CONTRIBUTING.md export-ignore
/bin/ export-ignore
/config/ export-ignore
+/docs/ export-ignore
/phpunit.xml export-ignore
/tests/ export-ignore
diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md
deleted file mode 100644
index 19269ae90..000000000
--- a/.github/CONTRIBUTING.md
+++ /dev/null
@@ -1,12 +0,0 @@
-# Contributing to PHP-CSS-Parser
-
-Those that wish to contribute bug fixes, new features, refactorings and
-clean-up to PHP-CSS-Parser are more than welcome.
-
-When you contribute, please take the following things into account:
-
-## Contributor Code of Conduct
-
-Please note that this project is released with a
-[Contributor Code of Conduct](../CODE_OF_CONDUCT.md). By participating in this
-project, you agree to abide by its terms.
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index 919acb304..76a72c265 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -6,6 +6,9 @@ updates:
directory: "/"
schedule:
interval: "daily"
+ commit-message:
+ prefix: "[Dependabot] "
+ milestone: 10
- package-ecosystem: "composer"
directory: "/"
@@ -13,4 +16,14 @@ updates:
interval: "daily"
allow:
- dependency-type: "development"
+ ignore:
+ - dependency-name: "phpstan/*"
+ - dependency-name: "phpunit/phpunit"
+ versions: [ ">= 9.0.0" ]
+ - dependency-name: "rector/rector"
+ - dependency-name: "thecodingmachine/safe"
+ - dependency-name: "thecodingmachine/phpstan-safe-rule"
versioning-strategy: "increase"
+ commit-message:
+ prefix: "[Dependabot] "
+ milestone: 10
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 76a2d3559..251e8fc4d 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -16,11 +16,11 @@ jobs:
runs-on: ubuntu-22.04
strategy:
matrix:
- php-version: [ '7.2', '7.3', '7.4', '8.0', '8.1', '8.2', '8.3' ]
+ php-version: [ '7.2', '7.3', '7.4', '8.0', '8.1', '8.2', '8.3', '8.4', '8.5' ]
steps:
- name: Checkout
- uses: actions/checkout@v4
+ uses: actions/checkout@v5
- name: Install PHP
uses: shivammathur/setup-php@v2
@@ -59,11 +59,11 @@ jobs:
strategy:
fail-fast: false
matrix:
- php-version: [ '7.2', '7.3', '7.4', '8.0', '8.1', '8.2', '8.3' ]
+ php-version: [ '7.2', '7.3', '7.4', '8.0', '8.1', '8.2', '8.3', '8.4', '8.5' ]
steps:
- name: Checkout
- uses: actions/checkout@v4
+ uses: actions/checkout@v5
- name: Install PHP
uses: shivammathur/setup-php@v2
@@ -103,15 +103,16 @@ jobs:
fail-fast: false
matrix:
command:
- - fixer
- - stan
- - rector
+ - composer:normalize
+ - php:fixer
+ - php:stan
+ - php:rector
php-version:
- '8.3'
steps:
- name: Checkout
- uses: actions/checkout@v4
+ uses: actions/checkout@v5
- name: Install PHP
uses: shivammathur/setup-php@v2
@@ -138,8 +139,10 @@ jobs:
composer show;
- name: Install development tools
+ env:
+ GITHUB_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
- phive --no-progress install --trust-gpg-keys BBAB5DF0A0D6672989CF1869E82B2FB314E9906E
+ phive --no-progress install --trust-gpg-keys 0FDE18AE1D09E19F60F6B1CBC00543248C87FB13,BBAB5DF0A0D6672989CF1869E82B2FB314E9906E
- name: Run Command
- run: composer ci:php:${{ matrix.command }}
+ run: composer ci:${{ matrix.command }}
diff --git a/.github/workflows/codecoverage.yml b/.github/workflows/codecoverage.yml
index 7a094fdc8..29c37ba50 100644
--- a/.github/workflows/codecoverage.yml
+++ b/.github/workflows/codecoverage.yml
@@ -15,12 +15,14 @@ jobs:
runs-on: ubuntu-22.04
strategy:
+ fail-fast: false
matrix:
- php-version: [ '7.4' ]
+ php-version:
+ - '7.4'
steps:
- name: Checkout
- uses: actions/checkout@v4
+ uses: actions/checkout@v5
- name: Install PHP
uses: shivammathur/setup-php@v2
@@ -30,6 +32,9 @@ jobs:
tools: composer:v2
coverage: xdebug
+ - name: Show the Composer version
+ run: composer --version
+
- name: Show the Composer configuration
run: composer config --global --list
@@ -47,11 +52,13 @@ jobs:
composer show;
- name: Run Tests
- run: ./vendor/bin/phpunit --coverage-clover build/coverage/xml
+ run: composer ci:tests:coverage
+
+ - name: Show generated coverage files
+ run: ls -lah
- - name: Upload coverage results to Codacy
+ - name: Upload coverage results to Coveralls
+ uses: coverallsapp/github-action@v2
env:
- CODACY_PROJECT_TOKEN: ${{ secrets.CODACY_PROJECT_TOKEN }}
- if: "${{ env.CODACY_PROJECT_TOKEN != '' }}"
- run: |
- ./vendor/bin/codacycoverage clover build/coverage/xml
+ github-token: ${{ secrets.GITHUB_TOKEN }}
+ file: coverage.xml
diff --git a/.gitignore b/.gitignore
index acf0d9d86..8bdbea99c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,6 +3,7 @@
/.php_cs.cache
/.phpunit.result.cache
/composer.lock
+/coverage.xml
/phpstan.neon
/vendor/
!/.phive/phars.xml
diff --git a/.phive/phars.xml b/.phive/phars.xml
index a19b8834e..d9ab49f38 100644
--- a/.phive/phars.xml
+++ b/.phive/phars.xml
@@ -1,4 +1,5 @@
-
+
+
diff --git a/CHANGELOG.md b/CHANGELOG.md
index ed390901c..8978fbb71 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,40 +3,96 @@
All notable changes to this project will be documented in this file.
This project adheres to [Semantic Versioning](https://semver.org/).
+Please also have a look at our
+[API and deprecation policy](docs/API-and-deprecation-policy.md).
+
## x.y.z
### Added
-- Support arithmetic operators in CSS function arguments (#607)
-- Add support for inserting an item in a CSS list (#545)
+
+### Changed
+
+### Deprecated
+
+### Removed
+
+### Fixed
+
+- Use typesafe versions of PHP functions (#1379, #1380, #1382, #1383, #1384)
+
+### Documentation
+
+## 9.1.0: Add support for PHP 8.5
+
+### Added
+
+- Add support for PHP 8.5 (#1355)
+
+### Fixed
+
+- Improve performance of selector validation
+ (avoiding silent PCRE catastrophic failure) (#1372)
+- Use typesafe versions of PHP functions (#1368, #1370)
+
+## 9.0.0: New features, deprecation removals and bug fixes
+
+### Added
+
+- Interface `RuleContainer` for `RuleSet` `Rule` manipulation methods (#1256)
+- Partial support for CSS Color Module Level 4:
+ - `rgb` and `rgba`, and `hsl` and `hsla` are now aliases (#797)
+ - Parse color functions that use the "modern" syntax (#800)
+ - Render RGB functions with "modern" syntax when required (#840)
+ - Support `none` as color function component value (#859)
- Add a class diagram to the README (#482)
-- Add support for the `dvh`, `lvh` and `svh` length units (#415)
- Add more tests (#449)
### Changed
-- Improve performance of Value::parseValue with many delimiters by refactoring to remove array_search() (#413)
+- `DeclarationBlock` no longer extends `RuleSet` and instead has a `RuleSet` as
+ a property; use `getRuleSet()` to access it directly (#1194)
+- The default line (and column) number is now `null` (not zero) (#1288)
+- `setPosition()` (in `Rule` and other classes) now has fluent interface,
+ returning itself (#1259)
+- `RuleSet::removeRule()` now only allows `Rule` as the parameter
+ (implementing classes are `AtRuleSet` and `DeclarationBlock`);
+ use `removeMatchingRules()` or `removeAllRules()` for other functions (#1255)
+- `RuleSet::getRules()` and `getRulesAssoc()` now only allow `string` or `null`
+ as the parameter (implementing classes are `AtRuleSet` and `DeclarationBlock`)
+ (#1253)
+- Initialize `KeyFrame` properties to sensible defaults (#1146)
+- Make `OutputFormat` `final` (#1128)
+- Make `Selector` a `Renderable` (#1017)
+- Only allow `string` for some `OutputFormat` properties (#885)
+- Use more native type declarations and strict mode
+ (#641, #772, #774, #778, #804, #841, #873, #875, #891, #922, #923, #933, #958,
+ #964, #967, #1000, #1044, #1134, #1136, #1137, #1139, #1140, #1141, #1145,
+ #1162, #1163, #1166, #1172, #1174, #1178, #1179, #1181, #1183, #1184, #1186,
+ #1187, #1190, #1192, #1193, #1203)
- Add visibility to all class/interface constants (#469)
-### Deprecated
-
-- Deprecate `DeclarationBlock::createBorderShorthand()` (#578)
-- Deprecate `DeclarationBlock::createFontShorthand()` (#580)
-- Deprecate `DeclarationBlock::createDimensionsShorthand()` (#579)
-- Deprecate `DeclarationBlock::createListStyleShorthand()` (#577)
-- Deprecate `DeclarationBlock::createBackgroundShorthand()` (#576)
-- Deprecate `DeclarationBlock::createShorthandProperties()` (#575)
-- Deprecate `DeclarationBlock::expandListStyleShorthand()` (#574)
-- Deprecate `DeclarationBlock::expandBackgroundShorthand()` (#573)
-- Deprecate `DeclarationBlock::expandFontShorthand()` (#572)
-- Deprecate `DeclarationBlock::expandDimensionsShorthand()` (#571)
-- Deprecate `DeclarationBlock::expandBorderShorthand()` (#570)
-- Deprecate `DeclarationBlock::createShorthands()` (#569)
-- Deprecate `Document::expandShorthands()` (#566)
-- Deprecate `Document::createShorthands()` (#567)
-- Deprecate `DeclarationBlock::expandShorthands()` (#558)
-
### Removed
+- Remove `getLineNo()` from these classes (use `getLineNumber()` instead):
+ `Comment`, `CSSList`, `SourceException`, `Charset`, `CSSNamespace`, `Import`,
+ `Rule`, `DeclarationBlock`, `RuleSet`, `CSSFunction`, `Value` (#1258)
+- Remove `Rule::getColNo()` (use `getColumnNumber()` instead) (#1287)
+- Passing a string as the first argument to `getAllValues()` is no longer
+ supported and will not work;
+ the search pattern should now be passed as the second argument (#1243)
+- Passing a Boolean as the second argument to `getAllValues()` is no longer
+ supported and will not work; the flag for searching in function arguments
+ should now be passed as the third argument (#1243)
+- Remove `__toString()` (#1046)
+- Drop magic method forwarding in `OutputFormat` (#898)
+- Drop `atRuleArgs()` from the `AtRule` interface (#1141)
+- Remove `OutputFormat::get()` and `::set()` (#1108, #1110)
+- Drop special support for vendor prefixes (#1083)
+- Remove the IE hack in `Rule` (#995)
+- Drop `getLineNo()` from the `Renderable` interface (#1038)
+- Remove `OutputFormat::level()` (#874)
+- Remove expansion of shorthand properties (#838)
+- Remove `Parser::setCharset/getCharset` (#808)
- Remove `Rule::getValues()` (#582)
- Remove `Rule::setValues()` (#562)
- Remove `Document::getAllSelectors()` (#561)
@@ -46,13 +102,166 @@ This project adheres to [Semantic Versioning](https://semver.org/).
### Fixed
-- Fix undefined local variable in `CalcFunction::parse()` (#593)
-- Fix PHP notice caused by parsing invalid color values having less than 6 characters (#485)
-- Fix (regression) failure to parse at-rules with strict parsing (#456)
+- Remove trailing semicolon from declaration blocks with 'compact'
+ `OutputFormat` (#1345)
+- Parse selector functions (like `:not`) with comma-separated arguments (#1292)
+- Parse quoted attribute selector value containing comma (#1323)
+- Allow comma in selectors (e.g. `:not(html, body)`) (#1293)
+- Insert `Rule` before sibling even with different property name
+ (in `RuleSet::addRule()`) (#1270)
+- Ensure `RuleSet::addRule()` sets non-negative column number when sibling
+ provided (#1268)
+- Don't render `rgb` colors with percentage values using hex notation (#803)
+
+### Documentation
+
+- Add an API and deprecation policy (#720)
@ziegenberg is a new contributor to this release and did a lot of the heavy
lifting. Thanks! :heart:
+## 8.9.0: New features, bug fixes and deprecations
+
+### Added
+
+- `RuleSet::removeMatchingRules()` method
+ (for the implementing classes `AtRuleSet` and `DeclarationBlock`) (#1249)
+- `RuleSet::removeAllRules()` method
+ (for the implementing classes `AtRuleSet` and `DeclarationBlock`) (#1249)
+- Add Interface `CSSElement` (#1231)
+- Methods `getLineNumber` and `getColumnNumber` which return a nullable `int`
+ for the following classes:
+ `Comment`, `CSSList`, `SourceException`, `Charset`, `CSSNamespace`, `Import`,
+ `Rule`, `DeclarationBlock`, `RuleSet`, `CSSFunction`, `Value` (#1225, #1263)
+- `Positionable` interface for CSS items that may have a position
+ (line and perhaps column number) in the parsed CSS (#1221)
+
+### Changed
+
+- Parameters for `getAllValues()` are deconflated, so it now takes three (all
+ optional), allowing `$element` and `$ruleSearchPattern` to be specified
+ separately (#1241)
+- Implement `Positionable` in the following CSS item classes:
+ `Comment`, `CSSList`, `SourceException`, `Charset`, `CSSNamespace`, `Import`,
+ `Rule`, `DeclarationBlock`, `RuleSet`, `CSSFunction`, `Value` (#1225)
+
+### Deprecated
+
+- Support for PHP < 7.2 is deprecated; version 9.0 will require PHP 7.2 or later
+ (#1264)
+- Passing a `string` or `null` to `RuleSet::removeRule()` is deprecated
+ (implementing classes are `AtRuleSet` and `DeclarationBlock`);
+ use `removeMatchingRules()` or `removeAllRules()` instead (#1249)
+- Passing a `Rule` to `RuleSet::getRules()` or `getRulesAssoc()` is deprecated,
+ affecting the implementing classes `AtRuleSet` and `DeclarationBlock`
+ (call e.g. `getRules($rule->getRule())` instead) (#1248)
+- Passing a string as the first argument to `getAllValues()` is deprecated;
+ the search pattern should now be passed as the second argument (#1241)
+- Passing a Boolean as the second argument to `getAllValues()` is deprecated;
+ the flag for searching in function arguments should now be passed as the third
+ argument (#1241)
+- `getLineNo()` is deprecated in these classes (use `getLineNumber()` instead):
+ `Comment`, `CSSList`, `SourceException`, `Charset`, `CSSNamespace`, `Import`,
+ `Rule`, `DeclarationBlock`, `RuleSet`, `CSSFunction`, `Value` (#1225, #1233)
+- `Rule::getColNo()` is deprecated (use `getColumnNumber()` instead)
+ (#1225, #1233)
+- Providing zero as the line number argument to `Rule::setPosition()` is
+ deprecated (pass `null` instead if there is no line number) (#1225, #1233)
+
+### Fixed
+
+- Set line number when `RuleSet::addRule()` called with only column number set
+ (#1265)
+- Ensure first rule added with `RuleSet::addRule()` has valid position (#1262)
+
+## 8.8.0: Bug fixes and deprecations
+
+### Added
+
+- `OutputFormat` properties for space around specific list separators (#880)
+
+### Changed
+
+- Mark the `OutputFormat` constructor as `@internal` (#1131)
+- Mark `OutputFormatter` as `@internal` (#896)
+- Mark `Selector::isValid()` as `@internal` (#1037)
+- Mark parsing-related methods of most CSS elements as `@internal` (#908)
+- Mark `OutputFormat::nextLevel()` as `@internal` (#901)
+- Make all non-private properties `@internal` (#886)
+
+### Deprecated
+
+- Deprecate extending `OutputFormat` (#1131)
+- Deprecate `OutputFormat::get()` and `::set()` (#1107)
+- Deprecate support for `-webkit-calc` and `-moz-calc` (#1086)
+- Deprecate magic method forwarding from `OutputFormat` to `OutputFormatter`
+ (#894)
+- Deprecate `__toString()` (#1006)
+- Deprecate greedy calculation of selector specificity (#1018)
+- Deprecate the IE hack in `Rule` (#993, #1003)
+- `OutputFormat` properties for space around list separators as an array (#880)
+- Deprecate `OutputFormat::level()` (#870)
+
+### Fixed
+
+- Include comments for all rules in declaration block (#1169)
+- Render rules in line and column number order (#1059)
+- Create `Size` with correct types in `expandBackgroundShorthand` (#814)
+- Parse `@font-face` `src` property as comma-delimited list (#794)
+
+## 8.7.0: Add support for PHP 8.4
+
+### Added
+
+- Add support for PHP 8.4 (#643, #657)
+
+### Changed
+
+- Mark parsing-internal classes and methods as `@internal` (#674)
+- Block installations on unsupported higher PHP versions (#691)
+
+### Deprecated
+
+- Deprecate the expansion of shorthand properties
+ (#578, #580, #579, #577, #576, #575, #574, #573, #572, #571, #570, #569, #566,
+ #567, #558, #714)
+- Deprecate `Parser::setCharset()` and `Parser::getCharset()` (#688)
+
+### Fixed
+
+- Fix type errors in PHP strict mode (#664)
+
+## 8.6.0
+
+### Added
+
+- Support arithmetic operators in CSS function arguments (#607)
+- Add support for inserting an item in a CSS list (#545)
+- Add support for the `dvh`, `lvh` and `svh` length units (#415)
+
+### Changed
+
+- Improve performance of `Value::parseValue` with many delimiters by refactoring
+ to remove `array_search()` (#413)
+
+## 8.5.2
+
+### Changed
+
+- Mark all class constants as `@internal` (#472)
+
+### Fixed
+
+- Fix undefined local variable in `CalcFunction::parse()` (#593)
+
+## 8.5.1
+
+### Fixed
+
+- Fix PHP notice caused by parsing invalid color values having less than
+ 6 characters (#485)
+- Fix (regression) failure to parse at-rules with strict parsing (#456)
+
## 8.5.0
### Added
@@ -75,7 +284,8 @@ lifting. Thanks! :heart:
* Support for PHP 8.x
* PHPDoc annotations
-* Allow usage of CSS variables inside color functions (by parsing them as regular functions)
+* Allow usage of CSS variables inside color functions (by parsing them as
+ regular functions)
* Use PSR-12 code style
* *No deprecations*
@@ -90,7 +300,10 @@ lifting. Thanks! :heart:
* Allow a file to end after an `@import`
* Preserve case of CSS variables as specced
* Allow identifiers to use escapes the same way as strings
-* No longer use `eval` for the comparison in `getSelectorsBySpecificity`, in case it gets passed untrusted input (CVE-2020-13756). Also fixed in 8.3.1, 8.2.1, 8.1.1, 8.0.1, 7.0.4, 6.0.2, 5.2.1, 5.1.3, 5.0.9, 4.0.1, 3.0.1, 2.0.1, 1.0.1.
+* No longer use `eval` for the comparison in `getSelectorsBySpecificity`, in
+ case it gets passed untrusted input (CVE-2020-13756). Also fixed in 8.3.1,
+ 8.2.1, 8.1.1, 8.0.1, 7.0.4, 6.0.2, 5.2.1, 5.1.3, 5.0.9, 4.0.1, 3.0.1, 2.0.1,
+ 1.0.1.
* Prevent an infinite loop when parsing invalid grid line names
* Remove invalid unit `vm`
* Retain rule order after expanding shorthands
@@ -102,11 +315,16 @@ lifting. Thanks! :heart:
## 8.3.0 (2019-02-22)
-* Refactor parsing logic to mostly reside in the class files whose data structure is to be parsed (this should eventually allow us to unit-test specific parts of the parsing logic individually).
-* Fix error in parsing `calc` expessions when the first operand is a negative number, thanks to @raxbg.
-* Support parsing CSS4 colors in hex notation with alpha values, thanks to @raxbg.
+* Refactor parsing logic to mostly reside in the class files whose data
+ structure is to be parsed (this should eventually allow us to unit-test
+ specific parts of the parsing logic individually).
+* Fix error in parsing `calc` expessions when the first operand is a negative
+ number, thanks to @raxbg.
+* Support parsing CSS4 colors in hex notation with alpha values, thanks to
+ @raxbg.
* Swallow more errors in lenient mode, thanks to @raxbg.
-* Allow specifying arbitrary strings to output before and after declaration blocks, thanks to @westonruter.
+* Allow specifying arbitrary strings to output before and after declaration
+ blocks, thanks to @westonruter.
* *No backwards-incompatible changes*
* *No deprecations*
@@ -114,16 +332,20 @@ lifting. Thanks! :heart:
* Support parsing `calc()`, thanks to @raxbg.
* Support parsing grid-lines, again thanks to @raxbg.
-* Support parsing legacy IE filters (`progid:`) in lenient mode, thanks to @FMCorz
+* Support parsing legacy IE filters (`progid:`) in lenient mode, thanks to
+ @FMCorz
* Performance improvements parsing large files, again thanks to @FMCorz
* *No backwards-incompatible changes*
* *No deprecations*
## 8.1.0 (2016-07-19)
-* Comments are no longer silently ignored but stored with the object with which they appear (no render support, though). Thanks to @FMCorz.
-* The IE hacks using `\0` and `\9` can now be parsed (and rendered) in lenient mode. Thanks (again) to @FMCorz.
-* Media queries with or without spaces before the query are parsed. Still no *real* parsing support, though. Sorry…
+* Comments are no longer silently ignored but stored with the object with which
+ they appear (no render support, though). Thanks to @FMCorz.
+* The IE hacks using `\0` and `\9` can now be parsed (and rendered) in lenient
+ mode. Thanks (again) to @FMCorz.
+* Media queries with or without spaces before the query are parsed. Still no
+ *real* parsing support, though. Sorry…
* PHPUnit is now listed as a dev-dependency in composer.json.
* *No backwards-incompatible changes*
* *No deprecations*
@@ -135,7 +357,8 @@ lifting. Thanks! :heart:
### Backwards-incompatible changes
-* Unrecoverable parser errors throw an exception of type `Sabberworm\CSS\Parsing\SourceException` instead of `\Exception`.
+* Unrecoverable parser errors throw an exception of type
+ `Sabberworm\CSS\Parsing\SourceException` instead of `\Exception`.
## 7.0.3 (2016-04-27)
@@ -145,7 +368,8 @@ lifting. Thanks! :heart:
## 7.0.2 (2016-02-11)
-* 150 time performance boost thanks to @[ossinkine](https://github.com/ossinkine)
+* 150 time performance boost thanks
+ to @[ossinkine](https://github.com/ossinkine)
* *No backwards-incompatible changes*
* *No deprecations*
@@ -162,7 +386,8 @@ lifting. Thanks! :heart:
### Backwards-incompatible changes
-* The `Sabberworm\CSS\Value\String` class has been renamed to `Sabberworm\CSS\Value\CSSString`.
+* The `Sabberworm\CSS\Value\String` class has been renamed to
+ `Sabberworm\CSS\Value\CSSString`.
## 6.0.1 (2015-08-24)
@@ -176,22 +401,27 @@ lifting. Thanks! :heart:
### Deprecations
-* The parse() method replaces __toString with an optional argument (instance of the OutputFormat class)
+* The parse() method replaces __toString with an optional argument (instance of
+ the OutputFormat class)
## 5.2.0 (2014-06-30)
-* Support removing a selector from a declaration block using `$oBlock->removeSelector($mSelector)`
-* Introduce a specialized exception (Sabberworm\CSS\Parsing\OuputException) for exceptions during output rendering
+* Support removing a selector from a declaration block using
+ `$oBlock->removeSelector($mSelector)`
+* Introduce a specialized exception (Sabberworm\CSS\Parsing\OuputException) for
+ exceptions during output rendering
* *No deprecations*
#### Backwards-incompatible changes
-* Outputting a declaration block that has no selectors throws an OuputException instead of outputting an invalid ` {…}` into the CSS document.
+* Outputting a declaration block that has no selectors throws an OuputException
+ instead of outputting an invalid ` {…}` into the CSS document.
## 5.1.2 (2013-10-30)
-* Remove the use of consumeUntil in comment parsing. This makes it possible to parse comments such as `/** Perfectly valid **/`
+* Remove the use of consumeUntil in comment parsing. This makes it possible to
+ parse comments such as `/** Perfectly valid **/`
* Add fr relative size unit
* Fix some issues with HHVM
* *No backwards-incompatible changes*
@@ -206,13 +436,15 @@ lifting. Thanks! :heart:
## 5.1.0 (2013-10-24)
* Performance enhancements by Michael M Slusarz
-* More rescue entry points for lenient parsing (unexpected tokens between declaration blocks and unclosed comments)
+* More rescue entry points for lenient parsing (unexpected tokens between
+ declaration blocks and unclosed comments)
* *No backwards-incompatible changes*
* *No deprecations*
## 5.0.8 (2013-08-15)
-* Make default settings’ multibyte parsing option dependent on whether or not the mbstring extension is actually installed.
+* Make default settings’ multibyte parsing option dependent on whether or not
+ the mbstring extension is actually installed.
* *No backwards-incompatible changes*
* *No deprecations*
@@ -230,7 +462,9 @@ lifting. Thanks! :heart:
## 5.0.5 (2013-04-17)
-* Initial support for lenient parsing (setting this parser option will catch some exceptions internally and recover the parser’s state as neatly as possible).
+* Initial support for lenient parsing (setting this parser option will catch
+ some exceptions internally and recover the parser’s state as neatly as
+ possible).
* *No backwards-incompatible changes*
* *No deprecations*
@@ -267,18 +501,22 @@ lifting. Thanks! :heart:
### Backwards-incompatible changes
-* `Sabberworm\CSS\Value\Color`’s `__toString` method overrides `CSSList`’s to maybe return something other than `type(value, …)` (see above).
+* `Sabberworm\CSS\Value\Color`’s `__toString` method overrides `CSSList`’s to
+ maybe return something other than `type(value, …)` (see above).
## 4.0.0 (2013-03-19)
* Support for more @-rules
-* Generic interface `Sabberworm\CSS\Property\AtRule`, implemented by all @-rule classes
+* Generic interface `Sabberworm\CSS\Property\AtRule`, implemented by all @-rule
+ classes
* *No deprecations*
### Backwards-incompatible changes
* `Sabberworm\CSS\RuleSet\AtRule` renamed to `Sabberworm\CSS\RuleSet\AtRuleSet`
-* `Sabberworm\CSS\CSSList\MediaQuery` renamed to `Sabberworm\CSS\RuleSet\CSSList\AtRuleBlockList` with differing semantics and API (which also works for other block-list-based @-rules like `@supports`).
+* `Sabberworm\CSS\CSSList\MediaQuery` renamed to
+ `Sabberworm\CSS\RuleSet\CSSList\AtRuleBlockList` with differing semantics and
+ API (which also works for other block-list-based @-rules like `@supports`).
## 3.0.0 (2013-03-06)
@@ -287,10 +525,18 @@ lifting. Thanks! :heart:
### Backwards-incompatible changes
-* All properties (like whether or not to use `mb_`-functions, which default charset to use and – new – whether or not to be forgiving when parsing) are now encapsulated in an instance of `Sabberworm\CSS\Settings` which can be passed as the second argument to `Sabberworm\CSS\Parser->__construct()`.
-* Specifying a charset as the second argument to `Sabberworm\CSS\Parser->__construct()` is no longer supported. Use `Sabberworm\CSS\Settings::create()->withDefaultCharset('some-charset')` instead.
-* Setting `Sabberworm\CSS\Parser->bUseMbFunctions` has no effect. Use `Sabberworm\CSS\Settings::create()->withMultibyteSupport(true/false)` instead.
-* `Sabberworm\CSS\Parser->parse()` may throw a `Sabberworm\CSS\Parsing\UnexpectedTokenException` when in strict parsing mode.
+* All properties (like whether or not to use `mb_`-functions, which default
+ charset to use and – new – whether or not to be forgiving when parsing) are
+ now encapsulated in an instance of `Sabberworm\CSS\Settings` which can be
+ passed as the second argument to `Sabberworm\CSS\Parser->__construct()`.
+* Specifying a charset as the second argument to
+ `Sabberworm\CSS\Parser->__construct()` is no longer supported. Use
+ `Sabberworm\CSS\Settings::create()->withDefaultCharset('some-charset')`
+ instead.
+* Setting `Sabberworm\CSS\Parser->bUseMbFunctions` has no effect. Use
+ `Sabberworm\CSS\Settings::create()->withMultibyteSupport(true/false)` instead.
+* `Sabberworm\CSS\Parser->parse()` may throw a
+ `Sabberworm\CSS\Parsing\UnexpectedTokenException` when in strict parsing mode.
## 2.0.0 (2013-01-29)
@@ -298,8 +544,13 @@ lifting. Thanks! :heart:
### Backwards-incompatible changes
-* `Sabberworm\CSS\RuleSet->getRules()` returns an index-based array instead of an associative array. Use `Sabberworm\CSS\RuleSet->getRulesAssoc()` (which eliminates duplicate rules and lets the later rule of the same name win).
-* `Sabberworm\CSS\RuleSet->removeRule()` works as it did before except when passed an instance of `Sabberworm\CSS\Rule\Rule`, in which case it would only remove the exact rule given instead of all the rules of the same type. To get the old behaviour, use `Sabberworm\CSS\RuleSet->removeRule($oRule->getRule()`;
+* `Sabberworm\CSS\RuleSet->getRules()` returns an index-based array instead of
+ an associative array. Use `Sabberworm\CSS\RuleSet->getRulesAssoc()` (which
+ eliminates duplicate rules and lets the later rule of the same name win).
+* `Sabberworm\CSS\RuleSet->removeRule()` works as it did before except when
+ passed an instance of `Sabberworm\CSS\Rule\Rule`, in which case it would only
+ remove the exact rule given instead of all the rules of the same type. To get
+ the old behaviour, use `Sabberworm\CSS\RuleSet->removeRule($oRule->getRule()`;
## 1.0
diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md
index 552b3aae5..1b87c0935 100644
--- a/CODE_OF_CONDUCT.md
+++ b/CODE_OF_CONDUCT.md
@@ -1,76 +1,119 @@
-# Contributor Code of Conduct
+# Contributor Covenant Code of Conduct
## Our Pledge
-In the interest of fostering an open and welcoming environment, we as
-contributors and maintainers pledge to making participation in our project and
-our community a harassment-free experience for everyone, regardless of age,
-body size, disability, ethnicity, gender identity and expression, level of
-experience, nationality, personal appearance, race, religion, or sexual
+We as members, contributors, and leaders pledge to make participation in our
+community a harassment-free experience for everyone, regardless of age, body
+size, visible or invisible disability, ethnicity, sex characteristics, gender
+identity and expression, level of experience, education, socio-economic status,
+nationality, personal appearance, race, caste, color, religion, or sexual
identity and orientation.
+We pledge to act and interact in ways that contribute to an open, welcoming,
+diverse, inclusive, and healthy community.
+
## Our Standards
-Examples of behavior that contributes to creating a positive environment
-include:
+Examples of behavior that contributes to a positive environment for our
+community include:
-* Using welcoming and inclusive language
-* Being respectful of differing viewpoints and experiences
-* Gracefully accepting constructive criticism
-* Focusing on what is best for the community
-* Showing empathy towards other community members
+* Demonstrating empathy and kindness toward other people
+* Being respectful of differing opinions, viewpoints, and experiences
+* Giving and gracefully accepting constructive feedback
+* Accepting responsibility and apologizing to those affected by our mistakes,
+ and learning from the experience
+* Focusing on what is best not just for us as individuals, but for the overall
+ community
-Examples of unacceptable behavior by participants include:
+Examples of unacceptable behavior include:
-* The use of sexualized language or imagery and unwelcome sexual attention or
- advances
-* Trolling, insulting/derogatory comments, and personal or political attacks
+* The use of sexualized language or imagery, and sexual attention or advances of
+ any kind
+* Trolling, insulting or derogatory comments, and personal or political attacks
* Public or private harassment
-* Publishing others' private information, such as a physical or electronic
- address, without explicit permission
+* Publishing others' private information, such as a physical or email address,
+ without their explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
-## Our Responsibilities
+## Enforcement Responsibilities
-Project maintainers are responsible for clarifying the standards of acceptable
-behavior and are expected to take appropriate and fair corrective action in
-response to any instances of unacceptable behavior.
+Community leaders are responsible for clarifying and enforcing our standards of
+acceptable behavior and will take appropriate and fair corrective action in
+response to any behavior that they deem inappropriate, threatening, offensive,
+or harmful.
-Project maintainers have the right and responsibility to remove, edit, or
-reject comments, commits, code, wiki edits, issues, and other contributions
-that are not aligned to this Code of Conduct, or to ban temporarily or
-permanently any contributor for other behaviors that they deem inappropriate,
-threatening, offensive, or harmful.
+Community leaders have the right and responsibility to remove, edit, or reject
+comments, commits, code, wiki edits, issues, and other contributions that are
+not aligned to this Code of Conduct, and will communicate reasons for moderation
+decisions when appropriate.
## Scope
-This Code of Conduct applies both within project spaces and in public spaces
-when an individual is representing the project or its community. Examples of
-representing a project or community include using an official project e-mail
-address, posting via an official social media account, or acting as an
-appointed representative at an online or offline event. Representation of a
-project may be further defined and clarified by project maintainers.
+This Code of Conduct applies within all community spaces, and also applies when
+an individual is officially representing the community in public spaces.
+Examples of representing our community include using an official email address,
+posting via an official social media account, or acting as an appointed
+representative at an online or offline event.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
-reported by contacting the project team at (emogrifier at myintervals dot com).
-All complaints will be reviewed and investigated and will result in a response
-that is deemed necessary and appropriate to the circumstances. The project team
-is obligated to maintain confidentiality with regard to the reporter of an
-incident. Further details of specific enforcement policies may be posted
-separately.
+reported to the community leaders responsible for enforcement at
+(myintervals-coc at gaggle dot email).
+All complaints will be reviewed and investigated promptly and fairly.
-Project maintainers who do not follow or enforce the Code of Conduct in good
-faith may face temporary or permanent repercussions as determined by other
-members of the project's leadership.
+All community leaders are obligated to respect the privacy and security of the
+reporter of any incident.
-## Attribution
+## Enforcement Guidelines
+
+Community leaders will follow these Community Impact Guidelines in determining
+the consequences for any action they deem in violation of this Code of Conduct:
+
+### 1. Correction
+
+**Community Impact**: Use of inappropriate language or other behavior deemed
+unprofessional or unwelcome in the community.
+
+**Consequence**: A private, written warning from community leaders, providing
+clarity around the nature of the violation and an explanation of why the
+behavior was inappropriate. A public apology may be requested.
+
+### 2. Warning
+
+**Community Impact**: A violation through a single incident or series of
+actions.
-This Code of Conduct is adapted from the [Contributor Covenant][homepage],
-version 1.4, available at
-[http://contributor-covenant.org/version/1/4/][version].
+**Consequence**: A warning with consequences for continued behavior. No
+interaction with the people involved, including unsolicited interaction with
+those enforcing the Code of Conduct, for a specified period of time. This
+includes avoiding interactions in community spaces as well as external channels
+like social media. Violating these terms may lead to a temporary or permanent
+ban.
+
+### 3. Temporary Ban
+
+**Community Impact**: A serious violation of community standards, including
+sustained inappropriate behavior.
+
+**Consequence**: A temporary ban from any sort of interaction or public
+communication with the community for a specified period of time. No public or
+private interaction with the people involved, including unsolicited interaction
+with those enforcing the Code of Conduct, is allowed during this period.
+Violating these terms may lead to a permanent ban.
+
+### 4. Permanent Ban
+
+**Community Impact**: Demonstrating a pattern of violation of community
+standards, including sustained inappropriate behavior, harassment of an
+individual, or aggression toward or disparagement of classes of individuals.
+
+**Consequence**: A permanent ban from any sort of public interaction within the
+community.
+
+## Attribution
-[homepage]: http://contributor-covenant.org
-[version]: http://contributor-covenant.org/version/1/4/
+This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org/),
+version 2.1, available at
+https://www.contributor-covenant.org/version/2/1/code_of_conduct.html.
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 000000000..1d0085f3a
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,196 @@
+# Contributing to PHP-CSS-Parser
+
+Those that wish to contribute bug fixes, new features, refactorings and
+clean-up to PHP-CSS-Parser are more than welcome.
+
+When you contribute, please take the following things into account:
+
+## Contributor Code of Conduct
+
+Please note that this project is released with a
+[Contributor Code of Conduct](../CODE_OF_CONDUCT.md). By participating in this
+project, you agree to abide by its terms.
+
+## General workflow
+
+This is the workflow for contributing changes to this project::
+
+1. [Fork the Git repository](https://docs.github.com/en/get-started/exploring-projects-on-github/contributing-to-a-project).
+1. Clone your forked repository locally and install the development
+ dependencies.
+1. Create a local branch for your changes.
+1. Add unit tests for your changes.
+ These tests should fail without your changes.
+1. Add your changes. Your added unit tests now should pass, and no other tests
+ should be broken. Check that your changes follow the same coding style as the
+ rest of the project.
+1. Add a changelog entry, newest on top.
+1. Commit and push your changes.
+1. [Create a pull request](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/about-pull-requests)
+ for your changes.
+1. Check that the CI build is green. (If it is not, fix the problems listed.)
+ Please note that for first-time contributors, you will need to wait for a
+ maintainer to allow your CI build to run.
+1. Wait for a review by the maintainers.
+1. Polish your changes as needed until they are ready to be merged.
+
+## About code reviews
+
+After you have submitted a pull request, the maintainers will review your
+changes. This will probably result in quite a few comments on ways to improve
+your pull request. This project receives contributions from developers around
+the world, so we need the code to be the most consistent, readable, and
+maintainable that it can be.
+
+Please do not feel frustrated by this - instead please view this both as our
+contribution to your pull request as well as a way to learn more about
+improving code quality.
+
+If you would like to know whether an idea would fit in the general strategy of
+this project or would like to get feedback on the best architecture for your
+ideas, we propose you open a ticket first and discuss your ideas there
+first before investing a lot of time in writing code.
+
+## Install the development dependencies
+
+To install the most important development dependencies, please run the following
+command:
+
+```bash
+composer install
+```
+
+We also have some optional development dependencies that require higher PHP
+versions than the lowest PHP version this project supports. Hence they are not
+installed by default.
+
+To install these, you will need to have [PHIVE](https://phar.io/) installed.
+You can then run the following command:
+
+```bash
+phive install
+```
+
+## Unit-test your changes
+
+Please cover all changes with unit tests and make sure that your code does not
+break any existing tests. We will only merge pull requests that include full
+code coverage of the fixed bugs and the new features.
+
+To run the existing PHPUnit tests, run this command:
+
+```bash
+composer ci:tests:unit
+```
+
+## Coding Style
+
+Please use the same coding style
+([PER 2.0](https://www.php-fig.org/per/coding-style/)) as the rest of the code.
+Indentation is four spaces.
+
+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
+```
+
+Please make your code clean, well-readable and easy to understand.
+
+If you add new methods or fields, please add proper PHPDoc for the new
+methods/fields. Please use grammatically correct, complete sentences in the
+code documentation.
+
+You can autoformat your code using the following command:
+
+```bash
+composer fix
+```
+
+## Git commits
+
+Commit message should have a <= 50-character summary, optionally followed by a
+blank line and a more in depth description of 79 characters per line.
+
+Please use grammatically correct, complete sentences in the commit messages.
+
+Also, please prefix the subject line of the commit message with either
+`[FEATURE]`, `[TASK]`, `[BUGFIX]` OR `[CLEANUP]`. This makes it faster to see
+what a commit is about.
+
+## Creating pull requests (PRs)
+
+When you create a pull request, please
+[make your PR editable](https://github.com/blog/2247-improving-collaboration-with-forks).
+
+## Rebasing
+
+If other PRs have been merged during the time between your initial PR creation
+and final approval, it may be required that you rebase your changes against the
+latest `main` branch.
+
+There are potential pitfalls here if you follow the suggestions from `git`,
+which could leave your branch in an unrecoverable mess,
+and you having to start over with a new branch and new PR.
+
+The procedure below is tried and tested, and will help you avoid frustration.
+
+To rebase a feature branch to the latest `main`:
+
+1. Make sure that your local copy of the repository has the most up-to-date
+ revisions of `main` (this is important, otherwise you may end up rebasing to
+ an older base point):
+ ```bash
+ git switch main
+ git pull
+ ```
+1. Switch to the (feature) branch to be rebased and make sure your copy is up to
+ date:
+ ```bash
+ git switch feature/something-cool
+ git pull
+ ```
+1. Consider taking a copy of the folder tree at this stage; this may help when
+ resolving conflicts in the next step.
+1. Begin the rebasing process
+ ```bash
+ git rebase main
+ ```
+1. Resolve the conflicts in the reported files. (This will typically require
+ reversing the order of the new entries in `CHANGELOG.md`.) You may use a
+ folder `diff` against the copy taken at step 3 to assist, but bear in mind
+ that at this stage `git` is partway through rebasing, so some files will have
+ been merged and include the latest changes from `main`, whilst others might
+ not. In any case, you should ignore changes to files not reported as having
+ conflicts.
+
+ If there were no conflicts, skip this and the next step.
+1. Mark the conflicting files as resolved and continue the rebase
+ ```bash
+ git add .
+ git rebase --continue
+ ```
+ (You can alternatively use more specific wildcards or specify individual
+ files with a full relative path.)
+
+ If there were no conflicts reported in the previous step, skip this step.
+
+ If there are more conflicts to resolve, repeat the previous step then this
+ step again.
+1. Force-push the rebased (feature) branch to the remote repository
+ ```bash
+ git push --force
+ ```
+ The `--force` option is important. Without it, you'll get an error with a
+ hint suggesting a `git pull` is required:
+ ```
+ hint: Updates were rejected because the tip of your current branch is behind
+ hint: its remote counterpart. Integrate the remote changes (e.g.
+ hint: 'git pull ...') before pushing again.
+ hint: See the 'Note about fast-forwards' in 'git push --help' for details.
+ ```
+ ***DO NOT*** follow the hint and execute `git pull`. This will result in the
+ set of all commits on the feature branch being duplicated, and the "patching
+ base" not being moved at all.
diff --git a/README.md b/README.md
index 597949e64..176f49ca8 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,7 @@
# PHP CSS Parser
-[](https://github.com/MyIntervals/PHP-CSS-Parser/actions/)
+[](https://github.com/MyIntervals/PHP-CSS-Parser/actions/)
+[](https://coveralls.io/github/MyIntervals/PHP-CSS-Parser?branch=main)
A Parser for CSS Files written in PHP. Allows extraction of CSS files into a data structure, manipulation of said structure and output as (optimized) CSS.
@@ -220,44 +221,44 @@ html, body {
```php
class Sabberworm\CSS\CSSList\Document#4 (2) {
- protected $aContents =>
+ protected $contents =>
array(4) {
[0] =>
class Sabberworm\CSS\Property\Charset#6 (2) {
- private $sCharset =>
+ private $charset =>
class Sabberworm\CSS\Value\CSSString#5 (2) {
- private $sString =>
+ private $string =>
string(5) "utf-8"
- protected $iLineNo =>
+ protected $lineNumber =>
int(1)
}
- protected $iLineNo =>
+ protected $lineNumber =>
int(1)
}
[1] =>
class Sabberworm\CSS\RuleSet\AtRuleSet#7 (4) {
- private $sType =>
+ private $type =>
string(9) "font-face"
- private $sArgs =>
+ private $arguments =>
string(0) ""
- private $aRules =>
+ private $rules =>
array(2) {
'font-family' =>
array(1) {
[0] =>
class Sabberworm\CSS\Rule\Rule#8 (4) {
- private $sRule =>
+ private $rule =>
string(11) "font-family"
- private $mValue =>
+ private $value =>
class Sabberworm\CSS\Value\CSSString#9 (2) {
- private $sString =>
+ private $string =>
string(10) "CrassRoots"
- protected $iLineNo =>
+ protected $lineNumber =>
int(4)
}
- private $bIsImportant =>
+ private $isImportant =>
bool(false)
- protected $iLineNo =>
+ protected $lineNumber =>
int(4)
}
}
@@ -265,76 +266,76 @@ class Sabberworm\CSS\CSSList\Document#4 (2) {
array(1) {
[0] =>
class Sabberworm\CSS\Rule\Rule#10 (4) {
- private $sRule =>
+ private $rule =>
string(3) "src"
- private $mValue =>
+ private $value =>
class Sabberworm\CSS\Value\URL#11 (2) {
- private $oURL =>
+ private $url =>
class Sabberworm\CSS\Value\CSSString#12 (2) {
- private $sString =>
+ private $string =>
string(15) "../media/cr.ttf"
- protected $iLineNo =>
+ protected $lineNumber =>
int(5)
}
- protected $iLineNo =>
+ protected $lineNumber =>
int(5)
}
- private $bIsImportant =>
+ private $isImportant =>
bool(false)
- protected $iLineNo =>
+ protected $lineNumber =>
int(5)
}
}
}
- protected $iLineNo =>
+ protected $lineNumber =>
int(3)
}
[2] =>
class Sabberworm\CSS\RuleSet\DeclarationBlock#13 (3) {
- private $aSelectors =>
+ private $selectors =>
array(2) {
[0] =>
class Sabberworm\CSS\Property\Selector#14 (2) {
- private $sSelector =>
+ private $selector =>
string(4) "html"
- private $iSpecificity =>
+ private $specificity =>
NULL
}
[1] =>
class Sabberworm\CSS\Property\Selector#15 (2) {
- private $sSelector =>
+ private $selector =>
string(4) "body"
- private $iSpecificity =>
+ private $specificity =>
NULL
}
}
- private $aRules =>
+ private $rules =>
array(1) {
'font-size' =>
array(1) {
[0] =>
class Sabberworm\CSS\Rule\Rule#16 (4) {
- private $sRule =>
+ private $rule =>
string(9) "font-size"
- private $mValue =>
+ private $value =>
class Sabberworm\CSS\Value\Size#17 (4) {
- private $fSize =>
+ private $size =>
double(1.6)
- private $sUnit =>
+ private $unit =>
string(2) "em"
- private $bIsColorComponent =>
+ private $isColorComponent =>
bool(false)
- protected $iLineNo =>
+ protected $lineNumber =>
int(9)
}
- private $bIsImportant =>
+ private $isImportant =>
bool(false)
- protected $iLineNo =>
+ protected $lineNumber =>
int(9)
}
}
}
- protected $iLineNo =>
+ protected $lineNumber =>
int(8)
}
[3] =>
@@ -343,96 +344,96 @@ class Sabberworm\CSS\CSSList\Document#4 (2) {
string(9) "keyframes"
private $animationName =>
string(6) "mymove"
- protected $aContents =>
+ protected $contents =>
array(2) {
[0] =>
class Sabberworm\CSS\RuleSet\DeclarationBlock#19 (3) {
- private $aSelectors =>
+ private $selectors =>
array(1) {
[0] =>
class Sabberworm\CSS\Property\Selector#20 (2) {
- private $sSelector =>
+ private $selector =>
string(4) "from"
- private $iSpecificity =>
+ private $specificity =>
NULL
}
}
- private $aRules =>
+ private $rules =>
array(1) {
'top' =>
array(1) {
[0] =>
class Sabberworm\CSS\Rule\Rule#21 (4) {
- private $sRule =>
+ private $rule =>
string(3) "top"
- private $mValue =>
+ private $value =>
class Sabberworm\CSS\Value\Size#22 (4) {
- private $fSize =>
+ private $size =>
double(0)
- private $sUnit =>
+ private $unit =>
string(2) "px"
- private $bIsColorComponent =>
+ private $isColorComponent =>
bool(false)
- protected $iLineNo =>
+ protected $lineNumber =>
int(13)
}
- private $bIsImportant =>
+ private $isImportant =>
bool(false)
- protected $iLineNo =>
+ protected $lineNumber =>
int(13)
}
}
}
- protected $iLineNo =>
+ protected $lineNumber =>
int(13)
}
[1] =>
class Sabberworm\CSS\RuleSet\DeclarationBlock#23 (3) {
- private $aSelectors =>
+ private $selectors =>
array(1) {
[0] =>
class Sabberworm\CSS\Property\Selector#24 (2) {
- private $sSelector =>
+ private $selector =>
string(2) "to"
- private $iSpecificity =>
+ private $specificity =>
NULL
}
}
- private $aRules =>
+ private $rules =>
array(1) {
'top' =>
array(1) {
[0] =>
class Sabberworm\CSS\Rule\Rule#25 (4) {
- private $sRule =>
+ private $rule =>
string(3) "top"
- private $mValue =>
+ private $value =>
class Sabberworm\CSS\Value\Size#26 (4) {
- private $fSize =>
+ private $size =>
double(200)
- private $sUnit =>
+ private $unit =>
string(2) "px"
- private $bIsColorComponent =>
+ private $isColorComponent =>
bool(false)
- protected $iLineNo =>
+ protected $lineNumber =>
int(14)
}
- private $bIsImportant =>
+ private $isImportant =>
bool(false)
- protected $iLineNo =>
+ protected $lineNumber =>
int(14)
}
}
}
- protected $iLineNo =>
+ protected $lineNumber =>
int(14)
}
}
- protected $iLineNo =>
+ protected $lineNumber =>
int(12)
}
}
- protected $iLineNo =>
+ protected $lineNumber =>
int(1)
}
@@ -466,85 +467,85 @@ html, body {font-size: 1.6em;}
```php
class Sabberworm\CSS\CSSList\Document#4 (2) {
- protected $aContents =>
+ protected $contents =>
array(1) {
[0] =>
class Sabberworm\CSS\RuleSet\DeclarationBlock#5 (3) {
- private $aSelectors =>
+ private $selectors =>
array(1) {
[0] =>
class Sabberworm\CSS\Property\Selector#6 (2) {
- private $sSelector =>
+ private $selector =>
string(7) "#header"
- private $iSpecificity =>
+ private $specificity =>
NULL
}
}
- private $aRules =>
+ private $rules =>
array(3) {
'margin' =>
array(1) {
[0] =>
class Sabberworm\CSS\Rule\Rule#7 (4) {
- private $sRule =>
+ private $rule =>
string(6) "margin"
- private $mValue =>
+ private $value =>
class Sabberworm\CSS\Value\RuleValueList#12 (3) {
- protected $aComponents =>
+ protected $components =>
array(4) {
[0] =>
class Sabberworm\CSS\Value\Size#8 (4) {
- private $fSize =>
+ private $size =>
double(10)
- private $sUnit =>
+ private $unit =>
string(2) "px"
- private $bIsColorComponent =>
+ private $isColorComponent =>
bool(false)
- protected $iLineNo =>
+ protected $lineNumber =>
int(2)
}
[1] =>
class Sabberworm\CSS\Value\Size#9 (4) {
- private $fSize =>
+ private $size =>
double(2)
- private $sUnit =>
+ private $unit =>
string(2) "em"
- private $bIsColorComponent =>
+ private $isColorComponent =>
bool(false)
- protected $iLineNo =>
+ protected $lineNumber =>
int(2)
}
[2] =>
class Sabberworm\CSS\Value\Size#10 (4) {
- private $fSize =>
+ private $size =>
double(1)
- private $sUnit =>
+ private $unit =>
string(2) "cm"
- private $bIsColorComponent =>
+ private $isColorComponent =>
bool(false)
- protected $iLineNo =>
+ protected $lineNumber =>
int(2)
}
[3] =>
class Sabberworm\CSS\Value\Size#11 (4) {
- private $fSize =>
+ private $size =>
double(2)
- private $sUnit =>
+ private $unit =>
string(1) "%"
- private $bIsColorComponent =>
+ private $isColorComponent =>
bool(false)
- protected $iLineNo =>
+ protected $lineNumber =>
int(2)
}
}
- protected $sSeparator =>
+ protected $separator =>
string(1) " "
- protected $iLineNo =>
+ protected $lineNumber =>
int(2)
}
- private $bIsImportant =>
+ private $isImportant =>
bool(false)
- protected $iLineNo =>
+ protected $lineNumber =>
int(2)
}
}
@@ -552,11 +553,11 @@ class Sabberworm\CSS\CSSList\Document#4 (2) {
array(1) {
[0] =>
class Sabberworm\CSS\Rule\Rule#13 (4) {
- private $sRule =>
+ private $rule =>
string(11) "font-family"
- private $mValue =>
+ private $value =>
class Sabberworm\CSS\Value\RuleValueList#15 (3) {
- protected $aComponents =>
+ protected $components =>
array(4) {
[0] =>
string(7) "Verdana"
@@ -564,9 +565,9 @@ class Sabberworm\CSS\CSSList\Document#4 (2) {
string(9) "Helvetica"
[2] =>
class Sabberworm\CSS\Value\CSSString#14 (2) {
- private $sString =>
+ private $string =>
string(9) "Gill Sans"
- protected $iLineNo =>
+ protected $lineNumber =>
int(3)
}
[3] =>
@@ -574,12 +575,12 @@ class Sabberworm\CSS\CSSList\Document#4 (2) {
}
protected $sSeparator =>
string(1) ","
- protected $iLineNo =>
+ protected $lineNumber =>
int(3)
}
- private $bIsImportant =>
+ private $isImportant =>
bool(false)
- protected $iLineNo =>
+ protected $lineNumber =>
int(3)
}
}
@@ -587,22 +588,22 @@ class Sabberworm\CSS\CSSList\Document#4 (2) {
array(1) {
[0] =>
class Sabberworm\CSS\Rule\Rule#16 (4) {
- private $sRule =>
+ private $rule =>
string(5) "color"
- private $mValue =>
+ private $value =>
string(3) "red"
- private $bIsImportant =>
+ private $isImportant =>
bool(true)
- protected $iLineNo =>
+ protected $lineNumber =>
int(4)
}
}
}
- protected $iLineNo =>
+ protected $lineNumber =>
int(1)
}
}
- protected $iLineNo =>
+ protected $lineNumber =>
int(1)
}
@@ -621,164 +622,201 @@ class Sabberworm\CSS\CSSList\Document#4 (2) {
classDiagram
direction LR
- %% Start of the part generated from the PHP code using tasuku43/mermaid-class-diagram
-
- class Renderable {
- <>
+ class Anchor {
}
- class DeclarationBlock {
+ class AtRule {
+ <>
}
- class RuleSet {
- <>
+ class AtRuleBlockList {
}
class AtRuleSet {
}
- class KeyframeSelector {
+ class CSSBlockList {
+ <>
}
- class AtRule {
+ class CSSElement {
<>
}
- class Charset {
+ class CSSFunction {
}
- class Import {
+ class CSSList {
+ <>
}
- class Selector {
+ class CSSListItem {
+ <>
}
class CSSNamespace {
}
- class Settings {
+ class CSSString {
}
- class Rule {
+ class CalcFunction {
}
- class Parser {
+ class CalcRuleValueList {
}
- class OutputFormatter {
+ class Charset {
}
- class OutputFormat {
+ class Color {
}
- class OutputException {
+ class Comment {
}
- class UnexpectedEOFException {
+ class Commentable {
+ <>
}
- class SourceException {
+ class DeclarationBlock {
}
- class UnexpectedTokenException {
+ class Document {
}
- class ParserState {
+ class Import {
}
- class Anchor {
+ class KeyFrame {
}
- class CSSBlockList {
- <>
+ class KeyframeSelector {
}
- class Document {
+ class LineName {
}
- class CSSList {
- <>
+ class OutputException {
}
- class KeyFrame {
+ class OutputFormat {
}
- class AtRuleBlockList {
+ class OutputFormatter {
}
- class Color {
+ class Parser {
}
- class URL {
+ class ParserState {
}
- class CalcRuleValueList {
+ class Positionable {
+ <>
}
- class ValueList {
+ class PrimitiveValue {
<>
}
- class CalcFunction {
+ class Renderable {
+ <>
}
- class LineName {
+ class Rule {
}
- class Value {
- <>
+ class RuleContainer {
+ <>
+ }
+ class RuleSet {
+ }
+ class RuleValueList {
+ }
+ class Selector {
+ }
+ class Settings {
}
class Size {
}
- class CSSString {
+ class SourceException {
}
- class PrimitiveValue {
- <>
+ class SpecificityCalculator {
}
- class CSSFunction {
+ class URL {
}
- class RuleValueList {
+ class UnexpectedEOFException {
}
- class Commentable {
- <>
+ class UnexpectedTokenException {
}
- class Comment {
+ class Value {
+ <>
+ }
+ class ValueList {
+ <>
}
- RuleSet <|-- DeclarationBlock: inheritance
- Renderable <|.. RuleSet: realization
- Commentable <|.. RuleSet: realization
- RuleSet <|-- AtRuleSet: inheritance
+ Anchor ..> ParserState: dependency
+ CSSListItem <|-- AtRule: inheritance
+ AtRule <|.. AtRuleBlockList: realization
+ CSSBlockList <|-- AtRuleBlockList: inheritance
AtRule <|.. AtRuleSet: realization
- Selector <|-- KeyframeSelector: inheritance
- Renderable <|-- AtRule: inheritance
- Commentable <|-- AtRule: inheritance
- AtRule <|.. Charset: realization
- AtRule <|.. Import: realization
- AtRule <|.. CSSNamespace: realization
- Renderable <|.. Rule: realization
- Commentable <|.. Rule: realization
- SourceException <|-- OutputException: inheritance
- UnexpectedTokenException <|-- UnexpectedEOFException: inheritance
- Exception <|-- SourceException: inheritance
- SourceException <|-- UnexpectedTokenException: inheritance
+ RuleSet <|-- AtRuleSet: inheritance
CSSList <|-- CSSBlockList: inheritance
+ Renderable <|-- CSSElement: inheritance
+ ValueList <|-- CSSFunction: inheritance
+ CSSElement <|.. CSSList: realization
+ CSSListItem <|.. CSSList: realization
+ CSSList ..> Charset: dependency
+ CSSList ..> Import: dependency
+ Positionable <|.. CSSList: realization
+ Commentable <|-- CSSListItem: inheritance
+ Renderable <|-- CSSListItem: inheritance
+ AtRule <|.. CSSNamespace: realization
+ Positionable <|.. CSSNamespace: realization
+ PrimitiveValue <|-- CSSString: inheritance
+ CSSFunction <|-- CalcFunction: inheritance
+ RuleValueList <|-- CalcRuleValueList: inheritance
+ AtRule <|.. Charset: realization
+ Charset ..> CSSString: dependency
+ Positionable <|.. Charset: realization
+ CSSFunction <|-- Color: inheritance
+ Positionable <|.. Comment: realization
+ Renderable <|.. Comment: realization
+ CSSElement <|.. DeclarationBlock: realization
+ CSSListItem <|.. DeclarationBlock: realization
+ Positionable <|.. DeclarationBlock: realization
+ RuleContainer <|.. DeclarationBlock: realization
+ DeclarationBlock ..> RuleSet : dependency
+ DeclarationBlock ..> Selector: dependency
CSSBlockList <|-- Document: inheritance
- Renderable <|.. CSSList: realization
- Commentable <|.. CSSList: realization
- CSSList <|-- KeyFrame: inheritance
+ AtRule <|.. Import: realization
+ Positionable <|.. Import: realization
AtRule <|.. KeyFrame: realization
- CSSBlockList <|-- AtRuleBlockList: inheritance
- AtRule <|.. AtRuleBlockList: realization
- CSSFunction <|-- Color: inheritance
- PrimitiveValue <|-- URL: inheritance
- RuleValueList <|-- CalcRuleValueList: inheritance
- Value <|-- ValueList: inheritance
- CSSFunction <|-- CalcFunction: inheritance
+ CSSList <|-- KeyFrame: inheritance
+ Selector <|-- KeyframeSelector: inheritance
ValueList <|-- LineName: inheritance
- Renderable <|.. Value: realization
- PrimitiveValue <|-- Size: inheritance
- PrimitiveValue <|-- CSSString: inheritance
+ SourceException <|-- OutputException: inheritance
+ OutputFormat ..> OutputFormatter: dependency
+ OutputFormatter ..> OutputFormat: dependency
+ Parser ..> ParserState: dependency
+ ParserState ..> Settings: dependency
Value <|-- PrimitiveValue: inheritance
- ValueList <|-- CSSFunction: inheritance
+ CSSElement <|.. Rule: realization
+ Commentable <|.. Rule: realization
+ Positionable <|.. Rule: realization
+ Rule ..> RuleValueList: dependency
+ CSSElement <|.. RuleSet: realization
+ CSSListItem <|.. RuleSet: realization
+ Positionable <|.. RuleSet: realization
+ RuleSet ..> Rule: dependency
+ RuleContainer <|.. RuleSet: realization
ValueList <|-- RuleValueList: inheritance
- Renderable <|.. Comment: realization
+ Renderable <|.. Selector: realization
+ PrimitiveValue <|-- Size: inheritance
+ Exception <|-- SourceException: inheritance
+ Positionable <|.. SourceException: realization
+ URL ..> CSSString: dependency
+ PrimitiveValue <|-- URL: inheritance
+ UnexpectedTokenException <|-- UnexpectedEOFException: inheritance
+ SourceException <|-- UnexpectedTokenException: inheritance
+ CSSElement <|.. Value: realization
+ Positionable <|.. Value: realization
+ Value <|-- ValueList: inheritance
- %% end of the generated part
-
-
- Anchor --> "1" ParserState : oParserState
- CSSList --> "*" CSSList : aContents
- CSSList --> "*" Charset : aContents
- CSSList --> "*" Comment : aComments
- CSSList --> "*" Import : aContents
- CSSList --> "*" RuleSet : aContents
- CSSNamespace --> "*" Comment : aComments
- Charset --> "*" Comment : aComments
- Charset --> "1" CSSString : oCharset
- DeclarationBlock --> "*" Selector : aSelectors
- Import --> "*" Comment : aComments
- OutputFormat --> "1" OutputFormat : oNextLevelFormat
- OutputFormat --> "1" OutputFormatter : oFormatter
- OutputFormatter --> "1" OutputFormat : oFormat
- Parser --> "1" ParserState : oParserState
- ParserState --> "1" Settings : oParserSettings
- Rule --> "*" Comment : aComments
- Rule --> "1" RuleValueList : mValue
- RuleSet --> "*" Comment : aComments
- RuleSet --> "*" Rule : aRules
- URL --> "1" CSSString : oURL
- ValueList --> "*" Value : aComponents
+ CSSList ..> CSSList: dependency
+ CSSList ..> Comment: dependency
+ CSSList ..> RuleSet: dependency
+ CSSNamespace ..> Comment: dependency
+ Charset ..> Comment: dependency
+ Import ..> Comment: dependency
+ OutputFormat ..> OutputFormat: dependency
+ Rule ..> Comment: dependency
+ RuleSet ..> Comment: dependency
+ ValueList ..> Value: dependency
```
+## API and deprecation policy
+
+Please have a look at our
+[API and deprecation policy](docs/API-and-deprecation-policy.md).
+
+## Contributing
+
+Contributions in the form of bug reports, feature requests, or pull requests are
+more than welcome. :pray: Please have a look at our
+[contribution guidelines](CONTRIBUTING.md) to learn more about how to
+contribute to PHP-CSS-Parser.
+
## Contributors/Thanks to
* [oliverklee](https://github.com/oliverklee) for lots of refactorings, code modernizations and CI integrations
@@ -801,14 +839,3 @@ classDiagram
### Legacy Support
The latest pre-PSR-0 version of this project can be checked with the `0.9.0` tag.
-
-### Running Tests
-
-To run all continuous integration (CI) checks for this project (including unit tests),
-* run `composer install` to install the development dependencies managed with Composer;
-* run `phive install` to install the development dependencies managed with PHIVE;
- * [Installation of PHIVE](https://github.com/phar-io/phive?tab=readme-ov-file#getting-phive)
-* run `composer ci` to run all static and dynamic CI checks.
-
-Details of other Composer scripts available (e.g. to run one specific CI check) are provided with `composer list`.
-
diff --git a/bin/quickdump.php b/bin/quickdump.php
index fa622abd4..dc0fe8697 100755
--- a/bin/quickdump.php
+++ b/bin/quickdump.php
@@ -1,23 +1,29 @@
#!/usr/bin/env php
parse();
+$document = $parser->parse();
echo "\n" . '#### Input' . "\n\n```css\n";
-print $sSource;
+print $source;
echo "\n```\n\n" . '#### Structure (`var_dump()`)' . "\n\n```php\n";
-\var_dump($oDoc);
+\var_dump($document);
echo "\n```\n\n" . '#### Output (`render()`)' . "\n\n```css\n";
-print $oDoc->render();
+print $document->render();
echo "\n```\n";
diff --git a/composer.json b/composer.json
index 2f85e5565..bec30b48d 100644
--- a/composer.json
+++ b/composer.json
@@ -1,14 +1,13 @@
{
"name": "sabberworm/php-css-parser",
- "type": "library",
"description": "Parser for CSS Files written in PHP",
+ "license": "MIT",
+ "type": "library",
"keywords": [
"parser",
"css",
"stylesheet"
],
- "homepage": "https://www.sabberworm.com/blog/2010/6/10/php-css-parser",
- "license": "MIT",
"authors": [
{
"name": "Raphael Schweikert"
@@ -22,18 +21,23 @@
"email": "jake.github@qzdesign.co.uk"
}
],
+ "homepage": "https://www.sabberworm.com/blog/2010/6/10/php-css-parser",
"require": {
- "php": ">=7.2.0",
- "ext-iconv": "*"
+ "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"
},
"require-dev": {
- "codacy/coverage": "^1.4.3",
- "php-parallel-lint/php-parallel-lint": "^1.4.0",
- "phpstan/extension-installer": "^1.4.1",
- "phpstan/phpstan": "^1.11.6",
- "phpstan/phpstan-phpunit": "^1.4.0",
- "phpunit/phpunit": "^8.5.38",
- "rector/rector": "^1.2.0"
+ "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",
+ "rawr/phpunit-data-provider": "3.3.1",
+ "rector/rector": "1.2.10 || 2.1.7",
+ "rector/type-perfect": "1.0.0 || 2.1.0",
+ "thecodingmachine/phpstan-safe-rule": "1.2.0 || 1.4.1"
},
"suggest": {
"ext-mbstring": "for parsing UTF-8 CSS"
@@ -59,7 +63,7 @@
},
"extra": {
"branch-alias": {
- "dev-main": "9.0.x-dev"
+ "dev-main": "9.2.x-dev"
}
},
"scripts": {
@@ -67,6 +71,7 @@
"@ci:static",
"@ci:dynamic"
],
+ "ci:composer:normalize": "\"./.phive/composer-normalize\" --dry-run",
"ci:dynamic": [
"@ci:tests"
],
@@ -75,6 +80,7 @@
"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",
@@ -83,27 +89,37 @@
"ci:tests": [
"@ci: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",
+ "fix": [
+ "@fix:php"
+ ],
+ "fix:composer:normalize": "\"./.phive/composer-normalize\" --no-check-lock",
"fix:php": [
- "@fix:php:fixer",
- "@fix:php:rector"
+ "@fix:composer:normalize",
+ "@fix:php:rector",
+ "@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"
+ "phpstan:baseline": "phpstan --configuration=config/phpstan.neon --generate-baseline=config/phpstan-baseline.neon --allow-empty-baseline"
},
"scripts-descriptions": {
"ci": "Runs all dynamic and static code checks.",
+ "ci:composer:normalize": "Checks the formatting and structure of the composer.json.",
"ci:dynamic": "Runs all dynamic code checks (i.e., currently, the unit tests).",
"ci:php:fixer": "Checks the code style with PHP CS Fixer.",
"ci:php:lint": "Checks the syntax of the PHP code.",
- "ci:php:stan": "Checks the types with PHPStan.",
"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.",
+ "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:fixer": "Fixes autofixable issues found by PHP CS Fixer.",
"fix:php:rector": "Fixes autofixable issues found by Rector.",
diff --git a/config/php-cs-fixer.php b/config/php-cs-fixer.php
index b91c0a134..c10bf59ae 100644
--- a/config/php-cs-fixer.php
+++ b/config/php-cs-fixer.php
@@ -40,10 +40,6 @@
'yoda_style' => ['equal' => false, 'identical' => false, 'less_and_greater' => false],
// function notation
- 'native_function_invocation' => ['include' => ['@all']],
- 'nullable_type_declaration' => [
- 'syntax' => 'question_mark',
- ],
'nullable_type_declaration_for_default_null_value' => true,
// import
@@ -54,6 +50,7 @@
'combine_consecutive_unsets' => true,
'dir_constant' => true,
'is_null' => true,
+ 'nullable_type_declaration' => true,
// namespace notation
'no_leading_namespace_whitespace' => true,
@@ -92,11 +89,14 @@
'semicolon_after_instruction' => true,
// strict
- // 'declare_strict_types' => true, // Note: We'll need to add some casts first.
+ 'declare_strict_types' => true,
'strict_param' => true,
// string notation
'single_quote' => true,
'string_implicit_backslashes' => ['single_quoted' => 'escape'],
+
+ // whitespace
+ 'statement_indentation' => false,
]
);
diff --git a/config/phpstan-baseline.neon b/config/phpstan-baseline.neon
index 82fcb3f45..6205096ae 100644
--- a/config/phpstan-baseline.neon
+++ b/config/phpstan-baseline.neon
@@ -1,12 +1,115 @@
parameters:
ignoreErrors:
-
- message: "#^Call to an undefined method Sabberworm\\\\CSS\\\\OutputFormat\\:\\:setIndentation\\(\\)\\.$#"
+ 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
+ count: 1
+ path: ../src/CSSList/CSSList.php
+
+ -
+ message: '#^Parameters should have "Sabberworm\\CSS\\CSSList\\CSSListItem\|array" types as the only types passed to this method$#'
+ identifier: typePerfect.narrowPublicClassMethodParamType
+ count: 1
+ path: ../src/CSSList/CSSList.php
+
+ -
+ message: '#^Parameters should have "string\|null" types as the only types passed to this method$#'
+ identifier: typePerfect.narrowPublicClassMethodParamType
+ count: 1
+ path: ../src/CSSList/Document.php
+
+ -
+ message: '#^Negated boolean expression is always true\.$#'
+ identifier: booleanNot.alwaysTrue
+ count: 1
+ path: ../src/Parsing/ParserState.php
+
+ -
+ message: '#^Parameters should have "string" types as the only types passed to this method$#'
+ identifier: typePerfect.narrowPublicClassMethodParamType
+ count: 1
+ path: ../src/RuleSet/DeclarationBlock.php
+
+ -
+ message: '#^Parameter \#2 \$arguments of class Sabberworm\\CSS\\Value\\CSSFunction constructor expects array\\|Sabberworm\\CSS\\Value\\RuleValueList, Sabberworm\\CSS\\Value\\Value\|string given\.$#'
+ identifier: argument.type
+ count: 1
+ path: ../src/Value/CSSFunction.php
+
+ -
+ message: '#^Parameter \#2 \$offset of method Sabberworm\\CSS\\Parsing\\ParserState\:\:peek\(\) expects int\<0, max\>, \-1 given\.$#'
+ identifier: argument.type
count: 2
- path: ../src/OutputFormat.php
+ path: ../src/Value/CalcFunction.php
+
+ -
+ message: '#^Cannot call method getSize\(\) on Sabberworm\\CSS\\Value\\Value\|string\.$#'
+ identifier: method.nonObject
+ count: 3
+ path: ../src/Value/Color.php
+
+ -
+ message: '#^Parameters should have "float" types as the only types passed to this method$#'
+ identifier: typePerfect.narrowPublicClassMethodParamType
+ count: 1
+ path: ../src/Value/Size.php
+
+ -
+ message: '#^Strict comparison using \!\=\= between non\-empty\-string and null will always evaluate to true\.$#'
+ identifier: notIdentical.alwaysTrue
+ count: 1
+ path: ../src/Value/Size.php
+
+ -
+ message: '#^Parameter \#2 \$arguments of class Sabberworm\\CSS\\Value\\CSSFunction constructor expects array\\|Sabberworm\\CSS\\Value\\RuleValueList, Sabberworm\\CSS\\Value\\Value\|string given\.$#'
+ identifier: argument.type
+ count: 1
+ path: ../src/Value/Value.php
+
+ -
+ message: '#^Parameter \#2 \$offset of method Sabberworm\\CSS\\Parsing\\ParserState\:\:peek\(\) expects int\<0, max\>, \-1 given\.$#'
+ identifier: argument.type
+ count: 1
+ path: ../src/Value/Value.php
+
+ -
+ message: '#^Call to static method PHPUnit\\Framework\\Assert\:\:assertSame\(\) with ''red'' and Sabberworm\\CSS\\Value\\Value will always evaluate to false\.$#'
+ identifier: staticMethod.impossibleType
+ count: 1
+ path: ../tests/ParserTest.php
-
- message: "#^Class Sabberworm\\\\CSS\\\\Value\\\\Size constructor invoked with 5 parameters, 1\\-4 required\\.$#"
+ message: '#^Parameter \#1 \$value of method Sabberworm\\CSS\\Rule\\Rule\:\:setValue\(\) expects Sabberworm\\CSS\\Value\\RuleValueList\|string\|null, Sabberworm\\CSS\\Value\\Size given\.$#'
+ identifier: argument.type
+ count: 3
+ path: ../tests/RuleSet/DeclarationBlockTest.php
+
+ -
+ message: '#^Parameter \#1 \$type of class Sabberworm\\CSS\\CSSList\\AtRuleBlockList constructor expects non\-empty\-string, '''' given\.$#'
+ identifier: argument.type
+ count: 3
+ 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\.$#'
+ identifier: argument.type
count: 2
- path: ../src/RuleSet/DeclarationBlock.php
+ 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\.$#'
+ identifier: argument.type
+ count: 10
+ path: ../tests/Unit/CSSList/CSSBlockListTest.php
+
+ -
+ message: '#^Parameter \#1 \$selectors of method Sabberworm\\CSS\\CSSList\\CSSList\:\:removeDeclarationBlockBySelector\(\) expects array\\|Sabberworm\\CSS\\RuleSet\\DeclarationBlock\|string, array\ given\.$#'
+ identifier: argument.type
+ count: 2
+ path: ../tests/Unit/CSSList/CSSListTest.php
diff --git a/config/phpstan.neon b/config/phpstan.neon
index 3d7611a6d..55ba682af 100644
--- a/config/phpstan.neon
+++ b/config/phpstan.neon
@@ -6,13 +6,23 @@ parameters:
# Don't be overly greedy on machines with more CPU's to be a good neighbor especially on CI
maximumNumberOfProcesses: 5
- level: 1
+ phpVersion: 70200
+
+ level: 6
- scanDirectories:
- - %currentWorkingDirectory%/bin/
- - %currentWorkingDirectory%/src/
- - %currentWorkingDirectory%/tests/
paths:
- %currentWorkingDirectory%/bin/
- %currentWorkingDirectory%/src/
- %currentWorkingDirectory%/tests/
+
+ type_perfect:
+ no_mixed_property: true
+ no_mixed_caller: true
+ null_over_false: true
+ narrow_param: true
+ narrow_return: true
+
+ ignoreErrors:
+ -
+ message: '#^Call to static method PHPUnit\\Framework\\Assert\:\:assertInstanceOf\(\) .* will always evaluate to#'
+ path: '../tests/'
diff --git a/config/rector.php b/config/rector.php
index 978e00642..61df84182 100644
--- a/config/rector.php
+++ b/config/rector.php
@@ -3,6 +3,9 @@
declare(strict_types=1);
use Rector\Config\RectorConfig;
+use Rector\PHPUnit\Set\PHPUnitSetList;
+use Rector\Set\ValueObject\LevelSetList;
+use Rector\Set\ValueObject\SetList;
return RectorConfig::configure()
->withPaths(
@@ -11,9 +14,24 @@
__DIR__ . '/../tests',
]
)
- ->withPhpSets()
- ->withRules(
- [
- // AddVoidReturnTypeWhereNoReturnRector::class,
- ]
- );
+ ->withSets([
+ // Rector sets
+
+ LevelSetList::UP_TO_PHP_72,
+
+ // SetList::CODE_QUALITY,
+ // SetList::CODING_STYLE,
+ // SetList::DEAD_CODE,
+ // SetList::EARLY_RETURN,
+ // SetList::INSTANCEOF,
+ // SetList::NAMING,
+ // SetList::PRIVATIZATION,
+ SetList::STRICT_BOOLEANS,
+ SetList::TYPE_DECLARATION,
+
+ // PHPUnit sets
+
+ PHPUnitSetList::PHPUNIT_80,
+ // PHPUnitSetList::PHPUNIT_CODE_QUALITY,
+ ])
+ ->withImportNames(true, true, false);
diff --git a/docs/API-and-deprecation-policy.md b/docs/API-and-deprecation-policy.md
new file mode 100644
index 000000000..57e2acec7
--- /dev/null
+++ b/docs/API-and-deprecation-policy.md
@@ -0,0 +1,52 @@
+# API and Deprecation Policy
+
+## API Policy
+
+The code in this library is intended to be called by other projects. It is not
+intended to be extended. If you want to extend any classes, you're on your own,
+and your code might break with any new release of this library.
+
+Any classes, methods and properties that are `public` and not marked as
+`@internal` are considered to be part of the API. Those methods will continue
+working in a compatible way over minor and bug-fix releases according
+to [Semantic Versioning](https://semver.org/), though we might change the native
+type declarations in a way that could break subclasses.
+
+Any classes, methods and properties that are `protected` or `private` are _not_
+considered part of the API. Please do not rely on them. If you do, you're on
+your own.
+
+Any code that is marked as `@internal` is subject to change or removal without
+notice. Please do not call it. There be dragons.
+
+If a class is marked as `@internal`, all properties and methods of this class
+are by definition considered to be internal as well.
+
+When we change some code from public to `@internal` in a release, the first
+release that might change that code in a breaking way will be the next major
+release after that. This will allow you to change your code accordingly. We'll
+also add since which version the code is internal.
+
+For example, we might mark some code as `@internal` in version 8.7.0. The first
+version that possibly changes this code in a breaking way will then be version
+9.0.0.
+
+Before you upgrade your code to the next major version of this library, please
+update to the latest release of the previous major version and make sure that
+your code does not reference any code that is marked as `@internal`.
+
+## Deprecation Policy
+
+Code that we plan to remove is marked as `@deprecated`. In the corresponding
+annotation, we also note in which release the code will be removed.
+
+When we mark some code as `@deprecated` in a release, we'll usually remove it in
+the next major release. We'll also add since which version the code is
+deprecated.
+
+For example, when we mark some code as `@deprecated` in version 8.7.0, we'll
+remove it in version 9.0.0 (or sometimes a later major release).
+
+Before you upgrade your code to the next major version of this library, please
+update to the latest release of the previous major version and make sure that
+your code does not reference any code that is marked as `@deprecated`.
diff --git a/docs/release-checklist.md b/docs/release-checklist.md
new file mode 100644
index 000000000..48d50b7e6
--- /dev/null
+++ b/docs/release-checklist.md
@@ -0,0 +1,15 @@
+# Steps to release a new version
+
+1. In the [composer.json](../composer.json), update the `branch-alias` entry to
+ point to the release _after_ the upcoming release.
+1. In the [CHANGELOG.md](../CHANGELOG.md), create a new section with subheadings
+ for changes _after_ the upcoming release, set the version number for the
+ upcoming release, and remove any empty sections.
+1. Update the target milestone in the Dependabot configuration.
+1. Create a pull request "Prepare release of version x.y.z" with those changes.
+1. Have the pull request reviewed and merged.
+1. Tag the new release.
+1. In the
+ [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.
diff --git a/phpunit.xml b/phpunit.xml
index 1060f3299..aab1f10c4 100644
--- a/phpunit.xml
+++ b/phpunit.xml
@@ -2,10 +2,13 @@
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/8.5/phpunit.xsd"
beStrictAboutChangesToGlobalState="true"
- beStrictAboutCoversAnnotation="true"
+ beStrictAboutOutputDuringTests="true"
+ beStrictAboutTodoAnnotatedTests="true"
cacheResult="false"
colors="true"
+ convertDeprecationsToExceptions="true"
forceCoversAnnotation="true"
+ verbose="true"
>
diff --git a/src/CSSElement.php b/src/CSSElement.php
new file mode 100644
index 000000000..944aabe2c
--- /dev/null
+++ b/src/CSSElement.php
@@ -0,0 +1,17 @@
+|null $lineNumber
*/
- public function __construct($sType, $sArgs = '', $iLineNo = 0)
+ public function __construct(string $type, string $arguments = '', ?int $lineNumber = null)
{
- parent::__construct($iLineNo);
- $this->sType = $sType;
- $this->sArgs = $sArgs;
+ parent::__construct($lineNumber);
+ $this->type = $type;
+ $this->arguments = $arguments;
}
/**
- * @return string
+ * @return non-empty-string
*/
- public function atRuleName()
+ public function atRuleName(): string
{
- return $this->sType;
+ return $this->type;
}
- /**
- * @return string
- */
- public function atRuleArgs()
+ public function atRuleArgs(): string
{
- return $this->sArgs;
+ return $this->arguments;
}
- public function __toString(): string
- {
- return $this->render(new OutputFormat());
- }
-
- public function render(OutputFormat $oOutputFormat): string
+ /**
+ * @return non-empty-string
+ */
+ public function render(OutputFormat $outputFormat): string
{
- $sResult = $oOutputFormat->comments($this);
- $sResult .= $oOutputFormat->sBeforeAtRuleBlock;
- $sArgs = $this->sArgs;
- if ($sArgs) {
- $sArgs = ' ' . $sArgs;
+ $formatter = $outputFormat->getFormatter();
+ $result = $formatter->comments($this);
+ $result .= $outputFormat->getContentBeforeAtRuleBlock();
+ $arguments = $this->arguments;
+ if ($arguments !== '') {
+ $arguments = ' ' . $arguments;
}
- $sResult .= "@{$this->sType}$sArgs{$oOutputFormat->spaceBeforeOpeningBrace()}{";
- $sResult .= $this->renderListContents($oOutputFormat);
- $sResult .= '}';
- $sResult .= $oOutputFormat->sAfterAtRuleBlock;
- return $sResult;
+ $result .= "@{$this->type}$arguments{$formatter->spaceBeforeOpeningBrace()}{";
+ $result .= $this->renderListContents($outputFormat);
+ $result .= '}';
+ $result .= $outputFormat->getContentAfterAtRuleBlock();
+ return $result;
}
public function isRootList(): bool
diff --git a/src/CSSList/CSSBlockList.php b/src/CSSList/CSSBlockList.php
index 8c58fbf9b..122ac5d1d 100644
--- a/src/CSSList/CSSBlockList.php
+++ b/src/CSSList/CSSBlockList.php
@@ -1,10 +1,14 @@
$aResult
+ * Gets all `DeclarationBlock` objects recursively, no matter how deeply nested the selectors are.
*
- * @return void
+ * @return list
*/
- protected function allDeclarationBlocks(array &$aResult)
+ public function getAllDeclarationBlocks(): array
{
- foreach ($this->aContents as $mContent) {
- if ($mContent instanceof DeclarationBlock) {
- $aResult[] = $mContent;
- } elseif ($mContent instanceof CSSBlockList) {
- $mContent->allDeclarationBlocks($aResult);
+ $result = [];
+
+ foreach ($this->contents as $item) {
+ if ($item instanceof DeclarationBlock) {
+ $result[] = $item;
+ } elseif ($item instanceof CSSBlockList) {
+ $result = \array_merge($result, $item->getAllDeclarationBlocks());
}
}
+
+ return $result;
}
/**
- * @param array $aResult
+ * Returns all `RuleSet` objects recursively found in the tree, no matter how deeply nested the rule sets are.
*
- * @return void
+ * @return list
*/
- protected function allRuleSets(array &$aResult)
+ public function getAllRuleSets(): array
{
- foreach ($this->aContents as $mContent) {
- if ($mContent instanceof RuleSet) {
- $aResult[] = $mContent;
- } elseif ($mContent instanceof CSSBlockList) {
- $mContent->allRuleSets($aResult);
+ $result = [];
+
+ foreach ($this->contents as $item) {
+ if ($item instanceof RuleSet) {
+ $result[] = $item;
+ } elseif ($item instanceof CSSBlockList) {
+ $result = \array_merge($result, $item->getAllRuleSets());
+ } elseif ($item instanceof DeclarationBlock) {
+ $result[] = $item->getRuleSet();
}
}
+
+ return $result;
}
/**
- * @param CSSList|Rule|RuleSet|Value $oElement
- * @param array $aResult
- * @param string|null $sSearchString
- * @param bool $bSearchInFunctionArguments
+ * Returns all `Value` objects found recursively in `Rule`s in the tree.
+ *
+ * @param CSSElement|null $element
+ * This is the `CSSList` or `RuleSet` to start the search from (defaults to the whole document).
+ * @param string|null $ruleSearchPattern
+ * This allows filtering rules by property name
+ * (e.g. if "color" is passed, only `Value`s from `color` properties will be returned,
+ * or if "font-" is provided, `Value`s from all font rules, like `font-size`, and including `font` itself,
+ * will be returned).
+ * @param bool $searchInFunctionArguments whether to also return `Value` objects used as `CSSFunction` arguments.
*
- * @return void
+ * @return list
+ *
+ * @see RuleSet->getRules()
*/
- protected function allValues($oElement, array &$aResult, $sSearchString = null, $bSearchInFunctionArguments = false)
- {
- if ($oElement instanceof CSSBlockList) {
- foreach ($oElement->getContents() as $oContent) {
- $this->allValues($oContent, $aResult, $sSearchString, $bSearchInFunctionArguments);
+ public function getAllValues(
+ ?CSSElement $element = null,
+ ?string $ruleSearchPattern = null,
+ bool $searchInFunctionArguments = false
+ ): array {
+ $element = $element ?? $this;
+
+ $result = [];
+ if ($element instanceof CSSBlockList) {
+ foreach ($element->getContents() as $contentItem) {
+ // Statement at-rules are skipped since they do not contain values.
+ if ($contentItem instanceof CSSElement) {
+ $result = \array_merge(
+ $result,
+ $this->getAllValues($contentItem, $ruleSearchPattern, $searchInFunctionArguments)
+ );
+ }
+ }
+ } elseif ($element instanceof RuleContainer) {
+ foreach ($element->getRules($ruleSearchPattern) as $rule) {
+ $result = \array_merge(
+ $result,
+ $this->getAllValues($rule, $ruleSearchPattern, $searchInFunctionArguments)
+ );
}
- } elseif ($oElement instanceof RuleSet) {
- foreach ($oElement->getRules($sSearchString) as $oRule) {
- $this->allValues($oRule, $aResult, $sSearchString, $bSearchInFunctionArguments);
+ } elseif ($element instanceof Rule) {
+ $value = $element->getValue();
+ // `string` values are discarded.
+ if ($value instanceof CSSElement) {
+ $result = \array_merge(
+ $result,
+ $this->getAllValues($value, $ruleSearchPattern, $searchInFunctionArguments)
+ );
}
- } elseif ($oElement instanceof Rule) {
- $this->allValues($oElement->getValue(), $aResult, $sSearchString, $bSearchInFunctionArguments);
- } elseif ($oElement instanceof ValueList) {
- if ($bSearchInFunctionArguments || !($oElement instanceof CSSFunction)) {
- foreach ($oElement->getListComponents() as $mComponent) {
- $this->allValues($mComponent, $aResult, $sSearchString, $bSearchInFunctionArguments);
+ } elseif ($element instanceof ValueList) {
+ if ($searchInFunctionArguments || !($element instanceof CSSFunction)) {
+ foreach ($element->getListComponents() as $component) {
+ // `string` components are discarded.
+ if ($component instanceof CSSElement) {
+ $result = \array_merge(
+ $result,
+ $this->getAllValues($component, $ruleSearchPattern, $searchInFunctionArguments)
+ );
+ }
}
}
- } else {
- // Non-List `Value` or `CSSString` (CSS identifier)
- $aResult[] = $oElement;
+ } elseif ($element instanceof Value) {
+ $result[] = $element;
}
+
+ return $result;
}
/**
- * @param array $aResult
- * @param string|null $sSpecificitySearch
- *
- * @return void
+ * @return list
*/
- protected function allSelectors(array &$aResult, $sSpecificitySearch = null)
+ protected function getAllSelectors(?string $specificitySearch = null): array
{
- /** @var array $aDeclarationBlocks */
- $aDeclarationBlocks = [];
- $this->allDeclarationBlocks($aDeclarationBlocks);
- foreach ($aDeclarationBlocks as $oBlock) {
- foreach ($oBlock->getSelectors() as $oSelector) {
- if ($sSpecificitySearch === null) {
- $aResult[] = $oSelector;
+ $result = [];
+
+ foreach ($this->getAllDeclarationBlocks() as $declarationBlock) {
+ foreach ($declarationBlock->getSelectors() as $selector) {
+ if ($specificitySearch === null) {
+ $result[] = $selector;
} else {
- $sComparator = '===';
- $aSpecificitySearch = \explode(' ', $sSpecificitySearch);
- $iTargetSpecificity = $aSpecificitySearch[0];
- if (\count($aSpecificitySearch) > 1) {
- $sComparator = $aSpecificitySearch[0];
- $iTargetSpecificity = $aSpecificitySearch[1];
+ $comparator = '===';
+ $expressionParts = \explode(' ', $specificitySearch);
+ $targetSpecificity = $expressionParts[0];
+ if (\count($expressionParts) > 1) {
+ $comparator = $expressionParts[0];
+ $targetSpecificity = $expressionParts[1];
}
- $iTargetSpecificity = (int) $iTargetSpecificity;
- $iSelectorSpecificity = $oSelector->getSpecificity();
- $bMatches = false;
- switch ($sComparator) {
+ $targetSpecificity = (int) $targetSpecificity;
+ $selectorSpecificity = $selector->getSpecificity();
+ $comparatorMatched = false;
+ switch ($comparator) {
case '<=':
- $bMatches = $iSelectorSpecificity <= $iTargetSpecificity;
+ $comparatorMatched = $selectorSpecificity <= $targetSpecificity;
break;
case '<':
- $bMatches = $iSelectorSpecificity < $iTargetSpecificity;
+ $comparatorMatched = $selectorSpecificity < $targetSpecificity;
break;
case '>=':
- $bMatches = $iSelectorSpecificity >= $iTargetSpecificity;
+ $comparatorMatched = $selectorSpecificity >= $targetSpecificity;
break;
case '>':
- $bMatches = $iSelectorSpecificity > $iTargetSpecificity;
+ $comparatorMatched = $selectorSpecificity > $targetSpecificity;
break;
default:
- $bMatches = $iSelectorSpecificity === $iTargetSpecificity;
+ $comparatorMatched = $selectorSpecificity === $targetSpecificity;
break;
}
- if ($bMatches) {
- $aResult[] = $oSelector;
+ if ($comparatorMatched) {
+ $result[] = $selector;
}
}
}
}
+
+ return $result;
}
}
diff --git a/src/CSSList/CSSList.php b/src/CSSList/CSSList.php
index a99a60499..e09a03a98 100644
--- a/src/CSSList/CSSList.php
+++ b/src/CSSList/CSSList.php
@@ -1,301 +1,285 @@
- */
- protected $aComments;
-
- /**
- * @var array
- */
- protected $aContents;
+ use CommentContainer;
+ use Position;
/**
- * @var int
+ * @var array, CSSListItem>
+ *
+ * @internal since 8.8.0
*/
- protected $iLineNo;
+ protected $contents = [];
/**
- * @param int $iLineNo
+ * @param int<1, max>|null $lineNumber
*/
- public function __construct($iLineNo = 0)
+ public function __construct(?int $lineNumber = null)
{
- $this->aComments = [];
- $this->aContents = [];
- $this->iLineNo = $iLineNo;
+ $this->setPosition($lineNumber);
}
/**
* @throws UnexpectedTokenException
* @throws SourceException
+ *
+ * @internal since V8.8.0
*/
- public static function parseList(ParserState $oParserState, CSSList $oList): void
+ public static function parseList(ParserState $parserState, CSSList $list): void
{
- $bIsRoot = $oList instanceof Document;
- if (\is_string($oParserState)) {
- $oParserState = new ParserState($oParserState, Settings::create());
- }
- $bLenientParsing = $oParserState->getSettings()->bLenientParsing;
- $aComments = [];
- while (!$oParserState->isEnd()) {
- $aComments = \array_merge($aComments, $oParserState->consumeWhiteSpace());
- $oListItem = null;
- if ($bLenientParsing) {
+ $isRoot = $list instanceof Document;
+ $usesLenientParsing = $parserState->getSettings()->usesLenientParsing();
+ $comments = [];
+ while (!$parserState->isEnd()) {
+ $comments = \array_merge($comments, $parserState->consumeWhiteSpace());
+ $listItem = null;
+ if ($usesLenientParsing) {
try {
- $oListItem = self::parseListItem($oParserState, $oList);
+ $listItem = self::parseListItem($parserState, $list);
} catch (UnexpectedTokenException $e) {
- $oListItem = false;
+ $listItem = false;
}
} else {
- $oListItem = self::parseListItem($oParserState, $oList);
+ $listItem = self::parseListItem($parserState, $list);
}
- if ($oListItem === null) {
+ if ($listItem === null) {
// List parsing finished
return;
}
- if ($oListItem) {
- $oListItem->addComments($aComments);
- $oList->append($oListItem);
+ if ($listItem) {
+ $listItem->addComments($comments);
+ $list->append($listItem);
}
- $aComments = $oParserState->consumeWhiteSpace();
+ $comments = $parserState->consumeWhiteSpace();
}
- $oList->addComments($aComments);
- if (!$bIsRoot && !$bLenientParsing) {
- throw new SourceException('Unexpected end of document', $oParserState->currentLine());
+ $list->addComments($comments);
+ if (!$isRoot && !$usesLenientParsing) {
+ throw new SourceException('Unexpected end of document', $parserState->currentLine());
}
}
/**
- * @return AtRuleBlockList|KeyFrame|Charset|CSSNamespace|Import|AtRuleSet|DeclarationBlock|false|null
+ * @return CSSListItem|false|null
+ * If `null` is returned, it means the end of the list has been reached.
+ * If `false` is returned, it means an invalid item has been encountered,
+ * but parsing of the next item should proceed.
*
* @throws SourceException
* @throws UnexpectedEOFException
* @throws UnexpectedTokenException
*/
- private static function parseListItem(ParserState $oParserState, CSSList $oList)
+ private static function parseListItem(ParserState $parserState, CSSList $list)
{
- $bIsRoot = $oList instanceof Document;
- if ($oParserState->comes('@')) {
- $oAtRule = self::parseAtRule($oParserState);
- if ($oAtRule instanceof Charset) {
- if (!$bIsRoot) {
+ $isRoot = $list instanceof Document;
+ if ($parserState->comes('@')) {
+ $atRule = self::parseAtRule($parserState);
+ if ($atRule instanceof Charset) {
+ if (!$isRoot) {
throw new UnexpectedTokenException(
'@charset may only occur in root document',
'',
'custom',
- $oParserState->currentLine()
+ $parserState->currentLine()
);
}
- if (\count($oList->getContents()) > 0) {
+ if (\count($list->getContents()) > 0) {
throw new UnexpectedTokenException(
'@charset must be the first parseable token in a document',
'',
'custom',
- $oParserState->currentLine()
+ $parserState->currentLine()
);
}
- $oParserState->setCharset($oAtRule->getCharset());
+ $parserState->setCharset($atRule->getCharset());
}
- return $oAtRule;
- } elseif ($oParserState->comes('}')) {
- if ($bIsRoot) {
- if ($oParserState->getSettings()->bLenientParsing) {
- return DeclarationBlock::parse($oParserState);
+ return $atRule;
+ } elseif ($parserState->comes('}')) {
+ if ($isRoot) {
+ if ($parserState->getSettings()->usesLenientParsing()) {
+ return DeclarationBlock::parse($parserState) ?? false;
} else {
- throw new SourceException('Unopened {', $oParserState->currentLine());
+ throw new SourceException('Unopened {', $parserState->currentLine());
}
} else {
// End of list
return null;
}
} else {
- return DeclarationBlock::parse($oParserState, $oList);
+ return DeclarationBlock::parse($parserState, $list) ?? false;
}
}
/**
- * @param ParserState $oParserState
- *
- * @return AtRuleBlockList|KeyFrame|Charset|CSSNamespace|Import|AtRuleSet|null
- *
* @throws SourceException
* @throws UnexpectedTokenException
* @throws UnexpectedEOFException
*/
- private static function parseAtRule(ParserState $oParserState)
+ private static function parseAtRule(ParserState $parserState): ?CSSListItem
{
- $oParserState->consume('@');
- $sIdentifier = $oParserState->parseIdentifier();
- $iIdentifierLineNum = $oParserState->currentLine();
- $oParserState->consumeWhiteSpace();
- if ($sIdentifier === 'import') {
- $oLocation = URL::parse($oParserState);
- $oParserState->consumeWhiteSpace();
- $sMediaQuery = null;
- if (!$oParserState->comes(';')) {
- $sMediaQuery = \trim($oParserState->consumeUntil([';', ParserState::EOF]));
+ $parserState->consume('@');
+ $identifier = $parserState->parseIdentifier();
+ $identifierLineNumber = $parserState->currentLine();
+ $parserState->consumeWhiteSpace();
+ if ($identifier === 'import') {
+ $location = URL::parse($parserState);
+ $parserState->consumeWhiteSpace();
+ $mediaQuery = null;
+ if (!$parserState->comes(';')) {
+ $mediaQuery = \trim($parserState->consumeUntil([';', ParserState::EOF]));
+ if ($mediaQuery === '') {
+ $mediaQuery = null;
+ }
}
- $oParserState->consumeUntil([';', ParserState::EOF], true, true);
- return new Import($oLocation, $sMediaQuery ?: null, $iIdentifierLineNum);
- } elseif ($sIdentifier === 'charset') {
- $oCharsetString = CSSString::parse($oParserState);
- $oParserState->consumeWhiteSpace();
- $oParserState->consumeUntil([';', ParserState::EOF], true, true);
- return new Charset($oCharsetString, $iIdentifierLineNum);
- } elseif (self::identifierIs($sIdentifier, 'keyframes')) {
- $oResult = new KeyFrame($iIdentifierLineNum);
- $oResult->setVendorKeyFrame($sIdentifier);
- $oResult->setAnimationName(\trim($oParserState->consumeUntil('{', false, true)));
- CSSList::parseList($oParserState, $oResult);
- if ($oParserState->comes('}')) {
- $oParserState->consume('}');
+ $parserState->consumeUntil([';', ParserState::EOF], true, true);
+ return new Import($location, $mediaQuery, $identifierLineNumber);
+ } elseif ($identifier === 'charset') {
+ $charsetString = CSSString::parse($parserState);
+ $parserState->consumeWhiteSpace();
+ $parserState->consumeUntil([';', ParserState::EOF], true, true);
+ return new Charset($charsetString, $identifierLineNumber);
+ } elseif (self::identifierIs($identifier, 'keyframes')) {
+ $result = new KeyFrame($identifierLineNumber);
+ $result->setVendorKeyFrame($identifier);
+ $result->setAnimationName(\trim($parserState->consumeUntil('{', false, true)));
+ CSSList::parseList($parserState, $result);
+ if ($parserState->comes('}')) {
+ $parserState->consume('}');
}
- return $oResult;
- } elseif ($sIdentifier === 'namespace') {
- $sPrefix = null;
- $mUrl = Value::parsePrimitiveValue($oParserState);
- if (!$oParserState->comes(';')) {
- $sPrefix = $mUrl;
- $mUrl = Value::parsePrimitiveValue($oParserState);
+ return $result;
+ } elseif ($identifier === 'namespace') {
+ $prefix = null;
+ $url = Value::parsePrimitiveValue($parserState);
+ if (!$parserState->comes(';')) {
+ $prefix = $url;
+ $url = Value::parsePrimitiveValue($parserState);
}
- $oParserState->consumeUntil([';', ParserState::EOF], true, true);
- if ($sPrefix !== null && !\is_string($sPrefix)) {
- throw new UnexpectedTokenException('Wrong namespace prefix', $sPrefix, 'custom', $iIdentifierLineNum);
+ $parserState->consumeUntil([';', ParserState::EOF], true, true);
+ if ($prefix !== null && !\is_string($prefix)) {
+ throw new UnexpectedTokenException('Wrong namespace prefix', $prefix, 'custom', $identifierLineNumber);
}
- if (!($mUrl instanceof CSSString || $mUrl instanceof URL)) {
+ if (!($url instanceof CSSString || $url instanceof URL)) {
throw new UnexpectedTokenException(
'Wrong namespace url of invalid type',
- $mUrl,
+ $url,
'custom',
- $iIdentifierLineNum
+ $identifierLineNumber
);
}
- return new CSSNamespace($mUrl, $sPrefix, $iIdentifierLineNum);
+ return new CSSNamespace($url, $prefix, $identifierLineNumber);
} else {
// Unknown other at rule (font-face or such)
- $sArgs = \trim($oParserState->consumeUntil('{', false, true));
- if (\substr_count($sArgs, '(') != \substr_count($sArgs, ')')) {
- if ($oParserState->getSettings()->bLenientParsing) {
+ $arguments = \trim($parserState->consumeUntil('{', false, true));
+ if (\substr_count($arguments, '(') !== \substr_count($arguments, ')')) {
+ if ($parserState->getSettings()->usesLenientParsing()) {
return null;
} else {
- throw new SourceException('Unmatched brace count in media query', $oParserState->currentLine());
+ throw new SourceException('Unmatched brace count in media query', $parserState->currentLine());
}
}
- $bUseRuleSet = true;
- foreach (\explode('/', AtRule::BLOCK_RULES) as $sBlockRuleName) {
- if (self::identifierIs($sIdentifier, $sBlockRuleName)) {
- $bUseRuleSet = false;
+ $useRuleSet = true;
+ foreach (\explode('/', AtRule::BLOCK_RULES) as $blockRuleName) {
+ if (self::identifierIs($identifier, $blockRuleName)) {
+ $useRuleSet = false;
break;
}
}
- if ($bUseRuleSet) {
- $oAtRule = new AtRuleSet($sIdentifier, $sArgs, $iIdentifierLineNum);
- RuleSet::parseRuleSet($oParserState, $oAtRule);
+ if ($useRuleSet) {
+ $atRule = new AtRuleSet($identifier, $arguments, $identifierLineNumber);
+ RuleSet::parseRuleSet($parserState, $atRule);
} else {
- $oAtRule = new AtRuleBlockList($sIdentifier, $sArgs, $iIdentifierLineNum);
- CSSList::parseList($oParserState, $oAtRule);
- if ($oParserState->comes('}')) {
- $oParserState->consume('}');
+ $atRule = new AtRuleBlockList($identifier, $arguments, $identifierLineNumber);
+ CSSList::parseList($parserState, $atRule);
+ if ($parserState->comes('}')) {
+ $parserState->consume('}');
}
}
- return $oAtRule;
+ return $atRule;
}
}
/**
* Tests an identifier for a given value. Since identifiers are all keywords, they can be vendor-prefixed.
* We need to check for these versions too.
- *
- * @param string $sIdentifier
- * @param string $sMatch
*/
- private static function identifierIs($sIdentifier, $sMatch): bool
+ private static function identifierIs(string $identifier, string $match): bool
{
- return (\strcasecmp($sIdentifier, $sMatch) === 0)
- ?: \preg_match("/^(-\\w+-)?$sMatch$/i", $sIdentifier) === 1;
- }
+ if (\strcasecmp($identifier, $match) === 0) {
+ return true;
+ }
- /**
- * @return int
- */
- public function getLineNo()
- {
- return $this->iLineNo;
+ return preg_match("/^(-\\w+-)?$match$/i", $identifier) === 1;
}
/**
* Prepends an item to the list of contents.
- *
- * @param RuleSet|CSSList|Import|Charset $oItem
*/
- public function prepend($oItem): void
+ public function prepend(CSSListItem $item): void
{
- \array_unshift($this->aContents, $oItem);
+ \array_unshift($this->contents, $item);
}
/**
* Appends an item to the list of contents.
- *
- * @param RuleSet|CSSList|Import|Charset $oItem
*/
- public function append($oItem): void
+ public function append(CSSListItem $item): void
{
- $this->aContents[] = $oItem;
+ $this->contents[] = $item;
}
/**
* Splices the list of contents.
*
- * @param int $iOffset
- * @param int $iLength
- * @param array $mReplacement
+ * @param array $replacement
*/
- public function splice($iOffset, $iLength = null, $mReplacement = null): void
+ public function splice(int $offset, ?int $length = null, ?array $replacement = null): void
{
- \array_splice($this->aContents, $iOffset, $iLength, $mReplacement);
+ \array_splice($this->contents, $offset, $length, $replacement);
}
/**
* Inserts an item in the CSS list before its sibling. If the desired sibling cannot be found,
* the item is appended at the end.
- *
- * @param RuleSet|CSSList|Import|Charset $item
- * @param RuleSet|CSSList|Import|Charset $sibling
*/
- public function insertBefore($item, $sibling): void
+ public function insertBefore(CSSListItem $item, CSSListItem $sibling): void
{
- if (\in_array($sibling, $this->aContents, true)) {
+ if (\in_array($sibling, $this->contents, true)) {
$this->replace($sibling, [$item, $sibling]);
} else {
$this->append($item);
@@ -305,52 +289,53 @@ public function insertBefore($item, $sibling): void
/**
* Removes an item from the CSS list.
*
- * @param RuleSet|Import|Charset|CSSList $oItemToRemove
- * May be a RuleSet (most likely a DeclarationBlock), a Import,
- * a Charset or another CSSList (most likely a MediaQuery)
+ * @param CSSListItem $itemToRemove
+ * May be a `RuleSet` (most likely a `DeclarationBlock`), an `Import`,
+ * a `Charset` or another `CSSList` (most likely a `MediaQuery`)
*
* @return bool whether the item was removed
*/
- public function remove($oItemToRemove)
+ public function remove(CSSListItem $itemToRemove): bool
{
- $iKey = \array_search($oItemToRemove, $this->aContents, true);
- if ($iKey !== false) {
- unset($this->aContents[$iKey]);
+ $key = \array_search($itemToRemove, $this->contents, true);
+ if ($key !== false) {
+ unset($this->contents[$key]);
return true;
}
+
return false;
}
/**
* Replaces an item from the CSS list.
*
- * @param RuleSet|Import|Charset|CSSList $oOldItem
+ * @param CSSListItem $oldItem
* May be a `RuleSet` (most likely a `DeclarationBlock`), an `Import`, a `Charset`
* or another `CSSList` (most likely a `MediaQuery`)
- *
- * @return bool
+ * @param CSSListItem|array $newItem
*/
- public function replace($oOldItem, $mNewItem)
+ public function replace(CSSListItem $oldItem, $newItem): bool
{
- $iKey = \array_search($oOldItem, $this->aContents, true);
- if ($iKey !== false) {
- if (\is_array($mNewItem)) {
- \array_splice($this->aContents, $iKey, 1, $mNewItem);
+ $key = \array_search($oldItem, $this->contents, true);
+ if ($key !== false) {
+ if (\is_array($newItem)) {
+ \array_splice($this->contents, $key, 1, $newItem);
} else {
- \array_splice($this->aContents, $iKey, 1, [$mNewItem]);
+ \array_splice($this->contents, $key, 1, [$newItem]);
}
return true;
}
+
return false;
}
/**
- * @param array $aContents
+ * @param array $contents
*/
- public function setContents(array $aContents): void
+ public function setContents(array $contents): void
{
- $this->aContents = [];
- foreach ($aContents as $content) {
+ $this->contents = [];
+ foreach ($contents as $content) {
$this->append($content);
}
}
@@ -358,120 +343,88 @@ public function setContents(array $aContents): void
/**
* Removes a declaration block from the CSS list if it matches all given selectors.
*
- * @param DeclarationBlock|array|string $mSelector the selectors to match
- * @param bool $bRemoveAll whether to stop at the first declaration block found or remove all blocks
+ * @param DeclarationBlock|array|string $selectors the selectors to match
+ * @param bool $removeAll whether to stop at the first declaration block found or remove all blocks
*/
- public function removeDeclarationBlockBySelector($mSelector, $bRemoveAll = false): void
+ public function removeDeclarationBlockBySelector($selectors, bool $removeAll = false): void
{
- if ($mSelector instanceof DeclarationBlock) {
- $mSelector = $mSelector->getSelectors();
+ if ($selectors instanceof DeclarationBlock) {
+ $selectors = $selectors->getSelectors();
}
- if (!\is_array($mSelector)) {
- $mSelector = \explode(',', $mSelector);
+ if (!\is_array($selectors)) {
+ $selectors = \explode(',', $selectors);
}
- foreach ($mSelector as $iKey => &$mSel) {
- if (!($mSel instanceof Selector)) {
- if (!Selector::isValid($mSel)) {
+ foreach ($selectors as $key => &$selector) {
+ if (!($selector instanceof Selector)) {
+ if (!Selector::isValid($selector)) {
throw new UnexpectedTokenException(
"Selector did not match '" . Selector::SELECTOR_VALIDATION_RX . "'.",
- $mSel,
+ $selector,
'custom'
);
}
- $mSel = new Selector($mSel);
+ $selector = new Selector($selector);
}
}
- foreach ($this->aContents as $iKey => $mItem) {
- if (!($mItem instanceof DeclarationBlock)) {
+ foreach ($this->contents as $key => $item) {
+ if (!($item instanceof DeclarationBlock)) {
continue;
}
- if ($mItem->getSelectors() == $mSelector) {
- unset($this->aContents[$iKey]);
- if (!$bRemoveAll) {
+ if ($item->getSelectors() == $selectors) {
+ unset($this->contents[$key]);
+ if (!$removeAll) {
return;
}
}
}
}
- public function __toString(): string
- {
- return $this->render(new OutputFormat());
- }
-
- /**
- * @return string
- */
- protected function renderListContents(OutputFormat $oOutputFormat)
+ protected function renderListContents(OutputFormat $outputFormat): string
{
- $sResult = '';
- $bIsFirst = true;
- $oNextLevel = $oOutputFormat;
+ $result = '';
+ $isFirst = true;
+ $nextLevelFormat = $outputFormat;
if (!$this->isRootList()) {
- $oNextLevel = $oOutputFormat->nextLevel();
+ $nextLevelFormat = $outputFormat->nextLevel();
}
- foreach ($this->aContents as $oContent) {
- $sRendered = $oOutputFormat->safely(function () use ($oNextLevel, $oContent) {
- return $oContent->render($oNextLevel);
+ $nextLevelFormatter = $nextLevelFormat->getFormatter();
+ $formatter = $outputFormat->getFormatter();
+ foreach ($this->contents as $listItem) {
+ $renderedCss = $formatter->safely(static function () use ($nextLevelFormat, $listItem): string {
+ return $listItem->render($nextLevelFormat);
});
- if ($sRendered === null) {
+ if ($renderedCss === null) {
continue;
}
- if ($bIsFirst) {
- $bIsFirst = false;
- $sResult .= $oNextLevel->spaceBeforeBlocks();
+ if ($isFirst) {
+ $isFirst = false;
+ $result .= $nextLevelFormatter->spaceBeforeBlocks();
} else {
- $sResult .= $oNextLevel->spaceBetweenBlocks();
+ $result .= $nextLevelFormatter->spaceBetweenBlocks();
}
- $sResult .= $sRendered;
+ $result .= $renderedCss;
}
- if (!$bIsFirst) {
+ if (!$isFirst) {
// Had some output
- $sResult .= $oOutputFormat->spaceAfterBlocks();
+ $result .= $formatter->spaceAfterBlocks();
}
- return $sResult;
+ return $result;
}
/**
* Return true if the list can not be further outdented. Only important when rendering.
- *
- * @return bool
*/
- abstract public function isRootList();
+ abstract public function isRootList(): bool;
/**
* Returns the stored items.
*
- * @return array
- */
- public function getContents()
- {
- return $this->aContents;
- }
-
- /**
- * @param array $aComments
- */
- public function addComments(array $aComments): void
- {
- $this->aComments = \array_merge($this->aComments, $aComments);
- }
-
- /**
- * @return array
- */
- public function getComments()
- {
- return $this->aComments;
- }
-
- /**
- * @param array $aComments
+ * @return array, CSSListItem>
*/
- public function setComments(array $aComments): void
+ public function getContents(): array
{
- $this->aComments = $aComments;
+ return $this->contents;
}
}
diff --git a/src/CSSList/CSSListItem.php b/src/CSSList/CSSListItem.php
new file mode 100644
index 000000000..3cf2509b6
--- /dev/null
+++ b/src/CSSList/CSSListItem.php
@@ -0,0 +1,18 @@
+currentLine());
- CSSList::parseList($oParserState, $oDocument);
- return $oDocument;
- }
-
- /**
- * Gets all `DeclarationBlock` objects recursively, no matter how deeply nested the selectors are.
- * Aliased as `getAllSelectors()`.
*
- * @return array
+ * @internal since V8.8.0
*/
- public function getAllDeclarationBlocks()
+ public static function parse(ParserState $parserState): Document
{
- /** @var array $aResult */
- $aResult = [];
- $this->allDeclarationBlocks($aResult);
- return $aResult;
- }
+ $document = new Document($parserState->currentLine());
+ CSSList::parseList($parserState, $document);
- /**
- * Returns all `RuleSet` objects recursively found in the tree, no matter how deeply nested the rule sets are.
- *
- * @return array
- */
- public function getAllRuleSets()
- {
- /** @var array $aResult */
- $aResult = [];
- $this->allRuleSets($aResult);
- return $aResult;
- }
-
- /**
- * Returns all `Value` objects found recursively in `Rule`s in the tree.
- *
- * @param CSSList|RuleSet|string $mElement
- * the `CSSList` or `RuleSet` to start the search from (defaults to the whole document).
- * If a string is given, it is used as rule name filter.
- * @param bool $bSearchInFunctionArguments whether to also return Value objects used as Function arguments.
- *
- * @return array
- *
- * @see RuleSet->getRules()
- */
- public function getAllValues($mElement = null, $bSearchInFunctionArguments = false)
- {
- $sSearchString = null;
- if ($mElement === null) {
- $mElement = $this;
- } elseif (\is_string($mElement)) {
- $sSearchString = $mElement;
- $mElement = $this;
- }
- /** @var array $aResult */
- $aResult = [];
- $this->allValues($mElement, $aResult, $sSearchString, $bSearchInFunctionArguments);
- return $aResult;
+ return $document;
}
/**
@@ -94,56 +34,28 @@ public function getAllValues($mElement = null, $bSearchInFunctionArguments = fal
* Note that this does not yield the full `DeclarationBlock` that the selector belongs to
* (and, currently, there is no way to get to that).
*
- * @param string|null $sSpecificitySearch
+ * @param string|null $specificitySearch
* An optional filter by specificity.
* May contain a comparison operator and a number or just a number (defaults to "==").
*
- * @return array
- * @example `getSelectorsBySpecificity('>= 100')`
- */
- public function getSelectorsBySpecificity($sSpecificitySearch = null)
- {
- /** @var array $aResult */
- $aResult = [];
- $this->allSelectors($aResult, $sSpecificitySearch);
- return $aResult;
- }
-
- /**
- * Expands all shorthand properties to their long value.
- *
- * @deprecated This will be removed without substitution in version 10.0.
- */
- public function expandShorthands(): void
- {
- foreach ($this->getAllDeclarationBlocks() as $oDeclaration) {
- $oDeclaration->expandShorthands();
- }
- }
-
- /**
- * Create shorthands properties whenever possible.
+ * @return list
*
- * @deprecated This will be removed without substitution in version 10.0.
+ * @example `getSelectorsBySpecificity('>= 100')`
*/
- public function createShorthands(): void
+ public function getSelectorsBySpecificity(?string $specificitySearch = null): array
{
- foreach ($this->getAllDeclarationBlocks() as $oDeclaration) {
- $oDeclaration->createShorthands();
- }
+ return $this->getAllSelectors($specificitySearch);
}
/**
* Overrides `render()` to make format argument optional.
- *
- * @param OutputFormat|null $oOutputFormat
*/
- public function render(?OutputFormat $oOutputFormat = null): string
+ public function render(?OutputFormat $outputFormat = null): string
{
- if ($oOutputFormat === null) {
- $oOutputFormat = new OutputFormat();
+ if ($outputFormat === null) {
+ $outputFormat = new OutputFormat();
}
- return $oOutputFormat->comments($this) . $this->renderListContents($oOutputFormat);
+ return $outputFormat->getFormatter()->comments($this) . $this->renderListContents($outputFormat);
}
public function isRootList(): bool
diff --git a/src/CSSList/KeyFrame.php b/src/CSSList/KeyFrame.php
index 557c3e74a..e632d088b 100644
--- a/src/CSSList/KeyFrame.php
+++ b/src/CSSList/KeyFrame.php
@@ -1,5 +1,7 @@
vendorKeyFrame = null;
- $this->animationName = null;
- }
+ private $animationName = 'none';
/**
- * @param string $vendorKeyFrame
+ * @param non-empty-string $vendorKeyFrame
*/
- public function setVendorKeyFrame($vendorKeyFrame): void
+ public function setVendorKeyFrame(string $vendorKeyFrame): void
{
$this->vendorKeyFrame = $vendorKeyFrame;
}
/**
- * @return string|null
+ * @return non-empty-string
*/
- public function getVendorKeyFrame()
+ public function getVendorKeyFrame(): string
{
return $this->vendorKeyFrame;
}
/**
- * @param string $animationName
+ * @param non-empty-string $animationName
*/
- public function setAnimationName($animationName): void
+ public function setAnimationName(string $animationName): void
{
$this->animationName = $animationName;
}
/**
- * @return string|null
+ * @return non-empty-string
*/
- public function getAnimationName()
+ public function getAnimationName(): string
{
return $this->animationName;
}
- public function __toString(): string
- {
- return $this->render(new OutputFormat());
- }
-
- public function render(OutputFormat $oOutputFormat): string
+ /**
+ * @return non-empty-string
+ */
+ public function render(OutputFormat $outputFormat): string
{
- $sResult = $oOutputFormat->comments($this);
- $sResult .= "@{$this->vendorKeyFrame} {$this->animationName}{$oOutputFormat->spaceBeforeOpeningBrace()}{";
- $sResult .= $this->renderListContents($oOutputFormat);
- $sResult .= '}';
- return $sResult;
+ $formatter = $outputFormat->getFormatter();
+ $result = $formatter->comments($this);
+ $result .= "@{$this->vendorKeyFrame} {$this->animationName}{$formatter->spaceBeforeOpeningBrace()}{";
+ $result .= $this->renderListContents($outputFormat);
+ $result .= '}';
+ return $result;
}
public function isRootList(): bool
@@ -79,17 +70,17 @@ public function isRootList(): bool
}
/**
- * @return string|null
+ * @return non-empty-string
*/
- public function atRuleName()
+ public function atRuleName(): string
{
return $this->vendorKeyFrame;
}
/**
- * @return string|null
+ * @return non-empty-string
*/
- public function atRuleArgs()
+ public function atRuleArgs(): string
{
return $this->animationName;
}
diff --git a/src/Comment/Comment.php b/src/Comment/Comment.php
index c698018a6..33188988f 100644
--- a/src/Comment/Comment.php
+++ b/src/Comment/Comment.php
@@ -1,63 +1,49 @@
|null $lineNumber
*/
- public function __construct($sComment = '', $iLineNo = 0)
+ public function __construct(string $commentText = '', ?int $lineNumber = null)
{
- $this->sComment = $sComment;
- $this->iLineNo = $iLineNo;
+ $this->commentText = $commentText;
+ $this->setPosition($lineNumber);
}
- /**
- * @return string
- */
- public function getComment()
+ public function getComment(): string
{
- return $this->sComment;
+ return $this->commentText;
}
- /**
- * @return int
- */
- public function getLineNo()
+ public function setComment(string $commentText): void
{
- return $this->iLineNo;
+ $this->commentText = $commentText;
}
/**
- * @param string $sComment
+ * @return non-empty-string
*/
- public function setComment($sComment): void
- {
- $this->sComment = $sComment;
- }
-
- public function __toString(): string
- {
- return $this->render(new OutputFormat());
- }
-
- public function render(OutputFormat $oOutputFormat): string
+ public function render(OutputFormat $outputFormat): string
{
- return '/*' . $this->sComment . '*/';
+ return '/*' . $this->commentText . '*/';
}
}
diff --git a/src/Comment/CommentContainer.php b/src/Comment/CommentContainer.php
new file mode 100644
index 000000000..87f6ff46c
--- /dev/null
+++ b/src/Comment/CommentContainer.php
@@ -0,0 +1,44 @@
+
+ */
+ protected $comments = [];
+
+ /**
+ * @param list $comments
+ */
+ public function addComments(array $comments): void
+ {
+ $this->comments = \array_merge($this->comments, $comments);
+ }
+
+ /**
+ * @return list
+ */
+ public function getComments(): array
+ {
+ return $this->comments;
+ }
+
+ /**
+ * @param list $comments
+ */
+ public function setComments(array $comments): void
+ {
+ $this->comments = $comments;
+ }
+}
diff --git a/src/Comment/Commentable.php b/src/Comment/Commentable.php
index 5e450bfb3..5f28021de 100644
--- a/src/Comment/Commentable.php
+++ b/src/Comment/Commentable.php
@@ -1,25 +1,26 @@
$aComments
- *
- * @return void
+ * @param list $comments
*/
- public function addComments(array $aComments);
+ public function addComments(array $comments): void;
/**
- * @return array
+ * @return list
*/
- public function getComments();
+ public function getComments(): array;
/**
- * @param array $aComments
- *
- * @return void
+ * @param list $comments
*/
- public function setComments(array $aComments);
+ public function setComments(array $comments): void;
}
diff --git a/src/OutputFormat.php b/src/OutputFormat.php
index 7ada59819..5c493865a 100644
--- a/src/OutputFormat.php
+++ b/src/OutputFormat.php
@@ -1,28 +1,24 @@
set('Space*Rules', "\n");`)
+ * The triples (After, Before, Between) can be set using a wildcard
+ * (e.g. `$outputFormat->set('Space*Rules', "\n");`)
+ *
+ * @var string
*/
- public $sSpaceAfterRuleName = ' ';
+ private $spaceAfterRuleName = ' ';
/**
* @var string
*/
- public $sSpaceBeforeRules = '';
+ private $spaceBeforeRules = '';
/**
* @var string
*/
- public $sSpaceAfterRules = '';
+ private $spaceAfterRules = '';
/**
* @var string
*/
- public $sSpaceBetweenRules = '';
+ private $spaceBetweenRules = '';
/**
* @var string
*/
- public $sSpaceBeforeBlocks = '';
+ private $spaceBeforeBlocks = '';
/**
* @var string
*/
- public $sSpaceAfterBlocks = '';
+ private $spaceAfterBlocks = '';
/**
* @var string
*/
- public $sSpaceBetweenBlocks = "\n";
+ private $spaceBetweenBlocks = "\n";
/**
* Content injected in and around at-rule blocks.
*
* @var string
*/
- public $sBeforeAtRuleBlock = '';
+ private $contentBeforeAtRuleBlock = '';
/**
* @var string
*/
- public $sAfterAtRuleBlock = '';
+ private $contentAfterAtRuleBlock = '';
/**
* This is what’s printed before and after the comma if a declaration block contains multiple selectors.
*
* @var string
*/
- public $sSpaceBeforeSelectorSeparator = '';
+ private $spaceBeforeSelectorSeparator = '';
/**
* @var string
*/
- public $sSpaceAfterSelectorSeparator = ' ';
+ private $spaceAfterSelectorSeparator = ' ';
/**
- * This is what’s printed after the comma of value lists
+ * This is what’s inserted before the separator in value lists, by default.
*
* @var string
*/
- public $sSpaceBeforeListArgumentSeparator = '';
+ private $spaceBeforeListArgumentSeparator = '';
+
+ /**
+ * Keys are separators (e.g. `,`). Values are the space sequence to insert, or an empty string.
+ *
+ * @var array
+ */
+ private $spaceBeforeListArgumentSeparators = [];
/**
+ * This is what’s inserted after the separator in value lists, by default.
+ *
* @var string
*/
- public $sSpaceAfterListArgumentSeparator = '';
+ private $spaceAfterListArgumentSeparator = '';
+
+ /**
+ * Keys are separators (e.g. `,`). Values are the space sequence to insert, or an empty string.
+ *
+ * @var array
+ */
+ private $spaceAfterListArgumentSeparators = [];
/**
* @var string
*/
- public $sSpaceBeforeOpeningBrace = ' ';
+ private $spaceBeforeOpeningBrace = ' ';
/**
* Content injected in and around declaration blocks.
*
* @var string
*/
- public $sBeforeDeclarationBlock = '';
+ private $contentBeforeDeclarationBlock = '';
/**
* @var string
*/
- public $sAfterDeclarationBlockSelectors = '';
+ private $contentAfterDeclarationBlockSelectors = '';
/**
* @var string
*/
- public $sAfterDeclarationBlock = '';
+ private $contentAfterDeclarationBlock = '';
/**
* Indentation character(s) per level. Only applicable if newlines are used in any of the spacing settings.
*
* @var string
*/
- public $sIndentation = "\t";
+ private $indentation = "\t";
/**
* Output exceptions.
*
* @var bool
*/
- public $bIgnoreExceptions = false;
+ private $shouldIgnoreExceptions = false;
/**
* Render comments for lists and RuleSets
*
* @var bool
*/
- public $bRenderComments = false;
+ private $shouldRenderComments = false;
/**
* @var OutputFormatter|null
*/
- private $oFormatter = null;
+ private $outputFormatter;
/**
* @var OutputFormat|null
*/
- private $oNextLevelFormat = null;
+ private $nextLevelFormat;
/**
- * @var int
+ * @var int<0, max>
*/
- private $iIndentationLevel = 0;
+ private $indentationLevel = 0;
- public function __construct() {}
+ /**
+ * @return non-empty-string
+ *
+ * @internal
+ */
+ public function getStringQuotingType(): string
+ {
+ return $this->stringQuotingType;
+ }
/**
- * @param string $sName
+ * @param non-empty-string $quotingType
*
- * @return string|null
+ * @return $this fluent interface
*/
- public function get($sName)
+ public function setStringQuotingType(string $quotingType): self
{
- $aVarPrefixes = ['a', 's', 'm', 'b', 'f', 'o', 'c', 'i'];
- foreach ($aVarPrefixes as $sPrefix) {
- $sFieldName = $sPrefix . \ucfirst($sName);
- if (isset($this->$sFieldName)) {
- return $this->$sFieldName;
- }
- }
- return null;
+ $this->stringQuotingType = $quotingType;
+
+ return $this;
+ }
+
+ /**
+ * @internal
+ */
+ public function usesRgbHashNotation(): bool
+ {
+ return $this->usesRgbHashNotation;
+ }
+
+ /**
+ * @return $this fluent interface
+ */
+ public function setRGBHashNotation(bool $usesRgbHashNotation): self
+ {
+ $this->usesRgbHashNotation = $usesRgbHashNotation;
+
+ return $this;
+ }
+
+ /**
+ * @internal
+ */
+ public function shouldRenderSemicolonAfterLastRule(): bool
+ {
+ return $this->renderSemicolonAfterLastRule;
+ }
+
+ /**
+ * @return $this fluent interface
+ */
+ public function setSemicolonAfterLastRule(bool $renderSemicolonAfterLastRule): self
+ {
+ $this->renderSemicolonAfterLastRule = $renderSemicolonAfterLastRule;
+
+ return $this;
+ }
+
+ /**
+ * @internal
+ */
+ public function getSpaceAfterRuleName(): string
+ {
+ return $this->spaceAfterRuleName;
+ }
+
+ /**
+ * @return $this fluent interface
+ */
+ public function setSpaceAfterRuleName(string $whitespace): self
+ {
+ $this->spaceAfterRuleName = $whitespace;
+
+ return $this;
+ }
+
+ /**
+ * @internal
+ */
+ public function getSpaceBeforeRules(): string
+ {
+ return $this->spaceBeforeRules;
+ }
+
+ /**
+ * @return $this fluent interface
+ */
+ public function setSpaceBeforeRules(string $whitespace): self
+ {
+ $this->spaceBeforeRules = $whitespace;
+
+ return $this;
+ }
+
+ /**
+ * @internal
+ */
+ public function getSpaceAfterRules(): string
+ {
+ return $this->spaceAfterRules;
+ }
+
+ /**
+ * @return $this fluent interface
+ */
+ public function setSpaceAfterRules(string $whitespace): self
+ {
+ $this->spaceAfterRules = $whitespace;
+
+ return $this;
+ }
+
+ /**
+ * @internal
+ */
+ public function getSpaceBetweenRules(): string
+ {
+ return $this->spaceBetweenRules;
+ }
+
+ /**
+ * @return $this fluent interface
+ */
+ public function setSpaceBetweenRules(string $whitespace): self
+ {
+ $this->spaceBetweenRules = $whitespace;
+
+ return $this;
+ }
+
+ /**
+ * @internal
+ */
+ public function getSpaceBeforeBlocks(): string
+ {
+ return $this->spaceBeforeBlocks;
+ }
+
+ /**
+ * @return $this fluent interface
+ */
+ public function setSpaceBeforeBlocks(string $whitespace): self
+ {
+ $this->spaceBeforeBlocks = $whitespace;
+
+ return $this;
+ }
+
+ /**
+ * @internal
+ */
+ public function getSpaceAfterBlocks(): string
+ {
+ return $this->spaceAfterBlocks;
+ }
+
+ /**
+ * @return $this fluent interface
+ */
+ public function setSpaceAfterBlocks(string $whitespace): self
+ {
+ $this->spaceAfterBlocks = $whitespace;
+
+ return $this;
+ }
+
+ /**
+ * @internal
+ */
+ public function getSpaceBetweenBlocks(): string
+ {
+ return $this->spaceBetweenBlocks;
+ }
+
+ /**
+ * @return $this fluent interface
+ */
+ public function setSpaceBetweenBlocks(string $whitespace): self
+ {
+ $this->spaceBetweenBlocks = $whitespace;
+
+ return $this;
+ }
+
+ /**
+ * @internal
+ */
+ public function getContentBeforeAtRuleBlock(): string
+ {
+ return $this->contentBeforeAtRuleBlock;
+ }
+
+ /**
+ * @return $this fluent interface
+ */
+ public function setBeforeAtRuleBlock(string $content): self
+ {
+ $this->contentBeforeAtRuleBlock = $content;
+
+ return $this;
+ }
+
+ /**
+ * @internal
+ */
+ public function getContentAfterAtRuleBlock(): string
+ {
+ return $this->contentAfterAtRuleBlock;
+ }
+
+ /**
+ * @return $this fluent interface
+ */
+ public function setAfterAtRuleBlock(string $content): self
+ {
+ $this->contentAfterAtRuleBlock = $content;
+
+ return $this;
+ }
+
+ /**
+ * @internal
+ */
+ public function getSpaceBeforeSelectorSeparator(): string
+ {
+ return $this->spaceBeforeSelectorSeparator;
+ }
+
+ /**
+ * @return $this fluent interface
+ */
+ public function setSpaceBeforeSelectorSeparator(string $whitespace): self
+ {
+ $this->spaceBeforeSelectorSeparator = $whitespace;
+
+ return $this;
}
/**
- * @param array|string $aNames
- * @param mixed $mValue
+ * @internal
+ */
+ public function getSpaceAfterSelectorSeparator(): string
+ {
+ return $this->spaceAfterSelectorSeparator;
+ }
+
+ /**
+ * @return $this fluent interface
+ */
+ public function setSpaceAfterSelectorSeparator(string $whitespace): self
+ {
+ $this->spaceAfterSelectorSeparator = $whitespace;
+
+ return $this;
+ }
+
+ /**
+ * @internal
+ */
+ public function getSpaceBeforeListArgumentSeparator(): string
+ {
+ return $this->spaceBeforeListArgumentSeparator;
+ }
+
+ /**
+ * @return $this fluent interface
+ */
+ public function setSpaceBeforeListArgumentSeparator(string $whitespace): self
+ {
+ $this->spaceBeforeListArgumentSeparator = $whitespace;
+
+ return $this;
+ }
+
+ /**
+ * @return array
*
- * @return self|false
- */
- public function set($aNames, $mValue)
- {
- $aVarPrefixes = ['a', 's', 'm', 'b', 'f', 'o', 'c', 'i'];
- if (\is_string($aNames) && \strpos($aNames, '*') !== false) {
- $aNames =
- [
- \str_replace('*', 'Before', $aNames),
- \str_replace('*', 'Between', $aNames),
- \str_replace('*', 'After', $aNames),
- ];
- } elseif (!\is_array($aNames)) {
- $aNames = [$aNames];
- }
- foreach ($aVarPrefixes as $sPrefix) {
- $bDidReplace = false;
- foreach ($aNames as $sName) {
- $sFieldName = $sPrefix . \ucfirst($sName);
- if (isset($this->$sFieldName)) {
- $this->$sFieldName = $mValue;
- $bDidReplace = true;
- }
- }
- if ($bDidReplace) {
- return $this;
- }
- }
- // Break the chain so the user knows this option is invalid
- return false;
+ * @internal
+ */
+ public function getSpaceBeforeListArgumentSeparators(): array
+ {
+ return $this->spaceBeforeListArgumentSeparators;
}
/**
- * @param string $sMethodName
- * @param array $aArguments
+ * @param array $separatorSpaces
*
- * @return mixed
+ * @return $this fluent interface
+ */
+ public function setSpaceBeforeListArgumentSeparators(array $separatorSpaces): self
+ {
+ $this->spaceBeforeListArgumentSeparators = $separatorSpaces;
+
+ return $this;
+ }
+
+ /**
+ * @internal
+ */
+ public function getSpaceAfterListArgumentSeparator(): string
+ {
+ return $this->spaceAfterListArgumentSeparator;
+ }
+
+ /**
+ * @return $this fluent interface
+ */
+ public function setSpaceAfterListArgumentSeparator(string $whitespace): self
+ {
+ $this->spaceAfterListArgumentSeparator = $whitespace;
+
+ return $this;
+ }
+
+ /**
+ * @return array
*
- * @throws \Exception
- */
- public function __call($sMethodName, array $aArguments)
- {
- if (\strpos($sMethodName, 'set') === 0) {
- return $this->set(\substr($sMethodName, 3), $aArguments[0]);
- } elseif (\strpos($sMethodName, 'get') === 0) {
- return $this->get(\substr($sMethodName, 3));
- } elseif (\method_exists(OutputFormatter::class, $sMethodName)) {
- return \call_user_func_array([$this->getFormatter(), $sMethodName], $aArguments);
- } else {
- throw new \Exception('Unknown OutputFormat method called: ' . $sMethodName);
- }
+ * @internal
+ */
+ public function getSpaceAfterListArgumentSeparators(): array
+ {
+ return $this->spaceAfterListArgumentSeparators;
}
/**
- * @param int $iNumber
+ * @param array $separatorSpaces
*
- * @return self
+ * @return $this fluent interface
*/
- public function indentWithTabs($iNumber = 1)
+ public function setSpaceAfterListArgumentSeparators(array $separatorSpaces): self
{
- return $this->setIndentation(\str_repeat("\t", $iNumber));
+ $this->spaceAfterListArgumentSeparators = $separatorSpaces;
+
+ return $this;
}
/**
- * @param int $iNumber
+ * @internal
+ */
+ public function getSpaceBeforeOpeningBrace(): string
+ {
+ return $this->spaceBeforeOpeningBrace;
+ }
+
+ /**
+ * @return $this fluent interface
+ */
+ public function setSpaceBeforeOpeningBrace(string $whitespace): self
+ {
+ $this->spaceBeforeOpeningBrace = $whitespace;
+
+ return $this;
+ }
+
+ /**
+ * @internal
+ */
+ public function getContentBeforeDeclarationBlock(): string
+ {
+ return $this->contentBeforeDeclarationBlock;
+ }
+
+ /**
+ * @return $this fluent interface
+ */
+ public function setBeforeDeclarationBlock(string $content): self
+ {
+ $this->contentBeforeDeclarationBlock = $content;
+
+ return $this;
+ }
+
+ /**
+ * @internal
+ */
+ public function getContentAfterDeclarationBlockSelectors(): string
+ {
+ return $this->contentAfterDeclarationBlockSelectors;
+ }
+
+ /**
+ * @return $this fluent interface
+ */
+ public function setAfterDeclarationBlockSelectors(string $content): self
+ {
+ $this->contentAfterDeclarationBlockSelectors = $content;
+
+ return $this;
+ }
+
+ /**
+ * @internal
+ */
+ public function getContentAfterDeclarationBlock(): string
+ {
+ return $this->contentAfterDeclarationBlock;
+ }
+
+ /**
+ * @return $this fluent interface
+ */
+ public function setAfterDeclarationBlock(string $content): self
+ {
+ $this->contentAfterDeclarationBlock = $content;
+
+ return $this;
+ }
+
+ /**
+ * @internal
+ */
+ public function getIndentation(): string
+ {
+ return $this->indentation;
+ }
+
+ /**
+ * @return $this fluent interface
+ */
+ public function setIndentation(string $indentation): self
+ {
+ $this->indentation = $indentation;
+
+ return $this;
+ }
+
+ /**
+ * @internal
+ */
+ public function shouldIgnoreExceptions(): bool
+ {
+ return $this->shouldIgnoreExceptions;
+ }
+
+ /**
+ * @return $this fluent interface
+ */
+ public function setIgnoreExceptions(bool $ignoreExceptions): self
+ {
+ $this->shouldIgnoreExceptions = $ignoreExceptions;
+
+ return $this;
+ }
+
+ /**
+ * @internal
+ */
+ public function shouldRenderComments(): bool
+ {
+ return $this->shouldRenderComments;
+ }
+
+ /**
+ * @return $this fluent interface
+ */
+ public function setRenderComments(bool $renderComments): self
+ {
+ $this->shouldRenderComments = $renderComments;
+
+ return $this;
+ }
+
+ /**
+ * @return int<0, max>
*
- * @return self
+ * @internal
*/
- public function indentWithSpaces($iNumber = 2)
+ public function getIndentationLevel(): int
{
- return $this->setIndentation(\str_repeat(' ', $iNumber));
+ return $this->indentationLevel;
}
/**
- * @return OutputFormat
+ * @param int<1, max> $numberOfTabs
+ *
+ * @return $this fluent interface
*/
- public function nextLevel()
+ public function indentWithTabs(int $numberOfTabs = 1): self
{
- if ($this->oNextLevelFormat === null) {
- $this->oNextLevelFormat = clone $this;
- $this->oNextLevelFormat->iIndentationLevel++;
- $this->oNextLevelFormat->oFormatter = null;
- }
- return $this->oNextLevelFormat;
+ return $this->setIndentation(\str_repeat("\t", $numberOfTabs));
}
- public function beLenient(): void
+ /**
+ * @param int<1, max> $numberOfSpaces
+ *
+ * @return $this fluent interface
+ */
+ public function indentWithSpaces(int $numberOfSpaces = 2): self
{
- $this->bIgnoreExceptions = true;
+ return $this->setIndentation(\str_repeat(' ', $numberOfSpaces));
}
/**
- * @return OutputFormatter
+ * @internal since V8.8.0
*/
- public function getFormatter()
+ public function nextLevel(): self
{
- if ($this->oFormatter === null) {
- $this->oFormatter = new OutputFormatter($this);
+ if ($this->nextLevelFormat === null) {
+ $this->nextLevelFormat = clone $this;
+ $this->nextLevelFormat->indentationLevel++;
+ $this->nextLevelFormat->outputFormatter = null;
}
- return $this->oFormatter;
+ return $this->nextLevelFormat;
+ }
+
+ public function beLenient(): void
+ {
+ $this->shouldIgnoreExceptions = true;
}
/**
- * @return int
+ * @internal since 8.8.0
*/
- public function level()
+ public function getFormatter(): OutputFormatter
{
- return $this->iIndentationLevel;
+ if ($this->outputFormatter === null) {
+ $this->outputFormatter = new OutputFormatter($this);
+ }
+
+ return $this->outputFormatter;
}
/**
* Creates an instance of this class without any particular formatting settings.
*/
- public static function create(): OutputFormat
+ public static function create(): self
{
return new OutputFormat();
}
/**
* Creates an instance of this class with a preset for compact formatting.
- *
- * @return self
*/
- public static function createCompact()
+ public static function createCompact(): self
{
$format = self::create();
- $format->set('Space*Rules', '')
- ->set('Space*Blocks', '')
+ $format
+ ->setSpaceBeforeRules('')
+ ->setSpaceBetweenRules('')
+ ->setSpaceAfterRules('')
+ ->setSpaceBeforeBlocks('')
+ ->setSpaceBetweenBlocks('')
+ ->setSpaceAfterBlocks('')
->setSpaceAfterRuleName('')
->setSpaceBeforeOpeningBrace('')
->setSpaceAfterSelectorSeparator('')
+ ->setSemicolonAfterLastRule(false)
->setRenderComments(false);
+
return $format;
}
/**
* Creates an instance of this class with a preset for pretty formatting.
- *
- * @return self
*/
- public static function createPretty()
+ public static function createPretty(): self
{
$format = self::create();
- $format->set('Space*Rules', "\n")
- ->set('Space*Blocks', "\n")
+ $format
+ ->setSpaceBeforeRules("\n")
+ ->setSpaceBetweenRules("\n")
+ ->setSpaceAfterRules("\n")
+ ->setSpaceBeforeBlocks("\n")
->setSpaceBetweenBlocks("\n\n")
- ->set('SpaceAfterListArgumentSeparator', ['default' => '', ',' => ' '])
+ ->setSpaceAfterBlocks("\n")
+ ->setSpaceAfterListArgumentSeparators([',' => ' '])
->setRenderComments(true);
+
return $format;
}
}
diff --git a/src/OutputFormatter.php b/src/OutputFormatter.php
index 75e1532d3..09918c38d 100644
--- a/src/OutputFormatter.php
+++ b/src/OutputFormatter.php
@@ -1,39 +1,76 @@
oFormat = $oFormat;
+ $this->outputFormat = $outputFormat;
}
/**
- * @param string $sName
- * @param string|null $sType
+ * @param non-empty-string $name
+ *
+ * @throws \InvalidArgumentException
*/
- public function space($sName, $sType = null): string
- {
- $sSpaceString = $this->oFormat->get("Space$sName");
- // If $sSpaceString is an array, we have multiple values configured
- // depending on the type of object the space applies to
- if (\is_array($sSpaceString)) {
- if ($sType !== null && isset($sSpaceString[$sType])) {
- $sSpaceString = $sSpaceString[$sType];
- } else {
- $sSpaceString = \reset($sSpaceString);
- }
+ public function space(string $name): string
+ {
+ switch ($name) {
+ case 'AfterRuleName':
+ $spaceString = $this->outputFormat->getSpaceAfterRuleName();
+ break;
+ case 'BeforeRules':
+ $spaceString = $this->outputFormat->getSpaceBeforeRules();
+ break;
+ case 'AfterRules':
+ $spaceString = $this->outputFormat->getSpaceAfterRules();
+ break;
+ case 'BetweenRules':
+ $spaceString = $this->outputFormat->getSpaceBetweenRules();
+ break;
+ case 'BeforeBlocks':
+ $spaceString = $this->outputFormat->getSpaceBeforeBlocks();
+ break;
+ case 'AfterBlocks':
+ $spaceString = $this->outputFormat->getSpaceAfterBlocks();
+ break;
+ case 'BetweenBlocks':
+ $spaceString = $this->outputFormat->getSpaceBetweenBlocks();
+ break;
+ case 'BeforeSelectorSeparator':
+ $spaceString = $this->outputFormat->getSpaceBeforeSelectorSeparator();
+ break;
+ case 'AfterSelectorSeparator':
+ $spaceString = $this->outputFormat->getSpaceAfterSelectorSeparator();
+ break;
+ case 'BeforeOpeningBrace':
+ $spaceString = $this->outputFormat->getSpaceBeforeOpeningBrace();
+ break;
+ case 'BeforeListArgumentSeparator':
+ $spaceString = $this->outputFormat->getSpaceBeforeListArgumentSeparator();
+ break;
+ case 'AfterListArgumentSeparator':
+ $spaceString = $this->outputFormat->getSpaceAfterListArgumentSeparator();
+ break;
+ default:
+ throw new \InvalidArgumentException("Unknown space type: $name", 1740049248);
}
- return $this->prepareSpace($sSpaceString);
+
+ return $this->prepareSpace($spaceString);
}
public function spaceAfterRuleName(): string
@@ -76,28 +113,29 @@ public function spaceBeforeSelectorSeparator(): string
return $this->space('BeforeSelectorSeparator');
}
- /**
- * @return string
- */
public function spaceAfterSelectorSeparator(): string
{
return $this->space('AfterSelectorSeparator');
}
/**
- * @param string $sSeparator
+ * @param non-empty-string $separator
*/
- public function spaceBeforeListArgumentSeparator($sSeparator): string
+ public function spaceBeforeListArgumentSeparator(string $separator): string
{
- return $this->space('BeforeListArgumentSeparator', $sSeparator);
+ $spaceForSeparator = $this->outputFormat->getSpaceBeforeListArgumentSeparators();
+
+ return $spaceForSeparator[$separator] ?? $this->space('BeforeListArgumentSeparator');
}
/**
- * @param string $sSeparator
+ * @param non-empty-string $separator
*/
- public function spaceAfterListArgumentSeparator($sSeparator): string
+ public function spaceAfterListArgumentSeparator(string $separator): string
{
- return $this->space('AfterListArgumentSeparator', $sSeparator);
+ $spaceForSeparator = $this->outputFormat->getSpaceAfterListArgumentSeparators();
+
+ return $spaceForSeparator[$separator] ?? $this->space('AfterListArgumentSeparator');
}
public function spaceBeforeOpeningBrace(): string
@@ -106,112 +144,92 @@ public function spaceBeforeOpeningBrace(): string
}
/**
- * Runs the given code, either swallowing or passing exceptions, depending on the `bIgnoreExceptions` setting.
- *
- * @param string $cCode the name of the function to call
- *
- * @return string|null
+ * Runs the given code, either swallowing or passing exceptions, depending on the `ignoreExceptions` setting.
*/
- public function safely($cCode)
+ public function safely(callable $callable): ?string
{
- if ($this->oFormat->get('IgnoreExceptions')) {
+ if ($this->outputFormat->shouldIgnoreExceptions()) {
// If output exceptions are ignored, run the code with exception guards
try {
- return $cCode();
+ return $callable();
} catch (OutputException $e) {
return null;
} // Do nothing
} else {
// Run the code as-is
- return $cCode();
+ return $callable();
}
}
/**
- * Clone of the `implode` function, but calls `render` with the current output format instead of `__toString()`.
+ * Clone of the `implode` function, but calls `render` with the current output format.
*
- * @param string $sSeparator
- * @param array $aValues
- * @param bool $bIncreaseLevel
+ * @param array $values
*/
- public function implode($sSeparator, array $aValues, $bIncreaseLevel = false): string
+ public function implode(string $separator, array $values, bool $increaseLevel = false): string
{
- $sResult = '';
- $oFormat = $this->oFormat;
- if ($bIncreaseLevel) {
- $oFormat = $oFormat->nextLevel();
+ $result = '';
+ $outputFormat = $this->outputFormat;
+ if ($increaseLevel) {
+ $outputFormat = $outputFormat->nextLevel();
}
- $bIsFirst = true;
- foreach ($aValues as $mValue) {
- if ($bIsFirst) {
- $bIsFirst = false;
+ $isFirst = true;
+ foreach ($values as $value) {
+ if ($isFirst) {
+ $isFirst = false;
} else {
- $sResult .= $sSeparator;
+ $result .= $separator;
}
- if ($mValue instanceof Renderable) {
- $sResult .= $mValue->render($oFormat);
+ if ($value instanceof Renderable) {
+ $result .= $value->render($outputFormat);
} else {
- $sResult .= $mValue;
+ $result .= $value;
}
}
- return $sResult;
+ return $result;
}
- /**
- * @param string $sString
- *
- * @return string
- */
- public function removeLastSemicolon($sString)
+ public function removeLastSemicolon(string $string): string
{
- if ($this->oFormat->get('SemicolonAfterLastRule')) {
- return $sString;
+ if ($this->outputFormat->shouldRenderSemicolonAfterLastRule()) {
+ return $string;
}
- $sString = \explode(';', $sString);
- if (\count($sString) < 2) {
- return $sString[0];
+
+ $parts = \explode(';', $string);
+ if (\count($parts) < 2) {
+ return $parts[0];
}
- $sLast = \array_pop($sString);
- $sNextToLast = \array_pop($sString);
- \array_push($sString, $sNextToLast . $sLast);
- return \implode(';', $sString);
+ $lastPart = \array_pop($parts);
+ $nextToLastPart = \array_pop($parts);
+ \array_push($parts, $nextToLastPart . $lastPart);
+
+ return \implode(';', $parts);
}
- /**
- * @param array $aComments
- *
- * @return string
- */
- public function comments(Commentable $oCommentable): string
+ public function comments(Commentable $commentable): string
{
- if (!$this->oFormat->bRenderComments) {
+ if (!$this->outputFormat->shouldRenderComments()) {
return '';
}
- $sResult = '';
- $aComments = $oCommentable->getComments();
- $iLastCommentIndex = \count($aComments) - 1;
+ $result = '';
+ $comments = $commentable->getComments();
+ $lastCommentIndex = \count($comments) - 1;
- foreach ($aComments as $i => $oComment) {
- $sResult .= $oComment->render($this->oFormat);
- $sResult .= $i === $iLastCommentIndex ? $this->spaceAfterBlocks() : $this->spaceBetweenBlocks();
+ foreach ($comments as $i => $comment) {
+ $result .= $comment->render($this->outputFormat);
+ $result .= $i === $lastCommentIndex ? $this->spaceAfterBlocks() : $this->spaceBetweenBlocks();
}
- return $sResult;
+ return $result;
}
- /**
- * @param string $sSpaceString
- */
- private function prepareSpace($sSpaceString): string
+ private function prepareSpace(string $spaceString): string
{
- return \str_replace("\n", "\n" . $this->indent(), $sSpaceString);
+ return \str_replace("\n", "\n" . $this->indent(), $spaceString);
}
- /**
- * @return string
- */
private function indent(): string
{
- return \str_repeat($this->oFormat->sIndentation, $this->oFormat->level());
+ return \str_repeat($this->outputFormat->getIndentation(), $this->outputFormat->getIndentationLevel());
}
}
diff --git a/src/Parser.php b/src/Parser.php
index 7cb044003..b34a5107c 100644
--- a/src/Parser.php
+++ b/src/Parser.php
@@ -1,5 +1,7 @@
$lineNumber the line number (starting from 1, not from 0)
*/
- public function __construct($sText, ?Settings $oParserSettings = null, $iLineNo = 1)
+ public function __construct(string $text, ?Settings $parserSettings = null, int $lineNumber = 1)
{
- if ($oParserSettings === null) {
- $oParserSettings = Settings::create();
+ if ($parserSettings === null) {
+ $parserSettings = Settings::create();
}
- $this->oParserState = new ParserState($sText, $oParserSettings, $iLineNo);
- }
-
- /**
- * Sets the charset to be used if the CSS does not contain an `@charset` declaration.
- *
- * @param string $sCharset
- */
- public function setCharset($sCharset): void
- {
- $this->oParserState->setCharset($sCharset);
- }
-
- /**
- * Returns the charset that is used if the CSS does not contain an `@charset` declaration.
- */
- public function getCharset(): void
- {
- // Note: The `return` statement is missing here. This is a bug that needs to be fixed.
- $this->oParserState->getCharset();
+ $this->parserState = new ParserState($text, $parserSettings, $lineNumber);
}
/**
@@ -55,6 +37,6 @@ public function getCharset(): void
*/
public function parse(): Document
{
- return Document::parse($this->oParserState);
+ return Document::parse($this->parserState);
}
}
diff --git a/src/Parsing/Anchor.php b/src/Parsing/Anchor.php
index 022d7502d..c27f436ad 100644
--- a/src/Parsing/Anchor.php
+++ b/src/Parsing/Anchor.php
@@ -1,31 +1,35 @@
*/
- private $iPosition;
+ private $position;
/**
- * @var \Sabberworm\CSS\Parsing\ParserState
+ * @var ParserState
*/
- private $oParserState;
+ private $parserState;
/**
- * @param int $iPosition
- * @param \Sabberworm\CSS\Parsing\ParserState $oParserState
+ * @param int<0, max> $position
*/
- public function __construct($iPosition, ParserState $oParserState)
+ public function __construct(int $position, ParserState $parserState)
{
- $this->iPosition = $iPosition;
- $this->oParserState = $oParserState;
+ $this->position = $position;
+ $this->parserState = $parserState;
}
public function backtrack(): void
{
- $this->oParserState->setPosition($this->iPosition);
+ $this->parserState->setPosition($this->position);
}
}
diff --git a/src/Parsing/OutputException.php b/src/Parsing/OutputException.php
index 9bfbc75fb..0a20dc967 100644
--- a/src/Parsing/OutputException.php
+++ b/src/Parsing/OutputException.php
@@ -1,18 +1,10 @@
*/
- private $aText;
+ private $characters;
/**
- * @var int
+ * @var int<0, max>
*/
- private $iCurrentPosition;
+ private $currentPosition = 0;
/**
* will only be used if the CSS does not contain an `@charset` declaration
*
* @var string
*/
- private $sCharset;
-
- /**
- * @var int
- */
- private $iLength;
+ private $charset;
/**
- * @var int
+ * @var int<1, max> $lineNumber
*/
- private $iLineNo;
+ private $lineNumber;
/**
- * @param string $sText the complete CSS as text (i.e., usually the contents of a CSS file)
- * @param int $iLineNo
+ * @param string $text the complete CSS as text (i.e., usually the contents of a CSS file)
+ * @param int<1, max> $lineNumber
*/
- public function __construct($sText, Settings $oParserSettings, $iLineNo = 1)
+ public function __construct(string $text, Settings $parserSettings, int $lineNumber = 1)
{
- $this->oParserSettings = $oParserSettings;
- $this->sText = $sText;
- $this->iCurrentPosition = 0;
- $this->iLineNo = $iLineNo;
- $this->setCharset($this->oParserSettings->sDefaultCharset);
+ $this->parserSettings = $parserSettings;
+ $this->text = $text;
+ $this->lineNumber = $lineNumber;
+ $this->setCharset($this->parserSettings->getDefaultCharset());
}
/**
* Sets the charset to be used if the CSS does not contain an `@charset` declaration.
- *
- * @param string $sCharset
*/
- public function setCharset($sCharset): void
+ public function setCharset(string $charset): void
{
- $this->sCharset = $sCharset;
- $this->aText = $this->strsplit($this->sText);
- if (\is_array($this->aText)) {
- $this->iLength = \count($this->aText);
- }
+ $this->charset = $charset;
+ $this->characters = $this->strsplit($this->text);
}
/**
- * Returns the charset that is used if the CSS does not contain an `@charset` declaration.
- *
- * @return string
+ * @return int<1, max>
*/
- public function getCharset()
+ public function currentLine(): int
{
- return $this->sCharset;
+ return $this->lineNumber;
}
/**
- * @return int
+ * @return int<0, max>
*/
- public function currentLine()
+ public function currentColumn(): int
{
- return $this->iLineNo;
+ return $this->currentPosition;
}
- /**
- * @return int
- */
- public function currentColumn()
+ public function getSettings(): Settings
{
- return $this->iCurrentPosition;
+ return $this->parserSettings;
}
- /**
- * @return Settings
- */
- public function getSettings()
- {
- return $this->oParserSettings;
- }
-
-
public function anchor(): Anchor
{
- return new Anchor($this->iCurrentPosition, $this);
+ return new Anchor($this->currentPosition, $this);
}
/**
- * @param int $iPosition
+ * @param int<0, max> $position
*/
- public function setPosition($iPosition): void
+ public function setPosition(int $position): void
{
- $this->iCurrentPosition = $iPosition;
+ $this->currentPosition = $position;
}
/**
- * @param bool $bIgnoreCase
- *
- * @return string
+ * @return non-empty-string
*
* @throws UnexpectedTokenException
*/
- public function parseIdentifier($bIgnoreCase = true)
+ public function parseIdentifier(bool $ignoreCase = true): string
{
if ($this->isEnd()) {
- throw new UnexpectedEOFException('', '', 'identifier', $this->iLineNo);
+ throw new UnexpectedEOFException('', '', 'identifier', $this->lineNumber);
}
- $sResult = $this->parseCharacter(true);
- if ($sResult === null) {
- throw new UnexpectedTokenException($sResult, $this->peek(5), 'identifier', $this->iLineNo);
+ $result = $this->parseCharacter(true);
+ if ($result === null) {
+ throw new UnexpectedTokenException('', $this->peek(5), 'identifier', $this->lineNumber);
}
- $sCharacter = null;
- while (!$this->isEnd() && ($sCharacter = $this->parseCharacter(true)) !== null) {
- if (\preg_match('/[a-zA-Z0-9\\x{00A0}-\\x{FFFF}_-]/Sux', $sCharacter)) {
- $sResult .= $sCharacter;
+ $character = null;
+ while (!$this->isEnd() && ($character = $this->parseCharacter(true)) !== null) {
+ if (preg_match('/[a-zA-Z0-9\\x{00A0}-\\x{FFFF}_-]/Sux', $character) !== 0) {
+ $result .= $character;
} else {
- $sResult .= '\\' . $sCharacter;
+ $result .= '\\' . $character;
}
}
- if ($bIgnoreCase) {
- $sResult = $this->strtolower($sResult);
+ if ($ignoreCase) {
+ $result = $this->strtolower($result);
}
- return $sResult;
+
+ return $result;
}
/**
- * @param bool $bIsForIdentifier
- *
- * @return string|null
- *
* @throws UnexpectedEOFException
* @throws UnexpectedTokenException
*/
- public function parseCharacter($bIsForIdentifier)
+ public function parseCharacter(bool $isForIdentifier): ?string
{
if ($this->peek() === '\\') {
- if (
- $bIsForIdentifier && $this->oParserSettings->bLenientParsing
- && ($this->comes('\\0') || $this->comes('\\9'))
- ) {
- // Non-strings can contain \0 or \9 which is an IE hack supported in lenient parsing.
- return null;
- }
$this->consume('\\');
if ($this->comes('\\n') || $this->comes('\\r')) {
return '';
}
- if (\preg_match('/[0-9a-fA-F]/Su', $this->peek()) === 0) {
+ if (preg_match('/[0-9a-fA-F]/Su', $this->peek()) === 0) {
return $this->consume(1);
}
- $sUnicode = $this->consumeExpression('/^[0-9a-fA-F]{1,6}/u', 6);
- if ($this->strlen($sUnicode) < 6) {
+ $hexCodePoint = $this->consumeExpression('/^[0-9a-fA-F]{1,6}/u', 6);
+ if ($this->strlen($hexCodePoint) < 6) {
// Consume whitespace after incomplete unicode escape
- if (\preg_match('/\\s/isSu', $this->peek())) {
+ if (preg_match('/\\s/isSu', $this->peek()) !== 0) {
if ($this->comes('\\r\\n')) {
$this->consume(2);
} else {
@@ -192,15 +162,15 @@ public function parseCharacter($bIsForIdentifier)
}
}
}
- $iUnicode = \intval($sUnicode, 16);
- $sUtf32 = '';
+ $codePoint = \intval($hexCodePoint, 16);
+ $utf32EncodedCharacter = '';
for ($i = 0; $i < 4; ++$i) {
- $sUtf32 .= \chr($iUnicode & 0xff);
- $iUnicode = $iUnicode >> 8;
+ $utf32EncodedCharacter .= \chr($codePoint & 0xff);
+ $codePoint = $codePoint >> 8;
}
- return \iconv('utf-32le', $this->sCharset, $sUtf32);
+ return iconv('utf-32le', $this->charset, $utf32EncodedCharacter);
}
- if ($bIsForIdentifier) {
+ if ($isForIdentifier) {
$peek = \ord($this->peek());
// Ranges: a-z A-Z 0-9 - _
if (
@@ -216,108 +186,118 @@ public function parseCharacter($bIsForIdentifier)
} else {
return $this->consume(1);
}
+
return null;
}
/**
- * @return array|void
+ * @return list
*
* @throws UnexpectedEOFException
* @throws UnexpectedTokenException
*/
public function consumeWhiteSpace(): array
{
- $aComments = [];
+ $comments = [];
do {
- while (\preg_match('/\\s/isSu', $this->peek()) === 1) {
+ while (preg_match('/\\s/isSu', $this->peek()) === 1) {
$this->consume(1);
}
- if ($this->oParserSettings->bLenientParsing) {
+ if ($this->parserSettings->usesLenientParsing()) {
try {
- $oComment = $this->consumeComment();
+ $comment = $this->consumeComment();
} catch (UnexpectedEOFException $e) {
- $this->iCurrentPosition = $this->iLength;
- return $aComments;
+ $this->currentPosition = \count($this->characters);
+ break;
}
} else {
- $oComment = $this->consumeComment();
+ $comment = $this->consumeComment();
}
- if ($oComment !== false) {
- $aComments[] = $oComment;
+ if ($comment instanceof Comment) {
+ $comments[] = $comment;
}
- } while ($oComment !== false);
- return $aComments;
+ } while ($comment instanceof Comment);
+
+ return $comments;
}
/**
- * @param string $sString
- * @param bool $bCaseInsensitive
+ * @param non-empty-string $string
*/
- public function comes($sString, $bCaseInsensitive = false): bool
+ public function comes(string $string, bool $caseInsensitive = false): bool
{
- $sPeek = $this->peek(\strlen($sString));
- return ($sPeek == '')
- ? false
- : $this->streql($sPeek, $sString, $bCaseInsensitive);
+ $peek = $this->peek(\strlen($string));
+
+ return ($peek !== '') && $this->streql($peek, $string, $caseInsensitive);
}
/**
- * @param int $iLength
- * @param int $iOffset
+ * @param int<1, max> $length
+ * @param int<0, max> $offset
*/
- public function peek($iLength = 1, $iOffset = 0): string
+ public function peek(int $length = 1, int $offset = 0): string
{
- $iOffset += $this->iCurrentPosition;
- if ($iOffset >= $this->iLength) {
+ $offset += $this->currentPosition;
+ if ($offset >= \count($this->characters)) {
return '';
}
- return $this->substr($iOffset, $iLength);
+
+ return $this->substr($offset, $length);
}
/**
- * @param int $mValue
+ * @param string|int<1, max> $value
*
* @throws UnexpectedEOFException
* @throws UnexpectedTokenException
*/
- public function consume($mValue = 1): string
+ public function consume($value = 1): string
{
- if (\is_string($mValue)) {
- $iLineCount = \substr_count($mValue, "\n");
- $iLength = $this->strlen($mValue);
- if (!$this->streql($this->substr($this->iCurrentPosition, $iLength), $mValue)) {
- throw new UnexpectedTokenException($mValue, $this->peek(\max($iLength, 5)), $this->iLineNo);
+ if (\is_string($value)) {
+ $numberOfLines = \substr_count($value, "\n");
+ $length = $this->strlen($value);
+ if (!$this->streql($this->substr($this->currentPosition, $length), $value)) {
+ throw new UnexpectedTokenException(
+ $value,
+ $this->peek(\max($length, 5)),
+ 'literal',
+ $this->lineNumber
+ );
}
- $this->iLineNo += $iLineCount;
- $this->iCurrentPosition += $this->strlen($mValue);
- return $mValue;
+
+ $this->lineNumber += $numberOfLines;
+ $this->currentPosition += $this->strlen($value);
+ $result = $value;
} else {
- if ($this->iCurrentPosition + $mValue > $this->iLength) {
- throw new UnexpectedEOFException($mValue, $this->peek(5), 'count', $this->iLineNo);
+ if ($this->currentPosition + $value > \count($this->characters)) {
+ throw new UnexpectedEOFException((string) $value, $this->peek(5), 'count', $this->lineNumber);
}
- $sResult = $this->substr($this->iCurrentPosition, $mValue);
- $iLineCount = \substr_count($sResult, "\n");
- $this->iLineNo += $iLineCount;
- $this->iCurrentPosition += $mValue;
- return $sResult;
+
+ $result = $this->substr($this->currentPosition, $value);
+ $numberOfLines = \substr_count($result, "\n");
+ $this->lineNumber += $numberOfLines;
+ $this->currentPosition += $value;
}
+
+ return $result;
}
/**
- * @param string $mExpression
- * @param int|null $iMaxLength
+ * @param string $expression
+ * @param int<1, max>|null $maximumLength
*
* @throws UnexpectedEOFException
* @throws UnexpectedTokenException
*/
- public function consumeExpression($mExpression, $iMaxLength = null): string
+ public function consumeExpression(string $expression, ?int $maximumLength = null): string
{
- $aMatches = null;
- $sInput = $iMaxLength !== null ? $this->peek($iMaxLength) : $this->inputLeft();
- if (\preg_match($mExpression, $sInput, $aMatches, PREG_OFFSET_CAPTURE) === 1) {
- return $this->consume($aMatches[0][0]);
+ $matches = null;
+ $input = ($maximumLength !== null) ? $this->peek($maximumLength) : $this->inputLeft();
+ if (preg_match($expression, $input, $matches, PREG_OFFSET_CAPTURE) !== 1) {
+ throw new UnexpectedTokenException($expression, $this->peek(5), 'expression', $this->lineNumber);
}
- throw new UnexpectedTokenException($mExpression, $this->peek(5), 'expression', $this->iLineNo);
+
+ return $this->consume($matches[0][0]);
}
/**
@@ -325,13 +305,14 @@ public function consumeExpression($mExpression, $iMaxLength = null): string
*/
public function consumeComment()
{
- $mComment = false;
+ $lineNumber = $this->lineNumber;
+ $comment = null;
+
if ($this->comes('/*')) {
- $iLineNo = $this->iLineNo;
$this->consume(1);
- $mComment = '';
+ $comment = '';
while (($char = $this->consume(1)) !== '') {
- $mComment .= $char;
+ $comment .= $char;
if ($this->comes('*/')) {
$this->consume(2);
break;
@@ -339,177 +320,142 @@ public function consumeComment()
}
}
- if ($mComment !== false) {
- // We skip the * which was included in the comment.
- return new Comment(\substr($mComment, 1), $iLineNo);
- }
-
- return $mComment;
+ // We skip the * which was included in the comment.
+ return \is_string($comment) ? new Comment(\substr($comment, 1), $lineNumber) : false;
}
public function isEnd(): bool
{
- return $this->iCurrentPosition >= $this->iLength;
+ return $this->currentPosition >= \count($this->characters);
}
/**
- * @param array|string $aEnd
- * @param string $bIncludeEnd
- * @param string $consumeEnd
+ * @param list|string|self::EOF $stopCharacters
* @param array $comments
*
- * @return string
- *
* @throws UnexpectedEOFException
* @throws UnexpectedTokenException
*/
- public function consumeUntil($aEnd, $bIncludeEnd = false, $consumeEnd = false, array &$comments = [])
- {
- $aEnd = \is_array($aEnd) ? $aEnd : [$aEnd];
- $out = '';
- $start = $this->iCurrentPosition;
+ public function consumeUntil(
+ $stopCharacters,
+ bool $includeEnd = false,
+ bool $consumeEnd = false,
+ array &$comments = []
+ ): string {
+ $stopCharacters = \is_array($stopCharacters) ? $stopCharacters : [$stopCharacters];
+ $consumedCharacters = '';
+ $start = $this->currentPosition;
while (!$this->isEnd()) {
- $char = $this->consume(1);
- if (\in_array($char, $aEnd, true)) {
- if ($bIncludeEnd) {
- $out .= $char;
+ $character = $this->consume(1);
+ if (\in_array($character, $stopCharacters, true)) {
+ if ($includeEnd) {
+ $consumedCharacters .= $character;
} elseif (!$consumeEnd) {
- $this->iCurrentPosition -= $this->strlen($char);
+ $this->currentPosition -= $this->strlen($character);
}
- return $out;
+ return $consumedCharacters;
}
- $out .= $char;
- if ($comment = $this->consumeComment()) {
+ $consumedCharacters .= $character;
+ $comment = $this->consumeComment();
+ if ($comment instanceof Comment) {
$comments[] = $comment;
}
}
- if (\in_array(self::EOF, $aEnd, true)) {
- return $out;
+ if (\in_array(self::EOF, $stopCharacters, true)) {
+ return $consumedCharacters;
}
- $this->iCurrentPosition = $start;
+ $this->currentPosition = $start;
throw new UnexpectedEOFException(
- 'One of ("' . \implode('","', $aEnd) . '")',
+ 'One of ("' . \implode('","', $stopCharacters) . '")',
$this->peek(5),
'search',
- $this->iLineNo
+ $this->lineNumber
);
}
private function inputLeft(): string
{
- return $this->substr($this->iCurrentPosition, -1);
+ return $this->substr($this->currentPosition, -1);
}
- /**
- * @param string $sString1
- * @param string $sString2
- * @param bool $bCaseInsensitive
- */
- public function streql($sString1, $sString2, $bCaseInsensitive = true): bool
+ public function streql(string $string1, string $string2, bool $caseInsensitive = true): bool
{
- if ($bCaseInsensitive) {
- return $this->strtolower($sString1) === $this->strtolower($sString2);
- } else {
- return $sString1 === $sString2;
- }
+ return $caseInsensitive
+ ? ($this->strtolower($string1) === $this->strtolower($string2))
+ : ($string1 === $string2);
}
/**
- * @param int $iAmount
+ * @param int<1, max> $numberOfCharacters
*/
- public function backtrack($iAmount): void
+ public function backtrack(int $numberOfCharacters): void
{
- $this->iCurrentPosition -= $iAmount;
+ $this->currentPosition -= $numberOfCharacters;
}
/**
- * @param string $sString
+ * @return int<0, max>
*/
- public function strlen($sString): int
+ public function strlen(string $string): int
{
- if ($this->oParserSettings->bMultibyteSupport) {
- return \mb_strlen($sString, $this->sCharset);
- } else {
- return \strlen($sString);
- }
+ return $this->parserSettings->hasMultibyteSupport()
+ ? \mb_strlen($string, $this->charset)
+ : \strlen($string);
}
/**
- * @param int $iStart
- * @param int $iLength
+ * @param int<0, max> $offset
*/
- private function substr($iStart, $iLength): string
+ private function substr(int $offset, int $length): string
{
- if ($iLength < 0) {
- $iLength = $this->iLength - $iStart + $iLength;
+ if ($length < 0) {
+ $length = \count($this->characters) - $offset + $length;
}
- if ($iStart + $iLength > $this->iLength) {
- $iLength = $this->iLength - $iStart;
+ if ($offset + $length > \count($this->characters)) {
+ $length = \count($this->characters) - $offset;
}
- $sResult = '';
- while ($iLength > 0) {
- $sResult .= $this->aText[$iStart];
- $iStart++;
- $iLength--;
+ $result = '';
+ while ($length > 0) {
+ $result .= $this->characters[$offset];
+ $offset++;
+ $length--;
}
- return $sResult;
+
+ return $result;
}
/**
- * @param string $sString
+ * @return ($string is non-empty-string ? non-empty-string : string)
*/
- private function strtolower($sString): string
+ private function strtolower(string $string): string
{
- if ($this->oParserSettings->bMultibyteSupport) {
- return \mb_strtolower($sString, $this->sCharset);
- } else {
- return \strtolower($sString);
- }
+ return $this->parserSettings->hasMultibyteSupport()
+ ? \mb_strtolower($string, $this->charset)
+ : \strtolower($string);
}
/**
- * @param string $sString
- *
- * @return array
+ * @return list
*/
- private function strsplit($sString)
+ private function strsplit(string $string): array
{
- if ($this->oParserSettings->bMultibyteSupport) {
- if ($this->streql($this->sCharset, 'utf-8')) {
- return \preg_split('//u', $sString, -1, PREG_SPLIT_NO_EMPTY);
+ if ($this->parserSettings->hasMultibyteSupport()) {
+ if ($this->streql($this->charset, 'utf-8')) {
+ $result = preg_split('//u', $string, -1, PREG_SPLIT_NO_EMPTY);
} else {
- $iLength = \mb_strlen($sString, $this->sCharset);
- $aResult = [];
- for ($i = 0; $i < $iLength; ++$i) {
- $aResult[] = \mb_substr($sString, $i, 1, $this->sCharset);
+ $length = \mb_strlen($string, $this->charset);
+ $result = [];
+ for ($i = 0; $i < $length; ++$i) {
+ $result[] = \mb_substr($string, $i, 1, $this->charset);
}
- return $aResult;
}
} else {
- if ($sString === '') {
- return [];
- } else {
- return \str_split($sString);
- }
+ $result = ($string !== '') ? \str_split($string) : [];
}
- }
- /**
- * @param string $sString
- * @param string $sNeedle
- * @param int $iOffset
- *
- * @return int|false
- */
- private function strpos($sString, $sNeedle, $iOffset)
- {
- if ($this->oParserSettings->bMultibyteSupport) {
- return \mb_strpos($sString, $sNeedle, $iOffset, $this->sCharset);
- } else {
- return \strpos($sString, $sNeedle, $iOffset);
- }
+ return $result;
}
}
diff --git a/src/Parsing/SourceException.php b/src/Parsing/SourceException.php
index 1ca668a99..43b3faf05 100644
--- a/src/Parsing/SourceException.php
+++ b/src/Parsing/SourceException.php
@@ -1,32 +1,25 @@
|null $lineNumber
*/
- public function __construct($sMessage, $iLineNo = 0)
+ public function __construct(string $message, ?int $lineNumber = null)
{
- $this->iLineNo = $iLineNo;
- if (!empty($iLineNo)) {
- $sMessage .= " [line no: $iLineNo]";
+ $this->setPosition($lineNumber);
+ if ($lineNumber !== null) {
+ $message .= " [line no: $lineNumber]";
}
- parent::__construct($sMessage);
- }
-
- /**
- * @return int
- */
- public function getLineNo()
- {
- return $this->iLineNo;
+ parent::__construct($message);
}
}
diff --git a/src/Parsing/UnexpectedEOFException.php b/src/Parsing/UnexpectedEOFException.php
index 825cc0c4f..17e2a2152 100644
--- a/src/Parsing/UnexpectedEOFException.php
+++ b/src/Parsing/UnexpectedEOFException.php
@@ -1,5 +1,7 @@
|null $lineNumber
*/
- public function __construct($sExpected, $sFound, $sMatchType = 'literal', $iLineNo = 0)
+ public function __construct(string $expected, string $found, string $matchType = 'literal', ?int $lineNumber = null)
{
- $this->sExpected = $sExpected;
- $this->sFound = $sFound;
- $this->sMatchType = $sMatchType;
- $sMessage = "Token “{$sExpected}” ({$sMatchType}) not found. Got “{$sFound}”.";
- if ($this->sMatchType === 'search') {
- $sMessage = "Search for “{$sExpected}” returned no results. Context: “{$sFound}”.";
- } elseif ($this->sMatchType === 'count') {
- $sMessage = "Next token was expected to have {$sExpected} chars. Context: “{$sFound}”.";
- } elseif ($this->sMatchType === 'identifier') {
- $sMessage = "Identifier expected. Got “{$sFound}”";
- } elseif ($this->sMatchType === 'custom') {
- $sMessage = \trim("$sExpected $sFound");
+ $message = "Token “{$expected}” ({$matchType}) not found. Got “{$found}”.";
+ if ($matchType === 'search') {
+ $message = "Search for “{$expected}” returned no results. Context: “{$found}”.";
+ } elseif ($matchType === 'count') {
+ $message = "Next token was expected to have {$expected} chars. Context: “{$found}”.";
+ } elseif ($matchType === 'identifier') {
+ $message = "Identifier expected. Got “{$found}”";
+ } elseif ($matchType === 'custom') {
+ $message = \trim("$expected $found");
}
- parent::__construct($sMessage, $iLineNo);
+ parent::__construct($message, $lineNumber);
}
}
diff --git a/src/Position/Position.php b/src/Position/Position.php
new file mode 100644
index 000000000..7b75f8eac
--- /dev/null
+++ b/src/Position/Position.php
@@ -0,0 +1,55 @@
+|null
+ */
+ protected $lineNumber;
+
+ /**
+ * @var int<0, max>|null
+ */
+ protected $columnNumber;
+
+ /**
+ * @return int<1, max>|null
+ */
+ public function getLineNumber(): ?int
+ {
+ return $this->lineNumber;
+ }
+
+ /**
+ * @return int<0, max>|null
+ */
+ public function getColumnNumber(): ?int
+ {
+ return $this->columnNumber;
+ }
+
+ /**
+ * @param int<1, max>|null $lineNumber
+ * @param int<0, max>|null $columnNumber
+ *
+ * @return $this fluent interface
+ */
+ public function setPosition(?int $lineNumber, ?int $columnNumber = null): Positionable
+ {
+ $this->lineNumber = $lineNumber;
+ $this->columnNumber = $columnNumber;
+
+ return $this;
+ }
+}
diff --git a/src/Position/Positionable.php b/src/Position/Positionable.php
new file mode 100644
index 000000000..d23d26a32
--- /dev/null
+++ b/src/Position/Positionable.php
@@ -0,0 +1,31 @@
+|null
+ */
+ public function getLineNumber(): ?int;
+
+ /**
+ * @return int<0, max>|null
+ */
+ public function getColumnNumber(): ?int;
+
+ /**
+ * @param int<1, max>|null $lineNumber
+ * @param int<0, max>|null $columnNumber
+ *
+ * @return $this fluent interface
+ */
+ public function setPosition(?int $lineNumber, ?int $columnNumber = null): Positionable;
+}
diff --git a/src/Property/AtRule.php b/src/Property/AtRule.php
index 64efd4ded..49a160a1a 100644
--- a/src/Property/AtRule.php
+++ b/src/Property/AtRule.php
@@ -1,38 +1,29 @@
+ * @var string|null
*/
- protected $aComments;
+ private $prefix;
/**
- * @param string $mUrl
- * @param string|null $sPrefix
- * @param int $iLineNo
+ * @param CSSString|URL $url
+ * @param int<1, max>|null $lineNumber
*/
- public function __construct($mUrl, $sPrefix = null, $iLineNo = 0)
+ public function __construct($url, ?string $prefix = null, ?int $lineNumber = null)
{
- $this->mUrl = $mUrl;
- $this->sPrefix = $sPrefix;
- $this->iLineNo = $iLineNo;
- $this->aComments = [];
+ $this->url = $url;
+ $this->prefix = $prefix;
+ $this->setPosition($lineNumber);
}
/**
- * @return int
+ * @return non-empty-string
*/
- public function getLineNo()
- {
- return $this->iLineNo;
- }
-
- public function __toString(): string
- {
- return $this->render(new OutputFormat());
- }
-
- public function render(OutputFormat $oOutputFormat): string
+ public function render(OutputFormat $outputFormat): string
{
- return '@namespace ' . ($this->sPrefix === null ? '' : $this->sPrefix . ' ')
- . $this->mUrl->render($oOutputFormat) . ';';
+ return '@namespace ' . ($this->prefix === null ? '' : $this->prefix . ' ')
+ . $this->url->render($outputFormat) . ';';
}
/**
- * @return string
+ * @return CSSString|URL
*/
public function getUrl()
{
- return $this->mUrl;
+ return $this->url;
}
- /**
- * @return string|null
- */
- public function getPrefix()
+ public function getPrefix(): ?string
{
- return $this->sPrefix;
+ return $this->prefix;
}
/**
- * @param string $mUrl
+ * @param CSSString|URL $url
*/
- public function setUrl($mUrl): void
+ public function setUrl($url): void
{
- $this->mUrl = $mUrl;
+ $this->url = $url;
}
- /**
- * @param string $sPrefix
- */
- public function setPrefix($sPrefix): void
+ public function setPrefix(string $prefix): void
{
- $this->sPrefix = $sPrefix;
+ $this->prefix = $prefix;
}
/**
- * @return string
+ * @return non-empty-string
*/
public function atRuleName(): string
{
@@ -103,38 +84,14 @@ public function atRuleName(): string
}
/**
- * @return array
+ * @return array{0: CSSString|URL|non-empty-string, 1?: CSSString|URL}
*/
public function atRuleArgs(): array
{
- $aResult = [$this->mUrl];
- if ($this->sPrefix) {
- \array_unshift($aResult, $this->sPrefix);
+ $result = [$this->url];
+ if (\is_string($this->prefix) && $this->prefix !== '') {
+ \array_unshift($result, $this->prefix);
}
- return $aResult;
- }
-
- /**
- * @param array $aComments
- */
- public function addComments(array $aComments): void
- {
- $this->aComments = \array_merge($this->aComments, $aComments);
- }
-
- /**
- * @return array
- */
- public function getComments()
- {
- return $this->aComments;
- }
-
- /**
- * @param array $aComments
- */
- public function setComments(array $aComments): void
- {
- $this->aComments = $aComments;
+ return $result;
}
}
diff --git a/src/Property/Charset.php b/src/Property/Charset.php
index 93a459ed5..c9488ad1e 100644
--- a/src/Property/Charset.php
+++ b/src/Property/Charset.php
@@ -1,9 +1,13 @@
+ * @var CSSString
*/
- protected $aComments;
+ private $charset;
/**
- * @param CSSString $oCharset
- * @param int $iLineNo
+ * @param int<1, max>|null $lineNumber
*/
- public function __construct(CSSString $oCharset, $iLineNo = 0)
+ public function __construct(CSSString $charset, ?int $lineNumber = null)
{
- $this->oCharset = $oCharset;
- $this->iLineNo = $iLineNo;
- $this->aComments = [];
+ $this->charset = $charset;
+ $this->setPosition($lineNumber);
}
/**
- * @return int
+ * @param string|CSSString $charset
*/
- public function getLineNo()
+ public function setCharset($charset): void
{
- return $this->iLineNo;
+ $charset = $charset instanceof CSSString ? $charset : new CSSString($charset);
+ $this->charset = $charset;
}
- /**
- * @param string|CSSString $oCharset
- *
- * @return void
- */
- public function setCharset($sCharset): void
+ public function getCharset(): string
{
- $sCharset = $sCharset instanceof CSSString ? $sCharset : new CSSString($sCharset);
- $this->oCharset = $sCharset;
+ return $this->charset->getString();
}
/**
- * @return string
+ * @return non-empty-string
*/
- public function getCharset()
+ public function render(OutputFormat $outputFormat): string
{
- return $this->oCharset->getString();
- }
-
- public function __toString(): string
- {
- return $this->render(new OutputFormat());
- }
-
- public function render(OutputFormat $oOutputFormat): string
- {
- return "{$oOutputFormat->comments($this)}@charset {$this->oCharset->render($oOutputFormat)};";
- }
-
- public function atRuleName(): string
- {
- return 'charset';
+ return "{$outputFormat->getFormatter()->comments($this)}@charset {$this->charset->render($outputFormat)};";
}
/**
- * @return string
+ * @return non-empty-string
*/
- public function atRuleArgs()
- {
- return $this->oCharset;
- }
-
- /**
- * @param array $aComments
- *
- * @return void
- */
- public function addComments(array $aComments): void
- {
- $this->aComments = \array_merge($this->aComments, $aComments);
- }
-
- /**
- * @return array
- */
- public function getComments()
+ public function atRuleName(): string
{
- return $this->aComments;
+ return 'charset';
}
- /**
- * @param array $aComments
- *
- * @return void
- */
- public function setComments(array $aComments): void
+ public function atRuleArgs(): CSSString
{
- $this->aComments = $aComments;
+ return $this->charset;
}
}
diff --git a/src/Property/Import.php b/src/Property/Import.php
index 04e801912..a9dabe979 100644
--- a/src/Property/Import.php
+++ b/src/Property/Import.php
@@ -1,130 +1,85 @@
+ * @var string|null
*/
- protected $aComments;
+ private $mediaQuery;
/**
- * @param URL $oLocation
- * @param string $sMediaQuery
- * @param int $iLineNo
+ * @param int<1, max>|null $lineNumber
*/
- public function __construct(URL $oLocation, $sMediaQuery, $iLineNo = 0)
+ public function __construct(URL $location, ?string $mediaQuery, ?int $lineNumber = null)
{
- $this->oLocation = $oLocation;
- $this->sMediaQuery = $sMediaQuery;
- $this->iLineNo = $iLineNo;
- $this->aComments = [];
+ $this->location = $location;
+ $this->mediaQuery = $mediaQuery;
+ $this->setPosition($lineNumber);
}
- /**
- * @return int
- */
- public function getLineNo()
+ public function setLocation(URL $location): void
{
- return $this->iLineNo;
+ $this->location = $location;
}
- /**
- * @param URL $oLocation
- */
- public function setLocation($oLocation): void
+ public function getLocation(): URL
{
- $this->oLocation = $oLocation;
+ return $this->location;
}
/**
- * @return URL
+ * @return non-empty-string
*/
- public function getLocation()
- {
- return $this->oLocation;
- }
-
- public function __toString(): string
- {
- return $this->render(new OutputFormat());
- }
-
- public function render(OutputFormat $oOutputFormat): string
+ public function render(OutputFormat $outputFormat): string
{
- return $oOutputFormat->comments($this) . '@import ' . $this->oLocation->render($oOutputFormat)
- . ($this->sMediaQuery === null ? '' : ' ' . $this->sMediaQuery) . ';';
+ return $outputFormat->getFormatter()->comments($this) . '@import ' . $this->location->render($outputFormat)
+ . ($this->mediaQuery === null ? '' : ' ' . $this->mediaQuery) . ';';
}
+ /**
+ * @return non-empty-string
+ */
public function atRuleName(): string
{
return 'import';
}
/**
- * @return array
+ * @return array{0: URL, 1?: non-empty-string}
*/
public function atRuleArgs(): array
{
- $aResult = [$this->oLocation];
- if ($this->sMediaQuery) {
- \array_push($aResult, $this->sMediaQuery);
+ $result = [$this->location];
+ if (\is_string($this->mediaQuery) && $this->mediaQuery !== '') {
+ $result[] = $this->mediaQuery;
}
- return $aResult;
- }
-
- /**
- * @param array $aComments
- */
- public function addComments(array $aComments): void
- {
- $this->aComments = \array_merge($this->aComments, $aComments);
- }
-
- /**
- * @return array
- */
- public function getComments()
- {
- return $this->aComments;
- }
- /**
- * @param array $aComments
- */
- public function setComments(array $aComments): void
- {
- $this->aComments = $aComments;
+ return $result;
}
- /**
- * @return string
- */
- public function getMediaQuery()
+ public function getMediaQuery(): ?string
{
- return $this->sMediaQuery;
+ return $this->mediaQuery;
}
}
diff --git a/src/Property/KeyframeSelector.php b/src/Property/KeyframeSelector.php
index e58633622..47881771d 100644
--- a/src/Property/KeyframeSelector.php
+++ b/src/Property/KeyframeSelector.php
@@ -1,25 +1,47 @@
]* # any sequence of valid unescaped characters
- (?:\\\\.)? # a single escaped character
- (?:([\'"]).*?(?]++
+ |
+ # 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}|$)
+ )
+ )*+ # zero or more times
+ |
+ # keyframe animation progress percentage (e.g. 50%), untrimmed
+ \\s*+(\\d++%)\\s*+
+ )$
+ /ux';
}
diff --git a/src/Property/Selector.php b/src/Property/Selector.php
index cf07a8f59..a647378e5 100644
--- a/src/Property/Selector.php
+++ b/src/Property/Selector.php
@@ -1,137 +1,94 @@
\\~]+)[\\w]+ # elements
- |
- \\:{1,2}( # pseudo-elements
- after|before|first-letter|first-line|selection
- ))
- /ix';
-
- /**
- * regexp for specificity calculations
- *
- * @var string
- *
- * @internal
+ * @internal since 8.5.2
*/
public const SELECTOR_VALIDATION_RX = '/
^(
(?:
- [a-zA-Z0-9\\x{00A0}-\\x{FFFF}_^$|*="\'~\\[\\]()\\-\\s\\.:#+>]* # any sequence of valid unescaped characters
- (?:\\\\.)? # a single escaped character
- (?:([\'"]).*?(?,]++
+ |
+ # 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}|$)
+ )
+ )*+ # zero or more times
)$
/ux';
/**
* @var string
*/
- private $sSelector;
+ private $selector;
/**
- * @var int|null
+ * @internal since V8.8.0
*/
- private $iSpecificity;
+ public static function isValid(string $selector): bool
+ {
+ // Note: We need to use `static::` here as the constant is overridden in the `KeyframeSelector` class.
+ $numberOfMatches = preg_match(static::SELECTOR_VALIDATION_RX, $selector);
- /**
- * @param string $sSelector
- *
- * @return bool
- */
- public static function isValid($sSelector)
+ return $numberOfMatches === 1;
+ }
+
+ public function __construct(string $selector)
{
- return \preg_match(static::SELECTOR_VALIDATION_RX, $sSelector);
+ $this->setSelector($selector);
}
- /**
- * @param string $sSelector
- * @param bool $bCalculateSpecificity
- */
- public function __construct($sSelector, $bCalculateSpecificity = false)
+ public function getSelector(): string
{
- $this->setSelector($sSelector);
- if ($bCalculateSpecificity) {
- $this->getSpecificity();
- }
+ return $this->selector;
}
- /**
- * @return string
- */
- public function getSelector()
+ public function setSelector(string $selector): void
{
- return $this->sSelector;
+ $this->selector = \trim($selector);
}
/**
- * @param string $sSelector
- *
- * @return void
+ * @return int<0, max>
*/
- public function setSelector($sSelector): void
+ public function getSpecificity(): int
{
- $this->sSelector = \trim($sSelector);
- $this->iSpecificity = null;
+ return SpecificityCalculator::calculate($this->selector);
}
- public function __toString(): string
+ public function render(OutputFormat $outputFormat): string
{
return $this->getSelector();
}
-
- /**
- * @return int
- */
- public function getSpecificity()
- {
- if ($this->iSpecificity === null) {
- $a = 0;
- /// @todo should exclude \# as well as "#"
- $aMatches = null;
- $b = \substr_count($this->sSelector, '#');
- $c = \preg_match_all(self::NON_ID_ATTRIBUTES_AND_PSEUDO_CLASSES_RX, $this->sSelector, $aMatches);
- $d = \preg_match_all(self::ELEMENTS_AND_PSEUDO_ELEMENTS_RX, $this->sSelector, $aMatches);
- $this->iSpecificity = ($a * 1000) + ($b * 100) + ($c * 10) + $d;
- }
- return $this->iSpecificity;
- }
}
diff --git a/src/Property/Selector/SpecificityCalculator.php b/src/Property/Selector/SpecificityCalculator.php
new file mode 100644
index 000000000..b2f1323e5
--- /dev/null
+++ b/src/Property/Selector/SpecificityCalculator.php
@@ -0,0 +1,87 @@
+\\~]+)[\\w]+ # elements
+ |
+ \\:{1,2}( # pseudo-elements
+ after|before|first-letter|first-line|selection
+ ))
+ /ix';
+
+ /**
+ * @var array>
+ */
+ private static $cache = [];
+
+ /**
+ * Calculates the specificity of the given CSS selector.
+ *
+ * @return int<0, max>
+ *
+ * @internal
+ */
+ public static function calculate(string $selector): int
+ {
+ if (!isset(self::$cache[$selector])) {
+ $a = 0;
+ /// @todo should exclude \# as well as "#"
+ $matches = null;
+ $b = \substr_count($selector, '#');
+ $c = preg_match_all(self::NON_ID_ATTRIBUTES_AND_PSEUDO_CLASSES_RX, $selector, $matches);
+ $d = preg_match_all(self::ELEMENTS_AND_PSEUDO_ELEMENTS_RX, $selector, $matches);
+ self::$cache[$selector] = ($a * 1000) + ($b * 100) + ($c * 10) + $d;
+ }
+
+ return self::$cache[$selector];
+ }
+
+ /**
+ * Clears the cache in order to lower memory usage.
+ */
+ public static function clearCache(): void
+ {
+ self::$cache = [];
+ }
+}
diff --git a/src/Renderable.php b/src/Renderable.php
index dc1bff3c1..9ebf9a9b9 100644
--- a/src/Renderable.php
+++ b/src/Renderable.php
@@ -1,21 +1,10 @@
- */
- private $aIeHack;
-
- /**
- * @var int
- */
- protected $iLineNo;
+ private $isImportant = false;
/**
- * @var int
+ * @param non-empty-string $rule
+ * @param int<1, max>|null $lineNumber
+ * @param int<0, max>|null $columnNumber
*/
- protected $iColNo;
-
- /**
- * @var array
- */
- protected $aComments;
-
- /**
- * @param string $sRule
- * @param int $iLineNo
- * @param int $iColNo
- */
- public function __construct($sRule, $iLineNo = 0, $iColNo = 0)
+ public function __construct(string $rule, ?int $lineNumber = null, ?int $columnNumber = null)
{
- $this->sRule = $sRule;
- $this->mValue = null;
- $this->bIsImportant = false;
- $this->aIeHack = [];
- $this->iLineNo = $iLineNo;
- $this->iColNo = $iColNo;
- $this->aComments = [];
+ $this->rule = $rule;
+ $this->setPosition($lineNumber, $columnNumber);
}
/**
+ * @param list $commentsBeforeRule
+ *
* @throws UnexpectedEOFException
* @throws UnexpectedTokenException
+ *
+ * @internal since V8.8.0
*/
- public static function parse(ParserState $oParserState): Rule
+ public static function parse(ParserState $parserState, array $commentsBeforeRule = []): Rule
{
- $aComments = $oParserState->consumeWhiteSpace();
- $oRule = new Rule(
- $oParserState->parseIdentifier(!$oParserState->comes('--')),
- $oParserState->currentLine(),
- $oParserState->currentColumn()
+ $comments = \array_merge($commentsBeforeRule, $parserState->consumeWhiteSpace());
+ $rule = new Rule(
+ $parserState->parseIdentifier(!$parserState->comes('--')),
+ $parserState->currentLine(),
+ $parserState->currentColumn()
);
- $oRule->setComments($aComments);
- $oRule->addComments($oParserState->consumeWhiteSpace());
- $oParserState->consume(':');
- $oValue = Value::parseValue($oParserState, self::listDelimiterForRule($oRule->getRule()));
- $oRule->setValue($oValue);
- if ($oParserState->getSettings()->bLenientParsing) {
- while ($oParserState->comes('\\')) {
- $oParserState->consume('\\');
- $oRule->addIeHack($oParserState->consume());
- $oParserState->consumeWhiteSpace();
- }
+ $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);
}
- $oParserState->consumeWhiteSpace();
- if ($oParserState->comes('!')) {
- $oParserState->consume('!');
- $oParserState->consumeWhiteSpace();
- $oParserState->consume('important');
- $oRule->setIsImportant(true);
+ $parserState->consumeWhiteSpace();
+ while ($parserState->comes(';')) {
+ $parserState->consume(';');
}
- $oParserState->consumeWhiteSpace();
- while ($oParserState->comes(';')) {
- $oParserState->consume(';');
- }
- $oParserState->consumeWhiteSpace();
- return $oRule;
+ return $rule;
}
/**
- * @param string $sRule
+ * 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 array
+ * @return list
*/
- private static function listDelimiterForRule($sRule): array
+ private static function listDelimiterForRule(string $rule): array
{
- if (\preg_match('/^font($|-)/', $sRule)) {
+ if (preg_match('/^font($|-)/', $rule) === 1) {
return [',', '/', ' '];
}
- return [',', ' ', '/'];
- }
-
- /**
- * @return int
- */
- public function getLineNo()
- {
- return $this->iLineNo;
- }
-
- /**
- * @return int
- */
- public function getColNo()
- {
- return $this->iColNo;
- }
- /**
- * @param int $iLine
- * @param int $iColumn
- */
- public function setPosition($iLine, $iColumn): void
- {
- $this->iColNo = $iColumn;
- $this->iLineNo = $iLine;
+ switch ($rule) {
+ case 'src':
+ return [' ', ','];
+ default:
+ return [',', ' ', '/'];
+ }
}
/**
- * @param string $sRule
+ * @param non-empty-string $rule
*/
- public function setRule($sRule): void
+ public function setRule(string $rule): void
{
- $this->sRule = $sRule;
+ $this->rule = $rule;
}
/**
- * @return string
+ * @return non-empty-string
*/
- public function getRule()
+ public function getRule(): string
{
- return $this->sRule;
+ return $this->rule;
}
/**
@@ -170,127 +135,66 @@ public function getRule()
*/
public function getValue()
{
- return $this->mValue;
+ return $this->value;
}
/**
- * @param RuleValueList|string|null $mValue
+ * @param RuleValueList|string|null $value
*/
- public function setValue($mValue): void
+ public function setValue($value): void
{
- $this->mValue = $mValue;
+ $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 $mValue
- * @param string $sType
+ * @param RuleValueList|array $value
*/
- public function addValue($mValue, $sType = ' '): void
+ public function addValue($value, string $type = ' '): void
{
- if (!\is_array($mValue)) {
- $mValue = [$mValue];
+ if (!\is_array($value)) {
+ $value = [$value];
}
- if (!$this->mValue instanceof RuleValueList || $this->mValue->getListSeparator() !== $sType) {
- $mCurrentValue = $this->mValue;
- $this->mValue = new RuleValueList($sType, $this->iLineNo);
- if ($mCurrentValue) {
- $this->mValue->addListComponent($mCurrentValue);
+ 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 ($mValue as $mValueItem) {
- $this->mValue->addListComponent($mValueItem);
+ foreach ($value as $valueItem) {
+ $this->value->addListComponent($valueItem);
}
}
- /**
- * @param int $iModifier
- */
- public function addIeHack($iModifier): void
- {
- $this->aIeHack[] = $iModifier;
- }
-
- /**
- * @param array $aModifiers
- *
- * @return void
- */
- public function setIeHack(array $aModifiers): void
- {
- $this->aIeHack = $aModifiers;
- }
-
- /**
- * @return array
- */
- public function getIeHack()
+ public function setIsImportant(bool $isImportant): void
{
- return $this->aIeHack;
+ $this->isImportant = $isImportant;
}
- /**
- * @param bool $bIsImportant
- */
- public function setIsImportant($bIsImportant): void
+ public function getIsImportant(): bool
{
- $this->bIsImportant = $bIsImportant;
+ return $this->isImportant;
}
/**
- * @return bool
+ * @return non-empty-string
*/
- public function getIsImportant()
- {
- return $this->bIsImportant;
- }
-
- public function __toString(): string
- {
- return $this->render(new OutputFormat());
- }
-
- public function render(OutputFormat $oOutputFormat): string
+ public function render(OutputFormat $outputFormat): string
{
- $sResult = "{$oOutputFormat->comments($this)}{$this->sRule}:{$oOutputFormat->spaceAfterRuleName()}";
- if ($this->mValue instanceof Value) { // Can also be a ValueList
- $sResult .= $this->mValue->render($oOutputFormat);
+ $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 {
- $sResult .= $this->mValue;
- }
- if (!empty($this->aIeHack)) {
- $sResult .= ' \\' . \implode('\\', $this->aIeHack);
+ $result .= $this->value;
}
- if ($this->bIsImportant) {
- $sResult .= ' !important';
+ if ($this->isImportant) {
+ $result .= ' !important';
}
- $sResult .= ';';
- return $sResult;
- }
-
- /**
- * @param array $aComments
- */
- public function addComments(array $aComments): void
- {
- $this->aComments = \array_merge($this->aComments, $aComments);
- }
-
- /**
- * @return array
- */
- public function getComments()
- {
- return $this->aComments;
- }
-
- /**
- * @param array $aComments
- */
- public function setComments(array $aComments): void
- {
- $this->aComments = $aComments;
+ $result .= ';';
+ return $result;
}
}
diff --git a/src/RuleSet/AtRuleSet.php b/src/RuleSet/AtRuleSet.php
index 6cc97e4dd..4cd5acc2a 100644
--- a/src/RuleSet/AtRuleSet.php
+++ b/src/RuleSet/AtRuleSet.php
@@ -1,5 +1,7 @@
|null $lineNumber
*/
- public function __construct($sType, $sArgs = '', $iLineNo = 0)
+ public function __construct(string $type, string $arguments = '', ?int $lineNumber = null)
{
- parent::__construct($iLineNo);
- $this->sType = $sType;
- $this->sArgs = $sArgs;
+ parent::__construct($lineNumber);
+ $this->type = $type;
+ $this->arguments = $arguments;
}
/**
- * @return string
+ * @return non-empty-string
*/
- public function atRuleName()
+ public function atRuleName(): string
{
- return $this->sType;
+ return $this->type;
}
- /**
- * @return string
- */
- public function atRuleArgs()
+ public function atRuleArgs(): string
{
- return $this->sArgs;
+ return $this->arguments;
}
- public function __toString(): string
- {
- return $this->render(new OutputFormat());
- }
-
- public function render(OutputFormat $oOutputFormat): string
+ /**
+ * @return non-empty-string
+ */
+ public function render(OutputFormat $outputFormat): string
{
- $sResult = $oOutputFormat->comments($this);
- $sArgs = $this->sArgs;
- if ($sArgs) {
- $sArgs = ' ' . $sArgs;
+ $formatter = $outputFormat->getFormatter();
+ $result = $formatter->comments($this);
+ $arguments = $this->arguments;
+ if ($arguments !== '') {
+ $arguments = ' ' . $arguments;
}
- $sResult .= "@{$this->sType}$sArgs{$oOutputFormat->spaceBeforeOpeningBrace()}{";
- $sResult .= $this->renderRules($oOutputFormat);
- $sResult .= '}';
- return $sResult;
+ $result .= "@{$this->type}$arguments{$formatter->spaceBeforeOpeningBrace()}{";
+ $result .= $this->renderRules($outputFormat);
+ $result .= '}';
+ return $result;
}
}
diff --git a/src/RuleSet/DeclarationBlock.php b/src/RuleSet/DeclarationBlock.php
index 6232a7faa..4d0775460 100644
--- a/src/RuleSet/DeclarationBlock.php
+++ b/src/RuleSet/DeclarationBlock.php
@@ -1,22 +1,24 @@
+ */
+ private $selectors = [];
+
/**
- * @var array
+ * @var RuleSet
*/
- private $aSelectors;
+ private $ruleSet;
/**
- * @param int $iLineNo
+ * @param int<1, max>|null $lineNumber
*/
- public function __construct($iLineNo = 0)
+ public function __construct(?int $lineNumber = null)
{
- parent::__construct($iLineNo);
- $this->aSelectors = [];
+ $this->ruleSet = new RuleSet($lineNumber);
+ $this->setPosition($lineNumber);
}
/**
- * @param CSSList|null $oList
- *
- * @return DeclarationBlock|false
- *
* @throws UnexpectedTokenException
* @throws UnexpectedEOFException
+ *
+ * @internal since V8.8.0
*/
- public static function parse(ParserState $oParserState, $oList = null)
+ public static function parse(ParserState $parserState, ?CSSList $list = null): ?DeclarationBlock
{
- $aComments = [];
- $oResult = new DeclarationBlock($oParserState->currentLine());
+ $comments = [];
+ $result = new DeclarationBlock($parserState->currentLine());
try {
- $aSelectorParts = [];
- $sStringWrapperChar = false;
+ $selectors = [];
+ $selectorParts = [];
+ $stringWrapperCharacter = null;
+ $functionNestingLevel = 0;
+ $consumedNextCharacter = false;
+ static $stopCharacters = ['{', '}', '\'', '"', '(', ')', ','];
do {
- $aSelectorParts[] = $oParserState->consume(1)
- . $oParserState->consumeUntil(['{', '}', '\'', '"'], false, false, $aComments);
- if (\in_array($oParserState->peek(), ['\'', '"'], true) && \substr(\end($aSelectorParts), -1) != '\\') {
- if ($sStringWrapperChar === false) {
- $sStringWrapperChar = $oParserState->peek();
- } elseif ($sStringWrapperChar == $oParserState->peek()) {
- $sStringWrapperChar = false;
- }
+ if (!$consumedNextCharacter) {
+ $selectorParts[] = $parserState->consume(1);
}
- } while (!\in_array($oParserState->peek(), ['{', '}'], true) || $sStringWrapperChar !== false);
- $oResult->setSelectors(\implode('', $aSelectorParts), $oList);
- if ($oParserState->comes('{')) {
- $oParserState->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
+ $result->setSelectors($selectors, $list);
+ if ($parserState->comes('{')) {
+ $parserState->consume(1);
}
} catch (UnexpectedTokenException $e) {
- if ($oParserState->getSettings()->bLenientParsing) {
- if (!$oParserState->comes('}')) {
- $oParserState->consumeUntil('}', false, true);
+ if ($parserState->getSettings()->usesLenientParsing()) {
+ if (!$parserState->comes('}')) {
+ $parserState->consumeUntil('}', false, true);
}
- return false;
+ return null;
} else {
throw $e;
}
}
- $oResult->setComments($aComments);
- RuleSet::parseRuleSet($oParserState, $oResult);
- return $oResult;
+ $result->setComments($comments);
+
+ RuleSet::parseRuleSet($parserState, $result->getRuleSet());
+
+ return $result;
}
/**
- * @param array|string $mSelector
- * @param CSSList|null $oList
+ * @param array|string $selectors
*
* @throws UnexpectedTokenException
*/
- public function setSelectors($mSelector, $oList = null): void
+ public function setSelectors($selectors, ?CSSList $list = null): void
{
- if (\is_array($mSelector)) {
- $this->aSelectors = $mSelector;
+ if (\is_array($selectors)) {
+ $this->selectors = $selectors;
} else {
- $this->aSelectors = \explode(',', $mSelector);
+ $this->selectors = \explode(',', $selectors);
}
- foreach ($this->aSelectors as $iKey => $mSelector) {
- if (!($mSelector instanceof Selector)) {
- if ($oList === null || !($oList instanceof KeyFrame)) {
- if (!Selector::isValid($mSelector)) {
+ foreach ($this->selectors as $key => $selector) {
+ if (!($selector instanceof Selector)) {
+ if ($list === null || !($list instanceof KeyFrame)) {
+ if (!Selector::isValid($selector)) {
throw new UnexpectedTokenException(
"Selector did not match '" . Selector::SELECTOR_VALIDATION_RX . "'.",
- $mSelector,
+ $selector,
'custom'
);
}
- $this->aSelectors[$iKey] = new Selector($mSelector);
+ $this->selectors[$key] = new Selector($selector);
} else {
- if (!KeyframeSelector::isValid($mSelector)) {
+ if (!KeyframeSelector::isValid($selector)) {
throw new UnexpectedTokenException(
"Selector did not match '" . KeyframeSelector::SELECTOR_VALIDATION_RX . "'.",
- $mSelector,
+ $selector,
'custom'
);
}
- $this->aSelectors[$iKey] = new KeyframeSelector($mSelector);
+ $this->selectors[$key] = new KeyframeSelector($selector);
}
}
}
@@ -128,16 +178,16 @@ public function setSelectors($mSelector, $oList = null): void
/**
* Remove one of the selectors of the block.
*
- * @param Selector|string $mSelector
+ * @param Selector|string $selectorToRemove
*/
- public function removeSelector($mSelector): bool
+ public function removeSelector($selectorToRemove): bool
{
- if ($mSelector instanceof Selector) {
- $mSelector = $mSelector->getSelector();
+ if ($selectorToRemove instanceof Selector) {
+ $selectorToRemove = $selectorToRemove->getSelector();
}
- foreach ($this->aSelectors as $iKey => $oSelector) {
- if ($oSelector->getSelector() === $mSelector) {
- unset($this->aSelectors[$iKey]);
+ foreach ($this->selectors as $key => $selector) {
+ if ($selector->getSelector() === $selectorToRemove) {
+ unset($this->selectors[$key]);
return true;
}
}
@@ -145,662 +195,107 @@ public function removeSelector($mSelector): bool
}
/**
- * @return array
+ * @return array
*/
- public function getSelectors()
+ public function getSelectors(): array
{
- return $this->aSelectors;
+ return $this->selectors;
}
- /**
- * Splits shorthand declarations (e.g. `margin` or `font`) into their constituent parts.
- *
- * @deprecated This will be removed without substitution in version 10.0.
- */
- public function expandShorthands(): void
+ public function getRuleSet(): RuleSet
{
- // border must be expanded before dimensions
- $this->expandBorderShorthand();
- $this->expandDimensionsShorthand();
- $this->expandFontShorthand();
- $this->expandBackgroundShorthand();
- $this->expandListStyleShorthand();
+ return $this->ruleSet;
}
/**
- * Creates shorthand declarations (e.g. `margin` or `font`) whenever possible.
- *
- * @deprecated This will be removed without substitution in version 10.0.
+ * @see RuleSet::addRule()
*/
- public function createShorthands(): void
+ public function addRule(Rule $ruleToAdd, ?Rule $sibling = null): void
{
- $this->createBackgroundShorthand();
- $this->createDimensionsShorthand();
- // border must be shortened after dimensions
- $this->createBorderShorthand();
- $this->createFontShorthand();
- $this->createListStyleShorthand();
+ $this->ruleSet->addRule($ruleToAdd, $sibling);
}
/**
- * Splits shorthand border declarations (e.g. `border: 1px red;`).
- *
- * Additional splitting happens in expandDimensionsShorthand.
- *
- * Multiple borders are not yet supported as of 3.
+ * @see RuleSet::getRules()
*
- * @deprecated This will be removed without substitution in version 10.0.
+ * @return array, Rule>
*/
- public function expandBorderShorthand(): void
+ public function getRules(?string $searchPattern = null): array
{
- $aBorderRules = [
- 'border',
- 'border-left',
- 'border-right',
- 'border-top',
- 'border-bottom',
- ];
- $aBorderSizes = [
- 'thin',
- 'medium',
- 'thick',
- ];
- $aRules = $this->getRulesAssoc();
- foreach ($aBorderRules as $sBorderRule) {
- if (!isset($aRules[$sBorderRule])) {
- continue;
- }
- $oRule = $aRules[$sBorderRule];
- $mRuleValue = $oRule->getValue();
- $aValues = [];
- if (!$mRuleValue instanceof RuleValueList) {
- $aValues[] = $mRuleValue;
- } else {
- $aValues = $mRuleValue->getListComponents();
- }
- foreach ($aValues as $mValue) {
- if ($mValue instanceof Value) {
- $mNewValue = clone $mValue;
- } else {
- $mNewValue = $mValue;
- }
- if ($mValue instanceof Size) {
- $sNewRuleName = $sBorderRule . '-width';
- } elseif ($mValue instanceof Color) {
- $sNewRuleName = $sBorderRule . '-color';
- } else {
- if (\in_array($mValue, $aBorderSizes, true)) {
- $sNewRuleName = $sBorderRule . '-width';
- } else {
- $sNewRuleName = $sBorderRule . '-style';
- }
- }
- $oNewRule = new Rule($sNewRuleName, $oRule->getLineNo(), $oRule->getColNo());
- $oNewRule->setIsImportant($oRule->getIsImportant());
- $oNewRule->addValue([$mNewValue]);
- $this->addRule($oNewRule);
- }
- $this->removeRule($sBorderRule);
- }
+ return $this->ruleSet->getRules($searchPattern);
}
/**
- * Splits shorthand dimensional declarations (e.g. `margin: 0px auto;`)
- * into their constituent parts.
- *
- * Handles `margin`, `padding`, `border-color`, `border-style` and `border-width`.
+ * @see RuleSet::setRules()
*
- * @deprecated This will be removed without substitution in version 10.0.
+ * @param array $rules
*/
- public function expandDimensionsShorthand(): void
+ public function setRules(array $rules): void
{
- $aExpansions = [
- 'margin' => 'margin-%s',
- 'padding' => 'padding-%s',
- 'border-color' => 'border-%s-color',
- 'border-style' => 'border-%s-style',
- 'border-width' => 'border-%s-width',
- ];
- $aRules = $this->getRulesAssoc();
- foreach ($aExpansions as $sProperty => $sExpanded) {
- if (!isset($aRules[$sProperty])) {
- continue;
- }
- $oRule = $aRules[$sProperty];
- $mRuleValue = $oRule->getValue();
- $aValues = [];
- if (!$mRuleValue instanceof RuleValueList) {
- $aValues[] = $mRuleValue;
- } else {
- $aValues = $mRuleValue->getListComponents();
- }
- $top = $right = $bottom = $left = null;
- switch (\count($aValues)) {
- case 1:
- $top = $right = $bottom = $left = $aValues[0];
- break;
- case 2:
- $top = $bottom = $aValues[0];
- $left = $right = $aValues[1];
- break;
- case 3:
- $top = $aValues[0];
- $left = $right = $aValues[1];
- $bottom = $aValues[2];
- break;
- case 4:
- $top = $aValues[0];
- $right = $aValues[1];
- $bottom = $aValues[2];
- $left = $aValues[3];
- break;
- }
- foreach (['top', 'right', 'bottom', 'left'] as $sPosition) {
- $oNewRule = new Rule(\sprintf($sExpanded, $sPosition), $oRule->getLineNo(), $oRule->getColNo());
- $oNewRule->setIsImportant($oRule->getIsImportant());
- $oNewRule->addValue(${$sPosition});
- $this->addRule($oNewRule);
- }
- $this->removeRule($sProperty);
- }
+ $this->ruleSet->setRules($rules);
}
/**
- * Converts shorthand font declarations
- * (e.g. `font: 300 italic 11px/14px verdana, helvetica, sans-serif;`)
- * into their constituent parts.
+ * @see RuleSet::getRulesAssoc()
*
- * @deprecated This will be removed without substitution in version 10.0.
+ * @return array
*/
- public function expandFontShorthand(): void
+ public function getRulesAssoc(?string $searchPattern = null): array
{
- $aRules = $this->getRulesAssoc();
- if (!isset($aRules['font'])) {
- return;
- }
- $oRule = $aRules['font'];
- // reset properties to 'normal' per http://www.w3.org/TR/21/fonts.html#font-shorthand
- $aFontProperties = [
- 'font-style' => 'normal',
- 'font-variant' => 'normal',
- 'font-weight' => 'normal',
- 'font-size' => 'normal',
- 'line-height' => 'normal',
- ];
- $mRuleValue = $oRule->getValue();
- $aValues = [];
- if (!$mRuleValue instanceof RuleValueList) {
- $aValues[] = $mRuleValue;
- } else {
- $aValues = $mRuleValue->getListComponents();
- }
- foreach ($aValues as $mValue) {
- if (!$mValue instanceof Value) {
- $mValue = \mb_strtolower($mValue);
- }
- if (\in_array($mValue, ['normal', 'inherit'], true)) {
- foreach (['font-style', 'font-weight', 'font-variant'] as $sProperty) {
- if (!isset($aFontProperties[$sProperty])) {
- $aFontProperties[$sProperty] = $mValue;
- }
- }
- } elseif (\in_array($mValue, ['italic', 'oblique'], true)) {
- $aFontProperties['font-style'] = $mValue;
- } elseif ($mValue == 'small-caps') {
- $aFontProperties['font-variant'] = $mValue;
- } elseif (
- \in_array($mValue, ['bold', 'bolder', 'lighter'], true)
- || ($mValue instanceof Size && \in_array($mValue->getSize(), \range(100.0, 900.0, 100.0), true))
- ) {
- $aFontProperties['font-weight'] = $mValue;
- } elseif ($mValue instanceof RuleValueList && $mValue->getListSeparator() == '/') {
- [$oSize, $oHeight] = $mValue->getListComponents();
- $aFontProperties['font-size'] = $oSize;
- $aFontProperties['line-height'] = $oHeight;
- } elseif ($mValue instanceof Size && $mValue->getUnit() !== null) {
- $aFontProperties['font-size'] = $mValue;
- } else {
- $aFontProperties['font-family'] = $mValue;
- }
- }
- foreach ($aFontProperties as $sProperty => $mValue) {
- $oNewRule = new Rule($sProperty, $oRule->getLineNo(), $oRule->getColNo());
- $oNewRule->addValue($mValue);
- $oNewRule->setIsImportant($oRule->getIsImportant());
- $this->addRule($oNewRule);
- }
- $this->removeRule('font');
+ return $this->ruleSet->getRulesAssoc($searchPattern);
}
/**
- * Converts shorthand background declarations
- * (e.g. `background: url("chess.png") gray 50% repeat fixed;`)
- * into their constituent parts.
- *
- * @see http://www.w3.org/TR/21/colors.html#propdef-background
- *
- * @deprecated This will be removed without substitution in version 10.0.
+ * @see RuleSet::removeRule()
*/
- public function expandBackgroundShorthand(): void
+ public function removeRule(Rule $ruleToRemove): void
{
- $aRules = $this->getRulesAssoc();
- if (!isset($aRules['background'])) {
- return;
- }
- $oRule = $aRules['background'];
- $aBgProperties = [
- 'background-color' => ['transparent'],
- 'background-image' => ['none'],
- 'background-repeat' => ['repeat'],
- 'background-attachment' => ['scroll'],
- 'background-position' => [
- new Size(0, '%', null, false, $this->iLineNo),
- new Size(0, '%', null, false, $this->iLineNo),
- ],
- ];
- $mRuleValue = $oRule->getValue();
- $aValues = [];
- if (!$mRuleValue instanceof RuleValueList) {
- $aValues[] = $mRuleValue;
- } else {
- $aValues = $mRuleValue->getListComponents();
- }
- if (\count($aValues) == 1 && $aValues[0] == 'inherit') {
- foreach ($aBgProperties as $sProperty => $mValue) {
- $oNewRule = new Rule($sProperty, $oRule->getLineNo(), $oRule->getColNo());
- $oNewRule->addValue('inherit');
- $oNewRule->setIsImportant($oRule->getIsImportant());
- $this->addRule($oNewRule);
- }
- $this->removeRule('background');
- return;
- }
- $iNumBgPos = 0;
- foreach ($aValues as $mValue) {
- if (!$mValue instanceof Value) {
- $mValue = \mb_strtolower($mValue);
- }
- if ($mValue instanceof URL) {
- $aBgProperties['background-image'] = $mValue;
- } elseif ($mValue instanceof Color) {
- $aBgProperties['background-color'] = $mValue;
- } elseif (\in_array($mValue, ['scroll', 'fixed'], true)) {
- $aBgProperties['background-attachment'] = $mValue;
- } elseif (\in_array($mValue, ['repeat', 'no-repeat', 'repeat-x', 'repeat-y'], true)) {
- $aBgProperties['background-repeat'] = $mValue;
- } elseif (
- \in_array($mValue, ['left', 'center', 'right', 'top', 'bottom'], true)
- || $mValue instanceof Size
- ) {
- if ($iNumBgPos == 0) {
- $aBgProperties['background-position'][0] = $mValue;
- $aBgProperties['background-position'][1] = 'center';
- } else {
- $aBgProperties['background-position'][$iNumBgPos] = $mValue;
- }
- $iNumBgPos++;
- }
- }
- foreach ($aBgProperties as $sProperty => $mValue) {
- $oNewRule = new Rule($sProperty, $oRule->getLineNo(), $oRule->getColNo());
- $oNewRule->setIsImportant($oRule->getIsImportant());
- $oNewRule->addValue($mValue);
- $this->addRule($oNewRule);
- }
- $this->removeRule('background');
+ $this->ruleSet->removeRule($ruleToRemove);
}
/**
- * @deprecated This will be removed without substitution in version 10.0.
+ * @see RuleSet::removeMatchingRules()
*/
- public function expandListStyleShorthand(): void
+ public function removeMatchingRules(string $searchPattern): void
{
- $aListProperties = [
- 'list-style-type' => 'disc',
- 'list-style-position' => 'outside',
- 'list-style-image' => 'none',
- ];
- $aListStyleTypes = [
- 'none',
- 'disc',
- 'circle',
- 'square',
- 'decimal-leading-zero',
- 'decimal',
- 'lower-roman',
- 'upper-roman',
- 'lower-greek',
- 'lower-alpha',
- 'lower-latin',
- 'upper-alpha',
- 'upper-latin',
- 'hebrew',
- 'armenian',
- 'georgian',
- 'cjk-ideographic',
- 'hiragana',
- 'hira-gana-iroha',
- 'katakana-iroha',
- 'katakana',
- ];
- $aListStylePositions = [
- 'inside',
- 'outside',
- ];
- $aRules = $this->getRulesAssoc();
- if (!isset($aRules['list-style'])) {
- return;
- }
- $oRule = $aRules['list-style'];
- $mRuleValue = $oRule->getValue();
- $aValues = [];
- if (!$mRuleValue instanceof RuleValueList) {
- $aValues[] = $mRuleValue;
- } else {
- $aValues = $mRuleValue->getListComponents();
- }
- if (\count($aValues) == 1 && $aValues[0] == 'inherit') {
- foreach ($aListProperties as $sProperty => $mValue) {
- $oNewRule = new Rule($sProperty, $oRule->getLineNo(), $oRule->getColNo());
- $oNewRule->addValue('inherit');
- $oNewRule->setIsImportant($oRule->getIsImportant());
- $this->addRule($oNewRule);
- }
- $this->removeRule('list-style');
- return;
- }
- foreach ($aValues as $mValue) {
- if (!$mValue instanceof Value) {
- $mValue = \mb_strtolower($mValue);
- }
- if ($mValue instanceof Url) {
- $aListProperties['list-style-image'] = $mValue;
- } elseif (\in_array($mValue, $aListStyleTypes, true)) {
- $aListProperties['list-style-types'] = $mValue;
- } elseif (\in_array($mValue, $aListStylePositions, true)) {
- $aListProperties['list-style-position'] = $mValue;
- }
- }
- foreach ($aListProperties as $sProperty => $mValue) {
- $oNewRule = new Rule($sProperty, $oRule->getLineNo(), $oRule->getColNo());
- $oNewRule->setIsImportant($oRule->getIsImportant());
- $oNewRule->addValue($mValue);
- $this->addRule($oNewRule);
- }
- $this->removeRule('list-style');
+ $this->ruleSet->removeMatchingRules($searchPattern);
}
/**
- * @param array $aProperties
- * @param string $sShorthand
- *
- * @deprecated This will be removed without substitution in version 10.0.
+ * @see RuleSet::removeAllRules()
*/
- public function createShorthandProperties(array $aProperties, $sShorthand): void
+ public function removeAllRules(): void
{
- $aRules = $this->getRulesAssoc();
- $oRule = null;
- $aNewValues = [];
- foreach ($aProperties as $sProperty) {
- if (!isset($aRules[$sProperty])) {
- continue;
- }
- $oRule = $aRules[$sProperty];
- if (!$oRule->getIsImportant()) {
- $mRuleValue = $oRule->getValue();
- $aValues = [];
- if (!$mRuleValue instanceof RuleValueList) {
- $aValues[] = $mRuleValue;
- } else {
- $aValues = $mRuleValue->getListComponents();
- }
- foreach ($aValues as $mValue) {
- $aNewValues[] = $mValue;
- }
- $this->removeRule($sProperty);
- }
- }
- if ($aNewValues !== [] && $oRule instanceof Rule) {
- $oNewRule = new Rule($sShorthand, $oRule->getLineNo(), $oRule->getColNo());
- foreach ($aNewValues as $mValue) {
- $oNewRule->addValue($mValue);
- }
- $this->addRule($oNewRule);
- }
+ $this->ruleSet->removeAllRules();
}
/**
- * @deprecated This will be removed without substitution in version 10.0.
- */
- public function createBackgroundShorthand(): void
- {
- $aProperties = [
- 'background-color',
- 'background-image',
- 'background-repeat',
- 'background-position',
- 'background-attachment',
- ];
- $this->createShorthandProperties($aProperties, 'background');
- }
-
- /**
- * @deprecated This will be removed without substitution in version 10.0.
- */
- public function createListStyleShorthand(): void
- {
- $aProperties = [
- 'list-style-type',
- 'list-style-position',
- 'list-style-image',
- ];
- $this->createShorthandProperties($aProperties, 'list-style');
- }
-
- /**
- * Combines `border-color`, `border-style` and `border-width` into `border`.
- *
- * Should be run after `create_dimensions_shorthand`!
+ * @return non-empty-string
*
- * @deprecated This will be removed without substitution in version 10.0.
- */
- public function createBorderShorthand(): void
- {
- $aProperties = [
- 'border-width',
- 'border-style',
- 'border-color',
- ];
- $this->createShorthandProperties($aProperties, 'border');
- }
-
- /**
- * Looks for long format CSS dimensional properties
- * (margin, padding, border-color, border-style and border-width)
- * and converts them into shorthand CSS properties.
- *
- * @deprecated This will be removed without substitution in version 10.0.
- */
- public function createDimensionsShorthand(): void
- {
- $aPositions = ['top', 'right', 'bottom', 'left'];
- $aExpansions = [
- 'margin' => 'margin-%s',
- 'padding' => 'padding-%s',
- 'border-color' => 'border-%s-color',
- 'border-style' => 'border-%s-style',
- 'border-width' => 'border-%s-width',
- ];
- $aRules = $this->getRulesAssoc();
- foreach ($aExpansions as $sProperty => $sExpanded) {
- $aFoldable = [];
- foreach ($aRules as $sRuleName => $oRule) {
- foreach ($aPositions as $sPosition) {
- if ($sRuleName == \sprintf($sExpanded, $sPosition)) {
- $aFoldable[$sRuleName] = $oRule;
- }
- }
- }
- // All four dimensions must be present
- if (\count($aFoldable) == 4) {
- $aValues = [];
- foreach ($aPositions as $sPosition) {
- $oRule = $aRules[\sprintf($sExpanded, $sPosition)];
- $mRuleValue = $oRule->getValue();
- $aRuleValues = [];
- if (!$mRuleValue instanceof RuleValueList) {
- $aRuleValues[] = $mRuleValue;
- } else {
- $aRuleValues = $mRuleValue->getListComponents();
- }
- $aValues[$sPosition] = $aRuleValues;
- }
- $oNewRule = new Rule($sProperty, $oRule->getLineNo(), $oRule->getColNo());
- if ((string) $aValues['left'][0] == (string) $aValues['right'][0]) {
- if ((string) $aValues['top'][0] == (string) $aValues['bottom'][0]) {
- if ((string) $aValues['top'][0] == (string) $aValues['left'][0]) {
- // All 4 sides are equal
- $oNewRule->addValue($aValues['top']);
- } else {
- // Top and bottom are equal, left and right are equal
- $oNewRule->addValue($aValues['top']);
- $oNewRule->addValue($aValues['left']);
- }
- } else {
- // Only left and right are equal
- $oNewRule->addValue($aValues['top']);
- $oNewRule->addValue($aValues['left']);
- $oNewRule->addValue($aValues['bottom']);
- }
- } else {
- // No sides are equal
- $oNewRule->addValue($aValues['top']);
- $oNewRule->addValue($aValues['left']);
- $oNewRule->addValue($aValues['bottom']);
- $oNewRule->addValue($aValues['right']);
- }
- $this->addRule($oNewRule);
- foreach ($aPositions as $sPosition) {
- $this->removeRule(\sprintf($sExpanded, $sPosition));
- }
- }
- }
- }
-
- /**
- * Looks for long format CSS font properties (e.g. `font-weight`) and
- * tries to convert them into a shorthand CSS `font` property.
- *
- * At least `font-size` AND `font-family` must be present in order to create a shorthand declaration.
- *
- * @deprecated This will be removed without substitution in version 10.0.
- */
- public function createFontShorthand(): void
- {
- $aFontProperties = [
- 'font-style',
- 'font-variant',
- 'font-weight',
- 'font-size',
- 'line-height',
- 'font-family',
- ];
- $aRules = $this->getRulesAssoc();
- if (!isset($aRules['font-size']) || !isset($aRules['font-family'])) {
- return;
- }
- $oOldRule = $aRules['font-size'] ?? $aRules['font-family'];
- $oNewRule = new Rule('font', $oOldRule->getLineNo(), $oOldRule->getColNo());
- unset($oOldRule);
- foreach (['font-style', 'font-variant', 'font-weight'] as $sProperty) {
- if (isset($aRules[$sProperty])) {
- $oRule = $aRules[$sProperty];
- $mRuleValue = $oRule->getValue();
- $aValues = [];
- if (!$mRuleValue instanceof RuleValueList) {
- $aValues[] = $mRuleValue;
- } else {
- $aValues = $mRuleValue->getListComponents();
- }
- if ($aValues[0] !== 'normal') {
- $oNewRule->addValue($aValues[0]);
- }
- }
- }
- // Get the font-size value
- $oRule = $aRules['font-size'];
- $mRuleValue = $oRule->getValue();
- $aFSValues = [];
- if (!$mRuleValue instanceof RuleValueList) {
- $aFSValues[] = $mRuleValue;
- } else {
- $aFSValues = $mRuleValue->getListComponents();
- }
- // But wait to know if we have line-height to add it
- if (isset($aRules['line-height'])) {
- $oRule = $aRules['line-height'];
- $mRuleValue = $oRule->getValue();
- $aLHValues = [];
- if (!$mRuleValue instanceof RuleValueList) {
- $aLHValues[] = $mRuleValue;
- } else {
- $aLHValues = $mRuleValue->getListComponents();
- }
- if ($aLHValues[0] !== 'normal') {
- $val = new RuleValueList('/', $this->iLineNo);
- $val->addListComponent($aFSValues[0]);
- $val->addListComponent($aLHValues[0]);
- $oNewRule->addValue($val);
- }
- } else {
- $oNewRule->addValue($aFSValues[0]);
- }
- $oRule = $aRules['font-family'];
- $mRuleValue = $oRule->getValue();
- $aFFValues = [];
- if (!$mRuleValue instanceof RuleValueList) {
- $aFFValues[] = $mRuleValue;
- } else {
- $aFFValues = $mRuleValue->getListComponents();
- }
- $oFFValue = new RuleValueList(',', $this->iLineNo);
- $oFFValue->setListComponents($aFFValues);
- $oNewRule->addValue($oFFValue);
-
- $this->addRule($oNewRule);
- foreach ($aFontProperties as $sProperty) {
- $this->removeRule($sProperty);
- }
- }
-
- /**
* @throws OutputException
*/
- public function __toString(): string
+ public function render(OutputFormat $outputFormat): string
{
- return $this->render(new OutputFormat());
- }
-
- /**
- * @throws OutputException
- */
- public function render(OutputFormat $oOutputFormat): string
- {
- $sResult = $oOutputFormat->comments($this);
- if (\count($this->aSelectors) === 0) {
+ $formatter = $outputFormat->getFormatter();
+ $result = $formatter->comments($this);
+ if (\count($this->selectors) === 0) {
// If all the selectors have been removed, this declaration block becomes invalid
- throw new OutputException('Attempt to print declaration block with missing selector', $this->iLineNo);
+ throw new OutputException(
+ 'Attempt to print declaration block with missing selector',
+ $this->getLineNumber()
+ );
}
- $sResult .= $oOutputFormat->sBeforeDeclarationBlock;
- $sResult .= $oOutputFormat->implode(
- $oOutputFormat->spaceBeforeSelectorSeparator() . ',' . $oOutputFormat->spaceAfterSelectorSeparator(),
- $this->aSelectors
+ $result .= $outputFormat->getContentBeforeDeclarationBlock();
+ $result .= $formatter->implode(
+ $formatter->spaceBeforeSelectorSeparator() . ',' . $formatter->spaceAfterSelectorSeparator(),
+ $this->selectors
);
- $sResult .= $oOutputFormat->sAfterDeclarationBlockSelectors;
- $sResult .= $oOutputFormat->spaceBeforeOpeningBrace() . '{';
- $sResult .= $this->renderRules($oOutputFormat);
- $sResult .= '}';
- $sResult .= $oOutputFormat->sAfterDeclarationBlock;
- return $sResult;
+ $result .= $outputFormat->getContentAfterDeclarationBlockSelectors();
+ $result .= $formatter->spaceBeforeOpeningBrace() . '{';
+ $result .= $this->ruleSet->render($outputFormat);
+ $result .= '}';
+ $result .= $outputFormat->getContentAfterDeclarationBlock();
+
+ return $result;
}
}
diff --git a/src/RuleSet/RuleContainer.php b/src/RuleSet/RuleContainer.php
new file mode 100644
index 000000000..0c6c5936c
--- /dev/null
+++ b/src/RuleSet/RuleContainer.php
@@ -0,0 +1,36 @@
+ $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;
+}
diff --git a/src/RuleSet/RuleSet.php b/src/RuleSet/RuleSet.php
index 9bf080b69..521a6ae9b 100644
--- a/src/RuleSet/RuleSet.php
+++ b/src/RuleSet/RuleSet.php
@@ -1,14 +1,18 @@
- */
- private $aRules;
+ use CommentContainer;
+ use Position;
/**
- * @var int
- */
- protected $iLineNo;
-
- /**
- * @var array
+ * the rules in this rule set, using the property name as the key,
+ * with potentially multiple rules per property name.
+ *
+ * @var array, Rule>>
*/
- protected $aComments;
+ private $rules = [];
/**
- * @param int $iLineNo
+ * @param int<1, max>|null $lineNumber
*/
- public function __construct($iLineNo = 0)
+ public function __construct(?int $lineNumber = null)
{
- $this->aRules = [];
- $this->iLineNo = $iLineNo;
- $this->aComments = [];
+ $this->setPosition($lineNumber);
}
/**
* @throws UnexpectedTokenException
* @throws UnexpectedEOFException
+ *
+ * @internal since V8.8.0
*/
- public static function parseRuleSet(ParserState $oParserState, RuleSet $oRuleSet): void
+ public static function parseRuleSet(ParserState $parserState, RuleSet $ruleSet): void
{
- while ($oParserState->comes(';')) {
- $oParserState->consume(';');
+ while ($parserState->comes(';')) {
+ $parserState->consume(';');
}
- while (!$oParserState->comes('}')) {
- $oRule = null;
- if ($oParserState->getSettings()->bLenientParsing) {
+ while (true) {
+ $commentsBeforeRule = $parserState->consumeWhiteSpace();
+ if ($parserState->comes('}')) {
+ break;
+ }
+ $rule = null;
+ if ($parserState->getSettings()->usesLenientParsing()) {
try {
- $oRule = Rule::parse($oParserState);
+ $rule = Rule::parse($parserState, $commentsBeforeRule);
} catch (UnexpectedTokenException $e) {
try {
- $sConsume = $oParserState->consumeUntil(["\n", ';', '}'], true);
+ $consumedText = $parserState->consumeUntil(["\n", ';', '}'], true);
// We need to “unfind” the matches to the end of the ruleSet as this will be matched later
- if ($oParserState->streql(\substr($sConsume, -1), '}')) {
- $oParserState->backtrack(1);
+ if ($parserState->streql(\substr($consumedText, -1), '}')) {
+ $parserState->backtrack(1);
} else {
- while ($oParserState->comes(';')) {
- $oParserState->consume(';');
+ while ($parserState->comes(';')) {
+ $parserState->consume(';');
}
}
} catch (UnexpectedTokenException $e) {
@@ -78,109 +84,137 @@ public static function parseRuleSet(ParserState $oParserState, RuleSet $oRuleSet
}
}
} else {
- $oRule = Rule::parse($oParserState);
+ $rule = Rule::parse($parserState, $commentsBeforeRule);
}
- if ($oRule) {
- $oRuleSet->addRule($oRule);
+ if ($rule instanceof Rule) {
+ $ruleSet->addRule($rule);
}
}
- $oParserState->consume('}');
- }
-
- /**
- * @return int
- */
- public function getLineNo()
- {
- return $this->iLineNo;
+ $parserState->consume('}');
}
/**
- * @param Rule|null $oSibling
+ * @throws \UnexpectedValueException
+ * if the last `Rule` is needed as a basis for setting position, but does not have a valid position,
+ * which should never happen
*/
- public function addRule(Rule $oRule, ?Rule $oSibling = null): void
+ public function addRule(Rule $ruleToAdd, ?Rule $sibling = null): void
{
- $sRule = $oRule->getRule();
- if (!isset($this->aRules[$sRule])) {
- $this->aRules[$sRule] = [];
+ $propertyName = $ruleToAdd->getRule();
+ if (!isset($this->rules[$propertyName])) {
+ $this->rules[$propertyName] = [];
}
- $iPosition = \count($this->aRules[$sRule]);
+ $position = \count($this->rules[$propertyName]);
- if ($oSibling !== null) {
- $iSiblingPos = \array_search($oSibling, $this->aRules[$sRule], true);
- if ($iSiblingPos !== false) {
- $iPosition = $iSiblingPos;
- $oRule->setPosition($oSibling->getLineNo(), $oSibling->getColNo() - 1);
+ if ($sibling !== null) {
+ $siblingIsInSet = false;
+ $siblingPosition = \array_search($sibling, $this->rules[$propertyName], true);
+ if ($siblingPosition !== false) {
+ $siblingIsInSet = true;
+ $position = $siblingPosition;
+ } else {
+ $siblingIsInSet = $this->hasRule($sibling);
+ if ($siblingIsInSet) {
+ // Maintain ordering within `$this->rules[$propertyName]`
+ // by inserting before first `Rule` with a same-or-later position than the sibling.
+ foreach ($this->rules[$propertyName] as $index => $rule) {
+ if (self::comparePositionable($rule, $sibling) >= 0) {
+ $position = $index;
+ break;
+ }
+ }
+ }
+ }
+ if ($siblingIsInSet) {
+ // Increment column number of all existing rules on same line, starting at sibling
+ $siblingLineNumber = $sibling->getLineNumber();
+ $siblingColumnNumber = $sibling->getColumnNumber();
+ foreach ($this->rules as $rulesForAProperty) {
+ foreach ($rulesForAProperty as $rule) {
+ if (
+ $rule->getLineNumber() === $siblingLineNumber &&
+ $rule->getColumnNumber() >= $siblingColumnNumber
+ ) {
+ $rule->setPosition($siblingLineNumber, $rule->getColumnNumber() + 1);
+ }
+ }
+ }
+ $ruleToAdd->setPosition($siblingLineNumber, $siblingColumnNumber);
}
}
- if ($oRule->getLineNo() === 0 && $oRule->getColNo() === 0) {
+
+ if ($ruleToAdd->getLineNumber() === null) {
//this node is added manually, give it the next best line
+ $columnNumber = $ruleToAdd->getColumnNumber() ?? 0;
$rules = $this->getRules();
- $pos = \count($rules);
- if ($pos > 0) {
- $last = $rules[$pos - 1];
- $oRule->setPosition($last->getLineNo() + 1, 0);
+ $rulesCount = \count($rules);
+ if ($rulesCount > 0) {
+ $last = $rules[$rulesCount - 1];
+ $lastsLineNumber = $last->getLineNumber();
+ if (!\is_int($lastsLineNumber)) {
+ throw new \UnexpectedValueException(
+ 'A Rule without a line number was found during addRule',
+ 1750718399
+ );
+ }
+ $ruleToAdd->setPosition($lastsLineNumber + 1, $columnNumber);
+ } else {
+ $ruleToAdd->setPosition(1, $columnNumber);
}
+ } elseif ($ruleToAdd->getColumnNumber() === null) {
+ $ruleToAdd->setPosition($ruleToAdd->getLineNumber(), 0);
}
- \array_splice($this->aRules[$sRule], $iPosition, 0, [$oRule]);
+ \array_splice($this->rules[$propertyName], $position, 0, [$ruleToAdd]);
}
/**
* Returns all rules matching the given rule name
*
- * @example $oRuleSet->getRules('font') // returns array(0 => $oRule, …) or array().
+ * @example $ruleSet->getRules('font') // returns array(0 => $rule, …) or array().
*
- * @example $oRuleSet->getRules('font-')
+ * @example $ruleSet->getRules('font-')
* //returns an array of all rules either beginning with font- or matching font.
*
- * @param Rule|string|null $mRule
+ * @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 excluded.
- * Passing a Rule behaves like calling `getRules($mRule->getRule())`.
*
- * @return array
+ * @return array, Rule>
*/
- public function getRules($mRule = null)
+ public function getRules(?string $searchPattern = null): array
{
- if ($mRule instanceof Rule) {
- $mRule = $mRule->getRule();
- }
- /** @var array $aResult */
- $aResult = [];
- foreach ($this->aRules as $sName => $aRules) {
+ $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.
if (
- !$mRule || $sName === $mRule
+ $searchPattern === null || $propertyName === $searchPattern
|| (
- \strrpos($mRule, '-') === \strlen($mRule) - \strlen('-')
- && (\strpos($sName, $mRule) === 0 || $sName === \substr($mRule, 0, -1))
+ \strrpos($searchPattern, '-') === \strlen($searchPattern) - \strlen('-')
+ && (\strpos($propertyName, $searchPattern) === 0
+ || $propertyName === \substr($searchPattern, 0, -1))
)
) {
- $aResult = \array_merge($aResult, $aRules);
+ $result = \array_merge($result, $rules);
}
}
- \usort($aResult, function (Rule $first, Rule $second) {
- if ($first->getLineNo() === $second->getLineNo()) {
- return $first->getColNo() - $second->getColNo();
- }
- return $first->getLineNo() - $second->getLineNo();
- });
- return $aResult;
+ \usort($result, [self::class, 'comparePositionable']);
+
+ return $result;
}
/**
* Overrides all the rules of this set.
*
- * @param array $aRules The rules to override with.
+ * @param array $rules The rules to override with.
*/
- public function setRules(array $aRules): void
+ public function setRules(array $rules): void
{
- $this->aRules = [];
- foreach ($aRules as $rule) {
+ $this->rules = [];
+ foreach ($rules as $rule) {
$this->addRule($rule);
}
}
@@ -193,125 +227,149 @@ public function setRules(array $aRules): void
* 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.
*
- * @param Rule|string|null $mRule $mRule
+ * @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
- * excluded. Passing a Rule behaves like calling `getRules($mRule->getRule())`.
+ * excluded.
*
* @return array
*/
- public function getRulesAssoc($mRule = null)
+ public function getRulesAssoc(?string $searchPattern = null): array
{
- /** @var array $aResult */
- $aResult = [];
- foreach ($this->getRules($mRule) as $oRule) {
- $aResult[$oRule->getRule()] = $oRule;
+ /** @var array $result */
+ $result = [];
+ foreach ($this->getRules($searchPattern) as $rule) {
+ $result[$rule->getRule()] = $rule;
}
- return $aResult;
+
+ return $result;
}
/**
- * Removes a rule from this RuleSet. This accepts all the possible values that `getRules()` accepts.
- *
- * If given a Rule, it will only remove this particular rule (by identity).
- * If given a name, it will remove all rules by that name.
- *
- * Note: this is different from pre-v.2.0 behaviour of PHP-CSS-Parser, where passing a Rule instance would
- * remove all rules with the same name. To get the old behaviour, use `removeRule($oRule->getRule())`.
+ * Removes a `Rule` from this `RuleSet` by identity.
+ */
+ public function removeRule(Rule $ruleToRemove): void
+ {
+ $nameOfPropertyToRemove = $ruleToRemove->getRule();
+ if (!isset($this->rules[$nameOfPropertyToRemove])) {
+ return;
+ }
+ foreach ($this->rules[$nameOfPropertyToRemove] as $key => $rule) {
+ if ($rule === $ruleToRemove) {
+ unset($this->rules[$nameOfPropertyToRemove][$key]);
+ }
+ }
+ }
+
+ /**
+ * Removes rules by property name or search pattern.
*
- * @param Rule|string|null $mRule
- * pattern to remove. If $mRule is null, all rules are removed. If the pattern ends in a dash,
+ * @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
- * excluded. Passing a Rule behaves matches by identity.
+ * excluded.
*/
- public function removeRule($mRule): void
+ public function removeMatchingRules(string $searchPattern): void
{
- if ($mRule instanceof Rule) {
- $sRule = $mRule->getRule();
- if (!isset($this->aRules[$sRule])) {
- return;
- }
- foreach ($this->aRules[$sRule] as $iKey => $oRule) {
- if ($oRule === $mRule) {
- unset($this->aRules[$sRule][$iKey]);
- }
- }
- } else {
- foreach ($this->aRules as $sName => $aRules) {
- // 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 or equals it
- // (without the trailing dash).
- if (
- !$mRule || $sName === $mRule
- || (\strrpos($mRule, '-') === \strlen($mRule) - \strlen('-')
- && (\strpos($sName, $mRule) === 0 || $sName === \substr($mRule, 0, -1)))
- ) {
- unset($this->aRules[$sName]);
- }
+ 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).
+ if (
+ $propertyName === $searchPattern
+ || (\strrpos($searchPattern, '-') === \strlen($searchPattern) - \strlen('-')
+ && (\strpos($propertyName, $searchPattern) === 0
+ || $propertyName === \substr($searchPattern, 0, -1)))
+ ) {
+ unset($this->rules[$propertyName]);
}
}
}
- public function __toString(): string
+ public function removeAllRules(): void
{
- return $this->render(new OutputFormat());
+ $this->rules = [];
}
/**
- * @return string
+ * @internal
*/
- protected function renderRules(OutputFormat $oOutputFormat)
+ public function render(OutputFormat $outputFormat): string
{
- $sResult = '';
- $bIsFirst = true;
- $oNextLevel = $oOutputFormat->nextLevel();
- foreach ($this->aRules as $aRules) {
- foreach ($aRules as $oRule) {
- $sRendered = $oNextLevel->safely(function () use ($oRule, $oNextLevel) {
- return $oRule->render($oNextLevel);
- });
- if ($sRendered === null) {
- continue;
- }
- if ($bIsFirst) {
- $bIsFirst = false;
- $sResult .= $oNextLevel->spaceBeforeRules();
- } else {
- $sResult .= $oNextLevel->spaceBetweenRules();
- }
- $sResult .= $sRendered;
+ return $this->renderRules($outputFormat);
+ }
+
+ protected function renderRules(OutputFormat $outputFormat): string
+ {
+ $result = '';
+ $isFirst = true;
+ $nextLevelFormat = $outputFormat->nextLevel();
+ foreach ($this->getRules() as $rule) {
+ $nextLevelFormatter = $nextLevelFormat->getFormatter();
+ $renderedRule = $nextLevelFormatter->safely(static function () use ($rule, $nextLevelFormat): string {
+ return $rule->render($nextLevelFormat);
+ });
+ if ($renderedRule === null) {
+ continue;
}
+ if ($isFirst) {
+ $isFirst = false;
+ $result .= $nextLevelFormatter->spaceBeforeRules();
+ } else {
+ $result .= $nextLevelFormatter->spaceBetweenRules();
+ }
+ $result .= $renderedRule;
}
- if (!$bIsFirst) {
+ $formatter = $outputFormat->getFormatter();
+ if (!$isFirst) {
// Had some output
- $sResult .= $oOutputFormat->spaceAfterRules();
+ $result .= $formatter->spaceAfterRules();
}
- return $oOutputFormat->removeLastSemicolon($sResult);
+ return $formatter->removeLastSemicolon($result);
}
/**
- * @param array $aComments
+ * @return int negative if `$first` is before `$second`; zero if they have the same position; positive otherwise
+ *
+ * @throws \UnexpectedValueException if either argument does not have a valid position, which should never happen
*/
- public function addComments(array $aComments): void
+ private static function comparePositionable(Positionable $first, Positionable $second): int
{
- $this->aComments = \array_merge($this->aComments, $aComments);
- }
+ $firstsLineNumber = $first->getLineNumber();
+ $secondsLineNumber = $second->getLineNumber();
+ if (!\is_int($firstsLineNumber) || !\is_int($secondsLineNumber)) {
+ throw new \UnexpectedValueException(
+ 'A Rule without a line number was passed to comparePositionable',
+ 1750637683
+ );
+ }
- /**
- * @return array
- */
- public function getComments()
- {
- return $this->aComments;
+ if ($firstsLineNumber === $secondsLineNumber) {
+ $firstsColumnNumber = $first->getColumnNumber();
+ $secondsColumnNumber = $second->getColumnNumber();
+ if (!\is_int($firstsColumnNumber) || !\is_int($secondsColumnNumber)) {
+ throw new \UnexpectedValueException(
+ 'A Rule without a column number was passed to comparePositionable',
+ 1750637761
+ );
+ }
+ return $firstsColumnNumber - $secondsColumnNumber;
+ }
+
+ return $firstsLineNumber - $secondsLineNumber;
}
- /**
- * @param array $aComments
- */
- public function setComments(array $aComments): void
+ private function hasRule(Rule $rule): bool
{
- $this->aComments = $aComments;
+ foreach ($this->rules as $rulesForAProperty) {
+ if (\in_array($rule, $rulesForAProperty, true)) {
+ return true;
+ }
+ }
+
+ return false;
}
}
diff --git a/src/Settings.php b/src/Settings.php
index 487c25f71..a26d10e9e 100644
--- a/src/Settings.php
+++ b/src/Settings.php
@@ -1,5 +1,7 @@
bMultibyteSupport = \extension_loaded('mbstring');
+ $this->multibyteSupport = \extension_loaded('mbstring');
}
- /**
- * @return self new instance
- */
- public static function create(): Settings
+ public static function create(): self
{
return new Settings();
}
@@ -52,49 +51,74 @@ public static function create(): Settings
* If `true` (`mbstring` extension must be enabled), will use (slower) `mb_strlen`, `mb_convert_case`, `mb_substr`
* and `mb_strpos` functions. Otherwise, the normal (ASCII-Only) functions will be used.
*
- * @param bool $bMultibyteSupport
- *
- * @return self fluent interface
+ * @return $this fluent interface
*/
- public function withMultibyteSupport($bMultibyteSupport = true)
+ public function withMultibyteSupport(bool $multibyteSupport = true): self
{
- $this->bMultibyteSupport = $bMultibyteSupport;
+ $this->multibyteSupport = $multibyteSupport;
+
return $this;
}
/**
* Sets the charset to be used if the CSS does not contain an `@charset` declaration.
*
- * @param string $sDefaultCharset
+ * @param non-empty-string $defaultCharset
*
- * @return self fluent interface
+ * @return $this fluent interface
*/
- public function withDefaultCharset($sDefaultCharset)
+ public function withDefaultCharset(string $defaultCharset): self
{
- $this->sDefaultCharset = $sDefaultCharset;
+ $this->defaultCharset = $defaultCharset;
+
return $this;
}
/**
* Configures whether the parser should silently ignore invalid rules.
*
- * @param bool $bLenientParsing
- *
- * @return self fluent interface
+ * @return $this fluent interface
*/
- public function withLenientParsing($bLenientParsing = true)
+ public function withLenientParsing(bool $usesLenientParsing = true): self
{
- $this->bLenientParsing = $bLenientParsing;
+ $this->lenientParsing = $usesLenientParsing;
+
return $this;
}
/**
* Configures the parser to choke on invalid rules.
*
- * @return self fluent interface
+ * @return $this fluent interface
*/
- public function beStrict()
+ public function beStrict(): self
{
return $this->withLenientParsing(false);
}
+
+ /**
+ * @internal
+ */
+ public function hasMultibyteSupport(): bool
+ {
+ return $this->multibyteSupport;
+ }
+
+ /**
+ * @return non-empty-string
+ *
+ * @internal
+ */
+ public function getDefaultCharset(): string
+ {
+ return $this->defaultCharset;
+ }
+
+ /**
+ * @internal
+ */
+ public function usesLenientParsing(): bool
+ {
+ return $this->lenientParsing;
+ }
}
diff --git a/src/Value/CSSFunction.php b/src/Value/CSSFunction.php
index 7ee7b91d4..86b56d9b1 100644
--- a/src/Value/CSSFunction.php
+++ b/src/Value/CSSFunction.php
@@ -1,9 +1,14 @@
$aArguments
- * @param string $sSeparator
- * @param int $iLineNo
+ * @param non-empty-string $name
+ * @param RuleValueList|array $arguments
+ * @param non-empty-string $separator
+ * @param int<1, max>|null $lineNumber
*/
- public function __construct($sName, $aArguments, $sSeparator = ',', $iLineNo = 0)
+ public function __construct(string $name, $arguments, string $separator = ',', ?int $lineNumber = null)
{
- if ($aArguments instanceof RuleValueList) {
- $sSeparator = $aArguments->getListSeparator();
- $aArguments = $aArguments->getListComponents();
+ if ($arguments instanceof RuleValueList) {
+ $separator = $arguments->getListSeparator();
+ $arguments = $arguments->getListComponents();
}
- $this->sName = $sName;
- $this->iLineNo = $iLineNo;
- parent::__construct($aArguments, $sSeparator, $iLineNo);
+ $this->name = $name;
+ $this->setPosition($lineNumber); // TODO: redundant?
+ parent::__construct($arguments, $separator, $lineNumber);
}
/**
* @throws SourceException
* @throws UnexpectedEOFException
* @throws UnexpectedTokenException
+ *
+ * @internal since V8.8.0
*/
- public static function parse(ParserState $oParserState, bool $bIgnoreCase = false): CSSFunction
+ public static function parse(ParserState $parserState, bool $ignoreCase = false): CSSFunction
{
- $sName = self::parseName($oParserState, $bIgnoreCase);
- $oParserState->consume('(');
- $mArguments = self::parseArguments($oParserState);
+ $name = self::parseName($parserState, $ignoreCase);
+ $parserState->consume('(');
+ $arguments = self::parseArguments($parserState);
- $oResult = new CSSFunction($sName, $mArguments, ',', $oParserState->currentLine());
- $oParserState->consume(')');
+ $result = new CSSFunction($name, $arguments, ',', $parserState->currentLine());
+ $parserState->consume(')');
- return $oResult;
+ return $result;
}
/**
@@ -55,9 +64,9 @@ public static function parse(ParserState $oParserState, bool $bIgnoreCase = fals
* @throws UnexpectedEOFException
* @throws UnexpectedTokenException
*/
- private static function parseName(ParserState $oParserState, bool $bIgnoreCase = false): string
+ private static function parseName(ParserState $parserState, bool $ignoreCase = false): string
{
- return $oParserState->parseIdentifier($bIgnoreCase);
+ return $parserState->parseIdentifier($ignoreCase);
}
/**
@@ -67,46 +76,41 @@ private static function parseName(ParserState $oParserState, bool $bIgnoreCase =
* @throws UnexpectedEOFException
* @throws UnexpectedTokenException
*/
- private static function parseArguments(ParserState $oParserState)
+ private static function parseArguments(ParserState $parserState)
{
- return Value::parseValue($oParserState, ['=', ' ', ',']);
+ return Value::parseValue($parserState, ['=', ' ', ',']);
}
/**
- * @return string
+ * @return non-empty-string
*/
- public function getName()
+ public function getName(): string
{
- return $this->sName;
+ return $this->name;
}
/**
- * @param string $sName
+ * @param non-empty-string $name
*/
- public function setName($sName): void
+ public function setName(string $name): void
{
- $this->sName = $sName;
+ $this->name = $name;
}
/**
- * @return array
+ * @return array
*/
- public function getArguments()
- {
- return $this->aComponents;
- }
-
- public function __toString(): string
+ public function getArguments(): array
{
- return $this->render(new OutputFormat());
+ return $this->components;
}
/**
- * @return string
+ * @return non-empty-string
*/
- public function render(OutputFormat $oOutputFormat)
+ public function render(OutputFormat $outputFormat): string
{
- $aArguments = parent::render($oOutputFormat);
- return "{$this->sName}({$aArguments})";
+ $arguments = parent::render($outputFormat);
+ return "{$this->name}({$arguments})";
}
}
diff --git a/src/Value/CSSString.php b/src/Value/CSSString.php
index 42e358c93..569311d77 100644
--- a/src/Value/CSSString.php
+++ b/src/Value/CSSString.php
@@ -1,5 +1,7 @@
|null $lineNumber
*/
- public function __construct($sString, $iLineNo = 0)
+ public function __construct(string $string, ?int $lineNumber = null)
{
- $this->sString = $sString;
- parent::__construct($iLineNo);
+ $this->string = $string;
+ parent::__construct($lineNumber);
}
/**
* @throws SourceException
* @throws UnexpectedEOFException
* @throws UnexpectedTokenException
+ *
+ * @internal since V8.8.0
*/
- public static function parse(ParserState $oParserState): CSSString
+ public static function parse(ParserState $parserState): CSSString
{
- $sBegin = $oParserState->peek();
- $sQuote = null;
- if ($sBegin === "'") {
- $sQuote = "'";
- } elseif ($sBegin === '"') {
- $sQuote = '"';
+ $begin = $parserState->peek();
+ $quote = null;
+ if ($begin === "'") {
+ $quote = "'";
+ } elseif ($begin === '"') {
+ $quote = '"';
}
- if ($sQuote !== null) {
- $oParserState->consume($sQuote);
+ if ($quote !== null) {
+ $parserState->consume($quote);
}
- $sResult = '';
- $sContent = null;
- if ($sQuote === null) {
+ $result = '';
+ $content = null;
+ if ($quote === null) {
// Unquoted strings end in whitespace or with braces, brackets, parentheses
- while (!\preg_match('/[\\s{}()<>\\[\\]]/isu', $oParserState->peek())) {
- $sResult .= $oParserState->parseCharacter(false);
+ while (preg_match('/[\\s{}()<>\\[\\]]/isu', $parserState->peek()) === 0) {
+ $result .= $parserState->parseCharacter(false);
}
} else {
- while (!$oParserState->comes($sQuote)) {
- $sContent = $oParserState->parseCharacter(false);
- if ($sContent === null) {
+ while (!$parserState->comes($quote)) {
+ $content = $parserState->parseCharacter(false);
+ if ($content === null) {
throw new SourceException(
- "Non-well-formed quoted string {$oParserState->peek(3)}",
- $oParserState->currentLine()
+ "Non-well-formed quoted string {$parserState->peek(3)}",
+ $parserState->currentLine()
);
}
- $sResult .= $sContent;
+ $result .= $content;
}
- $oParserState->consume($sQuote);
+ $parserState->consume($quote);
}
- return new CSSString($sResult, $oParserState->currentLine());
+ return new CSSString($result, $parserState->currentLine());
}
- /**
- * @param string $sString
- */
- public function setString($sString): void
+ public function setString(string $string): void
{
- $this->sString = $sString;
+ $this->string = $string;
}
- /**
- * @return string
- */
- public function getString()
+ public function getString(): string
{
- return $this->sString;
+ return $this->string;
}
- public function __toString(): string
- {
- return $this->render(new OutputFormat());
- }
-
- public function render(OutputFormat $oOutputFormat): string
+ /**
+ * @return non-empty-string
+ */
+ public function render(OutputFormat $outputFormat): string
{
- $sString = \addslashes($this->sString);
- $sString = \str_replace("\n", '\\A', $sString);
- return $oOutputFormat->getStringQuotingType() . $sString . $oOutputFormat->getStringQuotingType();
+ $string = \addslashes($this->string);
+ $string = \str_replace("\n", '\\A', $string);
+ return $outputFormat->getStringQuotingType() . $string . $outputFormat->getStringQuotingType();
}
}
diff --git a/src/Value/CalcFunction.php b/src/Value/CalcFunction.php
index 49a91b8af..dba6e1dd9 100644
--- a/src/Value/CalcFunction.php
+++ b/src/Value/CalcFunction.php
@@ -1,5 +1,7 @@
parseIdentifier();
- if ($oParserState->peek() != '(') {
+ $operators = ['+', '-', '*', '/'];
+ $function = $parserState->parseIdentifier();
+ if ($parserState->peek() !== '(') {
// Found ; or end of line before an opening bracket
- throw new UnexpectedTokenException('(', $oParserState->peek(), 'literal', $oParserState->currentLine());
- } elseif (!\in_array($sFunction, ['calc', '-moz-calc', '-webkit-calc'], true)) {
+ throw new UnexpectedTokenException('(', $parserState->peek(), 'literal', $parserState->currentLine());
+ } elseif ($function !== 'calc') {
// Found invalid calc definition. Example calc (...
- throw new UnexpectedTokenException('calc', $sFunction, 'literal', $oParserState->currentLine());
+ throw new UnexpectedTokenException('calc', $function, 'literal', $parserState->currentLine());
}
- $oParserState->consume('(');
- $oCalcList = new CalcRuleValueList($oParserState->currentLine());
- $oList = new RuleValueList(',', $oParserState->currentLine());
- $iNestingLevel = 0;
- $iLastComponentType = null;
- while (!$oParserState->comes(')') || $iNestingLevel > 0) {
- if ($oParserState->isEnd() && $iNestingLevel === 0) {
+ $parserState->consume('(');
+ $calcRuleValueList = new CalcRuleValueList($parserState->currentLine());
+ $list = new RuleValueList(',', $parserState->currentLine());
+ $nestingLevel = 0;
+ $lastComponentType = null;
+ while (!$parserState->comes(')') || $nestingLevel > 0) {
+ if ($parserState->isEnd() && $nestingLevel === 0) {
break;
}
- $oParserState->consumeWhiteSpace();
- if ($oParserState->comes('(')) {
- $iNestingLevel++;
- $oCalcList->addListComponent($oParserState->consume(1));
- $oParserState->consumeWhiteSpace();
+ $parserState->consumeWhiteSpace();
+ if ($parserState->comes('(')) {
+ $nestingLevel++;
+ $calcRuleValueList->addListComponent($parserState->consume(1));
+ $parserState->consumeWhiteSpace();
continue;
- } elseif ($oParserState->comes(')')) {
- $iNestingLevel--;
- $oCalcList->addListComponent($oParserState->consume(1));
- $oParserState->consumeWhiteSpace();
+ } elseif ($parserState->comes(')')) {
+ $nestingLevel--;
+ $calcRuleValueList->addListComponent($parserState->consume(1));
+ $parserState->consumeWhiteSpace();
continue;
}
- if ($iLastComponentType != CalcFunction::T_OPERAND) {
- $oVal = Value::parsePrimitiveValue($oParserState);
- $oCalcList->addListComponent($oVal);
- $iLastComponentType = CalcFunction::T_OPERAND;
+ if ($lastComponentType !== CalcFunction::T_OPERAND) {
+ $value = Value::parsePrimitiveValue($parserState);
+ $calcRuleValueList->addListComponent($value);
+ $lastComponentType = CalcFunction::T_OPERAND;
} else {
- if (\in_array($oParserState->peek(), $aOperators, true)) {
- if (($oParserState->comes('-') || $oParserState->comes('+'))) {
+ if (\in_array($parserState->peek(), $operators, true)) {
+ if (($parserState->comes('-') || $parserState->comes('+'))) {
if (
- $oParserState->peek(1, -1) != ' '
- || !($oParserState->comes('- ')
- || $oParserState->comes('+ '))
+ $parserState->peek(1, -1) !== ' '
+ || !($parserState->comes('- ')
+ || $parserState->comes('+ '))
) {
throw new UnexpectedTokenException(
- " {$oParserState->peek()} ",
- $oParserState->peek(1, -1) . $oParserState->peek(2),
+ " {$parserState->peek()} ",
+ $parserState->peek(1, -1) . $parserState->peek(2),
'literal',
- $oParserState->currentLine()
+ $parserState->currentLine()
);
}
}
- $oCalcList->addListComponent($oParserState->consume(1));
- $iLastComponentType = CalcFunction::T_OPERATOR;
+ $calcRuleValueList->addListComponent($parserState->consume(1));
+ $lastComponentType = CalcFunction::T_OPERATOR;
} else {
throw new UnexpectedTokenException(
\sprintf(
'Next token was expected to be an operand of type %s. Instead "%s" was found.',
- \implode(', ', $aOperators),
- $oParserState->peek()
+ \implode(', ', $operators),
+ $parserState->peek()
),
'',
'custom',
- $oParserState->currentLine()
+ $parserState->currentLine()
);
}
}
- $oParserState->consumeWhiteSpace();
+ $parserState->consumeWhiteSpace();
}
- $oList->addListComponent($oCalcList);
- if (!$oParserState->isEnd()) {
- $oParserState->consume(')');
+ $list->addListComponent($calcRuleValueList);
+ if (!$parserState->isEnd()) {
+ $parserState->consume(')');
}
- return new CalcFunction($sFunction, $oList, ',', $oParserState->currentLine());
+ return new CalcFunction($function, $list, ',', $parserState->currentLine());
}
}
diff --git a/src/Value/CalcRuleValueList.php b/src/Value/CalcRuleValueList.php
index 7dbd26a1b..f904f12c7 100644
--- a/src/Value/CalcRuleValueList.php
+++ b/src/Value/CalcRuleValueList.php
@@ -1,5 +1,7 @@
|null $lineNumber
*/
- public function __construct($iLineNo = 0)
+ public function __construct(?int $lineNumber = null)
{
- parent::__construct(',', $iLineNo);
+ parent::__construct(',', $lineNumber);
}
- /**
- * @return string
- */
- public function render(OutputFormat $oOutputFormat)
+ public function render(OutputFormat $outputFormat): string
{
- return $oOutputFormat->implode(' ', $this->aComponents);
+ return $outputFormat->getFormatter()->implode(' ', $this->components);
}
}
diff --git a/src/Value/Color.php b/src/Value/Color.php
index a084fd35d..63bccab8b 100644
--- a/src/Value/Color.php
+++ b/src/Value/Color.php
@@ -1,5 +1,7 @@
$aColor
- * @param int $iLineNo
+ * @param array $colorValues
+ * @param int<1, max>|null $lineNumber
*/
- public function __construct(array $aColor, $iLineNo = 0)
+ public function __construct(array $colorValues, ?int $lineNumber = null)
{
- parent::__construct(\implode('', \array_keys($aColor)), $aColor, ',', $iLineNo);
+ parent::__construct(\implode('', \array_keys($colorValues)), $colorValues, ',', $lineNumber);
}
/**
* @throws UnexpectedEOFException
* @throws UnexpectedTokenException
+ *
+ * @internal since V8.8.0
*/
- public static function parse(ParserState $oParserState, bool $bIgnoreCase = false): CSSFunction
- {
- $aColor = [];
- if ($oParserState->comes('#')) {
- $oParserState->consume('#');
- $sValue = $oParserState->parseIdentifier(false);
- if ($oParserState->strlen($sValue) === 3) {
- $sValue = $sValue[0] . $sValue[0] . $sValue[1] . $sValue[1] . $sValue[2] . $sValue[2];
- } elseif ($oParserState->strlen($sValue) === 4) {
- $sValue = $sValue[0] . $sValue[0] . $sValue[1] . $sValue[1] . $sValue[2] . $sValue[2] . $sValue[3]
- . $sValue[3];
- }
+ public static function parse(ParserState $parserState, bool $ignoreCase = false): CSSFunction
+ {
+ return $parserState->comes('#')
+ ? self::parseHexColor($parserState)
+ : self::parseColorFunction($parserState);
+ }
+
+ /**
+ * @throws UnexpectedEOFException
+ * @throws UnexpectedTokenException
+ */
+ private static function parseHexColor(ParserState $parserState): Color
+ {
+ $parserState->consume('#');
+ $hexValue = $parserState->parseIdentifier(false);
+ if ($parserState->strlen($hexValue) === 3) {
+ $hexValue = $hexValue[0] . $hexValue[0] . $hexValue[1] . $hexValue[1] . $hexValue[2] . $hexValue[2];
+ } elseif ($parserState->strlen($hexValue) === 4) {
+ $hexValue = $hexValue[0] . $hexValue[0] . $hexValue[1] . $hexValue[1] . $hexValue[2] . $hexValue[2]
+ . $hexValue[3] . $hexValue[3];
+ }
+
+ if ($parserState->strlen($hexValue) === 8) {
+ $colorValues = [
+ 'r' => new Size(\intval($hexValue[0] . $hexValue[1], 16), null, true, $parserState->currentLine()),
+ 'g' => new Size(\intval($hexValue[2] . $hexValue[3], 16), null, true, $parserState->currentLine()),
+ 'b' => new Size(\intval($hexValue[4] . $hexValue[5], 16), null, true, $parserState->currentLine()),
+ 'a' => new Size(
+ \round(self::mapRange(\intval($hexValue[6] . $hexValue[7], 16), 0, 255, 0, 1), 2),
+ null,
+ true,
+ $parserState->currentLine()
+ ),
+ ];
+ } elseif ($parserState->strlen($hexValue) === 6) {
+ $colorValues = [
+ 'r' => new Size(\intval($hexValue[0] . $hexValue[1], 16), null, true, $parserState->currentLine()),
+ 'g' => new Size(\intval($hexValue[2] . $hexValue[3], 16), null, true, $parserState->currentLine()),
+ 'b' => new Size(\intval($hexValue[4] . $hexValue[5], 16), null, true, $parserState->currentLine()),
+ ];
+ } else {
+ throw new UnexpectedTokenException(
+ 'Invalid hex color value',
+ $hexValue,
+ 'custom',
+ $parserState->currentLine()
+ );
+ }
+
+ return new Color($colorValues, $parserState->currentLine());
+ }
+
+ /**
+ * @throws UnexpectedEOFException
+ * @throws UnexpectedTokenException
+ */
+ private static function parseColorFunction(ParserState $parserState): CSSFunction
+ {
+ $colorValues = [];
+
+ $colorMode = $parserState->parseIdentifier(true);
+ $parserState->consumeWhiteSpace();
+ $parserState->consume('(');
- if ($oParserState->strlen($sValue) === 8) {
- $aColor = [
- 'r' => new Size(\intval($sValue[0] . $sValue[1], 16), null, true, $oParserState->currentLine()),
- 'g' => new Size(\intval($sValue[2] . $sValue[3], 16), null, true, $oParserState->currentLine()),
- 'b' => new Size(\intval($sValue[4] . $sValue[5], 16), null, true, $oParserState->currentLine()),
- 'a' => new Size(
- \round(self::mapRange(\intval($sValue[6] . $sValue[7], 16), 0, 255, 0, 1), 2),
- null,
- true,
- $oParserState->currentLine()
- ),
- ];
- } elseif ($oParserState->strlen($sValue) === 6) {
- $aColor = [
- 'r' => new Size(\intval($sValue[0] . $sValue[1], 16), null, true, $oParserState->currentLine()),
- 'g' => new Size(\intval($sValue[2] . $sValue[3], 16), null, true, $oParserState->currentLine()),
- 'b' => new Size(\intval($sValue[4] . $sValue[5], 16), null, true, $oParserState->currentLine()),
- ];
+ // CSS Color Module Level 4 says that `rgb` and `rgba` are now aliases; likewise `hsl` and `hsla`.
+ // So, attempt to parse with the `a`, and allow for it not being there.
+ switch ($colorMode) {
+ case 'rgb':
+ $colorModeForParsing = 'rgba';
+ $mayHaveOptionalAlpha = true;
+ break;
+ case 'hsl':
+ $colorModeForParsing = 'hsla';
+ $mayHaveOptionalAlpha = true;
+ break;
+ case 'rgba':
+ // This is handled identically to the following case.
+ case 'hsla':
+ $colorModeForParsing = $colorMode;
+ $mayHaveOptionalAlpha = true;
+ break;
+ default:
+ $colorModeForParsing = $colorMode;
+ $mayHaveOptionalAlpha = false;
+ }
+
+ $containsVar = false;
+ $containsNone = false;
+ $isLegacySyntax = false;
+ $expectedArgumentCount = $parserState->strlen($colorModeForParsing);
+ for ($argumentIndex = 0; $argumentIndex < $expectedArgumentCount; ++$argumentIndex) {
+ $parserState->consumeWhiteSpace();
+ $valueKey = $colorModeForParsing[$argumentIndex];
+ if ($parserState->comes('var')) {
+ $colorValues[$valueKey] = CSSFunction::parseIdentifierOrFunction($parserState);
+ $containsVar = true;
+ } elseif (!$isLegacySyntax && $parserState->comes('none')) {
+ $colorValues[$valueKey] = $parserState->parseIdentifier();
+ $containsNone = true;
} else {
- throw new UnexpectedTokenException(
- 'Invalid hex color value',
- $sValue,
- 'custom',
- $oParserState->currentLine()
- );
+ $colorValues[$valueKey] = Size::parse($parserState, true);
}
- } else {
- $sColorMode = $oParserState->parseIdentifier(true);
- $oParserState->consumeWhiteSpace();
- $oParserState->consume('(');
-
- $bContainsVar = false;
- $iLength = $oParserState->strlen($sColorMode);
- for ($i = 0; $i < $iLength; ++$i) {
- $oParserState->consumeWhiteSpace();
- if ($oParserState->comes('var')) {
- $aColor[$sColorMode[$i]] = CSSFunction::parseIdentifierOrFunction($oParserState);
- $bContainsVar = true;
- } else {
- $aColor[$sColorMode[$i]] = Size::parse($oParserState, true);
- }
- if ($bContainsVar && $oParserState->comes(')')) {
- // With a var argument the function can have fewer arguments
- break;
- }
+ // This must be done first, to consume comments as well, so that the `comes` test will work.
+ $parserState->consumeWhiteSpace();
+
+ // With a `var` argument, the function can have fewer arguments.
+ // And as of CSS Color Module Level 4, the alpha argument is optional.
+ $canCloseNow =
+ $containsVar
+ || ($mayHaveOptionalAlpha && $argumentIndex >= $expectedArgumentCount - 2);
+ if ($canCloseNow && $parserState->comes(')')) {
+ break;
+ }
- $oParserState->consumeWhiteSpace();
- if ($i < ($iLength - 1)) {
- $oParserState->consume(',');
+ // "Legacy" syntax is comma-delimited, and does not allow the `none` keyword.
+ // "Modern" syntax is space-delimited, with `/` as alpha delimiter.
+ // They cannot be mixed.
+ if ($argumentIndex === 0 && !$containsNone) {
+ // An immediate closing parenthesis is not valid.
+ if ($parserState->comes(')')) {
+ throw new UnexpectedTokenException(
+ 'Color function with no arguments',
+ '',
+ 'custom',
+ $parserState->currentLine()
+ );
}
+ $isLegacySyntax = $parserState->comes(',');
}
- $oParserState->consume(')');
- if ($bContainsVar) {
- return new CSSFunction($sColorMode, \array_values($aColor), ',', $oParserState->currentLine());
+ if ($isLegacySyntax && $argumentIndex < ($expectedArgumentCount - 1)) {
+ $parserState->consume(',');
+ }
+
+ // In the "modern" syntax, the alpha value must be delimited with `/`.
+ if (!$isLegacySyntax) {
+ if ($containsVar) {
+ // If the `var` substitution encompasses more than one argument,
+ // the alpha deliminator may come at any time.
+ if ($parserState->comes('/')) {
+ $parserState->consume('/');
+ }
+ } elseif (($colorModeForParsing[$argumentIndex + 1] ?? '') === 'a') {
+ // Alpha value is the next expected argument.
+ // Since a closing parenthesis was not found, a `/` separator is now required.
+ $parserState->consume('/');
+ }
}
}
- return new Color($aColor, $oParserState->currentLine());
+ $parserState->consume(')');
+
+ return $containsVar
+ ? new CSSFunction($colorMode, \array_values($colorValues), ',', $parserState->currentLine())
+ : new Color($colorValues, $parserState->currentLine());
+ }
+
+ private static function mapRange(float $value, float $fromMin, float $fromMax, float $toMin, float $toMax): float
+ {
+ $fromRange = $fromMax - $fromMin;
+ $toRange = $toMax - $toMin;
+ $multiplier = $toRange / $fromRange;
+ $newValue = $value - $fromMin;
+ $newValue *= $multiplier;
+
+ return $newValue + $toMin;
}
/**
- * @param float $fVal
- * @param float $fFromMin
- * @param float $fFromMax
- * @param float $fToMin
- * @param float $fToMax
- *
- * @return float
+ * @return array
*/
- private static function mapRange($fVal, $fFromMin, $fFromMax, $fToMin, $fToMax)
+ public function getColor(): array
{
- $fFromRange = $fFromMax - $fFromMin;
- $fToRange = $fToMax - $fToMin;
- $fMultiplier = $fToRange / $fFromRange;
- $fNewVal = $fVal - $fFromMin;
- $fNewVal *= $fMultiplier;
- return $fNewVal + $fToMin;
+ return $this->components;
}
/**
- * @return array
+ * @param array $colorValues
*/
- public function getColor()
+ public function setColor(array $colorValues): void
{
- return $this->aComponents;
+ $this->setName(\implode('', \array_keys($colorValues)));
+ $this->components = $colorValues;
}
/**
- * @param array $aColor
+ * @return non-empty-string
*/
- public function setColor(array $aColor): void
+ public function getColorDescription(): string
{
- $this->setName(\implode('', \array_keys($aColor)));
- $this->aComponents = $aColor;
+ return $this->getName();
}
/**
- * @return string
+ * @return non-empty-string
*/
- public function getColorDescription()
+ public function render(OutputFormat $outputFormat): string
{
- return $this->getName();
+ if ($this->shouldRenderAsHex($outputFormat)) {
+ return $this->renderAsHex();
+ }
+
+ if ($this->shouldRenderInModernSyntax()) {
+ return $this->renderInModernSyntax($outputFormat);
+ }
+
+ return parent::render($outputFormat);
}
- public function __toString(): string
+ private function shouldRenderAsHex(OutputFormat $outputFormat): bool
{
- return $this->render(new OutputFormat());
+ return
+ $outputFormat->usesRgbHashNotation()
+ && $this->getRealName() === 'rgb'
+ && $this->allComponentsAreNumbers();
}
/**
- * @return string
+ * The function name is a concatenation of the array keys of the components, which is passed to the constructor.
+ * However, this can be changed by calling {@see CSSFunction::setName},
+ * so is not reliable in situations where it's necessary to determine the function name based on the components.
*/
- public function render(OutputFormat $oOutputFormat)
- {
- // Shorthand RGB color values
- if ($oOutputFormat->getRGBHashNotation() && \implode('', \array_keys($this->aComponents)) === 'rgb') {
- $sResult = \sprintf(
- '%02x%02x%02x',
- $this->aComponents['r']->getSize(),
- $this->aComponents['g']->getSize(),
- $this->aComponents['b']->getSize()
- );
- return '#' . (($sResult[0] == $sResult[1]) && ($sResult[2] == $sResult[3]) && ($sResult[4] == $sResult[5])
- ? "$sResult[0]$sResult[2]$sResult[4]" : $sResult);
+ private function getRealName(): string
+ {
+ return \implode('', \array_keys($this->components));
+ }
+
+ /**
+ * Test whether all color components are absolute numbers (CSS type `number`), not percentages or anything else.
+ * If any component is not an instance of `Size`, the method will also return `false`.
+ */
+ private function allComponentsAreNumbers(): bool
+ {
+ foreach ($this->components as $component) {
+ if (!($component instanceof Size) || $component->getUnit() !== null) {
+ return false;
+ }
}
- return parent::render($oOutputFormat);
+
+ return true;
+ }
+
+ /**
+ * Note that this method assumes the following:
+ * - The `components` array has keys for `r`, `g` and `b`;
+ * - The values in the array are all instances of `Size`.
+ *
+ * Errors will be triggered or thrown if this is not the case.
+ *
+ * @return non-empty-string
+ */
+ private function renderAsHex(): string
+ {
+ $result = \sprintf(
+ '%02x%02x%02x',
+ $this->components['r']->getSize(),
+ $this->components['g']->getSize(),
+ $this->components['b']->getSize()
+ );
+ $canUseShortVariant = ($result[0] === $result[1]) && ($result[2] === $result[3]) && ($result[4] === $result[5]);
+
+ return '#' . ($canUseShortVariant ? $result[0] . $result[2] . $result[4] : $result);
+ }
+
+ /**
+ * The "legacy" syntax does not allow RGB colors to have a mixture of `percentage`s and `number`s,
+ * and does not allow `none` as any component value.
+ *
+ * The "legacy" and "modern" monikers are part of the formal W3C syntax.
+ * See the following for more information:
+ * - {@link
+ * https://developer.mozilla.org/en-US/docs/Web/CSS/color_value/rgb#formal_syntax
+ * Description of the formal syntax for `rgb()` on MDN
+ * };
+ * - {@link
+ * https://www.w3.org/TR/css-color-4/#rgb-functions
+ * The same in the CSS Color Module Level 4 W3C Candidate Recommendation Draft
+ * } (as of 13 February 2024, at time of writing).
+ */
+ private function shouldRenderInModernSyntax(): bool
+ {
+ if ($this->hasNoneAsComponentValue()) {
+ return true;
+ }
+
+ if (!$this->colorFunctionMayHaveMixedValueTypes($this->getRealName())) {
+ return false;
+ }
+
+ $hasPercentage = false;
+ $hasNumber = false;
+ foreach ($this->components as $key => $value) {
+ if ($key === 'a') {
+ // Alpha can have units that don't match those of the RGB components in the "legacy" syntax.
+ // So it is not necessary to check it. It's also always last, hence `break` rather than `continue`.
+ break;
+ }
+ if (!($value instanceof Size)) {
+ // Unexpected, unknown, or modified via the API
+ return false;
+ }
+ $unit = $value->getUnit();
+ // `switch` only does loose comparison
+ if ($unit === null) {
+ $hasNumber = true;
+ } elseif ($unit === '%') {
+ $hasPercentage = true;
+ } else {
+ // Invalid unit
+ return false;
+ }
+ }
+
+ return $hasPercentage && $hasNumber;
+ }
+
+ private function hasNoneAsComponentValue(): bool
+ {
+ return \in_array('none', $this->components, true);
+ }
+
+ /**
+ * Some color functions, such as `rgb`,
+ * may have a mixture of `percentage`, `number`, or possibly other types in their arguments.
+ *
+ * Note that this excludes the alpha component, which is treated separately.
+ */
+ private function colorFunctionMayHaveMixedValueTypes(string $function): bool
+ {
+ $functionsThatMayHaveMixedValueTypes = ['rgb', 'rgba'];
+
+ return \in_array($function, $functionsThatMayHaveMixedValueTypes, true);
+ }
+
+ /**
+ * @return non-empty-string
+ */
+ private function renderInModernSyntax(OutputFormat $outputFormat): string
+ {
+ // Maybe not yet without alpha, but will be...
+ $componentsWithoutAlpha = $this->components;
+ \end($componentsWithoutAlpha);
+ if (\key($componentsWithoutAlpha) === 'a') {
+ $alpha = $this->components['a'];
+ unset($componentsWithoutAlpha['a']);
+ }
+
+ $formatter = $outputFormat->getFormatter();
+ $arguments = $formatter->implode(' ', $componentsWithoutAlpha);
+ if (isset($alpha)) {
+ $separator = $formatter->spaceBeforeListArgumentSeparator('/')
+ . '/' . $formatter->spaceAfterListArgumentSeparator('/');
+ $arguments = $formatter->implode($separator, [$arguments, $alpha]);
+ }
+
+ return $this->getName() . '(' . $arguments . ')';
}
}
diff --git a/src/Value/LineName.php b/src/Value/LineName.php
index 5c83b5651..763cc48ea 100644
--- a/src/Value/LineName.php
+++ b/src/Value/LineName.php
@@ -1,5 +1,7 @@
$aComponents
- * @param int $iLineNo
+ * @param array $components
+ * @param int<1, max>|null $lineNumber
*/
- public function __construct(array $aComponents = [], $iLineNo = 0)
+ public function __construct(array $components = [], ?int $lineNumber = null)
{
- parent::__construct($aComponents, ' ', $iLineNo);
+ parent::__construct($components, ' ', $lineNumber);
}
/**
* @throws UnexpectedTokenException
* @throws UnexpectedEOFException
+ *
+ * @internal since V8.8.0
*/
- public static function parse(ParserState $oParserState): LineName
+ public static function parse(ParserState $parserState): LineName
{
- $oParserState->consume('[');
- $oParserState->consumeWhiteSpace();
- $aNames = [];
+ $parserState->consume('[');
+ $parserState->consumeWhiteSpace();
+ $names = [];
do {
- if ($oParserState->getSettings()->bLenientParsing) {
+ if ($parserState->getSettings()->usesLenientParsing()) {
try {
- $aNames[] = $oParserState->parseIdentifier();
+ $names[] = $parserState->parseIdentifier();
} catch (UnexpectedTokenException $e) {
- if (!$oParserState->comes(']')) {
+ if (!$parserState->comes(']')) {
throw $e;
}
}
} else {
- $aNames[] = $oParserState->parseIdentifier();
+ $names[] = $parserState->parseIdentifier();
}
- $oParserState->consumeWhiteSpace();
- } while (!$oParserState->comes(']'));
- $oParserState->consume(']');
- return new LineName($aNames, $oParserState->currentLine());
- }
-
- public function __toString(): string
- {
- return $this->render(new OutputFormat());
+ $parserState->consumeWhiteSpace();
+ } while (!$parserState->comes(']'));
+ $parserState->consume(']');
+ return new LineName($names, $parserState->currentLine());
}
- public function render(OutputFormat $oOutputFormat): string
+ /**
+ * @return non-empty-string
+ */
+ public function render(OutputFormat $outputFormat): string
{
return '[' . parent::render(OutputFormat::createCompact()) . ']';
}
diff --git a/src/Value/PrimitiveValue.php b/src/Value/PrimitiveValue.php
index 055a43975..f7f940928 100644
--- a/src/Value/PrimitiveValue.php
+++ b/src/Value/PrimitiveValue.php
@@ -1,14 +1,7 @@
|null $lineNumber
*/
- public function __construct($sSeparator = ',', $iLineNo = 0)
+ public function __construct(string $separator = ',', ?int $lineNumber = null)
{
- parent::__construct([], $sSeparator, $iLineNo);
+ parent::__construct([], $separator, $lineNumber);
}
}
diff --git a/src/Value/Size.php b/src/Value/Size.php
index bf40f4a07..eac736d79 100644
--- a/src/Value/Size.php
+++ b/src/Value/Size.php
@@ -1,5 +1,7 @@
+ * @var list
*/
private const ABSOLUTE_SIZE_UNITS = [
- 'px', 'pt', 'pc',
- 'cm', 'mm', 'mozmm', 'in',
- 'vh', 'dvh', 'svh', 'lvh',
- 'vw', 'vmin', 'vmax', 'rem',
+ 'px',
+ 'pt',
+ 'pc',
+ 'cm',
+ 'mm',
+ 'mozmm',
+ 'in',
+ 'vh',
+ 'dvh',
+ 'svh',
+ 'lvh',
+ 'vw',
+ 'vmin',
+ 'vmax',
+ 'rem',
];
/**
- * @var array
+ * @var list
*/
private const RELATIVE_SIZE_UNITS = ['%', 'em', 'ex', 'ch', 'fr'];
/**
- * @var array
+ * @var list
*/
private const NON_SIZE_UNITS = ['deg', 'grad', 'rad', 's', 'ms', 'turn', 'Hz', 'kHz'];
/**
- * @var array>|null
+ * @var array, array>|null
*/
private static $SIZE_UNITS = null;
/**
* @var float
*/
- private $fSize;
+ private $size;
/**
* @var string|null
*/
- private $sUnit;
+ private $unit;
/**
* @var bool
*/
- private $bIsColorComponent;
+ private $isColorComponent;
/**
- * @param float|int|string $fSize
- * @param string|null $sUnit
- * @param bool $bIsColorComponent
- * @param int $iLineNo
+ * @param float|int|string $size
+ * @param int<1, max>|null $lineNumber
*/
- public function __construct($fSize, $sUnit = null, $bIsColorComponent = false, $iLineNo = 0)
+ public function __construct($size, ?string $unit = null, bool $isColorComponent = false, ?int $lineNumber = null)
{
- parent::__construct($iLineNo);
- $this->fSize = (float) $fSize;
- $this->sUnit = $sUnit;
- $this->bIsColorComponent = $bIsColorComponent;
+ parent::__construct($lineNumber);
+ $this->size = (float) $size;
+ $this->unit = $unit;
+ $this->isColorComponent = $isColorComponent;
}
/**
- * @param bool $bIsColorComponent
- *
* @throws UnexpectedEOFException
* @throws UnexpectedTokenException
+ *
+ * @internal since V8.8.0
*/
- public static function parse(ParserState $oParserState, $bIsColorComponent = false): Size
+ public static function parse(ParserState $parserState, bool $isColorComponent = false): Size
{
- $sSize = '';
- if ($oParserState->comes('-')) {
- $sSize .= $oParserState->consume('-');
+ $size = '';
+ if ($parserState->comes('-')) {
+ $size .= $parserState->consume('-');
}
- while (\is_numeric($oParserState->peek()) || $oParserState->comes('.') || $oParserState->comes('e', true)) {
- if ($oParserState->comes('.')) {
- $sSize .= $oParserState->consume('.');
- } elseif ($oParserState->comes('e', true)) {
- $sLookahead = $oParserState->peek(1, 1);
- if (\is_numeric($sLookahead) || $sLookahead === '+' || $sLookahead === '-') {
- $sSize .= $oParserState->consume(2);
+ while (\is_numeric($parserState->peek()) || $parserState->comes('.') || $parserState->comes('e', true)) {
+ if ($parserState->comes('.')) {
+ $size .= $parserState->consume('.');
+ } elseif ($parserState->comes('e', true)) {
+ $lookahead = $parserState->peek(1, 1);
+ if (\is_numeric($lookahead) || $lookahead === '+' || $lookahead === '-') {
+ $size .= $parserState->consume(2);
} else {
break; // Reached the unit part of the number like "em" or "ex"
}
} else {
- $sSize .= $oParserState->consume(1);
+ $size .= $parserState->consume(1);
}
}
- $sUnit = null;
- $aSizeUnits = self::getSizeUnits();
- foreach ($aSizeUnits as $iLength => &$aValues) {
- $sKey = \strtolower($oParserState->peek($iLength));
- if (\array_key_exists($sKey, $aValues)) {
- if (($sUnit = $aValues[$sKey]) !== null) {
- $oParserState->consume($iLength);
+ $unit = null;
+ $sizeUnits = self::getSizeUnits();
+ foreach ($sizeUnits as $length => &$values) {
+ $key = \strtolower($parserState->peek($length));
+ if (\array_key_exists($key, $values)) {
+ if (($unit = $values[$key]) !== null) {
+ $parserState->consume($length);
break;
}
}
}
- return new Size((float) $sSize, $sUnit, $bIsColorComponent, $oParserState->currentLine());
+ return new Size((float) $size, $unit, $isColorComponent, $parserState->currentLine());
}
/**
- * @return array>
+ * @return array, array>
*/
- private static function getSizeUnits()
+ private static function getSizeUnits(): array
{
if (!\is_array(self::$SIZE_UNITS)) {
self::$SIZE_UNITS = [];
- foreach (\array_merge(self::ABSOLUTE_SIZE_UNITS, self::RELATIVE_SIZE_UNITS, self::NON_SIZE_UNITS) as $val) {
- $iSize = \strlen($val);
- if (!isset(self::$SIZE_UNITS[$iSize])) {
- self::$SIZE_UNITS[$iSize] = [];
+ $sizeUnits = \array_merge(self::ABSOLUTE_SIZE_UNITS, self::RELATIVE_SIZE_UNITS, self::NON_SIZE_UNITS);
+ foreach ($sizeUnits as $sizeUnit) {
+ $tokenLength = \strlen($sizeUnit);
+ if (!isset(self::$SIZE_UNITS[$tokenLength])) {
+ self::$SIZE_UNITS[$tokenLength] = [];
}
- self::$SIZE_UNITS[$iSize][\strtolower($val)] = $val;
+ self::$SIZE_UNITS[$tokenLength][\strtolower($sizeUnit)] = $sizeUnit;
}
\krsort(self::$SIZE_UNITS, SORT_NUMERIC);
@@ -130,54 +145,43 @@ private static function getSizeUnits()
return self::$SIZE_UNITS;
}
- /**
- * @param string $sUnit
- */
- public function setUnit($sUnit): void
+ public function setUnit(string $unit): void
{
- $this->sUnit = $sUnit;
+ $this->unit = $unit;
}
- /**
- * @return string|null
- */
- public function getUnit()
+ public function getUnit(): ?string
{
- return $this->sUnit;
+ return $this->unit;
}
/**
- * @param float|int|string $fSize
+ * @param float|int|string $size
*/
- public function setSize($fSize): void
+ public function setSize($size): void
{
- $this->fSize = (float) $fSize;
+ $this->size = (float) $size;
}
- /**
- * @return float
- */
- public function getSize()
+ public function getSize(): float
{
- return $this->fSize;
+ return $this->size;
}
- /**
- * @return bool
- */
- public function isColorComponent()
+ public function isColorComponent(): bool
{
- return $this->bIsColorComponent;
+ return $this->isColorComponent;
}
/**
* Returns whether the number stored in this Size really represents a size (as in a length of something on screen).
*
- * @return false if the unit an angle, a duration, a frequency or the number is a component in a Color object.
+ * Returns `false` if the unit is an angle, a duration, a frequency, or the number is a component in a `Color`
+ * object.
*/
public function isSize(): bool
{
- if (\in_array($this->sUnit, self::NON_SIZE_UNITS, true)) {
+ if (\in_array($this->unit, self::NON_SIZE_UNITS, true)) {
return false;
}
return !$this->isColorComponent();
@@ -185,27 +189,25 @@ public function isSize(): bool
public function isRelative(): bool
{
- if (\in_array($this->sUnit, self::RELATIVE_SIZE_UNITS, true)) {
+ if (\in_array($this->unit, self::RELATIVE_SIZE_UNITS, true)) {
return true;
}
- if ($this->sUnit === null && $this->fSize != 0) {
+ if ($this->unit === null && $this->size !== 0.0) {
return true;
}
return false;
}
- public function __toString(): string
+ /**
+ * @return non-empty-string
+ */
+ public function render(OutputFormat $outputFormat): string
{
- return $this->render(new OutputFormat());
- }
+ $locale = \localeconv();
+ $decimalPoint = \preg_quote($locale['decimal_point'], '/');
+ $size = preg_match('/[\\d\\.]+e[+-]?\\d+/i', (string) $this->size) === 1
+ ? preg_replace("/$decimalPoint?0+$/", '', \sprintf('%f', $this->size)) : (string) $this->size;
- public function render(OutputFormat $oOutputFormat): string
- {
- $l = \localeconv();
- $sPoint = \preg_quote($l['decimal_point'], '/');
- $sSize = \preg_match('/[\\d\\.]+e[+-]?\\d+/i', (string) $this->fSize)
- ? \preg_replace("/$sPoint?0+$/", '', \sprintf('%f', $this->fSize)) : $this->fSize;
- return \preg_replace(["/$sPoint/", '/^(-?)0\\./'], ['.', '$1.'], $sSize)
- . ($this->sUnit ?? '');
+ return preg_replace(["/$decimalPoint/", '/^(-?)0\\./'], ['.', '$1.'], $size) . ($this->unit ?? '');
}
}
diff --git a/src/Value/URL.php b/src/Value/URL.php
index b56f1288b..f6e7b974a 100644
--- a/src/Value/URL.php
+++ b/src/Value/URL.php
@@ -1,5 +1,7 @@
|null $lineNumber
*/
- public function __construct(CSSString $oURL, $iLineNo = 0)
+ public function __construct(CSSString $url, ?int $lineNumber = null)
{
- parent::__construct($iLineNo);
- $this->oURL = $oURL;
+ parent::__construct($lineNumber);
+ $this->url = $url;
}
/**
* @throws SourceException
* @throws UnexpectedEOFException
* @throws UnexpectedTokenException
+ *
+ * @internal since V8.8.0
*/
- public static function parse(ParserState $oParserState): URL
+ public static function parse(ParserState $parserState): URL
{
- $oAnchor = $oParserState->anchor();
- $sIdentifier = '';
+ $anchor = $parserState->anchor();
+ $identifier = '';
for ($i = 0; $i < 3; $i++) {
- $sChar = $oParserState->parseCharacter(true);
- if ($sChar === null) {
+ $character = $parserState->parseCharacter(true);
+ if ($character === null) {
break;
}
- $sIdentifier .= $sChar;
+ $identifier .= $character;
}
- $bUseUrl = $oParserState->streql($sIdentifier, 'url');
- if ($bUseUrl) {
- $oParserState->consumeWhiteSpace();
- $oParserState->consume('(');
+ $useUrl = $parserState->streql($identifier, 'url');
+ if ($useUrl) {
+ $parserState->consumeWhiteSpace();
+ $parserState->consume('(');
} else {
- $oAnchor->backtrack();
+ $anchor->backtrack();
}
- $oParserState->consumeWhiteSpace();
- $oResult = new URL(CSSString::parse($oParserState), $oParserState->currentLine());
- if ($bUseUrl) {
- $oParserState->consumeWhiteSpace();
- $oParserState->consume(')');
+ $parserState->consumeWhiteSpace();
+ $result = new URL(CSSString::parse($parserState), $parserState->currentLine());
+ if ($useUrl) {
+ $parserState->consumeWhiteSpace();
+ $parserState->consume(')');
}
- return $oResult;
- }
-
- public function setURL(CSSString $oURL): void
- {
- $this->oURL = $oURL;
+ return $result;
}
- /**
- * @return CSSString
- */
- public function getURL()
+ public function setURL(CSSString $url): void
{
- return $this->oURL;
+ $this->url = $url;
}
- public function __toString(): string
+ public function getURL(): CSSString
{
- return $this->render(new OutputFormat());
+ return $this->url;
}
- public function render(OutputFormat $oOutputFormat): string
+ /**
+ * @return non-empty-string
+ */
+ public function render(OutputFormat $outputFormat): string
{
- return "url({$this->oURL->render($oOutputFormat)})";
+ return "url({$this->url->render($outputFormat)})";
}
}
diff --git a/src/Value/Value.php b/src/Value/Value.php
index 5fb6e3d09..263c420da 100644
--- a/src/Value/Value.php
+++ b/src/Value/Value.php
@@ -1,137 +1,137 @@
|null $lineNumber
*/
- public function __construct($iLineNo = 0)
+ public function __construct(?int $lineNumber = null)
{
- $this->iLineNo = $iLineNo;
+ $this->setPosition($lineNumber);
}
/**
- * @param array $aListDelimiters
+ * @param array $listDelimiters
*
* @return Value|string
*
* @throws UnexpectedTokenException
* @throws UnexpectedEOFException
+ *
+ * @internal since V8.8.0
*/
- public static function parseValue(ParserState $oParserState, array $aListDelimiters = [])
+ public static function parseValue(ParserState $parserState, array $listDelimiters = [])
{
- /** @var array $aStack */
- $aStack = [];
- $oParserState->consumeWhiteSpace();
+ /** @var list $stack */
+ $stack = [];
+ $parserState->consumeWhiteSpace();
//Build a list of delimiters and parsed values
while (
- !($oParserState->comes('}') || $oParserState->comes(';') || $oParserState->comes('!')
- || $oParserState->comes(')')
- || $oParserState->comes('\\')
- || $oParserState->isEnd())
+ !($parserState->comes('}') || $parserState->comes(';') || $parserState->comes('!')
+ || $parserState->comes(')')
+ || $parserState->isEnd())
) {
- if (\count($aStack) > 0) {
- $bFoundDelimiter = false;
- foreach ($aListDelimiters as $sDelimiter) {
- if ($oParserState->comes($sDelimiter)) {
- \array_push($aStack, $oParserState->consume($sDelimiter));
- $oParserState->consumeWhiteSpace();
- $bFoundDelimiter = true;
+ if (\count($stack) > 0) {
+ $foundDelimiter = false;
+ foreach ($listDelimiters as $delimiter) {
+ if ($parserState->comes($delimiter)) {
+ \array_push($stack, $parserState->consume($delimiter));
+ $parserState->consumeWhiteSpace();
+ $foundDelimiter = true;
break;
}
}
- if (!$bFoundDelimiter) {
+ if (!$foundDelimiter) {
//Whitespace was the list delimiter
- \array_push($aStack, ' ');
+ \array_push($stack, ' ');
}
}
- \array_push($aStack, self::parsePrimitiveValue($oParserState));
- $oParserState->consumeWhiteSpace();
+ \array_push($stack, self::parsePrimitiveValue($parserState));
+ $parserState->consumeWhiteSpace();
}
// Convert the list to list objects
- foreach ($aListDelimiters as $sDelimiter) {
- $iStackLength = \count($aStack);
- if ($iStackLength === 1) {
- return $aStack[0];
+ foreach ($listDelimiters as $delimiter) {
+ $stackSize = \count($stack);
+ if ($stackSize === 1) {
+ return $stack[0];
}
- $aNewStack = [];
- for ($iStartPosition = 0; $iStartPosition < $iStackLength; ++$iStartPosition) {
- if ($iStartPosition === ($iStackLength - 1) || $sDelimiter !== $aStack[$iStartPosition + 1]) {
- $aNewStack[] = $aStack[$iStartPosition];
+ $newStack = [];
+ for ($offset = 0; $offset < $stackSize; ++$offset) {
+ if ($offset === ($stackSize - 1) || $delimiter !== $stack[$offset + 1]) {
+ $newStack[] = $stack[$offset];
continue;
}
- $iLength = 2; //Number of elements to be joined
- for ($i = $iStartPosition + 3; $i < $iStackLength; $i += 2, ++$iLength) {
- if ($sDelimiter !== $aStack[$i]) {
+ $length = 2; //Number of elements to be joined
+ for ($i = $offset + 3; $i < $stackSize; $i += 2, ++$length) {
+ if ($delimiter !== $stack[$i]) {
break;
}
}
- $oList = new RuleValueList($sDelimiter, $oParserState->currentLine());
- for ($i = $iStartPosition; $i - $iStartPosition < $iLength * 2; $i += 2) {
- $oList->addListComponent($aStack[$i]);
+ $list = new RuleValueList($delimiter, $parserState->currentLine());
+ for ($i = $offset; $i - $offset < $length * 2; $i += 2) {
+ $list->addListComponent($stack[$i]);
}
- $aNewStack[] = $oList;
- $iStartPosition += $iLength * 2 - 2;
+ $newStack[] = $list;
+ $offset += $length * 2 - 2;
}
- $aStack = $aNewStack;
+ $stack = $newStack;
}
- if (!isset($aStack[0])) {
+ if (!isset($stack[0])) {
throw new UnexpectedTokenException(
- " {$oParserState->peek()} ",
- $oParserState->peek(1, -1) . $oParserState->peek(2),
+ " {$parserState->peek()} ",
+ $parserState->peek(1, -1) . $parserState->peek(2),
'literal',
- $oParserState->currentLine()
+ $parserState->currentLine()
);
}
- return $aStack[0];
+ return $stack[0];
}
/**
- * @param bool $bIgnoreCase
- *
* @return CSSFunction|string
*
* @throws UnexpectedEOFException
* @throws UnexpectedTokenException
+ *
+ * @internal since V8.8.0
*/
- public static function parseIdentifierOrFunction(ParserState $oParserState, $bIgnoreCase = false)
+ public static function parseIdentifierOrFunction(ParserState $parserState, bool $ignoreCase = false)
{
- $oAnchor = $oParserState->anchor();
- $mResult = $oParserState->parseIdentifier($bIgnoreCase);
+ $anchor = $parserState->anchor();
+ $result = $parserState->parseIdentifier($ignoreCase);
- if ($oParserState->comes('(')) {
- $oAnchor->backtrack();
- if ($oParserState->streql('url', $mResult)) {
- $mResult = URL::parse($oParserState);
- } elseif (
- $oParserState->streql('calc', $mResult)
- || $oParserState->streql('-webkit-calc', $mResult)
- || $oParserState->streql('-moz-calc', $mResult)
- ) {
- $mResult = CalcFunction::parse($oParserState);
+ if ($parserState->comes('(')) {
+ $anchor->backtrack();
+ if ($parserState->streql('url', $result)) {
+ $result = URL::parse($parserState);
+ } elseif ($parserState->streql('calc', $result)) {
+ $result = CalcFunction::parse($parserState);
} else {
- $mResult = CSSFunction::parse($oParserState, $bIgnoreCase);
+ $result = CSSFunction::parse($parserState, $ignoreCase);
}
}
- return $mResult;
+ return $result;
}
/**
@@ -140,78 +140,76 @@ public static function parseIdentifierOrFunction(ParserState $oParserState, $bIg
* @throws UnexpectedEOFException
* @throws UnexpectedTokenException
* @throws SourceException
+ *
+ * @internal since V8.8.0
*/
- public static function parsePrimitiveValue(ParserState $oParserState)
+ public static function parsePrimitiveValue(ParserState $parserState)
{
- $oValue = null;
- $oParserState->consumeWhiteSpace();
+ $value = null;
+ $parserState->consumeWhiteSpace();
if (
- \is_numeric($oParserState->peek())
- || ($oParserState->comes('-.')
- && \is_numeric($oParserState->peek(1, 2)))
- || (($oParserState->comes('-') || $oParserState->comes('.')) && \is_numeric($oParserState->peek(1, 1)))
+ \is_numeric($parserState->peek())
+ || ($parserState->comes('-.')
+ && \is_numeric($parserState->peek(1, 2)))
+ || (($parserState->comes('-') || $parserState->comes('.')) && \is_numeric($parserState->peek(1, 1)))
) {
- $oValue = Size::parse($oParserState);
- } elseif ($oParserState->comes('#') || $oParserState->comes('rgb', true) || $oParserState->comes('hsl', true)) {
- $oValue = Color::parse($oParserState);
- } elseif ($oParserState->comes("'") || $oParserState->comes('"')) {
- $oValue = CSSString::parse($oParserState);
- } elseif ($oParserState->comes('progid:') && $oParserState->getSettings()->bLenientParsing) {
- $oValue = self::parseMicrosoftFilter($oParserState);
- } elseif ($oParserState->comes('[')) {
- $oValue = LineName::parse($oParserState);
- } elseif ($oParserState->comes('U+')) {
- $oValue = self::parseUnicodeRangeValue($oParserState);
+ $value = Size::parse($parserState);
+ } elseif ($parserState->comes('#') || $parserState->comes('rgb', true) || $parserState->comes('hsl', true)) {
+ $value = Color::parse($parserState);
+ } elseif ($parserState->comes("'") || $parserState->comes('"')) {
+ $value = CSSString::parse($parserState);
+ } elseif ($parserState->comes('progid:') && $parserState->getSettings()->usesLenientParsing()) {
+ $value = self::parseMicrosoftFilter($parserState);
+ } elseif ($parserState->comes('[')) {
+ $value = LineName::parse($parserState);
+ } elseif ($parserState->comes('U+')) {
+ $value = self::parseUnicodeRangeValue($parserState);
} else {
- $sNextChar = $oParserState->peek(1);
+ $nextCharacter = $parserState->peek(1);
try {
- $oValue = self::parseIdentifierOrFunction($oParserState);
+ $value = self::parseIdentifierOrFunction($parserState);
} catch (UnexpectedTokenException $e) {
- if (\in_array($sNextChar, ['+', '-', '*', '/'], true)) {
- $oValue = $oParserState->consume(1);
+ if (\in_array($nextCharacter, ['+', '-', '*', '/'], true)) {
+ $value = $parserState->consume(1);
} else {
throw $e;
}
}
}
- $oParserState->consumeWhiteSpace();
- return $oValue;
+ $parserState->consumeWhiteSpace();
+
+ return $value;
}
/**
* @throws UnexpectedEOFException
* @throws UnexpectedTokenException
*/
- private static function parseMicrosoftFilter(ParserState $oParserState): CSSFunction
+ private static function parseMicrosoftFilter(ParserState $parserState): CSSFunction
{
- $sFunction = $oParserState->consumeUntil('(', false, true);
- $aArguments = Value::parseValue($oParserState, [',', '=']);
- return new CSSFunction($sFunction, $aArguments, ',', $oParserState->currentLine());
+ $function = $parserState->consumeUntil('(', false, true);
+ $arguments = Value::parseValue($parserState, [',', '=']);
+ return new CSSFunction($function, $arguments, ',', $parserState->currentLine());
}
/**
* @throws UnexpectedEOFException
* @throws UnexpectedTokenException
*/
- private static function parseUnicodeRangeValue(ParserState $oParserState): string
+ private static function parseUnicodeRangeValue(ParserState $parserState): string
{
- $iCodepointMaxLength = 6; // Code points outside BMP can use up to six digits
- $sRange = '';
- $oParserState->consume('U+');
+ $codepointMaxLength = 6; // Code points outside BMP can use up to six digits
+ $range = '';
+ $parserState->consume('U+');
do {
- if ($oParserState->comes('-')) {
- $iCodepointMaxLength = 13; // Max length is 2 six digit code points + the dash(-) between them
+ if ($parserState->comes('-')) {
+ $codepointMaxLength = 13; // Max length is 2 six-digit code points + the dash(-) between them
}
- $sRange .= $oParserState->consume(1);
- } while (\strlen($sRange) < $iCodepointMaxLength && \preg_match('/[A-Fa-f0-9\\?-]/', $oParserState->peek()));
- return "U+{$sRange}";
- }
+ $range .= $parserState->consume(1);
+ } while (
+ (\strlen($range) < $codepointMaxLength) && (preg_match('/[A-Fa-f0-9\\?-]/', $parserState->peek()) === 1)
+ );
- /**
- * @return int
- */
- public function getLineNo()
- {
- return $this->iLineNo;
+ return "U+{$range}";
}
}
diff --git a/src/Value/ValueList.php b/src/Value/ValueList.php
index 6aff22770..6f85f895e 100644
--- a/src/Value/ValueList.php
+++ b/src/Value/ValueList.php
@@ -1,5 +1,7 @@
+ * @var array
+ *
+ * @internal since 8.8.0
*/
- protected $aComponents;
+ protected $components;
/**
- * @var string
+ * @var non-empty-string
+ *
+ * @internal since 8.8.0
*/
- protected $sSeparator;
+ protected $separator;
/**
- * @param array|Value|string $aComponents
- * @param string $sSeparator
- * @param int $iLineNo
+ * @param array|Value|string $components
+ * @param non-empty-string $separator
+ * @param int<1, max>|null $lineNumber
*/
- public function __construct($aComponents = [], $sSeparator = ',', $iLineNo = 0)
+ public function __construct($components = [], $separator = ',', ?int $lineNumber = null)
{
- parent::__construct($iLineNo);
- if (!\is_array($aComponents)) {
- $aComponents = [$aComponents];
+ parent::__construct($lineNumber);
+ if (!\is_array($components)) {
+ $components = [$components];
}
- $this->aComponents = $aComponents;
- $this->sSeparator = $sSeparator;
+ $this->components = $components;
+ $this->separator = $separator;
}
/**
- * @param Value|string $mComponent
+ * @param Value|string $component
*/
- public function addListComponent($mComponent): void
+ public function addListComponent($component): void
{
- $this->aComponents[] = $mComponent;
+ $this->components[] = $component;
}
/**
- * @return array
+ * @return array
*/
- public function getListComponents()
+ public function getListComponents(): array
{
- return $this->aComponents;
+ return $this->components;
}
/**
- * @param array $aComponents
+ * @param array $components
*/
- public function setListComponents(array $aComponents): void
+ public function setListComponents(array $components): void
{
- $this->aComponents = $aComponents;
+ $this->components = $components;
}
/**
- * @return string
+ * @return non-empty-string
*/
- public function getListSeparator()
+ public function getListSeparator(): string
{
- return $this->sSeparator;
+ return $this->separator;
}
/**
- * @param string $sSeparator
+ * @param non-empty-string $separator
*/
- public function setListSeparator($sSeparator): void
+ public function setListSeparator(string $separator): void
{
- $this->sSeparator = $sSeparator;
+ $this->separator = $separator;
}
- public function __toString(): string
+ public function render(OutputFormat $outputFormat): string
{
- return $this->render(new OutputFormat());
- }
+ $formatter = $outputFormat->getFormatter();
- /**
- * @return string
- */
- public function render(OutputFormat $oOutputFormat)
- {
- return $oOutputFormat->implode(
- $oOutputFormat->spaceBeforeListArgumentSeparator($this->sSeparator) . $this->sSeparator
- . $oOutputFormat->spaceAfterListArgumentSeparator($this->sSeparator),
- $this->aComponents
+ return $formatter->implode(
+ $formatter->spaceBeforeListArgumentSeparator($this->separator) . $this->separator
+ . $formatter->spaceAfterListArgumentSeparator($this->separator),
+ $this->components
);
}
}
diff --git a/tests/CSSList/AtRuleBlockListTest.php b/tests/CSSList/AtRuleBlockListTest.php
index 5398bfba6..9a725b212 100644
--- a/tests/CSSList/AtRuleBlockListTest.php
+++ b/tests/CSSList/AtRuleBlockListTest.php
@@ -1,16 +1,18 @@
*/
- public static function provideSyntacticlyCorrectAtRule(): array
+ public static function provideSyntacticallyCorrectAtRule(): array
{
return [
'media print' => ['@media print { html { background: white; color: black; } }'],
'keyframes' => ['@keyframes mymove { from { top: 0px; } }'],
- 'supports' => ['
- @supports (display: flex) {
- .flex-container > * {
- text-shadow: 0 0 2px blue;
- float: none;
- }
- .flex-container {
- display: flex;
+ 'supports' => [
+ '
+ @supports (display: flex) {
+ .flex-container > * {
+ text-shadow: 0 0 2px blue;
+ float: none;
+ }
+ .flex-container {
+ display: flex;
+ }
}
- }
- '],
+ ',
+ ],
];
}
- /**
- * @test
- */
- public function implementsAtRule(): void
- {
- $subject = new AtRuleBlockList('');
-
- self::assertInstanceOf(AtRuleBlockList::class, $subject);
- }
-
- /**
- * @test
- */
- public function implementsRenderable(): void
- {
- $subject = new AtRuleBlockList('');
-
- self::assertInstanceOf(Renderable::class, $subject);
- }
-
- /**
- * @test
- */
- public function implementsCommentable(): void
- {
- $subject = new AtRuleBlockList('');
-
- self::assertInstanceOf(Commentable::class, $subject);
- }
-
/**
* @test
*
@@ -87,6 +61,7 @@ public function parsesRuleNameOfMediaQueries(string $css): void
$contents = (new Parser($css))->parse()->getContents();
$atRuleBlockList = $contents[0];
+ self::assertInstanceOf(AtRuleBlockList::class, $atRuleBlockList);
self::assertSame('media', $atRuleBlockList->atRuleName());
}
@@ -100,6 +75,7 @@ public function parsesArgumentsOfMediaQueries(string $css): void
$contents = (new Parser($css))->parse()->getContents();
$atRuleBlockList = $contents[0];
+ self::assertInstanceOf(AtRuleBlockList::class, $atRuleBlockList);
self::assertSame('(min-width: 768px)', $atRuleBlockList->atRuleArgs());
}
@@ -107,9 +83,9 @@ public function parsesArgumentsOfMediaQueries(string $css): void
* @test
*
* @dataProvider provideMinWidthMediaRule
- * @dataProvider provideSyntacticlyCorrectAtRule
+ * @dataProvider provideSyntacticallyCorrectAtRule
*/
- public function parsesSyntacticlyCorrectAtRuleInStrictMode(string $css): void
+ public function parsesSyntacticallyCorrectAtRuleInStrictMode(string $css): void
{
$contents = (new Parser($css, Settings::create()->beStrict()))->parse()->getContents();
diff --git a/tests/CSSList/DocumentTest.php b/tests/CSSList/DocumentTest.php
deleted file mode 100644
index e700edbaf..000000000
--- a/tests/CSSList/DocumentTest.php
+++ /dev/null
@@ -1,143 +0,0 @@
-subject = new Document();
- }
-
- /**
- * @test
- */
- public function implementsRenderable(): void
- {
- self::assertInstanceOf(Renderable::class, $this->subject);
- }
-
- /**
- * @test
- */
- public function implementsCommentable(): void
- {
- self::assertInstanceOf(Commentable::class, $this->subject);
- }
-
- /**
- * @test
- */
- public function getContentsInitiallyReturnsEmptyArray(): void
- {
- self::assertSame([], $this->subject->getContents());
- }
-
- /**
- * @return array>>
- */
- public static function contentsDataProvider(): array
- {
- return [
- 'empty array' => [[]],
- '1 item' => [[new DeclarationBlock()]],
- '2 items' => [[new DeclarationBlock(), new DeclarationBlock()]],
- ];
- }
-
- /**
- * @test
- *
- * @param array $contents
- *
- * @dataProvider contentsDataProvider
- */
- public function setContentsSetsContents(array $contents): void
- {
- $this->subject->setContents($contents);
-
- self::assertSame($contents, $this->subject->getContents());
- }
-
- /**
- * @test
- */
- public function setContentsReplacesContentsSetInPreviousCall(): void
- {
- $contents2 = [new DeclarationBlock()];
-
- $this->subject->setContents([new DeclarationBlock()]);
- $this->subject->setContents($contents2);
-
- self::assertSame($contents2, $this->subject->getContents());
- }
-
- /**
- * @test
- */
- public function insertContentBeforeInsertsContentBeforeSibbling(): void
- {
- $bogusOne = new DeclarationBlock();
- $bogusOne->setSelectors('.bogus-one');
- $bogusTwo = new DeclarationBlock();
- $bogusTwo->setSelectors('.bogus-two');
-
- $item = new DeclarationBlock();
- $item->setSelectors('.item');
-
- $sibling = new DeclarationBlock();
- $sibling->setSelectors('.sibling');
-
- $this->subject->setContents([$bogusOne, $sibling, $bogusTwo]);
-
- self::assertCount(3, $this->subject->getContents());
-
- $this->subject->insertBefore($item, $sibling);
-
- self::assertCount(4, $this->subject->getContents());
- self::assertSame([$bogusOne, $item, $sibling, $bogusTwo], $this->subject->getContents());
- }
-
- /**
- * @test
- */
- public function insertContentBeforeAppendsIfSibblingNotFound(): void
- {
- $bogusOne = new DeclarationBlock();
- $bogusOne->setSelectors('.bogus-one');
- $bogusTwo = new DeclarationBlock();
- $bogusTwo->setSelectors('.bogus-two');
-
- $item = new DeclarationBlock();
- $item->setSelectors('.item');
-
- $sibling = new DeclarationBlock();
- $sibling->setSelectors('.sibling');
-
- $orphan = new DeclarationBlock();
- $orphan->setSelectors('.forever-alone');
-
- $this->subject->setContents([$bogusOne, $sibling, $bogusTwo]);
-
- self::assertCount(3, $this->subject->getContents());
-
- $this->subject->insertBefore($item, $orphan);
-
- self::assertCount(4, $this->subject->getContents());
- self::assertSame([$bogusOne, $sibling, $bogusTwo, $item], $this->subject->getContents());
- }
-}
diff --git a/tests/CSSList/KeyFrameTest.php b/tests/CSSList/KeyFrameTest.php
deleted file mode 100644
index a6c7edd15..000000000
--- a/tests/CSSList/KeyFrameTest.php
+++ /dev/null
@@ -1,49 +0,0 @@
-subject = new KeyFrame();
- }
-
- /**
- * @test
- */
- public function implementsAtRule(): void
- {
- self::assertInstanceOf(AtRule::class, $this->subject);
- }
-
- /**
- * @test
- */
- public function implementsRenderable(): void
- {
- self::assertInstanceOf(Renderable::class, $this->subject);
- }
-
- /**
- * @test
- */
- public function implementsCommentable(): void
- {
- self::assertInstanceOf(Commentable::class, $this->subject);
- }
-}
diff --git a/tests/Comment/CommentTest.php b/tests/Comment/CommentTest.php
index 79374afbe..52c3de550 100644
--- a/tests/Comment/CommentTest.php
+++ b/tests/Comment/CommentTest.php
@@ -1,118 +1,24 @@
getComment());
- }
-
- /**
- * @test
- */
- public function getCommentInitiallyReturnsCommentPassedToConstructor(): void
- {
- $comment = 'There is no spoon.';
- $subject = new Comment($comment);
-
- self::assertSame($comment, $subject->getComment());
- }
-
- /**
- * @test
- */
- public function setCommentSetsComments(): void
- {
- $comment = 'There is no spoon.';
- $subject = new Comment();
-
- $subject->setComment($comment);
-
- self::assertSame($comment, $subject->getComment());
- }
-
- /**
- * @test
- */
- public function getLineNoOnEmptyInstanceReturnsReturnsZero(): void
- {
- $subject = new Comment();
-
- self::assertSame(0, $subject->getLineNo());
- }
-
- /**
- * @test
- */
- public function getLineNoInitiallyReturnsLineNumberPassedToConstructor(): void
- {
- $lineNumber = 42;
- $subject = new Comment('', $lineNumber);
-
- self::assertSame($lineNumber, $subject->getLineNo());
- }
-
- /**
- * @test
- */
- public function toStringRendersCommentEnclosedInCommentDelimiters(): void
- {
- $comment = 'There is no spoon.';
- $subject = new Comment();
-
- $subject->setComment($comment);
-
- self::assertSame('/*' . $comment . '*/', (string) $subject);
- }
-
- /**
- * @test
- */
- public function renderRendersCommentEnclosedInCommentDelimiters(): void
- {
- $comment = 'There is no spoon.';
- $subject = new Comment();
-
- $subject->setComment($comment);
-
- self::assertSame('/*' . $comment . '*/', $subject->render(new OutputFormat()));
- }
-
/**
* @test
*/
public function keepCommentsInOutput(): void
{
- $oCss = TestsParserTest::parsedStructureForFile('comments');
+ $cssDocument = TestsParserTest::parsedStructureForFile('comments');
self::assertSame('/** Number 11 **/
/**
@@ -137,15 +43,15 @@ public function keepCommentsInOutput(): void
position: absolute;
}
}
-', $oCss->render(OutputFormat::createPretty()));
+', $cssDocument->render(OutputFormat::createPretty()));
self::assertSame(
'/** 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;}}',
- $oCss->render(OutputFormat::createCompact()->setRenderComments(true))
+ . '/* Number 6 */background-color:#000}@media screen{'
+ . '/** Number 10 **/#foo.bar{/** Number 10b **/position:absolute}}',
+ $cssDocument->render(OutputFormat::createCompact()->setRenderComments(true))
);
}
@@ -154,7 +60,7 @@ public function keepCommentsInOutput(): void
*/
public function stripCommentsFromOutput(): void
{
- $oCss = TestsParserTest::parsedStructureForFile('comments');
+ $css = TestsParserTest::parsedStructureForFile('comments');
self::assertSame('
@import url("some/url.css") screen;
@@ -167,12 +73,12 @@ public function stripCommentsFromOutput(): void
position: absolute;
}
}
-', $oCss->render(OutputFormat::createPretty()->setRenderComments(false)));
+', $css->render(OutputFormat::createPretty()->setRenderComments(false)));
self::assertSame(
'@import url("some/url.css") screen;'
- . '.foo,#bar{background-color:#000;}'
- . '@media screen{#foo.bar{position:absolute;}}',
- $oCss->render(OutputFormat::createCompact())
+ . '.foo,#bar{background-color:#000}'
+ . '@media screen{#foo.bar{position:absolute}}',
+ $css->render(OutputFormat::createCompact())
);
}
}
diff --git a/tests/Functional/CSSList/DocumentTest.php b/tests/Functional/CSSList/DocumentTest.php
new file mode 100644
index 000000000..71334f7f4
--- /dev/null
+++ b/tests/Functional/CSSList/DocumentTest.php
@@ -0,0 +1,137 @@
+render());
+ }
+
+ /**
+ * @test
+ */
+ public function renderWithVirginOutputFormatCanRenderEmptyDocument(): void
+ {
+ $subject = new Document();
+
+ self::assertSame('', $subject->render(new OutputFormat()));
+ }
+
+ /**
+ * @test
+ */
+ public function renderWithDefaultOutputFormatCanRenderEmptyDocument(): void
+ {
+ $subject = new Document();
+
+ self::assertSame('', $subject->render(OutputFormat::create()));
+ }
+
+ /**
+ * @test
+ */
+ public function renderWithCompactOutputFormatCanRenderEmptyDocument(): void
+ {
+ $subject = new Document();
+
+ self::assertSame('', $subject->render(OutputFormat::createCompact()));
+ }
+
+ /**
+ * @test
+ */
+ public function renderWithPrettyOutputFormatCanRenderEmptyDocument(): void
+ {
+ $subject = new Document();
+
+ self::assertSame('', $subject->render(OutputFormat::createPretty()));
+ }
+
+ /**
+ * Builds a subject with one `@charset` rule and one `@media` rule.
+ */
+ private function buildSubjectWithAtRules(): Document
+ {
+ $subject = new Document();
+ $charset = new Charset(new CSSString('UTF-8'));
+ $subject->append($charset);
+ $mediaQuery = new AtRuleBlockList('media', 'screen');
+ $subject->append($mediaQuery);
+
+ return $subject;
+ }
+
+ /**
+ * @test
+ */
+ public function renderWithoutOutputFormatCanRenderAtRules(): void
+ {
+ $subject = $this->buildSubjectWithAtRules();
+
+ $expected = '@charset "UTF-8";' . "\n" . '@media screen {}';
+ self::assertSame($expected, $subject->render());
+ }
+
+ /**
+ * @test
+ */
+ public function renderWithVirginOutputFormatCanRenderAtRules(): void
+ {
+ $subject = $this->buildSubjectWithAtRules();
+
+ $expected = '@charset "UTF-8";' . "\n" . '@media screen {}';
+ self::assertSame($expected, $subject->render(new OutputFormat()));
+ }
+
+ /**
+ * @test
+ */
+ public function renderWithDefaultOutputFormatCanRenderAtRules(): void
+ {
+ $subject = $this->buildSubjectWithAtRules();
+
+ $expected = '@charset "UTF-8";' . "\n" . '@media screen {}';
+ self::assertSame($expected, $subject->render(OutputFormat::create()));
+ }
+
+ /**
+ * @test
+ */
+ public function renderWithCompactOutputFormatCanRenderAtRules(): void
+ {
+ $subject = $this->buildSubjectWithAtRules();
+
+ $expected = '@charset "UTF-8";@media screen{}';
+ self::assertSame($expected, $subject->render(OutputFormat::createCompact()));
+ }
+
+ /**
+ * @test
+ */
+ public function renderWithPrettyOutputFormatCanRenderAtRules(): void
+ {
+ $subject = $this->buildSubjectWithAtRules();
+
+ $expected = "\n" . '@charset "UTF-8";' . "\n\n" . '@media screen {}' . "\n";
+ self::assertSame($expected, $subject->render(OutputFormat::createPretty()));
+ }
+}
diff --git a/tests/Functional/Comment/CommentTest.php b/tests/Functional/Comment/CommentTest.php
new file mode 100644
index 000000000..bbfa06a75
--- /dev/null
+++ b/tests/Functional/Comment/CommentTest.php
@@ -0,0 +1,67 @@
+setComment($comment);
+
+ self::assertSame('/*' . $comment . '*/', $subject->render(new OutputFormat()));
+ }
+
+ /**
+ * @test
+ */
+ public function renderWithDefaultOutputFormatRendersCommentEnclosedInCommentDelimiters(): void
+ {
+ $comment = 'There is no spoon.';
+ $subject = new Comment();
+
+ $subject->setComment($comment);
+
+ self::assertSame('/*' . $comment . '*/', $subject->render(OutputFormat::create()));
+ }
+
+ /**
+ * @test
+ */
+ public function renderWithCompactOutputFormatRendersCommentEnclosedInCommentDelimiters(): void
+ {
+ $comment = 'There is no spoon.';
+ $subject = new Comment();
+
+ $subject->setComment($comment);
+
+ self::assertSame('/*' . $comment . '*/', $subject->render(OutputFormat::createCompact()));
+ }
+
+ /**
+ * @test
+ */
+ public function renderWithPrettyOutputFormatRendersCommentEnclosedInCommentDelimiters(): void
+ {
+ $comment = 'There is no spoon.';
+ $subject = new Comment();
+
+ $subject->setComment($comment);
+
+ self::assertSame('/*' . $comment . '*/', $subject->render(OutputFormat::createPretty()));
+ }
+}
diff --git a/tests/Functional/ParserTest.php b/tests/Functional/ParserTest.php
new file mode 100644
index 000000000..982bb3a23
--- /dev/null
+++ b/tests/Functional/ParserTest.php
@@ -0,0 +1,39 @@
+parse();
+
+ self::assertInstanceOf(Document::class, $result);
+ }
+
+ /**
+ * @test
+ */
+ public function parseWithOneRuleSetReturnsDocument(): void
+ {
+ $parser = new Parser('.thing { }');
+
+ $result = $parser->parse();
+
+ self::assertInstanceOf(Document::class, $result);
+ }
+}
diff --git a/tests/Functional/Property/SelectorTest.php b/tests/Functional/Property/SelectorTest.php
new file mode 100644
index 000000000..397dbc722
--- /dev/null
+++ b/tests/Functional/Property/SelectorTest.php
@@ -0,0 +1,59 @@
+render(new OutputFormat()));
+ }
+
+ /**
+ * @test
+ */
+ public function renderWithDefaultOutputFormatRendersSelectorPassedToConstructor(): void
+ {
+ $pattern = 'a';
+ $subject = new Selector($pattern);
+
+ self::assertSame($pattern, $subject->render(OutputFormat::create()));
+ }
+
+ /**
+ * @test
+ */
+ public function renderWithCompactOutputFormatRendersSelectorPassedToConstructor(): void
+ {
+ $pattern = 'a';
+ $subject = new Selector($pattern);
+
+ self::assertSame($pattern, $subject->render(OutputFormat::createCompact()));
+ }
+
+ /**
+ * @test
+ */
+ public function renderWithPrettyOutputFormatRendersSelectorPassedToConstructor(): void
+ {
+ $pattern = 'a';
+ $subject = new Selector($pattern);
+
+ self::assertSame($pattern, $subject->render(OutputFormat::createPretty()));
+ }
+}
diff --git a/tests/Functional/RuleSet/DeclarationBlockTest.php b/tests/Functional/RuleSet/DeclarationBlockTest.php
new file mode 100644
index 000000000..fe6b9e51c
--- /dev/null
+++ b/tests/Functional/RuleSet/DeclarationBlockTest.php
@@ -0,0 +1,69 @@
+
+ */
+ public static function provideInvalidDeclarationBlock(): array
+ {
+ return [
+ 'no selector' => ['{ color: red; }'],
+ 'invalid selector' => ['/ { color: red; }'],
+ 'no opening brace' => ['body color: red; }'],
+ ];
+ }
+
+ /**
+ * @test
+ *
+ * @dataProvider provideInvalidDeclarationBlock
+ */
+ public function parseReturnsNullForInvalidDeclarationBlock(string $invalidDeclarationBlock): void
+ {
+ $parserState = new ParserState($invalidDeclarationBlock, Settings::create());
+
+ $result = DeclarationBlock::parse($parserState);
+
+ self::assertNull($result);
+ }
+
+ /**
+ * @test
+ */
+ public function rendersRulesInOrderProvided(): void
+ {
+ $declarationBlock = new DeclarationBlock();
+ $declarationBlock->setSelectors([new Selector('.test')]);
+
+ $rule1 = new Rule('background-color');
+ $rule1->setValue('transparent');
+ $declarationBlock->addRule($rule1);
+
+ $rule2 = new Rule('background');
+ $rule2->setValue('#222');
+ $declarationBlock->addRule($rule2);
+
+ $rule3 = new Rule('background-color');
+ $rule3->setValue('#fff');
+ $declarationBlock->addRule($rule3);
+
+ $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
new file mode 100644
index 000000000..b0d976055
--- /dev/null
+++ b/tests/Functional/RuleSet/RuleSetTest.php
@@ -0,0 +1,119 @@
+subject = new RuleSet();
+ }
+
+ /**
+ * @return array, 1: string}>
+ */
+ public static function providePropertyNamesAndValuesAndExpectedCss(): array
+ {
+ return [
+ 'no properties' => [[], ''],
+ 'one property' => [
+ [['name' => 'color', 'value' => 'green']],
+ 'color: green;',
+ ],
+ 'two different properties' => [
+ [
+ ['name' => 'color', 'value' => 'green'],
+ ['name' => 'display', 'value' => 'block'],
+ ],
+ 'color: green;display: block;',
+ ],
+ 'two of the same property' => [
+ [
+ ['name' => 'color', 'value' => '#40A040'],
+ ['name' => 'color', 'value' => 'rgba(0, 128, 0, 0.25)'],
+ ],
+ 'color: #40A040;color: rgba(0, 128, 0, 0.25);',
+ ],
+ ];
+ }
+
+ /**
+ * @test
+ *
+ * @param list $propertyNamesAndValuesToSet
+ *
+ * @dataProvider providePropertyNamesAndValuesAndExpectedCss
+ */
+ public function renderReturnsCssForRulesSet(array $propertyNamesAndValuesToSet, string $expectedCss): void
+ {
+ $this->setRulesFromPropertyNamesAndValues($propertyNamesAndValuesToSet);
+
+ $result = $this->subject->render(OutputFormat::create());
+
+ self::assertSame($expectedCss, $result);
+ }
+
+ /**
+ * @test
+ */
+ public function renderWithCompactOutputFormatReturnsCssWithoutWhitespaceOrTrailingSemicolon(): void
+ {
+ $this->setRulesFromPropertyNamesAndValues([
+ ['name' => 'color', 'value' => 'green'],
+ ['name' => 'display', 'value' => 'block'],
+ ]);
+
+ $result = $this->subject->render(OutputFormat::createCompact());
+
+ self::assertSame('color:green;display:block', $result);
+ }
+
+ /**
+ * @test
+ */
+ public function renderWithPrettyOutputFormatReturnsCssWithNewlinesAroundIndentedDeclarations(): void
+ {
+ $this->setRulesFromPropertyNamesAndValues([
+ ['name' => 'color', 'value' => 'green'],
+ ['name' => 'display', 'value' => 'block'],
+ ]);
+
+ $result = $this->subject->render(OutputFormat::createPretty());
+
+ self::assertSame("\n\tcolor: green;\n\tdisplay: block;\n", $result);
+ }
+
+ /**
+ * @param list $propertyNamesAndValues
+ */
+ private function setRulesFromPropertyNamesAndValues(array $propertyNamesAndValues): void
+ {
+ $rulesToSet = \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;
+ },
+ $propertyNamesAndValues
+ );
+ $this->subject->setRules($rulesToSet);
+ }
+}
diff --git a/tests/Functional/Value/ValueTest.php b/tests/Functional/Value/ValueTest.php
new file mode 100644
index 000000000..4d65ee2a8
--- /dev/null
+++ b/tests/Functional/Value/ValueTest.php
@@ -0,0 +1,45 @@
+
+ */
+ private const DEFAULT_DELIMITERS = [',', ' ', '/'];
+
+ /**
+ * @test
+ */
+ public function parsesFirstArgumentInMaxFunction(): void
+ {
+ $parsedValue = Value::parseValue(
+ new ParserState('max(300px, 400px);', Settings::create()),
+ self::DEFAULT_DELIMITERS
+ );
+
+ self::assertInstanceOf(CSSFunction::class, $parsedValue);
+ $size = $parsedValue->getArguments()[0];
+ self::assertInstanceOf(Size::class, $size);
+ self::assertSame(300.0, $size->getSize());
+ self::assertSame('px', $size->getUnit());
+ self::assertFalse($size->isColorComponent());
+ }
+}
diff --git a/tests/FunctionalDeprecated/.gitkeep b/tests/FunctionalDeprecated/.gitkeep
new file mode 100644
index 000000000..e69de29bb
diff --git a/tests/OutputFormatTest.php b/tests/OutputFormatTest.php
index 37fc14f8b..3a8deb30e 100644
--- a/tests/OutputFormatTest.php
+++ b/tests/OutputFormatTest.php
@@ -1,5 +1,7 @@
oParser = new Parser(self::TEST_CSS);
- $this->oDocument = $this->oParser->parse();
+ $this->parser = new Parser(self::TEST_CSS);
+ $this->document = $this->parser->parse();
}
/**
@@ -57,7 +59,7 @@ public function plain(): void
self::assertSame(
'.main, .test {font: italic normal bold 16px/1.2 "Helvetica",Verdana,sans-serif;background: white;}
@media screen {.main {background-size: 100% 100%;font-size: 1.3em;background-color: #fff;}}',
- $this->oDocument->render()
+ $this->document->render()
);
}
@@ -67,9 +69,9 @@ public function plain(): void
public function compact(): 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;}}',
- $this->oDocument->render(OutputFormat::createCompact())
+ '.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::createCompact())
);
}
@@ -78,7 +80,7 @@ public function compact(): void
*/
public function pretty(): void
{
- self::assertSame(self::TEST_CSS, $this->oDocument->render(OutputFormat::createPretty()));
+ self::assertSame(self::TEST_CSS, $this->document->render(OutputFormat::createPretty()));
}
/**
@@ -90,7 +92,7 @@ public function spaceAfterListArgumentSeparator(): void
'.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->oDocument->render(OutputFormat::create()->setSpaceAfterListArgumentSeparator(' '))
+ $this->document->render(OutputFormat::create()->setSpaceAfterListArgumentSeparator(' '))
);
}
@@ -102,12 +104,15 @@ 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;}}",
- $this->oDocument->render(OutputFormat::create()->setSpaceAfterListArgumentSeparator([
- 'default' => ' ',
- ',' => "\t",
- '/' => '',
- ' ' => '',
- ]))
+ $this->document->render(
+ OutputFormat::create()
+ ->setSpaceAfterListArgumentSeparator(' ')
+ ->setSpaceAfterListArgumentSeparators([
+ ',' => "\t",
+ '/' => '',
+ ' ' => '',
+ ])
+ )
);
}
@@ -120,7 +125,7 @@ public function spaceAfterSelectorSeparator(): void
'.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->oDocument->render(OutputFormat::create()->setSpaceAfterSelectorSeparator("\n"))
+ $this->document->render(OutputFormat::create()->setSpaceAfterSelectorSeparator("\n"))
);
}
@@ -132,7 +137,7 @@ 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;}}',
- $this->oDocument->render(OutputFormat::create()->setStringQuotingType("'"))
+ $this->document->render(OutputFormat::create()->setStringQuotingType("'"))
);
}
@@ -144,7 +149,7 @@ 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);}}',
- $this->oDocument->render(OutputFormat::create()->setRGBHashNotation(false))
+ $this->document->render(OutputFormat::create()->setRGBHashNotation(false))
);
}
@@ -156,7 +161,7 @@ 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}}',
- $this->oDocument->render(OutputFormat::create()->setSemicolonAfterLastRule(false))
+ $this->document->render(OutputFormat::create()->setSemicolonAfterLastRule(false))
);
}
@@ -168,7 +173,7 @@ 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;}}',
- $this->oDocument->render(OutputFormat::create()->setSpaceAfterRuleName("\t"))
+ $this->document->render(OutputFormat::create()->setSpaceAfterRuleName("\t"))
);
}
@@ -177,6 +182,11 @@ public function spaceAfterRuleName(): void
*/
public function spaceRules(): void
{
+ $outputFormat = OutputFormat::create()
+ ->setSpaceBeforeRules("\n")
+ ->setSpaceBetweenRules("\n")
+ ->setSpaceAfterRules("\n");
+
self::assertSame('.main, .test {
font: italic normal bold 16px/1.2 "Helvetica",Verdana,sans-serif;
background: white;
@@ -185,7 +195,7 @@ public function spaceRules(): void
background-size: 100% 100%;
font-size: 1.3em;
background-color: #fff;
- }}', $this->oDocument->render(OutputFormat::create()->set('Space*Rules', "\n")));
+ }}', $this->document->render($outputFormat));
}
/**
@@ -193,12 +203,17 @@ public function spaceRules(): void
*/
public function spaceBlocks(): void
{
+ $outputFormat = OutputFormat::create()
+ ->setSpaceBeforeBlocks("\n")
+ ->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->oDocument->render(OutputFormat::create()->set('Space*Blocks', "\n")));
+', $this->document->render($outputFormat));
}
/**
@@ -206,6 +221,14 @@ public function spaceBlocks(): void
*/
public function spaceBoth(): void
{
+ $outputFormat = OutputFormat::create()
+ ->setSpaceBeforeRules("\n")
+ ->setSpaceBetweenRules("\n")
+ ->setSpaceAfterRules("\n")
+ ->setSpaceBeforeBlocks("\n")
+ ->setSpaceBetweenBlocks("\n")
+ ->setSpaceAfterBlocks("\n");
+
self::assertSame('
.main, .test {
font: italic normal bold 16px/1.2 "Helvetica",Verdana,sans-serif;
@@ -218,7 +241,7 @@ public function spaceBoth(): void
background-color: #fff;
}
}
-', $this->oDocument->render(OutputFormat::create()->set('Space*Rules', "\n")->set('Space*Blocks', "\n")));
+', $this->document->render($outputFormat));
}
/**
@@ -226,10 +249,13 @@ public function spaceBoth(): void
*/
public function spaceBetweenBlocks(): void
{
+ $outputFormat = OutputFormat::create()
+ ->setSpaceBetweenBlocks('');
+
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->oDocument->render(OutputFormat::create()->setSpaceBetweenBlocks(''))
+ $this->document->render($outputFormat)
);
}
@@ -238,6 +264,15 @@ public function spaceBetweenBlocks(): void
*/
public function indentation(): void
{
+ $outputFormat = OutputFormat::create()
+ ->setSpaceBeforeRules("\n")
+ ->setSpaceBetweenRules("\n")
+ ->setSpaceAfterRules("\n")
+ ->setSpaceBeforeBlocks("\n")
+ ->setSpaceBetweenBlocks("\n")
+ ->setSpaceAfterBlocks("\n")
+ ->setIndentation('');
+
self::assertSame('
.main, .test {
font: italic normal bold 16px/1.2 "Helvetica",Verdana,sans-serif;
@@ -250,10 +285,7 @@ public function indentation(): void
background-color: #fff;
}
}
-', $this->oDocument->render(OutputFormat::create()
- ->set('Space*Rules', "\n")
- ->set('Space*Blocks', "\n")
- ->setIndentation('')));
+', $this->document->render($outputFormat));
}
/**
@@ -261,10 +293,13 @@ public function indentation(): void
*/
public function spaceBeforeBraces(): void
{
+ $outputFormat = OutputFormat::create()
+ ->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;}}',
- $this->oDocument->render(OutputFormat::create()->setSpaceBeforeOpeningBrace(''))
+ $this->document->render($outputFormat)
);
}
@@ -275,16 +310,18 @@ public function ignoreExceptionsOff(): void
{
$this->expectException(OutputException::class);
- $aBlocks = $this->oDocument->getAllDeclarationBlocks();
- $oFirstBlock = $aBlocks[0];
- $oFirstBlock->removeSelector('.main');
+ $outputFormat = OutputFormat::create()->setIgnoreExceptions(false);
+
+ $declarationBlocks = $this->document->getAllDeclarationBlocks();
+ $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;}}',
- $this->oDocument->render(OutputFormat::create()->setIgnoreExceptions(false))
+ $this->document->render($outputFormat)
);
- $oFirstBlock->removeSelector('.test');
- $this->oDocument->render(OutputFormat::create()->setIgnoreExceptions(false));
+ $firstDeclarationBlock->removeSelector('.test');
+ $this->document->render($outputFormat);
}
/**
@@ -292,13 +329,15 @@ public function ignoreExceptionsOff(): void
*/
public function ignoreExceptionsOn(): void
{
- $aBlocks = $this->oDocument->getAllDeclarationBlocks();
- $oFirstBlock = $aBlocks[0];
- $oFirstBlock->removeSelector('.main');
- $oFirstBlock->removeSelector('.test');
+ $outputFormat = OutputFormat::create()->setIgnoreExceptions(true);
+
+ $declarationBlocks = $this->document->getAllDeclarationBlocks();
+ $firstDeclarationBlock = $declarationBlocks[0];
+ $firstDeclarationBlock->removeSelector('.main');
+ $firstDeclarationBlock->removeSelector('.test');
self::assertSame(
'@media screen {.main {background-size: 100% 100%;font-size: 1.3em;background-color: #fff;}}',
- $this->oDocument->render(OutputFormat::create()->setIgnoreExceptions(true))
+ $this->document->render($outputFormat)
);
}
}
diff --git a/tests/ParserTest.php b/tests/ParserTest.php
index 2c1c72877..b80280a77 100644
--- a/tests/ParserTest.php
+++ b/tests/ParserTest.php
@@ -1,8 +1,12 @@
getContents();
self::assertCount(1, $cssList);
- self::assertInstanceOf(RuleSet::class, $cssList[0]);
+ self::assertInstanceOf(DeclarationBlock::class, $cssList[0]);
}
/**
@@ -59,30 +60,27 @@ public function parseForOneRuleSetReturnsDocumentWithOneRuleSet(): void
*/
public function files(): void
{
- $sDirectory = __DIR__ . '/fixtures';
- if ($rHandle = \opendir($sDirectory)) {
- /* This is the correct way to loop over the directory. */
- while (false !== ($sFileName = \readdir($rHandle))) {
- if (\strpos($sFileName, '.') === 0) {
- continue;
- }
- if (\strrpos($sFileName, '.css') !== \strlen($sFileName) - \strlen('.css')) {
- continue;
- }
- if (\strpos($sFileName, '-') === 0) {
- // Either a file which SHOULD fail (at least in strict mode)
- // or a future test of a as-of-now missing feature
- continue;
- }
- $oParser = new Parser(\file_get_contents($sDirectory . '/' . $sFileName));
- try {
- self::assertNotEquals('', $oParser->parse()->render());
- } catch (\Exception $e) {
- self::fail($e);
- }
+ $directory = __DIR__ . '/fixtures';
+ $directoryHandle = opendir($directory);
+
+ /* This is the correct way to loop over the directory. */
+ while (false !== ($filename = \readdir($directoryHandle))) {
+ if (\strpos($filename, '.') === 0) {
+ continue;
+ }
+ if (\strrpos($filename, '.css') !== \strlen($filename) - \strlen('.css')) {
+ continue;
+ }
+ if (\strpos($filename, '-') === 0) {
+ // Either a file which SHOULD fail (at least in strict mode)
+ // or a future test of an as-of-now missing feature
+ continue;
}
- \closedir($rHandle);
+ $parser = new Parser(file_get_contents($directory . '/' . $filename));
+ self::assertNotSame('', $parser->parse()->render());
}
+
+ \closedir($directoryHandle);
}
/**
@@ -92,66 +90,69 @@ public function files(): void
*/
public function colorParsing(): void
{
- $oDoc = self::parsedStructureForFile('colortest');
- foreach ($oDoc->getAllRuleSets() as $oRuleSet) {
- if (!$oRuleSet instanceof DeclarationBlock) {
- continue;
- }
- $sSelector = $oRuleSet->getSelectors();
- $sSelector = $sSelector[0]->getSelector();
- if ($sSelector === '#mine') {
- $aColorRule = $oRuleSet->getRules('color');
- $oColor = $aColorRule[0]->getValue();
- self::assertSame('red', $oColor);
- $aColorRule = $oRuleSet->getRules('background-');
- $oColor = $aColorRule[0]->getValue();
+ $document = self::parsedStructureForFile('colortest');
+ foreach ($document->getAllDeclarationBlocks() as $declarationBlock) {
+ $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);
self::assertEquals([
- 'r' => new Size(35.0, null, true, $oColor->getLineNo()),
- 'g' => new Size(35.0, null, true, $oColor->getLineNo()),
- 'b' => new Size(35.0, null, true, $oColor->getLineNo()),
- ], $oColor->getColor());
- $aColorRule = $oRuleSet->getRules('border-color');
- $oColor = $aColorRule[0]->getValue();
+ '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);
self::assertEquals([
- 'r' => new Size(10.0, null, true, $oColor->getLineNo()),
- 'g' => new Size(100.0, null, true, $oColor->getLineNo()),
- 'b' => new Size(230.0, null, true, $oColor->getLineNo()),
- ], $oColor->getColor());
- $oColor = $aColorRule[1]->getValue();
+ '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);
self::assertEquals([
- 'r' => new Size(10.0, null, true, $oColor->getLineNo()),
- 'g' => new Size(100.0, null, true, $oColor->getLineNo()),
- 'b' => new Size(231.0, null, true, $oColor->getLineNo()),
- 'a' => new Size('0000.3', null, true, $oColor->getLineNo()),
- ], $oColor->getColor());
- $aColorRule = $oRuleSet->getRules('outline-color');
- $oColor = $aColorRule[0]->getValue();
+ '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);
self::assertEquals([
- 'r' => new Size(34.0, null, true, $oColor->getLineNo()),
- 'g' => new Size(34.0, null, true, $oColor->getLineNo()),
- 'b' => new Size(34.0, null, true, $oColor->getLineNo()),
- ], $oColor->getColor());
- } elseif ($sSelector === '#yours') {
- $aColorRule = $oRuleSet->getRules('background-color');
- $oColor = $aColorRule[0]->getValue();
+ '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());
+ } elseif ($selector === '#yours') {
+ $colorRules = $declarationBlock->getRules('background-color');
+ $colorRuleValue = $colorRules[0]->getValue();
+ self::assertInstanceOf(Color::class, $colorRuleValue);
self::assertEquals([
- 'h' => new Size(220.0, null, true, $oColor->getLineNo()),
- 's' => new Size(10.0, '%', true, $oColor->getLineNo()),
- 'l' => new Size(220.0, '%', true, $oColor->getLineNo()),
- ], $oColor->getColor());
- $oColor = $aColorRule[1]->getValue();
+ '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);
self::assertEquals([
- 'h' => new Size(220.0, null, true, $oColor->getLineNo()),
- 's' => new Size(10.0, '%', true, $oColor->getLineNo()),
- 'l' => new Size(220.0, '%', true, $oColor->getLineNo()),
- 'a' => new Size(0000.3, null, true, $oColor->getLineNo()),
- ], $oColor->getColor());
- $aColorRule = $oRuleSet->getRules('outline-color');
- self::assertEmpty($aColorRule);
+ '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);
}
}
- foreach ($oDoc->getAllValues('color') as $sColor) {
- self::assertSame('red', $sColor);
+ foreach ($document->getAllValues(null, 'color') as $colorValue) {
+ self::assertSame('red', $colorValue);
}
self::assertSame(
'#mine {color: red;border-color: #0a64e6;border-color: rgba(10,100,231,.3);outline-color: #222;'
@@ -165,7 +166,7 @@ public function colorParsing(): void
. "\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);}',
- $oDoc->render()
+ $document->render()
);
}
@@ -174,47 +175,47 @@ public function colorParsing(): void
*/
public function unicodeParsing(): void
{
- $oDoc = self::parsedStructureForFile('unicode');
- foreach ($oDoc->getAllDeclarationBlocks() as $oRuleSet) {
- $sSelector = $oRuleSet->getSelectors();
- $sSelector = $sSelector[0]->getSelector();
- if (\substr($sSelector, 0, \strlen('.test-')) !== '.test-') {
+ $document = self::parsedStructureForFile('unicode');
+ foreach ($document->getAllDeclarationBlocks() as $declarationBlock) {
+ $selectors = $declarationBlock->getSelectors();
+ $selector = $selectors[0]->getSelector();
+ if (\substr($selector, 0, \strlen('.test-')) !== '.test-') {
continue;
}
- $aContentRules = $oRuleSet->getRules('content');
- $sString = $aContentRules[0]->getValue()->__toString();
- if ($sSelector == '.test-1') {
- self::assertSame('" "', $sString);
+ $contentRules = $declarationBlock->getRules('content');
+ $firstContentRuleAsString = $contentRules[0]->getValue()->render(OutputFormat::create());
+ if ($selector === '.test-1') {
+ self::assertSame('" "', $firstContentRuleAsString);
}
- if ($sSelector == '.test-2') {
- self::assertSame('"é"', $sString);
+ if ($selector === '.test-2') {
+ self::assertSame('"é"', $firstContentRuleAsString);
}
- if ($sSelector == '.test-3') {
- self::assertSame('" "', $sString);
+ if ($selector === '.test-3') {
+ self::assertSame('" "', $firstContentRuleAsString);
}
- if ($sSelector == '.test-4') {
- self::assertSame('"𝄞"', $sString);
+ if ($selector === '.test-4') {
+ self::assertSame('"𝄞"', $firstContentRuleAsString);
}
- if ($sSelector == '.test-5') {
- self::assertSame('"水"', $sString);
+ if ($selector === '.test-5') {
+ self::assertSame('"水"', $firstContentRuleAsString);
}
- if ($sSelector == '.test-6') {
- self::assertSame('"¥"', $sString);
+ if ($selector === '.test-6') {
+ self::assertSame('"¥"', $firstContentRuleAsString);
}
- if ($sSelector == '.test-7') {
- self::assertSame('"\\A"', $sString);
+ if ($selector === '.test-7') {
+ self::assertSame('"\\A"', $firstContentRuleAsString);
}
- if ($sSelector == '.test-8') {
- self::assertSame('"\\"\\""', $sString);
+ if ($selector === '.test-8') {
+ self::assertSame('"\\"\\""', $firstContentRuleAsString);
}
- if ($sSelector == '.test-9') {
- self::assertSame('"\\"\\\'"', $sString);
+ if ($selector === '.test-9') {
+ self::assertSame('"\\"\\\'"', $firstContentRuleAsString);
}
- if ($sSelector == '.test-10') {
- self::assertSame('"\\\'\\\\"', $sString);
+ if ($selector === '.test-10') {
+ self::assertSame('"\\\'\\\\"', $firstContentRuleAsString);
}
- if ($sSelector == '.test-11') {
- self::assertSame('"test"', $sString);
+ if ($selector === '.test-11') {
+ self::assertSame('"test"', $firstContentRuleAsString);
}
}
}
@@ -224,9 +225,9 @@ public function unicodeParsing(): void
*/
public function unicodeRangeParsing(): void
{
- $oDoc = self::parsedStructureForFile('unicode-range');
- $sExpected = '@font-face {unicode-range: U+0100-024F,U+0259,U+1E??-2EFF,U+202F;}';
- self::assertSame($sExpected, $oDoc->render());
+ $document = self::parsedStructureForFile('unicode-range');
+ $expected = '@font-face {unicode-range: U+0100-024F,U+0259,U+1E??-2EFF,U+202F;}';
+ self::assertSame($expected, $document->render());
}
/**
@@ -234,51 +235,27 @@ public function unicodeRangeParsing(): void
*/
public function specificity(): void
{
- $oDoc = self::parsedStructureForFile('specificity');
- $oDeclarationBlock = $oDoc->getAllDeclarationBlocks();
- $oDeclarationBlock = $oDeclarationBlock[0];
- $aSelectors = $oDeclarationBlock->getSelectors();
- foreach ($aSelectors as $oSelector) {
- switch ($oSelector->getSelector()) {
- case '#test .help':
- self::assertSame(110, $oSelector->getSpecificity());
- break;
- case '#file':
- self::assertSame(100, $oSelector->getSpecificity());
- break;
- case '.help:hover':
- self::assertSame(20, $oSelector->getSpecificity());
- break;
- case 'ol li::before':
- self::assertSame(3, $oSelector->getSpecificity());
- break;
- case 'li.green':
- self::assertSame(11, $oSelector->getSpecificity());
- break;
- default:
- self::fail('specificity: untested selector ' . $oSelector->getSelector());
- }
- }
- self::assertEquals([new Selector('#test .help', true)], $oDoc->getSelectorsBySpecificity('> 100'));
+ $document = self::parsedStructureForFile('specificity');
+ self::assertEquals([new Selector('#test .help')], $document->getSelectorsBySpecificity('> 100'));
self::assertEquals(
- [new Selector('#test .help', true), new Selector('#file', true)],
- $oDoc->getSelectorsBySpecificity('>= 100')
+ [new Selector('#test .help'), new Selector('#file')],
+ $document->getSelectorsBySpecificity('>= 100')
);
- self::assertEquals([new Selector('#file', true)], $oDoc->getSelectorsBySpecificity('=== 100'));
- self::assertEquals([new Selector('#file', true)], $oDoc->getSelectorsBySpecificity('== 100'));
+ self::assertEquals([new Selector('#file')], $document->getSelectorsBySpecificity('=== 100'));
+ self::assertEquals([new Selector('#file')], $document->getSelectorsBySpecificity('== 100'));
self::assertEquals([
- new Selector('#file', true),
- new Selector('.help:hover', true),
- new Selector('li.green', true),
- new Selector('ol li::before', true),
- ], $oDoc->getSelectorsBySpecificity('<= 100'));
+ new Selector('#file'),
+ new Selector('.help:hover'),
+ new Selector('li.green'),
+ new Selector('ol li::before'),
+ ], $document->getSelectorsBySpecificity('<= 100'));
self::assertEquals([
- new Selector('.help:hover', true),
- new Selector('li.green', true),
- new Selector('ol li::before', true),
- ], $oDoc->getSelectorsBySpecificity('< 100'));
- self::assertEquals([new Selector('li.green', true)], $oDoc->getSelectorsBySpecificity('11'));
- self::assertEquals([new Selector('ol li::before', true)], $oDoc->getSelectorsBySpecificity(3));
+ new Selector('.help:hover'),
+ new Selector('li.green'),
+ new Selector('ol li::before'),
+ ], $document->getSelectorsBySpecificity('< 100'));
+ self::assertEquals([new Selector('li.green')], $document->getSelectorsBySpecificity('11'));
+ self::assertEquals([new Selector('ol li::before')], $document->getSelectorsBySpecificity('3'));
}
/**
@@ -286,7 +263,7 @@ public function specificity(): void
*/
public function manipulation(): void
{
- $oDoc = self::parsedStructureForFile('atrules');
+ $document = self::parsedStructureForFile('atrules');
self::assertSame(
'@charset "utf-8";'
. "\n"
@@ -318,12 +295,12 @@ public function manipulation(): void
. '@media screen and (orientation: landscape) {@-ms-viewport {width: 1024px;height: 768px;}}'
. "\n"
. '@region-style #intro {p {color: blue;}}',
- $oDoc->render()
+ $document->render()
);
- foreach ($oDoc->getAllDeclarationBlocks() as $oBlock) {
- foreach ($oBlock->getSelectors() as $oSelector) {
+ foreach ($document->getAllDeclarationBlocks() as $declarationBlock) {
+ foreach ($declarationBlock->getSelectors() as $selector) {
//Loop over all selector parts (the comma-separated strings in a selector) and prepend the id
- $oSelector->setSelector('#my_id ' . $oSelector->getSelector());
+ $selector->setSelector('#my_id ' . $selector->getSelector());
}
}
self::assertSame(
@@ -357,33 +334,33 @@ public function manipulation(): void
. '@media screen and (orientation: landscape) {@-ms-viewport {width: 1024px;height: 768px;}}'
. "\n"
. '@region-style #intro {#my_id p {color: blue;}}',
- $oDoc->render(OutputFormat::create()->setRenderComments(false))
+ $document->render(OutputFormat::create()->setRenderComments(false))
);
- $oDoc = self::parsedStructureForFile('values');
+ $document = self::parsedStructureForFile('values');
self::assertSame(
'#header {margin: 10px 2em 1cm 2%;font-family: Verdana,Helvetica,"Gill Sans",sans-serif;'
. 'font-size: 10px;color: red !important;background-color: green;'
. 'background-color: rgba(0,128,0,.7);frequency: 30Hz;transform: rotate(1turn);}
body {color: green;font: 75% "Lucida Grande","Trebuchet MS",Verdana,sans-serif;}',
- $oDoc->render()
+ $document->render()
);
- foreach ($oDoc->getAllRuleSets() as $oRuleSet) {
- $oRuleSet->removeRule('font-');
+ foreach ($document->getAllRuleSets() as $ruleSet) {
+ $ruleSet->removeMatchingRules('font-');
}
self::assertSame(
'#header {margin: 10px 2em 1cm 2%;color: red !important;background-color: green;'
. 'background-color: rgba(0,128,0,.7);frequency: 30Hz;transform: rotate(1turn);}
body {color: green;}',
- $oDoc->render()
+ $document->render()
);
- foreach ($oDoc->getAllRuleSets() as $oRuleSet) {
- $oRuleSet->removeRule('background-');
+ foreach ($document->getAllRuleSets() as $ruleSet) {
+ $ruleSet->removeMatchingRules('background-');
}
self::assertSame(
'#header {margin: 10px 2em 1cm 2%;color: red !important;frequency: 30Hz;transform: rotate(1turn);}
body {color: green;}',
- $oDoc->render()
+ $document->render()
);
}
@@ -392,22 +369,22 @@ public function manipulation(): void
*/
public function ruleGetters(): void
{
- $oDoc = self::parsedStructureForFile('values');
- $aBlocks = $oDoc->getAllDeclarationBlocks();
- $oHeaderBlock = $aBlocks[0];
- $oBodyBlock = $aBlocks[1];
- $aHeaderRules = $oHeaderBlock->getRules('background-');
- self::assertCount(2, $aHeaderRules);
- self::assertSame('background-color', $aHeaderRules[0]->getRule());
- self::assertSame('background-color', $aHeaderRules[1]->getRule());
- $aHeaderRules = $oHeaderBlock->getRulesAssoc('background-');
- self::assertCount(1, $aHeaderRules);
- self::assertTrue($aHeaderRules['background-color']->getValue() instanceof Color);
- self::assertSame('rgba', $aHeaderRules['background-color']->getValue()->getColorDescription());
- $oHeaderBlock->removeRule($aHeaderRules['background-color']);
- $aHeaderRules = $oHeaderBlock->getRules('background-');
- self::assertCount(1, $aHeaderRules);
- self::assertSame('green', $aHeaderRules[0]->getValue());
+ $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());
}
/**
@@ -415,39 +392,43 @@ public function ruleGetters(): void
*/
public function slashedValues(): void
{
- $oDoc = self::parsedStructureForFile('slashed');
+ $document = self::parsedStructureForFile('slashed');
self::assertSame(
'.test {font: 12px/1.5 Verdana,Arial,sans-serif;border-radius: 5px 10px 5px 10px/10px 5px 10px 5px;}',
- $oDoc->render()
+ $document->render()
);
- foreach ($oDoc->getAllValues(null) as $mValue) {
- if ($mValue instanceof Size && $mValue->isSize() && !$mValue->isRelative()) {
- $mValue->setSize($mValue->getSize() * 3);
+ foreach ($document->getAllValues(null) as $value) {
+ if ($value instanceof Size && $value->isSize() && !$value->isRelative()) {
+ $value->setSize($value->getSize() * 3);
}
}
- foreach ($oDoc->getAllDeclarationBlocks() as $oBlock) {
- $oRule = $oBlock->getRules('font');
- $oRule = $oRule[0];
- $oSpaceList = $oRule->getValue();
- self::assertSame(' ', $oSpaceList->getListSeparator());
- $oSlashList = $oSpaceList->getListComponents();
- $oCommaList = $oSlashList[1];
- $oSlashList = $oSlashList[0];
- self::assertSame(',', $oCommaList->getListSeparator());
- self::assertSame('/', $oSlashList->getListSeparator());
- $oRule = $oBlock->getRules('border-radius');
- $oRule = $oRule[0];
- $oSlashList = $oRule->getValue();
- self::assertSame('/', $oSlashList->getListSeparator());
- $oSpaceList1 = $oSlashList->getListComponents();
- $oSpaceList2 = $oSpaceList1[1];
- $oSpaceList1 = $oSpaceList1[0];
- self::assertSame(' ', $oSpaceList1->getListSeparator());
- self::assertSame(' ', $oSpaceList2->getListSeparator());
+ 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];
+ self::assertInstanceOf(ValueList::class, $commaList);
+ $slashList = $fontRuleValueComponents[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();
+ self::assertSame('/', $slashList->getListSeparator());
+ $slashListComponents = $slashList->getListComponents();
+ $secondSlashListComponent = $slashListComponents[1];
+ self::assertInstanceOf(ValueList::class, $secondSlashListComponent);
+ $firstSlashListComponent = $slashListComponents[0];
+ self::assertInstanceOf(ValueList::class, $firstSlashListComponent);
+ self::assertSame(' ', $firstSlashListComponent->getListSeparator());
+ self::assertSame(' ', $secondSlashListComponent->getListSeparator());
}
self::assertSame(
'.test {font: 36px/1.5 Verdana,Arial,sans-serif;border-radius: 15px 30px 15px 30px/30px 15px 30px 15px;}',
- $oDoc->render()
+ $document->render()
);
}
@@ -456,8 +437,8 @@ public function slashedValues(): void
*/
public function functionSyntax(): void
{
- $oDoc = self::parsedStructureForFile('functions');
- $sExpected = 'div.main {background-image: linear-gradient(#000,#fff);}'
+ $document = self::parsedStructureForFile('functions');
+ $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;'
@@ -470,64 +451,23 @@ public function functionSyntax(): void
. '-moz-transition-duration: .3s;}'
. "\n"
. '.collapser.expanded + * {height: auto;}';
- self::assertSame($sExpected, $oDoc->render());
+ self::assertSame($expected, $document->render());
- foreach ($oDoc->getAllValues(null, true) as $mValue) {
- if ($mValue instanceof Size && $mValue->isSize()) {
- $mValue->setSize($mValue->getSize() * 3);
+ foreach ($document->getAllValues(null, null, true) as $value) {
+ if ($value instanceof Size && $value->isSize()) {
+ $value->setSize($value->getSize() * 3);
}
}
- $sExpected = \str_replace(['1.2em', '.2em', '60%'], ['3.6em', '.6em', '180%'], $sExpected);
- self::assertSame($sExpected, $oDoc->render());
+ $expected = \str_replace(['1.2em', '.2em', '60%'], ['3.6em', '.6em', '180%'], $expected);
+ self::assertSame($expected, $document->render());
- foreach ($oDoc->getAllValues(null, true) as $mValue) {
- if ($mValue instanceof Size && !$mValue->isRelative() && !$mValue->isColorComponent()) {
- $mValue->setSize($mValue->getSize() * 2);
+ foreach ($document->getAllValues(null, null, true) as $value) {
+ if ($value instanceof Size && !$value->isRelative() && !$value->isColorComponent()) {
+ $value->setSize($value->getSize() * 2);
}
}
- $sExpected = \str_replace(['.2s', '.3s', '90deg'], ['.4s', '.6s', '180deg'], $sExpected);
- self::assertSame($sExpected, $oDoc->render());
- }
-
- /**
- * @test
- */
- public function expandShorthands(): void
- {
- $oDoc = self::parsedStructureForFile('expand-shorthands');
- $sExpected = 'body {font: italic 500 14px/1.618 "Trebuchet MS",Georgia,serif;border: 2px solid #f0f;'
- . 'background: #ccc url("/images/foo.png") no-repeat left top;margin: 1em !important;'
- . 'padding: 2px 6px 3px;}';
- self::assertSame($sExpected, $oDoc->render());
- $oDoc->expandShorthands();
- $sExpected = 'body {margin-top: 1em !important;margin-right: 1em !important;margin-bottom: 1em !important;'
- . 'margin-left: 1em !important;padding-top: 2px;padding-right: 6px;padding-bottom: 3px;'
- . 'padding-left: 6px;border-top-color: #f0f;border-right-color: #f0f;border-bottom-color: #f0f;'
- . 'border-left-color: #f0f;border-top-style: solid;border-right-style: solid;'
- . 'border-bottom-style: solid;border-left-style: solid;border-top-width: 2px;'
- . 'border-right-width: 2px;border-bottom-width: 2px;border-left-width: 2px;font-style: italic;'
- . 'font-variant: normal;font-weight: 500;font-size: 14px;line-height: 1.618;'
- . 'font-family: "Trebuchet MS",Georgia,serif;background-color: #ccc;'
- . 'background-image: url("/images/foo.png");background-repeat: no-repeat;background-attachment: scroll;'
- . 'background-position: left top;}';
- self::assertSame($sExpected, $oDoc->render());
- }
-
- /**
- * @test
- */
- public function createShorthands(): void
- {
- $oDoc = self::parsedStructureForFile('create-shorthands');
- $sExpected = 'body {font-size: 2em;font-family: Helvetica,Arial,sans-serif;font-weight: bold;'
- . 'border-width: 2px;border-color: #999;border-style: dotted;background-color: #fff;'
- . 'background-image: url("foobar.png");background-repeat: repeat-y;margin-top: 2px;margin-right: 3px;'
- . 'margin-bottom: 4px;margin-left: 5px;}';
- self::assertSame($sExpected, $oDoc->render());
- $oDoc->createShorthands();
- $sExpected = 'body {background: #fff url("foobar.png") repeat-y;margin: 2px 5px 4px 3px;'
- . 'border: 2px dotted #999;font: bold 2em Helvetica,Arial,sans-serif;}';
- self::assertSame($sExpected, $oDoc->render());
+ $expected = \str_replace(['.2s', '.3s', '90deg'], ['.4s', '.6s', '180deg'], $expected);
+ self::assertSame($expected, $document->render());
}
/**
@@ -535,14 +475,14 @@ public function createShorthands(): void
*/
public function namespaces(): void
{
- $oDoc = self::parsedStructureForFile('namespaces');
- $sExpected = '@namespace toto "http://toto.example.org";
+ $document = self::parsedStructureForFile('namespaces');
+ $expected = '@namespace toto "http://toto.example.org";
@namespace "http://example.com/foo";
@namespace foo url("http://www.example.com/");
@namespace foo url("http://www.example.com/");
foo|test {gaga: 1;}
|test {gaga: 2;}';
- self::assertSame($sExpected, $oDoc->render());
+ self::assertSame($expected, $document->render());
}
/**
@@ -550,9 +490,9 @@ public function namespaces(): void
*/
public function innerColors(): void
{
- $oDoc = self::parsedStructureForFile('inner-color');
- $sExpected = 'test {background: -webkit-gradient(linear,0 0,0 bottom,from(#006cad),to(hsl(202,100%,49%)));}';
- self::assertSame($sExpected, $oDoc->render());
+ $document = self::parsedStructureForFile('inner-color');
+ $expected = 'test {background: -webkit-gradient(linear,0 0,0 bottom,from(#006cad),to(hsl(202,100%,49%)));}';
+ self::assertSame($expected, $document->render());
}
/**
@@ -560,9 +500,9 @@ public function innerColors(): void
*/
public function prefixedGradient(): void
{
- $oDoc = self::parsedStructureForFile('webkit');
- $sExpected = '.test {background: -webkit-linear-gradient(top right,white,black);}';
- self::assertSame($sExpected, $oDoc->render());
+ $document = self::parsedStructureForFile('webkit');
+ $expected = '.test {background: -webkit-linear-gradient(top right,white,black);}';
+ self::assertSame($expected, $document->render());
}
/**
@@ -570,36 +510,36 @@ public function prefixedGradient(): void
*/
public function listValueRemoval(): void
{
- $oDoc = self::parsedStructureForFile('atrules');
- foreach ($oDoc->getContents() as $oItem) {
- if ($oItem instanceof AtRule) {
- $oDoc->remove($oItem);
+ $document = self::parsedStructureForFile('atrules');
+ foreach ($document->getContents() as $contentItem) {
+ if ($contentItem instanceof AtRule) {
+ $document->remove($contentItem);
continue;
}
}
- self::assertSame('html, body {font-size: -.6em;}', $oDoc->render());
+ self::assertSame('html, body {font-size: -.6em;}', $document->render());
- $oDoc = self::parsedStructureForFile('nested');
- foreach ($oDoc->getAllDeclarationBlocks() as $oBlock) {
- $oDoc->removeDeclarationBlockBySelector($oBlock, false);
+ $document = self::parsedStructureForFile('nested');
+ foreach ($document->getAllDeclarationBlocks() as $declarationBlock) {
+ $document->removeDeclarationBlockBySelector($declarationBlock, false);
break;
}
self::assertSame(
'html {some-other: -test(val1);}
@media screen {html {some: -test(val2);}}
#unrelated {other: yes;}',
- $oDoc->render()
+ $document->render()
);
- $oDoc = self::parsedStructureForFile('nested');
- foreach ($oDoc->getAllDeclarationBlocks() as $oBlock) {
- $oDoc->removeDeclarationBlockBySelector($oBlock, true);
+ $document = self::parsedStructureForFile('nested');
+ foreach ($document->getAllDeclarationBlocks() as $declarationBlock) {
+ $document->removeDeclarationBlockBySelector($declarationBlock, true);
break;
}
self::assertSame(
'@media screen {html {some: -test(val2);}}
#unrelated {other: yes;}',
- $oDoc->render()
+ $document->render()
);
}
@@ -610,18 +550,18 @@ public function selectorRemoval(): void
{
$this->expectException(OutputException::class);
- $oDoc = self::parsedStructureForFile('1readme');
- $aBlocks = $oDoc->getAllDeclarationBlocks();
- $oBlock1 = $aBlocks[0];
- self::assertTrue($oBlock1->removeSelector('html'));
- $sExpected = '@charset "utf-8";
+ $document = self::parsedStructureForFile('1readme');
+ $declarationsBlocks = $document->getAllDeclarationBlocks();
+ $declarationBlock = $declarationsBlocks[0];
+ self::assertTrue($declarationBlock->removeSelector('html'));
+ $expected = '@charset "utf-8";
@font-face {font-family: "CrassRoots";src: url("../media/cr.ttf");}
body {font-size: 1.6em;}';
- self::assertSame($sExpected, $oDoc->render());
- self::assertFalse($oBlock1->removeSelector('html'));
- self::assertTrue($oBlock1->removeSelector('body'));
+ self::assertSame($expected, $document->render());
+ self::assertFalse($declarationBlock->removeSelector('html'));
+ self::assertTrue($declarationBlock->removeSelector('body'));
// This tries to output a declaration block without a selector and throws.
- $oDoc->render();
+ $document->render();
}
/**
@@ -629,13 +569,13 @@ public function selectorRemoval(): void
*/
public function comments(): void
{
- $oDoc = self::parsedStructureForFile('comments');
- $sExpected = <<render());
+ self::assertSame($expected, $document->render());
}
/**
@@ -643,10 +583,10 @@ public function comments(): void
*/
public function urlInFile(): void
{
- $oDoc = self::parsedStructureForFile('url', Settings::create()->withMultibyteSupport(true));
- $sExpected = 'body {background: #fff url("https://somesite.com/images/someimage.gif") repeat top center;}
+ $document = self::parsedStructureForFile('url', Settings::create()->withMultibyteSupport(true));
+ $expected = 'body {background: #fff url("https://somesite.com/images/someimage.gif") repeat top center;}
body {background-url: url("https://somesite.com/images/someimage.gif");}';
- self::assertSame($sExpected, $oDoc->render());
+ self::assertSame($expected, $document->render());
}
/**
@@ -654,10 +594,10 @@ public function urlInFile(): void
*/
public function hexAlphaInFile(): void
{
- $oDoc = self::parsedStructureForFile('hex-alpha', Settings::create()->withMultibyteSupport(true));
- $sExpected = 'div {background: rgba(17,34,51,.27);}
+ $document = self::parsedStructureForFile('hex-alpha', Settings::create()->withMultibyteSupport(true));
+ $expected = 'div {background: rgba(17,34,51,.27);}
div {background: rgba(17,34,51,.27);}';
- self::assertSame($sExpected, $oDoc->render());
+ self::assertSame($expected, $document->render());
}
/**
@@ -665,12 +605,12 @@ public function hexAlphaInFile(): void
*/
public function calcInFile(): void
{
- $oDoc = self::parsedStructureForFile('calc', Settings::create()->withMultibyteSupport(true));
- $sExpected = 'div {width: calc(100% / 4);}
+ $document = self::parsedStructureForFile('calc', Settings::create()->withMultibyteSupport(true));
+ $expected = 'div {width: calc(100% / 4);}
div {margin-top: calc(-120% - 4px);}
-div {height: -webkit-calc(9 / 16 * 100%) !important;width: -moz-calc(( 50px - 50% ) * 2);}
+div {height: calc(9 / 16 * 100%) !important;width: calc(( 50px - 50% ) * 2);}
div {width: calc(50% - ( ( 4% ) * .5 ));}';
- self::assertSame($sExpected, $oDoc->render());
+ self::assertSame($expected, $document->render());
}
/**
@@ -678,9 +618,9 @@ public function calcInFile(): void
*/
public function calcNestedInFile(): void
{
- $oDoc = self::parsedStructureForFile('calc-nested', Settings::create()->withMultibyteSupport(true));
- $sExpected = '.test {font-size: calc(( 3 * 4px ) + -2px);top: calc(200px - calc(20 * 3px));}';
- self::assertSame($sExpected, $oDoc->render());
+ $document = self::parsedStructureForFile('calc-nested', Settings::create()->withMultibyteSupport(true));
+ $expected = '.test {font-size: calc(( 3 * 4px ) + -2px);top: calc(200px - calc(20 * 3px));}';
+ self::assertSame($expected, $document->render());
}
/**
@@ -688,13 +628,13 @@ public function calcNestedInFile(): void
*/
public function invalidCalcInFile(): void
{
- $oDoc = self::parsedStructureForFile('calc-invalid', Settings::create()->withMultibyteSupport(true));
- $sExpected = 'div {}
+ $document = self::parsedStructureForFile('calc-invalid', Settings::create()->withMultibyteSupport(true));
+ $expected = 'div {}
div {}
div {}
div {height: -moz-calc;}
div {height: calc;}';
- self::assertSame($sExpected, $oDoc->render());
+ self::assertSame($expected, $document->render());
}
/**
@@ -703,28 +643,28 @@ public function invalidCalcInFile(): void
public function invalidCalc(): void
{
$parser = new Parser('div { height: calc(100px');
- $oDoc = $parser->parse();
- self::assertSame('div {height: calc(100px);}', $oDoc->render());
+ $document = $parser->parse();
+ self::assertSame('div {height: calc(100px);}', $document->render());
$parser = new Parser('div { height: calc(100px)');
- $oDoc = $parser->parse();
- self::assertSame('div {height: calc(100px);}', $oDoc->render());
+ $document = $parser->parse();
+ self::assertSame('div {height: calc(100px);}', $document->render());
$parser = new Parser('div { height: calc(100px);');
- $oDoc = $parser->parse();
- self::assertSame('div {height: calc(100px);}', $oDoc->render());
+ $document = $parser->parse();
+ self::assertSame('div {height: calc(100px);}', $document->render());
$parser = new Parser('div { height: calc(100px}');
- $oDoc = $parser->parse();
- self::assertSame('div {}', $oDoc->render());
+ $document = $parser->parse();
+ self::assertSame('div {}', $document->render());
$parser = new Parser('div { height: calc(100px;');
- $oDoc = $parser->parse();
- self::assertSame('div {}', $oDoc->render());
+ $document = $parser->parse();
+ self::assertSame('div {}', $document->render());
$parser = new Parser('div { height: calc(100px;}');
- $oDoc = $parser->parse();
- self::assertSame('div {}', $oDoc->render());
+ $document = $parser->parse();
+ self::assertSame('div {}', $document->render());
}
/**
@@ -732,10 +672,10 @@ public function invalidCalc(): void
*/
public function gridLineNameInFile(): void
{
- $oDoc = self::parsedStructureForFile('grid-linename', Settings::create()->withMultibyteSupport(true));
- $sExpected = "div {grid-template-columns: [linename] 100px;}\n"
+ $document = self::parsedStructureForFile('grid-linename', Settings::create()->withMultibyteSupport(true));
+ $expected = "div {grid-template-columns: [linename] 100px;}\n"
. 'span {grid-template-columns: [linename1 linename2] 100px;}';
- self::assertSame($sExpected, $oDoc->render());
+ self::assertSame($expected, $document->render());
}
/**
@@ -743,9 +683,9 @@ public function gridLineNameInFile(): void
*/
public function emptyGridLineNameLenientInFile(): void
{
- $oDoc = self::parsedStructureForFile('empty-grid-linename');
- $sExpected = '.test {grid-template-columns: [] 100px;}';
- self::assertSame($sExpected, $oDoc->render());
+ $document = self::parsedStructureForFile('empty-grid-linename');
+ $expected = '.test {grid-template-columns: [] 100px;}';
+ self::assertSame($expected, $document->render());
}
/**
@@ -753,9 +693,12 @@ public function emptyGridLineNameLenientInFile(): void
*/
public function invalidGridLineNameInFile(): void
{
- $oDoc = self::parsedStructureForFile('invalid-grid-linename', Settings::create()->withMultibyteSupport(true));
- $sExpected = 'div {}';
- self::assertSame($sExpected, $oDoc->render());
+ $document = self::parsedStructureForFile(
+ 'invalid-grid-linename',
+ Settings::create()->withMultibyteSupport(true)
+ );
+ $expected = 'div {}';
+ self::assertSame($expected, $document->render());
}
/**
@@ -763,9 +706,9 @@ public function invalidGridLineNameInFile(): void
*/
public function unmatchedBracesInFile(): void
{
- $oDoc = self::parsedStructureForFile('unmatched_braces', Settings::create()->withMultibyteSupport(true));
- $sExpected = 'button, input, checkbox, textarea {outline: 0;margin: 0;}';
- self::assertSame($sExpected, $oDoc->render());
+ $document = self::parsedStructureForFile('unmatched_braces', Settings::create()->withMultibyteSupport(true));
+ $expected = 'button, input, checkbox, textarea {outline: 0;margin: 0;}';
+ self::assertSame($expected, $document->render());
}
/**
@@ -773,20 +716,20 @@ public function unmatchedBracesInFile(): void
*/
public function invalidSelectorsInFile(): void
{
- $oDoc = self::parsedStructureForFile('invalid-selectors', Settings::create()->withMultibyteSupport(true));
- $sExpected = '@keyframes mymove {from {top: 0px;}}
+ $document = self::parsedStructureForFile('invalid-selectors', Settings::create()->withMultibyteSupport(true));
+ $expected = '@keyframes mymove {from {top: 0px;}}
#test {color: white;background: green;}
#test {display: block;background: white;color: black;}';
- self::assertSame($sExpected, $oDoc->render());
+ self::assertSame($expected, $document->render());
- $oDoc = self::parsedStructureForFile('invalid-selectors-2', Settings::create()->withMultibyteSupport(true));
- $sExpected = '@media only screen and (max-width: 1215px) {.breadcrumb {padding-left: 10px;}
+ $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($sExpected, $oDoc->render());
+ self::assertSame($expected, $document->render());
}
/**
@@ -794,19 +737,19 @@ public function invalidSelectorsInFile(): void
*/
public function selectorEscapesInFile(): void
{
- $oDoc = self::parsedStructureForFile('selector-escapes', Settings::create()->withMultibyteSupport(true));
- $sExpected = '#\\# {color: red;}
+ $document = self::parsedStructureForFile('selector-escapes', Settings::create()->withMultibyteSupport(true));
+ $expected = '#\\# {color: red;}
.col-sm-1\\/5 {width: 20%;}';
- self::assertSame($sExpected, $oDoc->render());
+ self::assertSame($expected, $document->render());
- $oDoc = self::parsedStructureForFile('invalid-selectors-2', Settings::create()->withMultibyteSupport(true));
- $sExpected = '@media only screen and (max-width: 1215px) {.breadcrumb {padding-left: 10px;}
+ $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($sExpected, $oDoc->render());
+ self::assertSame($expected, $document->render());
}
/**
@@ -814,10 +757,10 @@ public function selectorEscapesInFile(): void
*/
public function identifierEscapesInFile(): void
{
- $oDoc = self::parsedStructureForFile('identifier-escapes', Settings::create()->withMultibyteSupport(true));
- $sExpected = 'div {font: 14px Font Awesome\\ 5 Pro;font: 14px Font Awesome\\} 5 Pro;'
+ $document = self::parsedStructureForFile('identifier-escapes', Settings::create()->withMultibyteSupport(true));
+ $expected = 'div {font: 14px Font Awesome\\ 5 Pro;font: 14px Font Awesome\\} 5 Pro;'
. 'font: 14px Font Awesome\\; 5 Pro;f\\;ont: 14px Font Awesome\\; 5 Pro;}';
- self::assertSame($sExpected, $oDoc->render());
+ self::assertSame($expected, $document->render());
}
/**
@@ -825,13 +768,13 @@ public function identifierEscapesInFile(): void
*/
public function selectorIgnoresInFile(): void
{
- $oDoc = self::parsedStructureForFile('selector-ignores', Settings::create()->withMultibyteSupport(true));
- $sExpected = '.some[selectors-may=\'contain-a-{\'] {}'
+ $document = self::parsedStructureForFile('selector-ignores', Settings::create()->withMultibyteSupport(true));
+ $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($sExpected, $oDoc->render());
+ self::assertSame($expected, $document->render());
}
/**
@@ -839,16 +782,16 @@ public function selectorIgnoresInFile(): void
*/
public function keyframeSelectors(): void
{
- $oDoc = self::parsedStructureForFile(
+ $document = self::parsedStructureForFile(
'keyframe-selector-validation',
Settings::create()->withMultibyteSupport(true)
);
- $sExpected = '@-webkit-keyframes zoom {0% {-webkit-transform: scale(1,1);}'
+ $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);}}';
- self::assertSame($sExpected, $oDoc->render());
+ self::assertSame($expected, $document->render());
}
/**
@@ -876,11 +819,11 @@ public function calcFailure(): void
*/
public function urlInFileMbOff(): void
{
- $oDoc = self::parsedStructureForFile('url', Settings::create()->withMultibyteSupport(false));
- $sExpected = 'body {background: #fff url("https://somesite.com/images/someimage.gif") repeat top center;}'
+ $document = self::parsedStructureForFile('url', Settings::create()->withMultibyteSupport(false));
+ $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($sExpected, $oDoc->render());
+ self::assertSame($expected, $document->render());
}
/**
@@ -888,9 +831,9 @@ public function urlInFileMbOff(): void
*/
public function emptyFile(): void
{
- $oDoc = self::parsedStructureForFile('-empty', Settings::create()->withMultibyteSupport(true));
- $sExpected = '';
- self::assertSame($sExpected, $oDoc->render());
+ $document = self::parsedStructureForFile('-empty', Settings::create()->withMultibyteSupport(true));
+ $expected = '';
+ self::assertSame($expected, $document->render());
}
/**
@@ -898,9 +841,9 @@ public function emptyFile(): void
*/
public function emptyFileMbOff(): void
{
- $oDoc = self::parsedStructureForFile('-empty', Settings::create()->withMultibyteSupport(false));
- $sExpected = '';
- self::assertSame($sExpected, $oDoc->render());
+ $document = self::parsedStructureForFile('-empty', Settings::create()->withMultibyteSupport(false));
+ $expected = '';
+ self::assertSame($expected, $document->render());
}
/**
@@ -908,9 +851,9 @@ public function emptyFileMbOff(): void
*/
public function charsetLenient1(): void
{
- $oDoc = self::parsedStructureForFile('-charset-after-rule', Settings::create()->withLenientParsing(true));
- $sExpected = '#id {prop: var(--val);}';
- self::assertSame($sExpected, $oDoc->render());
+ $document = self::parsedStructureForFile('-charset-after-rule', Settings::create()->withLenientParsing(true));
+ $expected = '#id {prop: var(--val);}';
+ self::assertSame($expected, $document->render());
}
/**
@@ -918,9 +861,9 @@ public function charsetLenient1(): void
*/
public function charsetLenient2(): void
{
- $oDoc = self::parsedStructureForFile('-charset-in-block', Settings::create()->withLenientParsing(true));
- $sExpected = '@media print {}';
- self::assertSame($sExpected, $oDoc->render());
+ $document = self::parsedStructureForFile('-charset-in-block', Settings::create()->withLenientParsing(true));
+ $expected = '@media print {}';
+ self::assertSame($expected, $document->render());
}
/**
@@ -928,9 +871,9 @@ public function charsetLenient2(): void
*/
public function trailingWhitespace(): void
{
- $oDoc = self::parsedStructureForFile('trailing-whitespace', Settings::create()->withLenientParsing(false));
- $sExpected = 'div {width: 200px;}';
- self::assertSame($sExpected, $oDoc->render());
+ $document = self::parsedStructureForFile('trailing-whitespace', Settings::create()->withLenientParsing(false));
+ $expected = 'div {width: 200px;}';
+ self::assertSame($expected, $document->render());
}
/**
@@ -966,8 +909,6 @@ public function unopenedClosingBracketFailure(): void
/**
* Ensure that a missing property value raises an exception.
*
- * @covers \Sabberworm\CSS\Value\Value::parseValue()
- *
* @test
*/
public function missingPropertyValueStrict(): void
@@ -980,16 +921,14 @@ public function missingPropertyValueStrict(): void
/**
* Ensure that a missing property value is ignored when in lenient parsing mode.
*
- * @covers \Sabberworm\CSS\Value\Value::parseValue()
- *
* @test
*/
public function missingPropertyValueLenient(): void
{
$parsed = self::parsedStructureForFile('missing-property-value', Settings::create()->withLenientParsing(true));
- $rulesets = $parsed->getAllRuleSets();
- self::assertCount(1, $rulesets);
- $block = $rulesets[0];
+ $declarationBlocks = $parsed->getAllDeclarationBlocks();
+ self::assertCount(1, $declarationBlocks);
+ $block = $declarationBlocks[0];
self::assertInstanceOf(DeclarationBlock::class, $block);
self::assertEquals([new Selector('div')], $block->getSelectors());
$rules = $block->getRules();
@@ -1002,14 +941,14 @@ public function missingPropertyValueLenient(): void
/**
* Parses structure for file.
*
- * @param string $sFileName
- * @param Settings|null $oSettings
+ * @param string $filename
+ * @param Settings|null $settings
*/
- public static function parsedStructureForFile($sFileName, $oSettings = null): Document
+ public static function parsedStructureForFile($filename, $settings = null): Document
{
- $sFile = __DIR__ . "/fixtures/$sFileName.css";
- $oParser = new Parser(\file_get_contents($sFile), $oSettings);
- return $oParser->parse();
+ $filename = __DIR__ . "/fixtures/$filename.css";
+ $parser = new Parser(file_get_contents($filename), $settings);
+ return $parser->parse();
}
/**
@@ -1019,9 +958,9 @@ public static function parsedStructureForFile($sFileName, $oSettings = null): Do
*/
public function lineNumbersParsing(): void
{
- $oDoc = self::parsedStructureForFile('line-numbers');
+ $document = self::parsedStructureForFile('line-numbers');
// array key is the expected line number
- $aExpected = [
+ $expected = [
1 => [Charset::class],
3 => [CSSNamespace::class],
5 => [AtRuleSet::class],
@@ -1032,42 +971,45 @@ public function lineNumbersParsing(): void
25 => [DeclarationBlock::class],
];
- $aActual = [];
- foreach ($oDoc->getContents() as $oContent) {
- $aActual[$oContent->getLineNo()] = [\get_class($oContent)];
- if ($oContent instanceof KeyFrame) {
- foreach ($oContent->getContents() as $block) {
- $aActual[$oContent->getLineNo()][] = $block->getLineNo();
+ $actual = [];
+ foreach ($document->getContents() as $contentItem) {
+ self::assertInstanceOf(Positionable::class, $contentItem);
+ $actual[$contentItem->getLineNumber()] = [\get_class($contentItem)];
+ if ($contentItem instanceof KeyFrame) {
+ foreach ($contentItem->getContents() as $block) {
+ self::assertInstanceOf(Positionable::class, $block);
+ $actual[$contentItem->getLineNumber()][] = $block->getLineNumber();
}
}
}
- $aUrlExpected = [7, 26]; // expected line numbers
- $aUrlActual = [];
- foreach ($oDoc->getAllValues() as $oValue) {
- if ($oValue instanceof URL) {
- $aUrlActual[] = $oValue->getLineNo();
+ $expectedLineNumbers = [7, 26];
+ $actualLineNumbers = [];
+ foreach ($document->getAllValues() as $value) {
+ if ($value instanceof URL) {
+ $actualLineNumbers[] = $value->getLineNumber();
}
}
// Checking for the multiline color rule lines 27-31
- $aExpectedColorLines = [28, 29, 30];
- $aDeclBlocks = $oDoc->getAllDeclarationBlocks();
+ $expectedColorLineNumbers = [28, 29, 30];
+ $declarationBlocks = $document->getAllDeclarationBlocks();
// Choose the 2nd one
- $oDeclBlock = $aDeclBlocks[1];
- $aRules = $oDeclBlock->getRules();
+ $secondDeclarationBlock = $declarationBlocks[1];
+ $rules = $secondDeclarationBlock->getRules();
// Choose the 2nd one
- $oColor = $aRules[1]->getValue();
- self::assertSame(27, $aRules[1]->getLineNo());
+ $valueOfSecondRule = $rules[1]->getValue();
+ self::assertInstanceOf(Color::class, $valueOfSecondRule);
+ self::assertSame(27, $rules[1]->getLineNumber());
- $aActualColorLines = [];
- foreach ($oColor->getColor() as $oSize) {
- $aActualColorLines[] = $oSize->getLineNo();
+ $actualColorLineNumbers = [];
+ foreach ($valueOfSecondRule->getColor() as $size) {
+ $actualColorLineNumbers[] = $size->getLineNumber();
}
- self::assertSame($aExpectedColorLines, $aActualColorLines);
- self::assertSame($aUrlExpected, $aUrlActual);
- self::assertSame($aExpected, $aActual);
+ self::assertSame($expectedColorLineNumbers, $actualColorLineNumbers);
+ self::assertSame($expectedLineNumbers, $actualLineNumbers);
+ self::assertSame($expected, $actual);
}
/**
@@ -1077,37 +1019,15 @@ public function unexpectedTokenExceptionLineNo(): void
{
$this->expectException(UnexpectedTokenException::class);
- $oParser = new Parser("\ntest: 1;", Settings::create()->beStrict());
+ $parser = new Parser("\ntest: 1;", Settings::create()->beStrict());
try {
- $oParser->parse();
+ $parser->parse();
} catch (UnexpectedTokenException $e) {
- self::assertSame(2, $e->getLineNo());
+ self::assertSame(2, $e->getLineNumber());
throw $e;
}
}
- /**
- * @test
- */
- public function ieHacksStrictParsing(): void
- {
- $this->expectException(UnexpectedTokenException::class);
-
- // We can't strictly parse IE hacks.
- self::parsedStructureForFile('ie-hacks', Settings::create()->beStrict());
- }
-
- /**
- * @test
- */
- public function ieHacksParsing(): void
- {
- $oDoc = self::parsedStructureForFile('ie-hacks', Settings::create()->withLenientParsing(true));
- $sExpected = 'p {padding-right: .75rem \\9;background-image: none \\9;color: red \\9\\0;'
- . 'background-color: red \\9\\0;background-color: red \\9\\0 !important;content: "red \\0";content: "red઼";}';
- self::assertSame($sExpected, $oDoc->render());
- }
-
/**
* @depends files
*
@@ -1115,17 +1035,19 @@ public function ieHacksParsing(): void
*/
public function commentExtracting(): void
{
- $oDoc = self::parsedStructureForFile('comments');
- $aNodes = $oDoc->getContents();
+ $document = self::parsedStructureForFile('comments');
+ $nodes = $document->getContents();
// Import property.
- $importComments = $aNodes[0]->getComments();
+ self::assertInstanceOf(Commentable::class, $nodes[0]);
+ $importComments = $nodes[0]->getComments();
self::assertCount(2, $importComments);
self::assertSame("*\n * Comments\n ", $importComments[0]->getComment());
self::assertSame(' Hell ', $importComments[1]->getComment());
// Declaration block.
- $fooBarBlock = $aNodes[1];
+ $fooBarBlock = $nodes[1];
+ self::assertInstanceOf(Commentable::class, $fooBarBlock);
$fooBarBlockComments = $fooBarBlock->getComments();
// TODO Support comments in selectors.
// $this->assertCount(2, $fooBarBlockComments);
@@ -1133,6 +1055,7 @@ public function commentExtracting(): void
// $this->assertSame("* Number 5 *", $fooBarBlockComments[1]->getComment());
// Declaration rules.
+ self::assertInstanceOf(DeclarationBlock::class, $fooBarBlock);
$fooBarRules = $fooBarBlock->getRules();
$fooBarRule = $fooBarRules[0];
$fooBarRuleComments = $fooBarRule->getComments();
@@ -1140,16 +1063,20 @@ public function commentExtracting(): void
self::assertSame(' Number 6 ', $fooBarRuleComments[0]->getComment());
// Media property.
- $mediaComments = $aNodes[2]->getComments();
+ self::assertInstanceOf(Commentable::class, $nodes[2]);
+ $mediaComments = $nodes[2]->getComments();
self::assertCount(0, $mediaComments);
// Media children.
- $mediaRules = $aNodes[2]->getContents();
+ self::assertInstanceOf(CSSList::class, $nodes[2]);
+ $mediaRules = $nodes[2]->getContents();
+ self::assertInstanceOf(Commentable::class, $mediaRules[0]);
$fooBarComments = $mediaRules[0]->getComments();
self::assertCount(1, $fooBarComments);
self::assertSame('* Number 10 *', $fooBarComments[0]->getComment());
// Media -> declaration -> rule.
+ self::assertInstanceOf(DeclarationBlock::class, $mediaRules[0]);
$fooBarRules = $mediaRules[0]->getRules();
$fooBarChildComments = $fooBarRules[0]->getComments();
self::assertCount(1, $fooBarChildComments);
@@ -1159,25 +1086,85 @@ public function commentExtracting(): void
/**
* @test
*/
- public function flatCommentExtracting(): void
+ public function flatCommentExtractingOneComment(): void
{
$parser = new Parser('div {/*Find Me!*/left:10px; text-align:left;}');
- $doc = $parser->parse();
- $contents = $doc->getContents();
+ $document = $parser->parse();
+
+ $contents = $document->getContents();
+ self::assertInstanceOf(DeclarationBlock::class, $contents[0]);
$divRules = $contents[0]->getRules();
$comments = $divRules[0]->getComments();
+
self::assertCount(1, $comments);
self::assertSame('Find Me!', $comments[0]->getComment());
}
+ /**
+ * @test
+ */
+ public function flatCommentExtractingTwoConjoinedCommentsForOneRule(): 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();
+
+ self::assertCount(2, $comments);
+ self::assertSame('Find Me!', $comments[0]->getComment());
+ self::assertSame('Find Me Too!', $comments[1]->getComment());
+ }
+
+ /**
+ * @test
+ */
+ public function flatCommentExtractingTwoSpaceSeparatedCommentsForOneRule(): 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();
+
+ self::assertCount(2, $comments);
+ self::assertSame('Find Me!', $comments[0]->getComment());
+ self::assertSame('Find Me Too!', $comments[1]->getComment());
+ }
+
+ /**
+ * @test
+ */
+ public function flatCommentExtractingCommentsForTwoRules(): 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());
+ }
+
/**
* @test
*/
public function topLevelCommentExtracting(): void
{
$parser = new Parser('/*Find Me!*/div {left:10px; text-align:left;}');
- $doc = $parser->parse();
- $contents = $doc->getContents();
+ $document = $parser->parse();
+ $contents = $document->getContents();
+ self::assertInstanceOf(Commentable::class, $contents[0]);
$comments = $contents[0]->getComments();
self::assertCount(1, $comments);
self::assertSame('Find Me!', $comments[0]->getComment());
@@ -1190,7 +1177,7 @@ public function microsoftFilterStrictParsing(): void
{
$this->expectException(UnexpectedTokenException::class);
- $oDoc = self::parsedStructureForFile('ms-filter', Settings::create()->beStrict());
+ $document = self::parsedStructureForFile('ms-filter', Settings::create()->beStrict());
}
/**
@@ -1198,10 +1185,10 @@ public function microsoftFilterStrictParsing(): void
*/
public function microsoftFilterParsing(): void
{
- $oDoc = self::parsedStructureForFile('ms-filter');
- $sExpected = '.test {filter: progid:DXImageTransform.Microsoft.gradient(startColorstr="#80000000",'
+ $document = self::parsedStructureForFile('ms-filter');
+ $expected = '.test {filter: progid:DXImageTransform.Microsoft.gradient(startColorstr="#80000000",'
. 'endColorstr="#00000000",GradientType=1);}';
- self::assertSame($sExpected, $oDoc->render());
+ self::assertSame($expected, $document->render());
}
/**
@@ -1209,9 +1196,9 @@ public function microsoftFilterParsing(): void
*/
public function largeSizeValuesInFile(): void
{
- $oDoc = self::parsedStructureForFile('large-z-index', Settings::create()->withMultibyteSupport(false));
- $sExpected = '.overlay {z-index: 10000000000000000000000;}';
- self::assertSame($sExpected, $oDoc->render());
+ $document = self::parsedStructureForFile('large-z-index', Settings::create()->withMultibyteSupport(false));
+ $expected = '.overlay {z-index: 10000000000000000000000;}';
+ self::assertSame($expected, $document->render());
}
/**
@@ -1219,14 +1206,14 @@ public function largeSizeValuesInFile(): void
*/
public function scientificNotationSizeValuesInFile(): void
{
- $oDoc = $this->parsedStructureForFile(
+ $document = self::parsedStructureForFile(
'scientific-notation-numbers',
Settings::create()->withMultibyteSupport(false)
);
- $sExpected = ''
+ $expected = ''
. 'body {background-color: rgba(62,174,151,3041820656523200167936);'
. 'z-index: .030418206565232;font-size: 1em;top: 192.3478px;}';
- self::assertSame($sExpected, $oDoc->render());
+ self::assertSame($expected, $document->render());
}
/**
@@ -1234,19 +1221,20 @@ public function scientificNotationSizeValuesInFile(): void
*/
public function lonelyImport(): void
{
- $oDoc = self::parsedStructureForFile('lonely-import');
- $sExpected = '@import url("example.css") only screen and (max-width: 600px);';
- self::assertSame($sExpected, $oDoc->render());
+ $document = self::parsedStructureForFile('lonely-import');
+ $expected = '@import url("example.css") only screen and (max-width: 600px);';
+ self::assertSame($expected, $document->render());
}
public function escapedSpecialCaseTokens(): void
{
- $oDoc = $this->parsedStructureForFile('escaped-tokens');
- $contents = $oDoc->getContents();
+ $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::assertTrue(\is_a($urlRule->getValue(), '\\Sabberworm\\CSS\\Value\\URL'));
- self::assertTrue(\is_a($calcRule->getValue(), '\\Sabberworm\\CSS\\Value\\CalcFunction'));
+ self::assertInstanceOf(URL::class, $urlRule->getValue());
+ self::assertInstanceOf(CalcFunction::class, $calcRule->getValue());
}
}
diff --git a/tests/RuleSet/DeclarationBlockTest.php b/tests/RuleSet/DeclarationBlockTest.php
index e94f1c54e..63411a646 100644
--- a/tests/RuleSet/DeclarationBlockTest.php
+++ b/tests/RuleSet/DeclarationBlockTest.php
@@ -1,10 +1,15 @@
parse();
- foreach ($oDoc->getAllDeclarationBlocks() as $oDeclaration) {
- $oDeclaration->expandBorderShorthand();
- }
- self::assertSame(\trim((string) $oDoc), $sExpected);
- }
+ $css = '.wrapper { left: 10px; text-align: left; }';
+ $parser = new Parser($css);
+ $document = $parser->parse();
+ $rule = new Rule('right');
+ $rule->setValue('-10px');
+ $contents = $document->getContents();
+ $wrapper = $contents[0];
- /**
- * @return array>
- */
- public static function expandBorderShorthandProvider(): array
- {
- return [
- ['body{ border: 2px solid #000 }', 'body {border-width: 2px;border-style: solid;border-color: #000;}'],
- ['body{ border: none }', 'body {border-style: none;}'],
- ['body{ border: 2px }', 'body {border-width: 2px;}'],
- ['body{ border: #f00 }', 'body {border-color: #f00;}'],
- ['body{ border: 1em solid }', 'body {border-width: 1em;border-style: solid;}'],
- ['body{ margin: 1em; }', 'body {margin: 1em;}'],
- ];
- }
-
- /**
- * @dataProvider expandFontShorthandProvider
- *
- * @test
- */
- public function expandFontShorthand(string $sCss, string $sExpected): void
- {
- $oParser = new Parser($sCss);
- $oDoc = $oParser->parse();
- foreach ($oDoc->getAllDeclarationBlocks() as $oDeclaration) {
- $oDeclaration->expandFontShorthand();
- }
- self::assertSame(\trim((string) $oDoc), $sExpected);
- }
+ self::assertInstanceOf(DeclarationBlock::class, $wrapper);
+ self::assertCount(2, $wrapper->getRules());
+ $wrapper->setRules([$rule]);
- /**
- * @return array>
- */
- public static function expandFontShorthandProvider(): array
- {
- return [
- [
- 'body{ margin: 1em; }',
- 'body {margin: 1em;}',
- ],
- [
- 'body {font: 12px serif;}',
- 'body {font-style: normal;font-variant: normal;font-weight: normal;font-size: 12px;'
- . 'line-height: normal;font-family: serif;}',
- ],
- [
- 'body {font: italic 12px serif;}',
- 'body {font-style: italic;font-variant: normal;font-weight: normal;font-size: 12px;'
- . 'line-height: normal;font-family: serif;}',
- ],
- [
- 'body {font: italic bold 12px serif;}',
- 'body {font-style: italic;font-variant: normal;font-weight: bold;font-size: 12px;'
- . 'line-height: normal;font-family: serif;}',
- ],
- [
- 'body {font: italic bold 12px/1.6 serif;}',
- 'body {font-style: italic;font-variant: normal;font-weight: bold;font-size: 12px;'
- . 'line-height: 1.6;font-family: serif;}',
- ],
- [
- 'body {font: italic small-caps bold 12px/1.6 serif;}',
- 'body {font-style: italic;font-variant: small-caps;font-weight: bold;font-size: 12px;'
- . 'line-height: 1.6;font-family: serif;}',
- ],
- ];
+ $rules = $wrapper->getRules();
+ self::assertCount(1, $rules);
+ self::assertSame('right', $rules[0]->getRule());
+ self::assertSame('-10px', $rules[0]->getValue());
}
/**
- * @dataProvider expandBackgroundShorthandProvider
- *
* @test
*/
- public function expandBackgroundShorthand(string $sCss, string $sExpected): void
+ public function ruleInsertion(): void
{
- $oParser = new Parser($sCss);
- $oDoc = $oParser->parse();
- foreach ($oDoc->getAllDeclarationBlocks() as $oDeclaration) {
- $oDeclaration->expandBackgroundShorthand();
- }
- self::assertSame(\trim((string) $oDoc), $sExpected);
- }
+ $css = '.wrapper { left: 10px; text-align: left; }';
+ $parser = new Parser($css);
+ $document = $parser->parse();
+ $contents = $document->getContents();
+ $wrapper = $contents[0];
- /**
- * @return array>
- */
- public static function expandBackgroundShorthandProvider(): array
- {
- return [
- ['body {border: 1px;}', 'body {border: 1px;}'],
- [
- 'body {background: #f00;}',
- 'body {background-color: #f00;background-image: none;background-repeat: repeat;'
- . 'background-attachment: scroll;background-position: 0% 0%;}',
- ],
- [
- 'body {background: #f00 url("foobar.png");}',
- 'body {background-color: #f00;background-image: url("foobar.png");background-repeat: repeat;'
- . 'background-attachment: scroll;background-position: 0% 0%;}',
- ],
- [
- 'body {background: #f00 url("foobar.png") no-repeat;}',
- 'body {background-color: #f00;background-image: url("foobar.png");background-repeat: no-repeat;'
- . 'background-attachment: scroll;background-position: 0% 0%;}',
- ],
- [
- 'body {background: #f00 url("foobar.png") no-repeat center;}',
- 'body {background-color: #f00;background-image: url("foobar.png");background-repeat: no-repeat;'
- . 'background-attachment: scroll;background-position: center center;}',
- ],
- [
- 'body {background: #f00 url("foobar.png") no-repeat top left;}',
- 'body {background-color: #f00;background-image: url("foobar.png");background-repeat: no-repeat;'
- . 'background-attachment: scroll;background-position: top left;}',
- ],
- ];
- }
+ self::assertInstanceOf(DeclarationBlock::class, $wrapper);
- /**
- * @dataProvider expandDimensionsShorthandProvider
- *
- * @test
- */
- public function expandDimensionsShorthand(string $sCss, string $sExpected): void
- {
- $oParser = new Parser($sCss);
- $oDoc = $oParser->parse();
- foreach ($oDoc->getAllDeclarationBlocks() as $oDeclaration) {
- $oDeclaration->expandDimensionsShorthand();
- }
- self::assertSame(\trim((string) $oDoc), $sExpected);
- }
+ $leftRules = $wrapper->getRules('left');
+ self::assertCount(1, $leftRules);
+ $firstLeftRule = $leftRules[0];
- /**
- * @return array>
- */
- public static function expandDimensionsShorthandProvider(): array
- {
- return [
- ['body {border: 1px;}', 'body {border: 1px;}'],
- ['body {margin-top: 1px;}', 'body {margin-top: 1px;}'],
- ['body {margin: 1em;}', 'body {margin-top: 1em;margin-right: 1em;margin-bottom: 1em;margin-left: 1em;}'],
- [
- 'body {margin: 1em 2em;}',
- 'body {margin-top: 1em;margin-right: 2em;margin-bottom: 1em;margin-left: 2em;}',
- ],
- [
- 'body {margin: 1em 2em 3em;}',
- 'body {margin-top: 1em;margin-right: 2em;margin-bottom: 3em;margin-left: 2em;}',
- ],
- ];
- }
+ $textRules = $wrapper->getRules('text-');
+ self::assertCount(1, $textRules);
+ $firstTextRule = $textRules[0];
- /**
- * @dataProvider createBorderShorthandProvider
- *
- * @test
- */
- public function createBorderShorthand(string $sCss, string $sExpected): void
- {
- $oParser = new Parser($sCss);
- $oDoc = $oParser->parse();
- foreach ($oDoc->getAllDeclarationBlocks() as $oDeclaration) {
- $oDeclaration->createBorderShorthand();
- }
- self::assertSame(\trim((string) $oDoc), $sExpected);
- }
+ $leftPrefixRule = new Rule('left');
+ $leftPrefixRule->setValue(new Size(16, 'em'));
- /**
- * @return array>
- */
- public static function createBorderShorthandProvider(): array
- {
- return [
- ['body {border-width: 2px;border-style: solid;border-color: #000;}', 'body {border: 2px solid #000;}'],
- ['body {border-style: none;}', 'body {border: none;}'],
- ['body {border-width: 1em;border-style: solid;}', 'body {border: 1em solid;}'],
- ['body {margin: 1em;}', 'body {margin: 1em;}'],
- ];
- }
+ $textAlignRule = new Rule('text-align');
+ $textAlignRule->setValue(new Size(1));
- /**
- * @dataProvider createFontShorthandProvider
- *
- * @test
- */
- public function createFontShorthand(string $sCss, string $sExpected): void
- {
- $oParser = new Parser($sCss);
- $oDoc = $oParser->parse();
- foreach ($oDoc->getAllDeclarationBlocks() as $oDeclaration) {
- $oDeclaration->createFontShorthand();
- }
- self::assertSame(\trim((string) $oDoc), $sExpected);
- }
+ $borderBottomRule = new Rule('border-bottom-width');
+ $borderBottomRule->setValue(new Size(1, 'px'));
- /**
- * @return array>
- */
- public static function createFontShorthandProvider(): array
- {
- return [
- ['body {font-size: 12px; font-family: serif}', 'body {font: 12px serif;}'],
- ['body {font-size: 12px; font-family: serif; font-style: italic;}', 'body {font: italic 12px serif;}'],
- [
- 'body {font-size: 12px; font-family: serif; font-style: italic; font-weight: bold;}',
- 'body {font: italic bold 12px serif;}',
- ],
- [
- 'body {font-size: 12px; font-family: serif; font-style: italic; font-weight: bold; line-height: 1.6;}',
- 'body {font: italic bold 12px/1.6 serif;}',
- ],
- [
- 'body {font-size: 12px; font-family: serif; font-style: italic; font-weight: bold; '
- . 'line-height: 1.6; font-variant: small-caps;}',
- 'body {font: italic small-caps bold 12px/1.6 serif;}',
- ],
- ['body {margin: 1em;}', 'body {margin: 1em;}'],
- ];
- }
+ $wrapper->addRule($borderBottomRule);
+ $wrapper->addRule($leftPrefixRule, $firstLeftRule);
+ $wrapper->addRule($textAlignRule, $firstTextRule);
- /**
- * @dataProvider createDimensionsShorthandProvider
- *
- * @test
- */
- public function createDimensionsShorthand(string $sCss, string $sExpected): void
- {
- $oParser = new Parser($sCss);
- $oDoc = $oParser->parse();
- foreach ($oDoc->getAllDeclarationBlocks() as $oDeclaration) {
- $oDeclaration->createDimensionsShorthand();
- }
- self::assertSame(\trim((string) $oDoc), $sExpected);
- }
+ $rules = $wrapper->getRules();
- /**
- * @return array>
- */
- public static function createDimensionsShorthandProvider(): array
- {
- return [
- ['body {border: 1px;}', 'body {border: 1px;}'],
- ['body {margin-top: 1px;}', 'body {margin-top: 1px;}'],
- ['body {margin-top: 1em; margin-right: 1em; margin-bottom: 1em; margin-left: 1em;}', 'body {margin: 1em;}'],
- [
- 'body {margin-top: 1em; margin-right: 2em; margin-bottom: 1em; margin-left: 2em;}',
- 'body {margin: 1em 2em;}',
- ],
- [
- 'body {margin-top: 1em; margin-right: 2em; margin-bottom: 3em; margin-left: 2em;}',
- 'body {margin: 1em 2em 3em;}',
- ],
- ];
- }
+ 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]);
- /**
- * @dataProvider createBackgroundShorthandProvider
- *
- * @test
- */
- public function createBackgroundShorthand(string $sCss, string $sExpected): void
- {
- $oParser = new Parser($sCss);
- $oDoc = $oParser->parse();
- foreach ($oDoc->getAllDeclarationBlocks() as $oDeclaration) {
- $oDeclaration->createBackgroundShorthand();
- }
- self::assertSame(\trim((string) $oDoc), $sExpected);
+ self::assertSame(
+ '.wrapper {left: 16em;left: 10px;text-align: 1;text-align: left;border-bottom-width: 1px;}',
+ $document->render()
+ );
}
/**
- * @return array>
+ * @return array
*/
- public static function createBackgroundShorthandProvider(): array
+ public static function declarationBlocksWithCommentsProvider(): array
{
return [
- ['body {border: 1px;}', 'body {border: 1px;}'],
- ['body {background-color: #f00;}', 'body {background: #f00;}'],
- [
- 'body {background-color: #f00;background-image: url(foobar.png);}',
- 'body {background: #f00 url("foobar.png");}',
- ],
- [
- 'body {background-color: #f00;background-image: url(foobar.png);background-repeat: no-repeat;}',
- 'body {background: #f00 url("foobar.png") no-repeat;}',
- ],
- [
- 'body {background-color: #f00;background-image: url(foobar.png);background-repeat: no-repeat;}',
- 'body {background: #f00 url("foobar.png") no-repeat;}',
- ],
- [
- 'body {background-color: #f00;background-image: url(foobar.png);background-repeat: no-repeat;'
- . 'background-position: center;}',
- 'body {background: #f00 url("foobar.png") no-repeat center;}',
- ],
- [
- 'body {background-color: #f00;background-image: url(foobar.png);background-repeat: no-repeat;'
- . 'background-position: top left;}',
- 'body {background: #f00 url("foobar.png") no-repeat top left;}',
- ],
+ 'CSS comments with one asterisk' => ['p {color: #000;/* black */}', 'p {color: #000;}'],
+ 'CSS comments with two asterisks' => ['p {color: #000;/** black */}', 'p {color: #000;}'],
];
}
/**
* @test
+ * @dataProvider declarationBlocksWithCommentsProvider
*/
- public function overrideRules(): void
- {
- $sCss = '.wrapper { left: 10px; text-align: left; }';
- $oParser = new Parser($sCss);
- $oDoc = $oParser->parse();
- $oRule = new Rule('right');
- $oRule->setValue('-10px');
- $aContents = $oDoc->getContents();
- $oWrapper = $aContents[0];
+ public function canRemoveCommentsFromRulesUsingLenientParsing(
+ string $cssWithComments,
+ string $cssWithoutComments
+ ): void {
+ $parserSettings = ParserSettings::create()->withLenientParsing(true);
+ $document = (new Parser($cssWithComments, $parserSettings))->parse();
- self::assertCount(2, $oWrapper->getRules());
- $aContents[0]->setRules([$oRule]);
+ $outputFormat = (new OutputFormat())->setRenderComments(false);
+ $renderedDocument = $document->render($outputFormat);
- $aRules = $oWrapper->getRules();
- self::assertCount(1, $aRules);
- self::assertSame('right', $aRules[0]->getRule());
- self::assertSame('-10px', $aRules[0]->getValue());
+ self::assertSame($cssWithoutComments, $renderedDocument);
}
/**
* @test
+ * @dataProvider declarationBlocksWithCommentsProvider
*/
- public function ruleInsertion(): void
- {
- $sCss = '.wrapper { left: 10px; text-align: left; }';
- $oParser = new Parser($sCss);
- $oDoc = $oParser->parse();
- $aContents = $oDoc->getContents();
- $oWrapper = $aContents[0];
-
- $oFirst = $oWrapper->getRules('left');
- self::assertCount(1, $oFirst);
- $oFirst = $oFirst[0];
-
- $oSecond = $oWrapper->getRules('text-');
- self::assertCount(1, $oSecond);
- $oSecond = $oSecond[0];
+ public function canRemoveCommentsFromRulesUsingStrictParsing(
+ string $cssWithComments,
+ string $cssWithoutComments
+ ): void {
+ $parserSettings = ParserSettings::create()->withLenientParsing(false);
+ $document = (new Parser($cssWithComments, $parserSettings))->parse();
- $oBefore = new Rule('left');
- $oBefore->setValue(new Size(16, 'em'));
+ $outputFormat = (new OutputFormat())->setRenderComments(false);
+ $renderedDocument = $document->render($outputFormat);
- $oMiddle = new Rule('text-align');
- $oMiddle->setValue(new Size(1));
-
- $oAfter = new Rule('border-bottom-width');
- $oAfter->setValue(new Size(1, 'px'));
-
- $oWrapper->addRule($oAfter);
- $oWrapper->addRule($oBefore, $oFirst);
- $oWrapper->addRule($oMiddle, $oSecond);
-
- $aRules = $oWrapper->getRules();
-
- self::assertSame($oBefore, $aRules[0]);
- self::assertSame($oFirst, $aRules[1]);
- self::assertSame($oMiddle, $aRules[2]);
- self::assertSame($oSecond, $aRules[3]);
- self::assertSame($oAfter, $aRules[4]);
-
- self::assertSame(
- '.wrapper {left: 16em;left: 10px;text-align: 1;text-align: left;border-bottom-width: 1px;}',
- $oDoc->render()
- );
- }
-
- /**
- * @test
- *
- * TODO: The order is different on PHP 5.6 than on PHP >= 7.0.
- */
- public function orderOfElementsMatchingOriginalOrderAfterExpandingShorthands(): void
- {
- $sCss = '.rule{padding:5px;padding-top: 20px}';
- $oParser = new Parser($sCss);
- $oDoc = $oParser->parse();
- $aDocs = $oDoc->getAllDeclarationBlocks();
-
- self::assertCount(1, $aDocs);
-
- $oDeclaration = \array_pop($aDocs);
- $oDeclaration->expandShorthands();
-
- self::assertEquals(
- [
- 'padding-top' => 'padding-top: 20px;',
- 'padding-right' => 'padding-right: 5px;',
- 'padding-bottom' => 'padding-bottom: 5px;',
- 'padding-left' => 'padding-left: 5px;',
- ],
- \array_map('strval', $oDeclaration->getRulesAssoc())
- );
+ self::assertSame($cssWithoutComments, $renderedDocument);
}
}
diff --git a/tests/RuleSet/LenientParsingTest.php b/tests/RuleSet/LenientParsingTest.php
index 3d670f74a..ae249701d 100644
--- a/tests/RuleSet/LenientParsingTest.php
+++ b/tests/RuleSet/LenientParsingTest.php
@@ -1,5 +1,7 @@
expectException(UnexpectedTokenException::class);
- $sFile = __DIR__ . '/../fixtures/-fault-tolerance.css';
- $oParser = new Parser(\file_get_contents($sFile), Settings::create()->beStrict());
- $oParser->parse();
+ $pathToFile = __DIR__ . '/../fixtures/-fault-tolerance.css';
+ $parser = new Parser(file_get_contents($pathToFile), Settings::create()->beStrict());
+ $parser->parse();
}
/**
@@ -39,13 +34,13 @@ public function faultToleranceOff(): void
*/
public function faultToleranceOn(): void
{
- $sFile = __DIR__ . '/../fixtures/-fault-tolerance.css';
- $oParser = new Parser(\file_get_contents($sFile), Settings::create()->withLenientParsing(true));
- $oResult = $oParser->parse();
+ $pathToFile = __DIR__ . '/../fixtures/-fault-tolerance.css';
+ $parser = new Parser(file_get_contents($pathToFile), Settings::create()->withLenientParsing(true));
+ $result = $parser->parse();
self::assertSame(
'.test1 {}' . "\n" . '.test2 {hello: 2.2;hello: 2000000000000.2;}' . "\n" . '#test {}' . "\n"
. '#test2 {help: none;}',
- $oResult->render()
+ $result->render()
);
}
@@ -56,9 +51,9 @@ public function endToken(): void
{
$this->expectException(UnexpectedTokenException::class);
- $sFile = __DIR__ . '/../fixtures/-end-token.css';
- $oParser = new Parser(\file_get_contents($sFile), Settings::create()->beStrict());
- $oParser->parse();
+ $pathToFile = __DIR__ . '/../fixtures/-end-token.css';
+ $parser = new Parser(file_get_contents($pathToFile), Settings::create()->beStrict());
+ $parser->parse();
}
/**
@@ -68,9 +63,9 @@ public function endToken2(): void
{
$this->expectException(UnexpectedTokenException::class);
- $sFile = __DIR__ . '/../fixtures/-end-token-2.css';
- $oParser = new Parser(\file_get_contents($sFile), Settings::create()->beStrict());
- $oParser->parse();
+ $pathToFile = __DIR__ . '/../fixtures/-end-token-2.css';
+ $parser = new Parser(file_get_contents($pathToFile), Settings::create()->beStrict());
+ $parser->parse();
}
/**
@@ -78,10 +73,10 @@ public function endToken2(): void
*/
public function endTokenPositive(): void
{
- $sFile = __DIR__ . '/../fixtures/-end-token.css';
- $oParser = new Parser(\file_get_contents($sFile), Settings::create()->withLenientParsing(true));
- $oResult = $oParser->parse();
- self::assertSame('', $oResult->render());
+ $pathToFile = __DIR__ . '/../fixtures/-end-token.css';
+ $parser = new Parser(file_get_contents($pathToFile), Settings::create()->withLenientParsing(true));
+ $result = $parser->parse();
+ self::assertSame('', $result->render());
}
/**
@@ -89,12 +84,12 @@ public function endTokenPositive(): void
*/
public function endToken2Positive(): void
{
- $sFile = __DIR__ . '/../fixtures/-end-token-2.css';
- $oParser = new Parser(\file_get_contents($sFile), Settings::create()->withLenientParsing(true));
- $oResult = $oParser->parse();
+ $pathToFile = __DIR__ . '/../fixtures/-end-token-2.css';
+ $parser = new Parser(file_get_contents($pathToFile), Settings::create()->withLenientParsing(true));
+ $result = $parser->parse();
self::assertSame(
'#home .bg-layout {background-image: url("/bundles/main/img/bg1.png?5");}',
- $oResult->render()
+ $result->render()
);
}
@@ -104,13 +99,13 @@ public function endToken2Positive(): void
public function localeTrap(): void
{
\setlocale(LC_ALL, 'pt_PT', 'no');
- $sFile = __DIR__ . '/../fixtures/-fault-tolerance.css';
- $oParser = new Parser(\file_get_contents($sFile), Settings::create()->withLenientParsing(true));
- $oResult = $oParser->parse();
+ $pathToFile = __DIR__ . '/../fixtures/-fault-tolerance.css';
+ $parser = new Parser(file_get_contents($pathToFile), Settings::create()->withLenientParsing(true));
+ $result = $parser->parse();
self::assertSame(
'.test1 {}' . "\n" . '.test2 {hello: 2.2;hello: 2000000000000.2;}' . "\n" . '#test {}' . "\n"
. '#test2 {help: none;}',
- $oResult->render()
+ $result->render()
);
}
@@ -119,9 +114,9 @@ public function localeTrap(): void
*/
public function caseInsensitivity(): void
{
- $sFile = __DIR__ . '/../fixtures/case-insensitivity.css';
- $oParser = new Parser(\file_get_contents($sFile));
- $oResult = $oParser->parse();
+ $pathToFile = __DIR__ . '/../fixtures/case-insensitivity.css';
+ $parser = new Parser(file_get_contents($pathToFile));
+ $result = $parser->parse();
self::assertSame(
'@charset "utf-8";' . "\n"
@@ -129,7 +124,7 @@ public function caseInsensitivity(): void
. "\n@media screen {}"
. "\n#myid {case: insensitive !important;frequency: 30Hz;font-size: 1em;color: #ff0;"
. 'color: hsl(40,40%,30%);font-family: Arial;}',
- $oResult->render()
+ $result->render()
);
}
@@ -138,9 +133,9 @@ public function caseInsensitivity(): void
*/
public function cssWithInvalidColorStillGetsParsedAsDocument(): void
{
- $sFile = __DIR__ . '/../fixtures/invalid-color.css';
- $oParser = new Parser(\file_get_contents($sFile), Settings::create()->withLenientParsing(true));
- $result = $oParser->parse();
+ $pathToFile = __DIR__ . '/../fixtures/invalid-color.css';
+ $parser = new Parser(file_get_contents($pathToFile), Settings::create()->withLenientParsing(true));
+ $result = $parser->parse();
self::assertInstanceOf(Document::class, $result);
}
@@ -152,8 +147,8 @@ public function invalidColorStrict(): void
{
$this->expectException(UnexpectedTokenException::class);
- $sFile = __DIR__ . '/../fixtures/invalid-color.css';
- $oParser = new Parser(\file_get_contents($sFile), Settings::create()->beStrict());
- $oParser->parse();
+ $pathToFile = __DIR__ . '/../fixtures/invalid-color.css';
+ $parser = new Parser(file_get_contents($pathToFile), Settings::create()->beStrict());
+ $parser->parse();
}
}
diff --git a/tests/Unit/CSSList/AtRuleBlockListTest.php b/tests/Unit/CSSList/AtRuleBlockListTest.php
new file mode 100644
index 000000000..3b61e437e
--- /dev/null
+++ b/tests/Unit/CSSList/AtRuleBlockListTest.php
@@ -0,0 +1,147 @@
+atRuleName());
+ }
+
+ /**
+ * @test
+ */
+ public function atRuleArgsByDefaultReturnsEmptyString(): void
+ {
+ $subject = new AtRuleBlockList('supports');
+
+ self::assertSame('', $subject->atRuleArgs());
+ }
+
+ /**
+ * @test
+ */
+ public function atRuleArgsReturnsArgumentsProvidedToConstructor(): void
+ {
+ $arguments = 'bar';
+
+ $subject = new AtRuleBlockList('', $arguments);
+
+ self::assertSame($arguments, $subject->atRuleArgs());
+ }
+
+ /**
+ * @test
+ */
+ public function getLineNumberByDefaultReturnsNull(): void
+ {
+ $subject = new AtRuleBlockList('');
+
+ self::assertNull($subject->getLineNumber());
+ }
+
+ /**
+ * @test
+ */
+ public function getLineNumberReturnsLineNumberProvidedToConstructor(): void
+ {
+ $lineNumber = 42;
+ $subject = new AtRuleBlockList('', '', $lineNumber);
+
+ self::assertSame($lineNumber, $subject->getLineNumber());
+ }
+
+ /**
+ * @test
+ */
+ public function isRootListAlwaysReturnsFalse(): void
+ {
+ $subject = new AtRuleBlockList('supports');
+
+ self::assertFalse($subject->isRootList());
+ }
+}
diff --git a/tests/Unit/CSSList/CSSBlockListTest.php b/tests/Unit/CSSList/CSSBlockListTest.php
new file mode 100644
index 000000000..ce7e54477
--- /dev/null
+++ b/tests/Unit/CSSList/CSSBlockListTest.php
@@ -0,0 +1,474 @@
+getAllDeclarationBlocks());
+ }
+
+ /**
+ * @test
+ */
+ public function getAllDeclarationBlocksReturnsOneDeclarationBlockDirectlySetAsContent(): void
+ {
+ $subject = new ConcreteCSSBlockList();
+
+ $declarationBlock = new DeclarationBlock();
+ $subject->setContents([$declarationBlock]);
+
+ $result = $subject->getAllDeclarationBlocks();
+
+ self::assertSame([$declarationBlock], $result);
+ }
+
+ /**
+ * @test
+ */
+ public function getAllDeclarationBlocksReturnsMultipleDeclarationBlocksDirectlySetAsContents(): void
+ {
+ $subject = new ConcreteCSSBlockList();
+
+ $declarationBlock1 = new DeclarationBlock();
+ $declarationBlock2 = new DeclarationBlock();
+ $subject->setContents([$declarationBlock1, $declarationBlock2]);
+
+ $result = $subject->getAllDeclarationBlocks();
+
+ self::assertSame([$declarationBlock1, $declarationBlock2], $result);
+ }
+
+ /**
+ * @test
+ */
+ public function getAllDeclarationBlocksReturnsDeclarationBlocksWithinAtRuleBlockList(): void
+ {
+ $subject = new ConcreteCSSBlockList();
+
+ $declarationBlock = new DeclarationBlock();
+ $atRuleBlockList = new AtRuleBlockList('media');
+ $atRuleBlockList->setContents([$declarationBlock]);
+ $subject->setContents([$atRuleBlockList]);
+
+ $result = $subject->getAllDeclarationBlocks();
+
+ self::assertSame([$declarationBlock], $result);
+ }
+
+ /**
+ * @test
+ */
+ public function getAllDeclarationBlocksIgnoresImport(): void
+ {
+ $subject = new ConcreteCSSBlockList();
+
+ $import = new Import(new URL(new CSSString('https://www.example.com/')), '');
+ $subject->setContents([$import]);
+
+ $result = $subject->getAllDeclarationBlocks();
+
+ self::assertSame([], $result);
+ }
+
+ /**
+ * @test
+ */
+ public function getAllDeclarationBlocksIgnoresCharset(): void
+ {
+ $subject = new ConcreteCSSBlockList();
+
+ $charset = new Charset(new CSSString('UTF-8'));
+ $subject->setContents([$charset]);
+
+ $result = $subject->getAllDeclarationBlocks();
+
+ self::assertSame([], $result);
+ }
+
+ /**
+ * @test
+ */
+ public function getAllRuleSetsWhenNoContentSetReturnsEmptyArray(): void
+ {
+ $subject = new ConcreteCSSBlockList();
+
+ self::assertSame([], $subject->getAllRuleSets());
+ }
+
+ /**
+ * @test
+ */
+ public function getAllRuleSetsReturnsRuleSetFromOneDeclarationBlockDirectlySetAsContent(): void
+ {
+ $subject = new ConcreteCSSBlockList();
+
+ $declarationBlock = new DeclarationBlock();
+ $subject->setContents([$declarationBlock]);
+
+ $result = $subject->getAllRuleSets();
+
+ self::assertSame([$declarationBlock->getRuleSet()], $result);
+ }
+
+ /**
+ * @test
+ */
+ public function getAllRuleSetsReturnsOneAtRuleSetDirectlySetAsContent(): void
+ {
+ $subject = new ConcreteCSSBlockList();
+
+ $atRuleSet = new AtRuleSet('media');
+ $subject->setContents([$atRuleSet]);
+
+ $result = $subject->getAllRuleSets();
+
+ self::assertSame([$atRuleSet], $result);
+ }
+
+ /**
+ * @test
+ */
+ public function getAllRuleSetsReturnsRuleSetsFromMultipleDeclarationBlocksDirectlySetAsContents(): void
+ {
+ $subject = new ConcreteCSSBlockList();
+
+ $declarationBlock1 = new DeclarationBlock();
+ $declarationBlock2 = new DeclarationBlock();
+ $subject->setContents([$declarationBlock1, $declarationBlock2]);
+
+ $result = $subject->getAllRuleSets();
+
+ self::assertSame([$declarationBlock1->getRuleSet(), $declarationBlock2->getRuleSet()], $result);
+ }
+
+ /**
+ * @test
+ */
+ public function getAllRuleSetsReturnsMultipleAtRuleSetsDirectlySetAsContents(): void
+ {
+ $subject = new ConcreteCSSBlockList();
+
+ $atRuleSet1 = new AtRuleSet('media');
+ $atRuleSet2 = new AtRuleSet('media');
+ $subject->setContents([$atRuleSet1, $atRuleSet2]);
+
+ $result = $subject->getAllRuleSets();
+
+ self::assertSame([$atRuleSet1, $atRuleSet2], $result);
+ }
+
+ /**
+ * @test
+ */
+ public function getAllRuleSetsReturnsRuleSetsFromDeclarationBlocksWithinAtRuleBlockList(): void
+ {
+ $subject = new ConcreteCSSBlockList();
+
+ $declarationBlock = new DeclarationBlock();
+ $atRuleBlockList = new AtRuleBlockList('media');
+ $atRuleBlockList->setContents([$declarationBlock]);
+ $subject->setContents([$atRuleBlockList]);
+
+ $result = $subject->getAllRuleSets();
+
+ self::assertSame([$declarationBlock->getRuleSet()], $result);
+ }
+
+ /**
+ * @test
+ */
+ public function getAllRuleSetsReturnsAtRuleSetsWithinAtRuleBlockList(): void
+ {
+ $subject = new ConcreteCSSBlockList();
+
+ $atRule = new AtRuleSet('media');
+ $atRuleBlockList = new AtRuleBlockList('media');
+ $atRuleBlockList->setContents([$atRule]);
+ $subject->setContents([$atRuleBlockList]);
+
+ $result = $subject->getAllRuleSets();
+
+ self::assertSame([$atRule], $result);
+ }
+
+ /**
+ * @test
+ */
+ public function getAllRuleSetsIgnoresImport(): void
+ {
+ $subject = new ConcreteCSSBlockList();
+
+ $import = new Import(new URL(new CSSString('https://www.example.com/')), '');
+ $subject->setContents([$import]);
+
+ $result = $subject->getAllRuleSets();
+
+ self::assertSame([], $result);
+ }
+
+ /**
+ * @test
+ */
+ public function getAllRuleSetsIgnoresCharset(): void
+ {
+ $subject = new ConcreteCSSBlockList();
+
+ $charset = new Charset(new CSSString('UTF-8'));
+ $subject->setContents([$charset]);
+
+ $result = $subject->getAllRuleSets();
+
+ self::assertSame([], $result);
+ }
+
+ /**
+ * @test
+ */
+ public function getAllValuesWhenNoContentSetReturnsEmptyArray(): void
+ {
+ $subject = new ConcreteCSSBlockList();
+
+ self::assertSame([], $subject->getAllValues());
+ }
+
+ /**
+ * @test
+ */
+ public function getAllValuesReturnsOneValueDirectlySetAsContent(): void
+ {
+ $subject = new ConcreteCSSBlockList();
+
+ $value = new CSSString('Superfont');
+
+ $declarationBlock = new DeclarationBlock();
+ $rule = new Rule('font-family');
+ $rule->setValue($value);
+ $declarationBlock->addRule($rule);
+ $subject->setContents([$declarationBlock]);
+
+ $result = $subject->getAllValues();
+
+ self::assertSame([$value], $result);
+ }
+
+ /**
+ * @test
+ */
+ public function getAllValuesReturnsMultipleValuesDirectlySetAsContentInOneDeclarationBlock(): void
+ {
+ $subject = new ConcreteCSSBlockList();
+
+ $value1 = new CSSString('Superfont');
+ $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);
+ $subject->setContents([$declarationBlock]);
+
+ $result = $subject->getAllValues();
+
+ self::assertSame([$value1, $value2], $result);
+ }
+
+ /**
+ * @test
+ */
+ public function getAllValuesReturnsMultipleValuesDirectlySetAsContentInMultipleDeclarationBlocks(): void
+ {
+ $subject = new ConcreteCSSBlockList();
+
+ $value1 = new CSSString('Superfont');
+ $value2 = new CSSString('aquamarine');
+
+ $declarationBlock1 = new DeclarationBlock();
+ $rule1 = new Rule('font-family');
+ $rule1->setValue($value1);
+ $declarationBlock1->addRule($rule1);
+ $declarationBlock2 = new DeclarationBlock();
+ $rule2 = new Rule('color');
+ $rule2->setValue($value2);
+ $declarationBlock2->addRule($rule2);
+ $subject->setContents([$declarationBlock1, $declarationBlock2]);
+
+ $result = $subject->getAllValues();
+
+ self::assertSame([$value1, $value2], $result);
+ }
+
+ /**
+ * @test
+ */
+ public function getAllValuesReturnsValuesWithinAtRuleBlockList(): void
+ {
+ $subject = new ConcreteCSSBlockList();
+
+ $value = new CSSString('Superfont');
+
+ $declarationBlock = new DeclarationBlock();
+ $rule = new Rule('font-family');
+ $rule->setValue($value);
+ $declarationBlock->addRule($rule);
+ $atRuleBlockList = new AtRuleBlockList('media');
+ $atRuleBlockList->setContents([$declarationBlock]);
+ $subject->setContents([$atRuleBlockList]);
+
+ $result = $subject->getAllValues();
+
+ self::assertSame([$value], $result);
+ }
+
+ /**
+ * @test
+ */
+ public function getAllValuesWithElementProvidedReturnsOnlyValuesWithinThatElement(): void
+ {
+ $subject = new ConcreteCSSBlockList();
+
+ $value1 = new CSSString('Superfont');
+ $value2 = new CSSString('aquamarine');
+
+ $declarationBlock1 = new DeclarationBlock();
+ $rule1 = new Rule('font-family');
+ $rule1->setValue($value1);
+ $declarationBlock1->addRule($rule1);
+ $declarationBlock2 = new DeclarationBlock();
+ $rule2 = new Rule('color');
+ $rule2->setValue($value2);
+ $declarationBlock2->addRule($rule2);
+ $subject->setContents([$declarationBlock1, $declarationBlock2]);
+
+ $result = $subject->getAllValues($declarationBlock1);
+
+ self::assertSame([$value1], $result);
+ }
+
+ /**
+ * @test
+ */
+ public function getAllValuesWithSearchStringProvidedReturnsOnlyValuesFromMatchingRules(): void
+ {
+ $subject = new ConcreteCSSBlockList();
+
+ $value1 = new CSSString('Superfont');
+ $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);
+ $subject->setContents([$declarationBlock]);
+
+ $result = $subject->getAllValues(null, 'font-');
+
+ self::assertSame([$value1], $result);
+ }
+
+ /**
+ * @test
+ */
+ public function getAllValuesByDefaultDoesNotReturnValuesInFunctionArguments(): void
+ {
+ $subject = new ConcreteCSSBlockList();
+
+ $value1 = new Size(10, 'px');
+ $value2 = new Size(2, '%');
+
+ $declarationBlock = new DeclarationBlock();
+ $rule = new Rule('margin');
+ $rule->setValue(new CSSFunction('max', [$value1, $value2]));
+ $declarationBlock->addRule($rule);
+ $subject->setContents([$declarationBlock]);
+
+ $result = $subject->getAllValues();
+
+ self::assertSame([], $result);
+ }
+
+ /**
+ * @test
+ */
+ public function getAllValuesWithSearchInFunctionArgumentsReturnsValuesInFunctionArguments(): void
+ {
+ $subject = new ConcreteCSSBlockList();
+
+ $value1 = new Size(10, 'px');
+ $value2 = new Size(2, '%');
+
+ $declarationBlock = new DeclarationBlock();
+ $rule = new Rule('margin');
+ $rule->setValue(new CSSFunction('max', [$value1, $value2]));
+ $declarationBlock->addRule($rule);
+ $subject->setContents([$declarationBlock]);
+
+ $result = $subject->getAllValues(null, null, true);
+
+ self::assertSame([$value1, $value2], $result);
+ }
+}
diff --git a/tests/Unit/CSSList/CSSListTest.php b/tests/Unit/CSSList/CSSListTest.php
new file mode 100644
index 000000000..ada176e9a
--- /dev/null
+++ b/tests/Unit/CSSList/CSSListTest.php
@@ -0,0 +1,329 @@
+getLineNumber());
+ }
+
+ /**
+ * @test
+ */
+ public function getLineNumberReturnsLineNumberProvidedToConstructor(): void
+ {
+ $lineNumber = 42;
+ $subject = new ConcreteCSSList($lineNumber);
+
+ self::assertSame($lineNumber, $subject->getLineNumber());
+ }
+
+ /**
+ * @test
+ */
+ public function getContentsInitiallyReturnsEmptyArray(): void
+ {
+ $subject = new ConcreteCSSList();
+
+ self::assertSame([], $subject->getContents());
+ }
+
+ /**
+ * @return array}>
+ */
+ public static function contentsDataProvider(): array
+ {
+ return [
+ 'empty array' => [[]],
+ '1 item' => [[new DeclarationBlock()]],
+ '2 items' => [[new DeclarationBlock(), new DeclarationBlock()]],
+ ];
+ }
+
+ /**
+ * @test
+ *
+ * @param list $contents
+ *
+ * @dataProvider contentsDataProvider
+ */
+ public function setContentsSetsContents(array $contents): void
+ {
+ $subject = new ConcreteCSSList();
+
+ $subject->setContents($contents);
+
+ self::assertSame($contents, $subject->getContents());
+ }
+
+ /**
+ * @test
+ */
+ public function setContentsReplacesContentsSetInPreviousCall(): void
+ {
+ $subject = new ConcreteCSSList();
+
+ $contents2 = [new DeclarationBlock()];
+
+ $subject->setContents([new DeclarationBlock()]);
+ $subject->setContents($contents2);
+
+ self::assertSame($contents2, $subject->getContents());
+ }
+
+ /**
+ * @test
+ */
+ public function insertBeforeInsertsContentBeforeSibling(): void
+ {
+ $subject = new ConcreteCSSList();
+
+ $bogusOne = new DeclarationBlock();
+ $bogusOne->setSelectors('.bogus-one');
+ $bogusTwo = new DeclarationBlock();
+ $bogusTwo->setSelectors('.bogus-two');
+
+ $item = new DeclarationBlock();
+ $item->setSelectors('.item');
+
+ $sibling = new DeclarationBlock();
+ $sibling->setSelectors('.sibling');
+
+ $subject->setContents([$bogusOne, $sibling, $bogusTwo]);
+
+ self::assertCount(3, $subject->getContents());
+
+ $subject->insertBefore($item, $sibling);
+
+ self::assertCount(4, $subject->getContents());
+ self::assertSame([$bogusOne, $item, $sibling, $bogusTwo], $subject->getContents());
+ }
+
+ /**
+ * @test
+ */
+ public function insertBeforeAppendsIfSiblingNotFound(): void
+ {
+ $subject = new ConcreteCSSList();
+
+ $bogusOne = new DeclarationBlock();
+ $bogusOne->setSelectors('.bogus-one');
+ $bogusTwo = new DeclarationBlock();
+ $bogusTwo->setSelectors('.bogus-two');
+
+ $item = new DeclarationBlock();
+ $item->setSelectors('.item');
+
+ $sibling = new DeclarationBlock();
+ $sibling->setSelectors('.sibling');
+
+ $orphan = new DeclarationBlock();
+ $orphan->setSelectors('.forever-alone');
+
+ $subject->setContents([$bogusOne, $sibling, $bogusTwo]);
+
+ self::assertCount(3, $subject->getContents());
+
+ $subject->insertBefore($item, $orphan);
+
+ self::assertCount(4, $subject->getContents());
+ self::assertSame([$bogusOne, $sibling, $bogusTwo, $item], $subject->getContents());
+ }
+
+ /**
+ * @test
+ */
+ public function removeDeclarationBlockBySelectorRemovesDeclarationBlockProvided(): 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($declarationBlock);
+
+ self::assertSame([], $subject->getContents());
+ }
+
+ /**
+ * @test
+ */
+ public function removeDeclarationBlockBySelectorRemovesDeclarationBlockWithSelectorsProvidedFromItself(): 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($declarationBlock->getSelectors());
+
+ self::assertSame([], $subject->getContents());
+ }
+
+ /**
+ * @test
+ */
+ public function removeDeclarationBlockBySelectorRemovesDeclarationBlockWithOutsourcedSelectorsProvided(): 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('html'), new Selector('body')]);
+
+ self::assertSame([], $subject->getContents());
+ }
+
+ /**
+ * @test
+ */
+ public function removeDeclarationBlockBySelectorRemovesDeclarationBlockWithStringSelectorsProvided(): 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(['html', 'body']);
+
+ self::assertSame([], $subject->getContents());
+ }
+
+ /**
+ * @test
+ */
+ public function removeDeclarationBlockBySelectorRemovesDeclarationBlockProvidedAndAnotherWithSameSelectors(): void
+ {
+ $subject = new ConcreteCSSList();
+ $declarationBlock1 = new DeclarationBlock();
+ $declarationBlock1->setSelectors(['html', 'body']);
+ $declarationBlock2 = new DeclarationBlock();
+ $declarationBlock2->setSelectors(['html', 'body']);
+ $subject->setContents([$declarationBlock1, $declarationBlock2]);
+ self::assertNotSame([], $subject->getContents()); // make sure contents are set
+
+ $subject->removeDeclarationBlockBySelector($declarationBlock1, true);
+
+ self::assertSame([], $subject->getContents());
+ }
+
+ /**
+ * @test
+ */
+ public function removeDeclarationBlockBySelectorRemovesBlockWithSelectorsFromItselfAndAnotherMatching(): void
+ {
+ $subject = new ConcreteCSSList();
+ $declarationBlock1 = new DeclarationBlock();
+ $declarationBlock1->setSelectors(['html', 'body']);
+ $declarationBlock2 = new DeclarationBlock();
+ $declarationBlock2->setSelectors(['html', 'body']);
+ $subject->setContents([$declarationBlock1, $declarationBlock2]);
+ self::assertNotSame([], $subject->getContents()); // make sure contents are set
+
+ $subject->removeDeclarationBlockBySelector($declarationBlock1->getSelectors(), true);
+
+ self::assertSame([], $subject->getContents());
+ }
+
+ /**
+ * @test
+ */
+ public function removeDeclarationBlockBySelectorRemovesMultipleBlocksWithOutsourcedSelectors(): void
+ {
+ $subject = new ConcreteCSSList();
+ $declarationBlock1 = new DeclarationBlock();
+ $declarationBlock1->setSelectors(['html', 'body']);
+ $declarationBlock2 = new DeclarationBlock();
+ $declarationBlock2->setSelectors(['html', 'body']);
+ $subject->setContents([$declarationBlock1, $declarationBlock2]);
+ self::assertNotSame([], $subject->getContents()); // make sure contents are set
+
+ $subject->removeDeclarationBlockBySelector([new Selector('html'), new Selector('body')], true);
+
+ self::assertSame([], $subject->getContents());
+ }
+
+ /**
+ * @test
+ */
+ public function removeDeclarationBlockBySelectorRemovesMultipleBlocksWithStringSelectorsProvided(): void
+ {
+ $subject = new ConcreteCSSList();
+ $declarationBlock1 = new DeclarationBlock();
+ $declarationBlock1->setSelectors(['html', 'body']);
+ $declarationBlock2 = new DeclarationBlock();
+ $declarationBlock2->setSelectors(['html', 'body']);
+ $subject->setContents([$declarationBlock1, $declarationBlock2]);
+ self::assertNotSame([], $subject->getContents()); // make sure contents are set
+
+ $subject->removeDeclarationBlockBySelector(['html', 'body'], true);
+
+ self::assertSame([], $subject->getContents());
+ }
+}
diff --git a/tests/Unit/CSSList/DocumentTest.php b/tests/Unit/CSSList/DocumentTest.php
new file mode 100644
index 000000000..77e8aa81c
--- /dev/null
+++ b/tests/Unit/CSSList/DocumentTest.php
@@ -0,0 +1,66 @@
+isRootList());
+ }
+}
diff --git a/tests/Unit/CSSList/Fixtures/ConcreteCSSBlockList.php b/tests/Unit/CSSList/Fixtures/ConcreteCSSBlockList.php
new file mode 100644
index 000000000..956c7036a
--- /dev/null
+++ b/tests/Unit/CSSList/Fixtures/ConcreteCSSBlockList.php
@@ -0,0 +1,21 @@
+getLineNumber());
+ }
+
+ /**
+ * @test
+ */
+ public function getLineNumberReturnsLineNumberProvidedToConstructor(): void
+ {
+ $lineNumber = 42;
+ $subject = new KeyFrame($lineNumber);
+
+ self::assertSame($lineNumber, $subject->getLineNumber());
+ }
+
+ /**
+ * @test
+ */
+ public function getAnimationNameByDefaultReturnsNone(): void
+ {
+ $subject = new KeyFrame();
+
+ self::assertSame('none', $subject->getAnimationName());
+ }
+
+ /**
+ * @test
+ */
+ public function getVendorKeyFrameByDefaultReturnsKeyframes(): void
+ {
+ $subject = new KeyFrame();
+
+ self::assertSame('keyframes', $subject->getVendorKeyFrame());
+ }
+}
diff --git a/tests/Unit/Comment/CommentContainerTest.php b/tests/Unit/Comment/CommentContainerTest.php
new file mode 100644
index 000000000..d0e844a45
--- /dev/null
+++ b/tests/Unit/Comment/CommentContainerTest.php
@@ -0,0 +1,236 @@
+subject = new ConcreteCommentContainer();
+ }
+
+ /**
+ * @test
+ */
+ public function getCommentsInitiallyReturnsEmptyArray(): void
+ {
+ self::assertSame([], $this->subject->getComments());
+ }
+
+ /**
+ * @return array}>
+ */
+ public function provideCommentArray(): array
+ {
+ return [
+ 'no comment' => [[]],
+ 'one comment' => [[new Comment('Is this really a spoon?')]],
+ 'two comments' => [
+ [
+ new Comment('I’m a teapot.'),
+ new Comment('I’m a cafetière.'),
+ ],
+ ],
+ ];
+ }
+
+ /**
+ * @test
+ *
+ * @param list $comments
+ *
+ * @dataProvider provideCommentArray
+ */
+ public function addCommentsOnVirginContainerAddsCommentsProvided(array $comments): void
+ {
+ $this->subject->addComments($comments);
+
+ self::assertSame($comments, $this->subject->getComments());
+ }
+
+ /**
+ * @test
+ *
+ * @param list $comments
+ *
+ * @dataProvider provideCommentArray
+ */
+ public function addCommentsWithEmptyArrayKeepsOriginalCommentsUnchanged(array $comments): void
+ {
+ $this->subject->setComments($comments);
+
+ $this->subject->addComments([]);
+
+ self::assertSame($comments, $this->subject->getComments());
+ }
+
+ /**
+ * @return array}>
+ */
+ public function provideAlternativeCommentArray(): array
+ {
+ return [
+ 'no comment' => [[]],
+ 'one comment' => [[new Comment('Can I eat it with my hands?')]],
+ 'two comments' => [
+ [
+ new Comment('I’m a beer barrel.'),
+ new Comment('I’m a vineyard.'),
+ ],
+ ],
+ ];
+ }
+
+ /**
+ * @return array}>
+ */
+ public function provideAlternativeNonemptyCommentArray(): array
+ {
+ $data = $this->provideAlternativeCommentArray();
+
+ unset($data['no comment']);
+
+ return $data;
+ }
+
+ /**
+ * This provider crosses two comment arrays (0, 1 or 2 comments) with different comments,
+ * so that all combinations can be tested.
+ *
+ * @return DataProvider, 1: list}>
+ */
+ public function provideTwoDistinctCommentArrays(): DataProvider
+ {
+ return DataProvider::cross($this->provideCommentArray(), $this->provideAlternativeCommentArray());
+ }
+
+ /**
+ * @return DataProvider, 1: non-empty-list}>
+ */
+ public function provideTwoDistinctCommentArraysWithSecondNonempty(): DataProvider
+ {
+ return DataProvider::cross($this->provideCommentArray(), $this->provideAlternativeNonemptyCommentArray());
+ }
+
+ private static function createContainsConstraint(Comment $comment): TraversableContains
+ {
+ return new TraversableContains($comment);
+ }
+
+ /**
+ * @param non-empty-list $comments
+ *
+ * @return non-empty-list
+ */
+ private static function createContainsConstraints(array $comments): array
+ {
+ return \array_map([self::class, 'createContainsConstraint'], $comments);
+ }
+
+ /**
+ * @test
+ *
+ * @param list $commentsToAdd
+ * @param non-empty-list $originalComments
+ *
+ * @dataProvider provideTwoDistinctCommentArraysWithSecondNonempty
+ */
+ public function addCommentsKeepsOriginalComments(array $commentsToAdd, array $originalComments): void
+ {
+ $this->subject->setComments($originalComments);
+
+ $this->subject->addComments($commentsToAdd);
+
+ self::assertThat(
+ $this->subject->getComments(),
+ LogicalAnd::fromConstraints(...self::createContainsConstraints($originalComments))
+ );
+ }
+
+ /**
+ * @test
+ *
+ * @param list $originalComments
+ * @param non-empty-list $commentsToAdd
+ *
+ * @dataProvider provideTwoDistinctCommentArraysWithSecondNonempty
+ */
+ public function addCommentsAfterCommentsSetAddsCommentsProvided(array $originalComments, array $commentsToAdd): void
+ {
+ $this->subject->setComments($originalComments);
+
+ $this->subject->addComments($commentsToAdd);
+
+ self::assertThat(
+ $this->subject->getComments(),
+ LogicalAnd::fromConstraints(...self::createContainsConstraints($commentsToAdd))
+ );
+ }
+
+ /**
+ * @test
+ *
+ * @param non-empty-list $comments
+ *
+ * @dataProvider provideAlternativeNonemptyCommentArray
+ */
+ public function addCommentsAppends(array $comments): void
+ {
+ $firstComment = new Comment('I must be first!');
+ $this->subject->setComments([$firstComment]);
+
+ $this->subject->addComments($comments);
+
+ $result = $this->subject->getComments();
+ self::assertNotEmpty($result);
+ self::assertSame($firstComment, $result[0]);
+ }
+
+ /**
+ * @test
+ *
+ * @param list $comments
+ *
+ * @dataProvider provideCommentArray
+ */
+ public function setCommentsOnVirginContainerSetsCommentsProvided(array $comments): void
+ {
+ $this->subject->setComments($comments);
+
+ self::assertSame($comments, $this->subject->getComments());
+ }
+
+ /**
+ * @test
+ *
+ * @param list $originalComments
+ * @param list $commentsToSet
+ *
+ * @dataProvider provideTwoDistinctCommentArrays
+ */
+ public function setCommentsReplacesWithCommentsProvided(array $originalComments, array $commentsToSet): void
+ {
+ $this->subject->setComments($originalComments);
+
+ $this->subject->setComments($commentsToSet);
+
+ self::assertSame($commentsToSet, $this->subject->getComments());
+ }
+}
diff --git a/tests/Unit/Comment/CommentTest.php b/tests/Unit/Comment/CommentTest.php
new file mode 100644
index 000000000..69572e903
--- /dev/null
+++ b/tests/Unit/Comment/CommentTest.php
@@ -0,0 +1,80 @@
+getComment());
+ }
+
+ /**
+ * @test
+ */
+ public function getCommentInitiallyReturnsCommentPassedToConstructor(): void
+ {
+ $comment = 'There is no spoon.';
+ $subject = new Comment($comment);
+
+ self::assertSame($comment, $subject->getComment());
+ }
+
+ /**
+ * @test
+ */
+ public function setCommentSetsComments(): void
+ {
+ $comment = 'There is no spoon.';
+ $subject = new Comment();
+
+ $subject->setComment($comment);
+
+ self::assertSame($comment, $subject->getComment());
+ }
+
+ /**
+ * @test
+ */
+ public function getLineNumberByDefaultReturnsNull(): void
+ {
+ $subject = new Comment();
+
+ self::assertNull($subject->getLineNumber());
+ }
+
+ /**
+ * @test
+ */
+ public function getLineNumberReturnsLineNumberProvidedToConstructor(): void
+ {
+ $lineNumber = 42;
+ $subject = new Comment('', $lineNumber);
+
+ self::assertSame($lineNumber, $subject->getLineNumber());
+ }
+}
diff --git a/tests/Unit/Comment/Fixtures/ConcreteCommentContainer.php b/tests/Unit/Comment/Fixtures/ConcreteCommentContainer.php
new file mode 100644
index 000000000..39f6ec37f
--- /dev/null
+++ b/tests/Unit/Comment/Fixtures/ConcreteCommentContainer.php
@@ -0,0 +1,13 @@
+subject = new OutputFormat();
+ }
+
+ /**
+ * @test
+ */
+ public function getStringQuotingTypeInitiallyReturnsDoubleQuote(): void
+ {
+ self::assertSame('"', $this->subject->getStringQuotingType());
+ }
+
+ /**
+ * @test
+ */
+ public function setStringQuotingTypeSetsStringQuotingType(): void
+ {
+ $value = "'";
+ $this->subject->setStringQuotingType($value);
+
+ self::assertSame($value, $this->subject->getStringQuotingType());
+ }
+
+ /**
+ * @test
+ */
+ public function setStringQuotingTypeProvidesFluentInterface(): void
+ {
+ self::assertSame($this->subject, $this->subject->setStringQuotingType('"'));
+ }
+
+ /**
+ * @test
+ */
+ public function usesRgbHashNotationInitiallyReturnsTrue(): void
+ {
+ self::assertTrue($this->subject->usesRgbHashNotation());
+ }
+
+ /**
+ * @return array
+ */
+ public static function provideBooleans(): array
+ {
+ return [
+ 'true' => [true],
+ 'false' => [false],
+ ];
+ }
+
+ /**
+ * @test
+ *
+ * @dataProvider provideBooleans
+ */
+ public function setRGBHashNotationSetsRGBHashNotation(bool $value): void
+ {
+ $this->subject->setRGBHashNotation($value);
+
+ self::assertSame($value, $this->subject->usesRgbHashNotation());
+ }
+
+ /**
+ * @test
+ */
+ public function setRGBHashNotationProvidesFluentInterface(): void
+ {
+ self::assertSame($this->subject, $this->subject->setRGBHashNotation(true));
+ }
+
+ /**
+ * @test
+ */
+ public function shouldRenderSemicolonAfterLastRuleInitiallyReturnsTrue(): void
+ {
+ self::assertTrue($this->subject->shouldRenderSemicolonAfterLastRule());
+ }
+
+ /**
+ * @test
+ *
+ * @dataProvider provideBooleans
+ */
+ public function setSemicolonAfterLastRuleSetsSemicolonAfterLastRule(bool $value): void
+ {
+ $this->subject->setSemicolonAfterLastRule($value);
+
+ self::assertSame($value, $this->subject->shouldRenderSemicolonAfterLastRule());
+ }
+
+ /**
+ * @test
+ */
+ public function setSemicolonAfterLastRuleProvidesFluentInterface(): void
+ {
+ self::assertSame($this->subject, $this->subject->setSemicolonAfterLastRule(true));
+ }
+
+ /**
+ * @test
+ */
+ public function getSpaceAfterRuleNameInitiallyReturnsSingleSpace(): void
+ {
+ self::assertSame(' ', $this->subject->getSpaceAfterRuleName());
+ }
+
+ /**
+ * @test
+ */
+ public function setSpaceAfterRuleNameSetsSpaceAfterRuleName(): void
+ {
+ $value = "\n";
+ $this->subject->setSpaceAfterRuleName($value);
+
+ self::assertSame($value, $this->subject->getSpaceAfterRuleName());
+ }
+
+ /**
+ * @test
+ */
+ public function setSpaceAfterRuleNameProvidesFluentInterface(): void
+ {
+ self::assertSame($this->subject, $this->subject->setSpaceAfterRuleName("\n"));
+ }
+
+ /**
+ * @test
+ */
+ public function getSpaceBeforeRulesInitiallyReturnsEmptyString(): void
+ {
+ self::assertSame('', $this->subject->getSpaceBeforeRules());
+ }
+
+ /**
+ * @test
+ */
+ public function setSpaceBeforeRulesSetsSpaceBeforeRules(): void
+ {
+ $value = ' ';
+ $this->subject->setSpaceBeforeRules($value);
+
+ self::assertSame($value, $this->subject->getSpaceBeforeRules());
+ }
+
+ /**
+ * @test
+ */
+ public function setSpaceBeforeRulesProvidesFluentInterface(): void
+ {
+ self::assertSame($this->subject, $this->subject->setSpaceBeforeRules(' '));
+ }
+
+ /**
+ * @test
+ */
+ public function getSpaceAfterRulesInitiallyReturnsEmptyString(): void
+ {
+ self::assertSame('', $this->subject->getSpaceAfterRules());
+ }
+
+ /**
+ * @test
+ */
+ public function setSpaceAfterRulesSetsSpaceAfterRules(): void
+ {
+ $value = ' ';
+ $this->subject->setSpaceAfterRules($value);
+
+ self::assertSame($value, $this->subject->getSpaceAfterRules());
+ }
+
+ /**
+ * @test
+ */
+ public function setSpaceAfterRulesProvidesFluentInterface(): void
+ {
+ self::assertSame($this->subject, $this->subject->setSpaceAfterRules(' '));
+ }
+
+ /**
+ * @test
+ */
+ public function getSpaceBetweenRulesInitiallyReturnsEmptyString(): void
+ {
+ self::assertSame('', $this->subject->getSpaceBetweenRules());
+ }
+
+ /**
+ * @test
+ */
+ public function setSpaceBetweenRulesSetsSpaceBetweenRules(): void
+ {
+ $value = ' ';
+ $this->subject->setSpaceBetweenRules($value);
+
+ self::assertSame($value, $this->subject->getSpaceBetweenRules());
+ }
+
+ /**
+ * @test
+ */
+ public function setSpaceBetweenRulesProvidesFluentInterface(): void
+ {
+ self::assertSame($this->subject, $this->subject->setSpaceBetweenRules(' '));
+ }
+
+ /**
+ * @test
+ */
+ public function getSpaceBeforeBlocksInitiallyReturnsEmptyString(): void
+ {
+ self::assertSame('', $this->subject->getSpaceBeforeBlocks());
+ }
+
+ /**
+ * @test
+ */
+ public function setSpaceBeforeBlocksSetsSpaceBeforeBlocks(): void
+ {
+ $value = ' ';
+ $this->subject->setSpaceBeforeBlocks($value);
+
+ self::assertSame($value, $this->subject->getSpaceBeforeBlocks());
+ }
+
+ /**
+ * @test
+ */
+ public function setSpaceBeforeBlocksProvidesFluentInterface(): void
+ {
+ self::assertSame($this->subject, $this->subject->setSpaceBeforeBlocks(' '));
+ }
+
+ /**
+ * @test
+ */
+ public function getSpaceAfterBlocksInitiallyReturnsEmptyString(): void
+ {
+ self::assertSame('', $this->subject->getSpaceAfterBlocks());
+ }
+
+ /**
+ * @test
+ */
+ public function setSpaceAfterBlocksSetsSpaceAfterBlocks(): void
+ {
+ $value = ' ';
+ $this->subject->setSpaceAfterBlocks($value);
+
+ self::assertSame($value, $this->subject->getSpaceAfterBlocks());
+ }
+
+ /**
+ * @test
+ */
+ public function setSpaceAfterBlocksProvidesFluentInterface(): void
+ {
+ self::assertSame($this->subject, $this->subject->setSpaceAfterBlocks(' '));
+ }
+
+ /**
+ * @test
+ */
+ public function getSpaceBetweenBlocksInitiallyReturnsNewline(): void
+ {
+ self::assertSame("\n", $this->subject->getSpaceBetweenBlocks());
+ }
+
+ /**
+ * @test
+ */
+ public function setSpaceBetweenBlocksSetsSpaceBetweenBlocks(): void
+ {
+ $value = ' ';
+ $this->subject->setSpaceBetweenBlocks($value);
+
+ self::assertSame($value, $this->subject->getSpaceBetweenBlocks());
+ }
+
+ /**
+ * @test
+ */
+ public function setSpaceBetweenBlocksProvidesFluentInterface(): void
+ {
+ self::assertSame($this->subject, $this->subject->setSpaceBetweenBlocks(' '));
+ }
+
+ /**
+ * @test
+ */
+ public function getContentBeforeAtRuleBlockInitiallyReturnsEmptyString(): void
+ {
+ self::assertSame('', $this->subject->getContentBeforeAtRuleBlock());
+ }
+
+ /**
+ * @test
+ */
+ public function setBeforeAtRuleBlockSetsBeforeAtRuleBlock(): void
+ {
+ $value = ' ';
+ $this->subject->setBeforeAtRuleBlock($value);
+
+ self::assertSame($value, $this->subject->getContentBeforeAtRuleBlock());
+ }
+
+ /**
+ * @test
+ */
+ public function setBeforeAtRuleBlockProvidesFluentInterface(): void
+ {
+ self::assertSame($this->subject, $this->subject->setBeforeAtRuleBlock(' '));
+ }
+
+ /**
+ * @test
+ */
+ public function getContentAfterAtRuleBlockInitiallyReturnsEmptyString(): void
+ {
+ self::assertSame('', $this->subject->getContentAfterAtRuleBlock());
+ }
+
+ /**
+ * @test
+ */
+ public function setAfterAtRuleBlockSetsAfterAtRuleBlock(): void
+ {
+ $value = ' ';
+ $this->subject->setAfterAtRuleBlock($value);
+
+ self::assertSame($value, $this->subject->getContentAfterAtRuleBlock());
+ }
+
+ /**
+ * @test
+ */
+ public function setAfterAtRuleBlockProvidesFluentInterface(): void
+ {
+ self::assertSame($this->subject, $this->subject->setAfterAtRuleBlock(' '));
+ }
+
+ /**
+ * @test
+ */
+ public function getSpaceBeforeSelectorSeparatorInitiallyReturnsEmptyString(): void
+ {
+ self::assertSame('', $this->subject->getSpaceBeforeSelectorSeparator());
+ }
+
+ /**
+ * @test
+ */
+ public function setSpaceBeforeSelectorSeparatorSetsSpaceBeforeSelectorSeparator(): void
+ {
+ $value = ' ';
+ $this->subject->setSpaceBeforeSelectorSeparator($value);
+
+ self::assertSame($value, $this->subject->getSpaceBeforeSelectorSeparator());
+ }
+
+ /**
+ * @test
+ */
+ public function setSpaceBeforeSelectorSeparatorProvidesFluentInterface(): void
+ {
+ self::assertSame($this->subject, $this->subject->setSpaceBeforeSelectorSeparator(' '));
+ }
+
+ /**
+ * @test
+ */
+ public function getSpaceAfterSelectorSeparatorInitiallyReturnsSpace(): void
+ {
+ self::assertSame(' ', $this->subject->getSpaceAfterSelectorSeparator());
+ }
+
+ /**
+ * @test
+ */
+ public function setSpaceAfterSelectorSeparatorSetsSpaceAfterSelectorSeparator(): void
+ {
+ $value = ' ';
+ $this->subject->setSpaceAfterSelectorSeparator($value);
+
+ self::assertSame($value, $this->subject->getSpaceAfterSelectorSeparator());
+ }
+
+ /**
+ * @test
+ */
+ public function setSpaceAfterSelectorSeparatorProvidesFluentInterface(): void
+ {
+ self::assertSame($this->subject, $this->subject->setSpaceAfterSelectorSeparator(' '));
+ }
+
+ /**
+ * @test
+ */
+ public function getSpaceBeforeListArgumentSeparatorInitiallyReturnsEmptyString(): void
+ {
+ self::assertSame('', $this->subject->getSpaceBeforeListArgumentSeparator());
+ }
+
+ /**
+ * @test
+ */
+ public function setSpaceBeforeListArgumentSeparatorSetsSpaceBeforeListArgumentSeparator(): void
+ {
+ $value = ' ';
+ $this->subject->setSpaceBeforeListArgumentSeparator($value);
+
+ self::assertSame($value, $this->subject->getSpaceBeforeListArgumentSeparator());
+ }
+
+ /**
+ * @test
+ */
+ public function setSpaceBeforeListArgumentSeparatorProvidesFluentInterface(): void
+ {
+ self::assertSame($this->subject, $this->subject->setSpaceBeforeListArgumentSeparator(' '));
+ }
+
+ /**
+ * @test
+ */
+ public function getSpaceBeforeListArgumentSeparatorsInitiallyReturnsEmptyArray(): void
+ {
+ self::assertSame([], $this->subject->getSpaceBeforeListArgumentSeparators());
+ }
+
+ /**
+ * @test
+ */
+ public function setSpaceBeforeListArgumentSeparatorsSetsSpaceBeforeListArgumentSeparators(): void
+ {
+ $value = ['/' => ' '];
+ $this->subject->setSpaceBeforeListArgumentSeparators($value);
+
+ self::assertSame($value, $this->subject->getSpaceBeforeListArgumentSeparators());
+ }
+
+ /**
+ * @test
+ */
+ public function setSpaceBeforeListArgumentSeparatorsProvidesFluentInterface(): void
+ {
+ self::assertSame($this->subject, $this->subject->setSpaceBeforeListArgumentSeparators([]));
+ }
+
+ /**
+ * @test
+ */
+ public function getSpaceAfterListArgumentSeparatorInitiallyReturnsEmptyString(): void
+ {
+ self::assertSame('', $this->subject->getSpaceAfterListArgumentSeparator());
+ }
+
+ /**
+ * @test
+ */
+ public function setSpaceAfterListArgumentSeparatorSetsSpaceAfterListArgumentSeparator(): void
+ {
+ $value = ' ';
+ $this->subject->setSpaceAfterListArgumentSeparator($value);
+
+ self::assertSame($value, $this->subject->getSpaceAfterListArgumentSeparator());
+ }
+
+ /**
+ * @test
+ */
+ public function setSpaceAfterListArgumentSeparatorProvidesFluentInterface(): void
+ {
+ self::assertSame($this->subject, $this->subject->setSpaceAfterListArgumentSeparator(' '));
+ }
+
+ /**
+ * @test
+ */
+ public function getSpaceAfterListArgumentSeparatorsInitiallyReturnsEmptyArray(): void
+ {
+ self::assertSame([], $this->subject->getSpaceAfterListArgumentSeparators());
+ }
+
+ /**
+ * @test
+ */
+ public function setSpaceAfterListArgumentSeparatorsSetsSpaceAfterListArgumentSeparators(): void
+ {
+ $value = [',' => ' '];
+ $this->subject->setSpaceAfterListArgumentSeparators($value);
+
+ self::assertSame($value, $this->subject->getSpaceAfterListArgumentSeparators());
+ }
+
+ /**
+ * @test
+ */
+ public function setSpaceAfterListArgumentSeparatorsProvidesFluentInterface(): void
+ {
+ self::assertSame($this->subject, $this->subject->setSpaceAfterListArgumentSeparators([]));
+ }
+
+ /**
+ * @test
+ */
+ public function getSpaceBeforeOpeningBraceInitiallyReturnsSpace(): void
+ {
+ self::assertSame(' ', $this->subject->getSpaceBeforeOpeningBrace());
+ }
+
+ /**
+ * @test
+ */
+ public function setSpaceBeforeOpeningBraceSetsSpaceBeforeOpeningBrace(): void
+ {
+ $value = "\t";
+ $this->subject->setSpaceBeforeOpeningBrace($value);
+
+ self::assertSame($value, $this->subject->getSpaceBeforeOpeningBrace());
+ }
+
+ /**
+ * @test
+ */
+ public function setSpaceBeforeOpeningBraceProvidesFluentInterface(): void
+ {
+ self::assertSame($this->subject, $this->subject->setSpaceBeforeOpeningBrace(' '));
+ }
+
+ /**
+ * @test
+ */
+ public function getContentBeforeDeclarationBlockInitiallyReturnsEmptyString(): void
+ {
+ self::assertSame('', $this->subject->getContentBeforeDeclarationBlock());
+ }
+
+ /**
+ * @test
+ */
+ public function setBeforeDeclarationBlockSetsBeforeDeclarationBlock(): void
+ {
+ $value = ' ';
+ $this->subject->setBeforeDeclarationBlock($value);
+
+ self::assertSame($value, $this->subject->getContentBeforeDeclarationBlock());
+ }
+
+ /**
+ * @test
+ */
+ public function setBeforeDeclarationBlockProvidesFluentInterface(): void
+ {
+ self::assertSame($this->subject, $this->subject->setBeforeDeclarationBlock(' '));
+ }
+
+ /**
+ * @test
+ */
+ public function getContentAfterDeclarationBlockSelectorsInitiallyReturnsEmptyString(): void
+ {
+ self::assertSame('', $this->subject->getContentAfterDeclarationBlockSelectors());
+ }
+
+ /**
+ * @test
+ */
+ public function setAfterDeclarationBlockSelectorsSetsAfterDeclarationBlockSelectors(): void
+ {
+ $value = ' ';
+ $this->subject->setAfterDeclarationBlockSelectors($value);
+
+ self::assertSame($value, $this->subject->getContentAfterDeclarationBlockSelectors());
+ }
+
+ /**
+ * @test
+ */
+ public function setAfterDeclarationBlockSelectorsProvidesFluentInterface(): void
+ {
+ self::assertSame($this->subject, $this->subject->setAfterDeclarationBlockSelectors(' '));
+ }
+
+ /**
+ * @test
+ */
+ public function getContentAfterDeclarationBlockInitiallyReturnsEmptyString(): void
+ {
+ self::assertSame('', $this->subject->getContentAfterDeclarationBlock());
+ }
+
+ /**
+ * @test
+ */
+ public function setAfterDeclarationBlockSetsAfterDeclarationBlock(): void
+ {
+ $value = ' ';
+ $this->subject->setAfterDeclarationBlock($value);
+
+ self::assertSame($value, $this->subject->getContentAfterDeclarationBlock());
+ }
+
+ /**
+ * @test
+ */
+ public function setAfterDeclarationBlockProvidesFluentInterface(): void
+ {
+ self::assertSame($this->subject, $this->subject->setAfterDeclarationBlock(' '));
+ }
+
+ /**
+ * @test
+ */
+ public function getIndentationInitiallyReturnsTab(): void
+ {
+ self::assertSame("\t", $this->subject->getIndentation());
+ }
+
+ /**
+ * @test
+ */
+ public function setIndentationSetsIndentation(): void
+ {
+ $value = ' ';
+ $this->subject->setIndentation($value);
+
+ self::assertSame($value, $this->subject->getIndentation());
+ }
+
+ /**
+ * @test
+ */
+ public function setIndentationProvidesFluentInterface(): void
+ {
+ self::assertSame($this->subject, $this->subject->setIndentation(' '));
+ }
+
+ /**
+ * @test
+ */
+ public function shouldIgnoreExceptionsInitiallyReturnsFalse(): void
+ {
+ self::assertFalse($this->subject->shouldIgnoreExceptions());
+ }
+
+ /**
+ * @test
+ *
+ * @dataProvider provideBooleans
+ */
+ public function setIgnoreExceptionsSetsIgnoreExceptions(bool $value): void
+ {
+ $this->subject->setIgnoreExceptions($value);
+
+ self::assertSame($value, $this->subject->shouldIgnoreExceptions());
+ }
+
+ /**
+ * @test
+ */
+ public function setIgnoreExceptionsProvidesFluentInterface(): void
+ {
+ self::assertSame($this->subject, $this->subject->setIgnoreExceptions(true));
+ }
+
+ /**
+ * @test
+ */
+ public function shouldRenderCommentsInitiallyReturnsFalse(): void
+ {
+ self::assertFalse($this->subject->shouldRenderComments());
+ }
+
+ /**
+ * @test
+ *
+ * @dataProvider provideBooleans
+ */
+ public function setRenderCommentsSetsRenderComments(bool $value): void
+ {
+ $this->subject->setRenderComments($value);
+
+ self::assertSame($value, $this->subject->shouldRenderComments());
+ }
+
+ /**
+ * @test
+ */
+ public function setRenderCommentsProvidesFluentInterface(): void
+ {
+ self::assertSame($this->subject, $this->subject->setRenderComments(true));
+ }
+
+ /**
+ * @test
+ */
+ public function getIndentationLevelInitiallyReturnsZero(): void
+ {
+ self::assertSame(0, $this->subject->getIndentationLevel());
+ }
+
+ /**
+ * @test
+ */
+ public function indentWithTabsByDefaultSetsIndentationToOneTab(): void
+ {
+ $this->subject->indentWithTabs();
+
+ self::assertSame("\t", $this->subject->getIndentation());
+ }
+
+ /**
+ * @return array, 1: string}>
+ */
+ public static function provideTabIndentation(): array
+ {
+ return [
+ 'zero tabs' => [0, ''],
+ 'one tab' => [1, "\t"],
+ 'two tabs' => [2, "\t\t"],
+ 'three tabs' => [3, "\t\t\t"],
+ ];
+ }
+
+ /**
+ * @test
+ * @dataProvider provideTabIndentation
+ */
+ public function indentWithTabsSetsIndentationToTheProvidedNumberOfTabs(
+ int $numberOfTabs,
+ string $expectedIndentation
+ ): void {
+ $this->subject->indentWithTabs($numberOfTabs);
+
+ self::assertSame($expectedIndentation, $this->subject->getIndentation());
+ }
+
+ /**
+ * @test
+ */
+ public function indentWithTabsProvidesFluentInterface(): void
+ {
+ self::assertSame($this->subject, $this->subject->indentWithTabs());
+ }
+
+ /**
+ * @test
+ */
+ public function indentWithSpacesByDefaultSetsIndentationToTwoSpaces(): void
+ {
+ $this->subject->indentWithSpaces();
+
+ self::assertSame(' ', $this->subject->getIndentation());
+ }
+
+ /**
+ * @return array, 1: string}>
+ */
+ public static function provideSpaceIndentation(): array
+ {
+ return [
+ 'zero spaces' => [0, ''],
+ 'one space' => [1, ' '],
+ 'two spaces' => [2, ' '],
+ 'three spaces' => [3, ' '],
+ 'four spaces' => [4, ' '],
+ ];
+ }
+
+ /**
+ * @test
+ * @dataProvider provideSpaceIndentation
+ */
+ public function indentWithSpacesSetsIndentationToTheProvidedNumberOfSpaces(
+ int $numberOfSpaces,
+ string $expectedIndentation
+ ): void {
+ $this->subject->indentWithSpaces($numberOfSpaces);
+
+ self::assertSame($expectedIndentation, $this->subject->getIndentation());
+ }
+
+ /**
+ * @test
+ */
+ public function indentWithSpacesProvidesFluentInterface(): void
+ {
+ self::assertSame($this->subject, $this->subject->indentWithSpaces());
+ }
+
+ /**
+ * @test
+ */
+ public function nextLevelReturnsOutputFormatInstance(): void
+ {
+ self::assertInstanceOf(OutputFormat::class, $this->subject->nextLevel());
+ }
+
+ /**
+ * @test
+ */
+ public function nextLevelReturnsDifferentInstance(): void
+ {
+ self::assertNotSame($this->subject, $this->subject->nextLevel());
+ }
+
+ /**
+ * @test
+ */
+ public function nextLevelReturnsCloneWithSameProperties(): void
+ {
+ $space = ' ';
+ $this->subject->setSpaceAfterRuleName($space);
+
+ self::assertSame($space, $this->subject->nextLevel()->getSpaceAfterRuleName());
+ }
+
+ /**
+ * @test
+ */
+ public function nextLevelReturnsInstanceWithIndentationLevelIncreasedByOne(): void
+ {
+ $originalIndentationLevel = $this->subject->getIndentationLevel();
+
+ self::assertSame($originalIndentationLevel + 1, $this->subject->nextLevel()->getIndentationLevel());
+ }
+
+ /**
+ * @test
+ */
+ public function nextLevelReturnsInstanceWithDifferentFormatterInstance(): void
+ {
+ $formatter = $this->subject->getFormatter();
+
+ self::assertNotSame($formatter, $this->subject->nextLevel()->getFormatter());
+ }
+
+ /**
+ * @test
+ */
+ public function beLenientSetsIgnoreExceptionsToTrue(): void
+ {
+ $this->subject->setIgnoreExceptions(false);
+
+ $this->subject->beLenient();
+
+ self::assertTrue($this->subject->shouldIgnoreExceptions());
+ }
+
+ /**
+ * @test
+ */
+ public function getFormatterReturnsOutputFormatterInstance(): void
+ {
+ self::assertInstanceOf(OutputFormatter::class, $this->subject->getFormatter());
+ }
+
+ /**
+ * @test
+ */
+ public function getFormatterCalledTwoTimesReturnsSameInstance(): void
+ {
+ $firstCallResult = $this->subject->getFormatter();
+ $secondCallResult = $this->subject->getFormatter();
+
+ self::assertSame($firstCallResult, $secondCallResult);
+ }
+
+ /**
+ * @test
+ */
+ public function createReturnsOutputFormatInstance(): void
+ {
+ self::assertInstanceOf(OutputFormat::class, OutputFormat::create());
+ }
+
+ /**
+ * @test
+ */
+ public function createCreatesInstanceWithDefaultSettings(): void
+ {
+ self::assertEquals(new OutputFormat(), OutputFormat::create());
+ }
+
+ /**
+ * @test
+ */
+ public function createCalledTwoTimesReturnsDifferentInstances(): void
+ {
+ $firstCallResult = OutputFormat::create();
+ $secondCallResult = OutputFormat::create();
+
+ self::assertNotSame($firstCallResult, $secondCallResult);
+ }
+
+ /**
+ * @test
+ */
+ public function createCompactReturnsOutputFormatInstance(): void
+ {
+ self::assertInstanceOf(OutputFormat::class, OutputFormat::createCompact());
+ }
+
+ /**
+ * @test
+ */
+ public function createCompactCalledTwoTimesReturnsDifferentInstances(): void
+ {
+ $firstCallResult = OutputFormat::createCompact();
+ $secondCallResult = OutputFormat::createCompact();
+
+ self::assertNotSame($firstCallResult, $secondCallResult);
+ }
+
+ /**
+ * @test
+ */
+ public function createCompactReturnsInstanceWithSpaceBeforeRulesSetToEmptyString(): void
+ {
+ $newInstance = OutputFormat::createCompact();
+
+ self::assertSame('', $newInstance->getSpaceBeforeRules());
+ }
+
+ /**
+ * @test
+ */
+ public function createCompactReturnsInstanceWithSpaceBetweenRulesSetToEmptyString(): void
+ {
+ $newInstance = OutputFormat::createCompact();
+
+ self::assertSame('', $newInstance->getSpaceBetweenRules());
+ }
+
+ /**
+ * @test
+ */
+ public function createCompactReturnsInstanceWithSpaceAfterRulesSetToEmptyString(): void
+ {
+ $newInstance = OutputFormat::createCompact();
+
+ self::assertSame('', $newInstance->getSpaceAfterRules());
+ }
+
+ /**
+ * @test
+ */
+ public function createCompactReturnsInstanceWithSpaceBeforeBlocksSetToEmptyString(): void
+ {
+ $newInstance = OutputFormat::createCompact();
+
+ self::assertSame('', $newInstance->getSpaceBeforeBlocks());
+ }
+
+ /**
+ * @test
+ */
+ public function createCompactReturnsInstanceWithSpaceBetweenBlocksSetToEmptyString(): void
+ {
+ $newInstance = OutputFormat::createCompact();
+
+ self::assertSame('', $newInstance->getSpaceBetweenBlocks());
+ }
+
+ /**
+ * @test
+ */
+ public function createCompactReturnsInstanceWithSpaceAfterBlocksSetToEmptyString(): void
+ {
+ $newInstance = OutputFormat::createCompact();
+
+ self::assertSame('', $newInstance->getSpaceAfterBlocks());
+ }
+
+ /**
+ * @test
+ */
+ public function createCompactReturnsInstanceWithSpaceAfterRuleNameSetToEmptyString(): void
+ {
+ $newInstance = OutputFormat::createCompact();
+
+ self::assertSame('', $newInstance->getSpaceAfterRuleName());
+ }
+
+ /**
+ * @test
+ */
+ public function createCompactReturnsInstanceWithSpaceBeforeOpeningBraceSetToEmptyString(): void
+ {
+ $newInstance = OutputFormat::createCompact();
+
+ self::assertSame('', $newInstance->getSpaceBeforeOpeningBrace());
+ }
+
+ /**
+ * @test
+ */
+ public function createCompactReturnsInstanceWithSpaceAfterSelectorSeparatorSetToEmptyString(): void
+ {
+ $newInstance = OutputFormat::createCompact();
+
+ self::assertSame('', $newInstance->getSpaceAfterSelectorSeparator());
+ }
+
+ /**
+ * @test
+ */
+ public function createCompactReturnsInstanceWithSpaceAfterListArgumentSeparatorsSetToEmptyArray(): void
+ {
+ $newInstance = OutputFormat::createCompact();
+
+ self::assertSame([], $newInstance->getSpaceAfterListArgumentSeparators());
+ }
+
+ /**
+ * @test
+ */
+ public function createCompactReturnsInstanceWithRenderSemicolonAfterLastRuleDisabled(): void
+ {
+ $newInstance = OutputFormat::createCompact();
+
+ self::assertFalse($newInstance->shouldRenderSemicolonAfterLastRule());
+ }
+
+ /**
+ * @test
+ */
+ public function createCompactReturnsInstanceWithRenderCommentsDisabled(): void
+ {
+ $newInstance = OutputFormat::createCompact();
+
+ self::assertFalse($newInstance->shouldRenderComments());
+ }
+
+ /**
+ * @test
+ */
+ public function createPrettyReturnsOutputFormatInstance(): void
+ {
+ self::assertInstanceOf(OutputFormat::class, OutputFormat::createPretty());
+ }
+
+ /**
+ * @test
+ */
+ public function createPrettyCalledTwoTimesReturnsDifferentInstances(): void
+ {
+ $firstCallResult = OutputFormat::createPretty();
+ $secondCallResult = OutputFormat::createPretty();
+
+ self::assertNotSame($firstCallResult, $secondCallResult);
+ }
+
+ /**
+ * @test
+ */
+ public function createPrettyReturnsInstanceWithSpaceBeforeRulesSetToNewline(): void
+ {
+ $newInstance = OutputFormat::createPretty();
+
+ self::assertSame("\n", $newInstance->getSpaceBeforeRules());
+ }
+
+ /**
+ * @test
+ */
+ public function createPrettyReturnsInstanceWithSpaceBetweenRulesSetToNewline(): void
+ {
+ $newInstance = OutputFormat::createPretty();
+
+ self::assertSame("\n", $newInstance->getSpaceBetweenRules());
+ }
+
+ /**
+ * @test
+ */
+ public function createPrettyReturnsInstanceWithSpaceAfterRulesSetToNewline(): void
+ {
+ $newInstance = OutputFormat::createPretty();
+
+ self::assertSame("\n", $newInstance->getSpaceAfterRules());
+ }
+
+ /**
+ * @test
+ */
+ public function createPrettyReturnsInstanceWithSpaceBeforeBlocksSetToNewline(): void
+ {
+ $newInstance = OutputFormat::createPretty();
+
+ self::assertSame("\n", $newInstance->getSpaceBeforeBlocks());
+ }
+
+ /**
+ * @test
+ */
+ public function createPrettyReturnsInstanceWithSpaceBetweenBlocksSetToTwoNewlines(): void
+ {
+ $newInstance = OutputFormat::createPretty();
+
+ self::assertSame("\n\n", $newInstance->getSpaceBetweenBlocks());
+ }
+
+ /**
+ * @test
+ */
+ public function createPrettyReturnsInstanceWithSpaceAfterBlocksSetToNewline(): void
+ {
+ $newInstance = OutputFormat::createPretty();
+
+ self::assertSame("\n", $newInstance->getSpaceAfterBlocks());
+ }
+
+ /**
+ * @test
+ */
+ public function createPrettyReturnsInstanceWithSpaceAfterRuleNameSetToSpace(): void
+ {
+ $newInstance = OutputFormat::createPretty();
+
+ self::assertSame(' ', $newInstance->getSpaceAfterRuleName());
+ }
+
+ /**
+ * @test
+ */
+ public function createPrettyReturnsInstanceWithSpaceBeforeOpeningBraceSetToSpace(): void
+ {
+ $newInstance = OutputFormat::createPretty();
+
+ self::assertSame(' ', $newInstance->getSpaceBeforeOpeningBrace());
+ }
+
+ /**
+ * @test
+ */
+ public function createPrettyReturnsInstanceWithSpaceAfterSelectorSeparatorSetToSpace(): void
+ {
+ $newInstance = OutputFormat::createPretty();
+
+ self::assertSame(' ', $newInstance->getSpaceAfterSelectorSeparator());
+ }
+
+ /**
+ * @test
+ */
+ public function createPrettyReturnsInstanceWithSpaceAfterListArgumentSeparatorsSetToSpaceForCommaOnly(): void
+ {
+ $newInstance = OutputFormat::createPretty();
+
+ self::assertSame([',' => ' '], $newInstance->getSpaceAfterListArgumentSeparators());
+ }
+
+ /**
+ * @test
+ */
+ public function createPrettyReturnsInstanceWithRenderCommentsEnabled(): void
+ {
+ $newInstance = OutputFormat::createPretty();
+
+ self::assertTrue($newInstance->shouldRenderComments());
+ }
+}
diff --git a/tests/Unit/OutputFormatterTest.php b/tests/Unit/OutputFormatterTest.php
new file mode 100644
index 000000000..2caf30e40
--- /dev/null
+++ b/tests/Unit/OutputFormatterTest.php
@@ -0,0 +1,622 @@
+outputFormat = new OutputFormat();
+ $this->subject = new OutputFormatter($this->outputFormat);
+ }
+
+ /**
+ * @test
+ */
+ public function spaceAfterRuleNameReturnsSpaceAfterRuleNameFromOutputFormat(): void
+ {
+ $space = ' ';
+ $this->outputFormat->setSpaceAfterRuleName($space);
+
+ self::assertSame($space, $this->subject->spaceAfterRuleName());
+ }
+
+ /**
+ * @test
+ */
+ public function spaceBeforeRulesReturnsSpaceBeforeRulesFromOutputFormat(): void
+ {
+ $space = ' ';
+ $this->outputFormat->setSpaceBeforeRules($space);
+
+ self::assertSame($space, $this->subject->spaceBeforeRules());
+ }
+
+ /**
+ * @test
+ */
+ public function spaceAfterRulesReturnsSpaceAfterRulesFromOutputFormat(): void
+ {
+ $space = ' ';
+ $this->outputFormat->setSpaceAfterRules($space);
+
+ self::assertSame($space, $this->subject->spaceAfterRules());
+ }
+
+ /**
+ * @test
+ */
+ public function spaceBetweenRulesReturnsSpaceBetweenRulesFromOutputFormat(): void
+ {
+ $space = ' ';
+ $this->outputFormat->setSpaceBetweenRules($space);
+
+ self::assertSame($space, $this->subject->spaceBetweenRules());
+ }
+
+ /**
+ * @test
+ */
+ public function spaceBeforeBlocksReturnsSpaceBeforeBlocksFromOutputFormat(): void
+ {
+ $space = ' ';
+ $this->outputFormat->setSpaceBeforeBlocks($space);
+
+ self::assertSame($space, $this->subject->spaceBeforeBlocks());
+ }
+
+ /**
+ * @test
+ */
+ public function spaceAfterBlocksReturnsSpaceAfterBlocksFromOutputFormat(): void
+ {
+ $space = ' ';
+ $this->outputFormat->setSpaceAfterBlocks($space);
+
+ self::assertSame($space, $this->subject->spaceAfterBlocks());
+ }
+
+ /**
+ * @test
+ */
+ public function spaceBetweenBlocksReturnsSpaceBetweenBlocksFromOutputFormat(): void
+ {
+ $space = ' ';
+ $this->outputFormat->setSpaceBetweenBlocks($space);
+
+ self::assertSame($space, $this->subject->spaceBetweenBlocks());
+ }
+
+ /**
+ * @test
+ */
+ public function spaceBeforeSelectorSeparatorReturnsSpaceBeforeSelectorSeparatorFromOutputFormat(): void
+ {
+ $space = ' ';
+ $this->outputFormat->setSpaceBeforeSelectorSeparator($space);
+
+ self::assertSame($space, $this->subject->spaceBeforeSelectorSeparator());
+ }
+
+ /**
+ * @test
+ */
+ public function spaceAfterSelectorSeparatorReturnsSpaceAfterSelectorSeparatorFromOutputFormat(): void
+ {
+ $space = ' ';
+ $this->outputFormat->setSpaceAfterSelectorSeparator($space);
+
+ self::assertSame($space, $this->subject->spaceAfterSelectorSeparator());
+ }
+
+ /**
+ * @test
+ */
+ public function spaceBeforeListArgumentSeparatorReturnsSpaceSetForSpecificSeparator(): void
+ {
+ $separator = ',';
+ $space = ' ';
+ $this->outputFormat->setSpaceBeforeListArgumentSeparators([$separator => $space]);
+ $defaultSpace = "\t\t\t\t";
+ $this->outputFormat->setSpaceBeforeListArgumentSeparator($defaultSpace);
+
+ self::assertSame($space, $this->subject->spaceBeforeListArgumentSeparator($separator));
+ }
+
+ /**
+ * @test
+ */
+ public function spaceBeforeListArgumentSeparatorWithoutSpecificSettingReturnsDefaultSpace(): void
+ {
+ $space = ' ';
+ $this->outputFormat->setSpaceBeforeListArgumentSeparators([',' => $space]);
+ $defaultSpace = "\t\t\t\t";
+ $this->outputFormat->setSpaceBeforeListArgumentSeparator($defaultSpace);
+
+ self::assertSame($defaultSpace, $this->subject->spaceBeforeListArgumentSeparator(';'));
+ }
+
+ /**
+ * @test
+ */
+ public function spaceAfterListArgumentSeparatorReturnsSpaceSetForSpecificSeparator(): void
+ {
+ $separator = ',';
+ $space = ' ';
+ $this->outputFormat->setSpaceAfterListArgumentSeparators([$separator => $space]);
+ $defaultSpace = "\t\t\t\t";
+ $this->outputFormat->setSpaceAfterListArgumentSeparator($defaultSpace);
+
+ self::assertSame($space, $this->subject->spaceAfterListArgumentSeparator($separator));
+ }
+
+ /**
+ * @test
+ */
+ public function spaceAfterListArgumentSeparatorWithoutSpecificSettingReturnsDefaultSpace(): void
+ {
+ $space = ' ';
+ $this->outputFormat->setSpaceAfterListArgumentSeparators([',' => $space]);
+ $defaultSpace = "\t\t\t\t";
+ $this->outputFormat->setSpaceAfterListArgumentSeparator($defaultSpace);
+
+ self::assertSame($defaultSpace, $this->subject->spaceAfterListArgumentSeparator(';'));
+ }
+
+ /**
+ * @test
+ */
+ public function spaceBeforeOpeningBraceReturnsSpaceBeforeOpeningBraceFromOutputFormat(): void
+ {
+ $space = ' ';
+ $this->outputFormat->setSpaceBeforeOpeningBrace($space);
+
+ self::assertSame($space, $this->subject->spaceBeforeOpeningBrace());
+ }
+
+ /**
+ * @test
+ */
+ public function implodeForEmptyValuesReturnsEmptyString(): void
+ {
+ $values = [];
+
+ $result = $this->subject->implode(', ', $values);
+
+ self::assertSame('', $result);
+ }
+
+ /**
+ * @test
+ */
+ public function implodeWithOneStringValueReturnsStringValue(): void
+ {
+ $value = 'tea';
+ $values = [$value];
+
+ $result = $this->subject->implode(', ', $values);
+
+ self::assertSame($value, $result);
+ }
+
+ /**
+ * @test
+ */
+ public function implodeWithMultipleStringValuesReturnsValuesSeparatedBySeparator(): void
+ {
+ $value1 = 'tea';
+ $value2 = 'coffee';
+ $values = [$value1, $value2];
+ $separator = ', ';
+
+ $result = $this->subject->implode($separator, $values);
+
+ self::assertSame($value1 . $separator . $value2, $result);
+ }
+
+ /**
+ * @test
+ */
+ public function implodeWithOneRenderableReturnsRenderedRenderable(): void
+ {
+ $renderable = $this->createMock(Renderable::class);
+ $renderedRenderable = 'tea';
+ $renderable->method('render')->with($this->outputFormat)->willReturn($renderedRenderable);
+ $values = [$renderable];
+
+ $result = $this->subject->implode(', ', $values);
+
+ self::assertSame($renderedRenderable, $result);
+ }
+
+ /**
+ * @test
+ */
+ public function implodeWithMultipleRenderablesReturnsRenderedRenderablesSeparatedBySeparator(): void
+ {
+ $renderable1 = $this->createMock(Renderable::class);
+ $renderedRenderable1 = 'tea';
+ $renderable1->method('render')->with($this->outputFormat)->willReturn($renderedRenderable1);
+ $renderable2 = $this->createMock(Renderable::class);
+ $renderedRenderable2 = 'coffee';
+ $renderable2->method('render')->with($this->outputFormat)->willReturn($renderedRenderable2);
+ $values = [$renderable1, $renderable2];
+ $separator = ', ';
+
+ $result = $this->subject->implode($separator, $values);
+
+ self::assertSame($renderedRenderable1 . $separator . $renderedRenderable2, $result);
+ }
+
+ /**
+ * @test
+ */
+ public function implodeWithIncreaseLevelFalseUsesDefaultIndentationLevelForRendering(): void
+ {
+ $renderable = $this->createMock(Renderable::class);
+ $renderedRenderable = 'tea';
+ $renderable->method('render')->with($this->outputFormat)->willReturn($renderedRenderable);
+ $values = [$renderable];
+
+ $result = $this->subject->implode(', ', $values, false);
+
+ self::assertSame($renderedRenderable, $result);
+ }
+
+ /**
+ * @test
+ */
+ public function implodeWithIncreaseLevelTrueIncreasesIndentationLevelForRendering(): void
+ {
+ $renderable = $this->createMock(Renderable::class);
+ $renderedRenderable = 'tea';
+ $renderable->method('render')->with($this->outputFormat->nextLevel())->willReturn($renderedRenderable);
+ $values = [$renderable];
+
+ $result = $this->subject->implode(', ', $values, true);
+
+ self::assertSame($renderedRenderable, $result);
+ }
+
+ /**
+ * @return array
+ */
+ public function provideUnchangedStringForRemoveLastSemicolon(): array
+ {
+ return [
+ 'empty string' => [''],
+ 'string without semicolon' => ['earl-grey: hot'],
+ 'string with trailing semicolon' => ['Earl Grey: hot;'],
+ 'string with semicolon in the middle' => ['Earl Grey: hot; Coffee: Americano'],
+ 'string with semicolons in the middle and trailing' => ['Earl Grey: hot; Coffee: Americano;'],
+ ];
+ }
+
+ /**
+ * @test
+ * @dataProvider provideUnchangedStringForRemoveLastSemicolon
+ */
+ public function removeLastSemicolonWithSemicolonAfterLastRuleEnabledReturnsUnchangedArgument(string $string): void
+ {
+ $this->outputFormat->setSemicolonAfterLastRule(true);
+
+ $result = $this->subject->removeLastSemicolon($string);
+
+ self::assertSame($string, $result);
+ }
+
+ /**
+ * @return array
+ */
+ public function provideChangedStringForRemoveLastSemicolon(): array
+ {
+ return [
+ 'empty string' => ['', ''],
+ 'non-empty string without semicolon' => ['Earl Grey: hot', 'Earl Grey: hot'],
+ 'just 1 semicolon' => [';', ''],
+ 'just 2 semicolons' => [';;', ';'],
+ 'string with trailing semicolon' => ['Earl Grey: hot;', 'Earl Grey: hot'],
+ 'string with semicolon in the middle' => [
+ 'Earl Grey: hot; Coffee: Americano',
+ 'Earl Grey: hot Coffee: Americano',
+ ],
+ 'string with semicolon in the middle and trailing' => [
+ 'Earl Grey: hot; Coffee: Americano;',
+ 'Earl Grey: hot; Coffee: Americano',
+ ],
+ 'string with 2 semicolons in the middle' => ['tea; coffee; Club-Mate', 'tea; coffee Club-Mate'],
+ 'string with 2 semicolons in the middle surrounded by spaces' => [
+ 'Earl Grey: hot ; Coffee: Americano ; Club-Mate: cold',
+ 'Earl Grey: hot ; Coffee: Americano Club-Mate: cold',
+ ],
+ 'string with 2 adjacent semicolons in the middle' => [
+ 'Earl Grey: hot;; Coffee: Americano',
+ 'Earl Grey: hot; Coffee: Americano',
+ ],
+ 'string with 3 adjacent semicolons in the middle' => [
+ 'Earl Grey: hot;;; Coffee: Americano',
+ 'Earl Grey: hot;; Coffee: Americano',
+ ],
+ ];
+ }
+
+ /**
+ * @test
+ * @dataProvider provideChangedStringForRemoveLastSemicolon
+ */
+ public function removeLastSemicolonWithSemicolonAfterLastRuleDisabledRemovesLastSemicolon(
+ string $input,
+ string $expected
+ ): void {
+ $this->outputFormat->setSemicolonAfterLastRule(false);
+
+ $result = $this->subject->removeLastSemicolon($input);
+
+ self::assertSame($expected, $result);
+ }
+
+ /**
+ * @test
+ */
+ public function commentsWithEmptyCommentableAndRenderCommentsDisabledDoesNotReturnSpaceBetweenBlocks(): void
+ {
+ $this->outputFormat->setRenderComments(false);
+ $spaceBetweenBlocks = ' between-space ';
+ $this->outputFormat->setSpaceBetweenBlocks($spaceBetweenBlocks);
+
+ $commentable = $this->createMock(Commentable::class);
+ $commentable->method('getComments')->willReturn([]);
+
+ $result = $this->subject->comments($commentable);
+
+ self::assertStringNotContainsString($spaceBetweenBlocks, $result);
+ }
+
+ /**
+ * @test
+ */
+ public function commentsWithEmptyCommentableAndRenderCommentsDisabledDoesNotReturnSpaceAfterBlocks(): void
+ {
+ $this->outputFormat->setRenderComments(false);
+ $spaceAfterBlocks = ' after-space ';
+ $this->outputFormat->setSpaceAfterBlocks($spaceAfterBlocks);
+
+ $commentable = $this->createMock(Commentable::class);
+ $commentable->method('getComments')->willReturn([]);
+
+ $result = $this->subject->comments($commentable);
+
+ self::assertStringNotContainsString($spaceAfterBlocks, $result);
+ }
+
+ /**
+ * @test
+ */
+ public function commentsWithEmptyCommentableAndRenderCommentsDisabledReturnsEmptyString(): void
+ {
+ $this->outputFormat->setRenderComments(false);
+
+ $commentable = $this->createMock(Commentable::class);
+ $commentable->method('getComments')->willReturn([]);
+
+ $result = $this->subject->comments($commentable);
+
+ self::assertSame('', $result);
+ }
+
+ /**
+ * @test
+ */
+ public function commentsWithEmptyCommentableAndRenderCommentsEnabledDoesNotReturnSpaceBetweenBlocks(): void
+ {
+ $this->outputFormat->setRenderComments(true);
+ $spaceBetweenBlocks = ' between-space ';
+ $this->outputFormat->setSpaceBetweenBlocks($spaceBetweenBlocks);
+
+ $commentable = $this->createMock(Commentable::class);
+ $commentable->method('getComments')->willReturn([]);
+
+ $result = $this->subject->comments($commentable);
+
+ self::assertStringNotContainsString($spaceBetweenBlocks, $result);
+ }
+
+ /**
+ * @test
+ */
+ public function commentsWithEmptyCommentableAndRenderCommentsEnabledDoesNotReturnSpaceAfterBlocks(): void
+ {
+ $this->outputFormat->setRenderComments(true);
+ $spaceAfterBlocks = ' after-space ';
+ $this->outputFormat->setSpaceAfterBlocks($spaceAfterBlocks);
+
+ $commentable = $this->createMock(Commentable::class);
+ $commentable->method('getComments')->willReturn([]);
+
+ $result = $this->subject->comments($commentable);
+
+ self::assertStringNotContainsString($spaceAfterBlocks, $result);
+ }
+
+ /**
+ * @test
+ */
+ public function commentsWithEmptyCommentableAndRenderCommentsEnabledReturnsEmptyString(): void
+ {
+ $this->outputFormat->setRenderComments(true);
+
+ $commentable = $this->createMock(Commentable::class);
+ $commentable->method('getComments')->willReturn([]);
+
+ $result = $this->subject->comments($commentable);
+
+ self::assertSame('', $result);
+ }
+
+ /**
+ * @test
+ */
+ public function commentsWithCommentableWithOneCommentAndRenderCommentsDisabledReturnsEmptyString(): void
+ {
+ $this->outputFormat->setRenderComments(false);
+
+ $commentText = 'I am a teapot.';
+ $comment = new Comment($commentText);
+ $commentable = $this->createMock(Commentable::class);
+ $commentable->method('getComments')->willReturn([$comment]);
+
+ $result = $this->subject->comments($commentable);
+
+ self::assertSame('', $result);
+ }
+
+ /**
+ * @test
+ */
+ public function commentsWithCommentableWithOneCommentRendersComment(): void
+ {
+ $this->outputFormat->setRenderComments(true);
+
+ $commentText = 'I am a teapot.';
+ $comment = new Comment($commentText);
+ $commentable = $this->createMock(Commentable::class);
+ $commentable->method('getComments')->willReturn([$comment]);
+
+ $result = $this->subject->comments($commentable);
+
+ self::assertStringContainsString('/*' . $commentText . '*/', $result);
+ }
+
+ /**
+ * @test
+ */
+ public function commentsWithCommentableWithOneCommentPutsSpaceAfterBlocksAfterRenderedComment(): void
+ {
+ $this->outputFormat->setRenderComments(true);
+ $afterSpace = ' after-space ';
+ $this->outputFormat->setSpaceAfterBlocks($afterSpace);
+
+ $commentText = 'I am a teapot.';
+ $comment = new Comment($commentText);
+ $commentable = $this->createMock(Commentable::class);
+ $commentable->method('getComments')->willReturn([$comment]);
+
+ $result = $this->subject->comments($commentable);
+
+ self::assertSame('/*' . $commentText . '*/' . $afterSpace, $result);
+ }
+
+ /**
+ * @test
+ */
+ public function commentsWithCommentableWithTwoCommentsPutsSpaceAfterBlocksAfterLastRenderedComment(): void
+ {
+ $this->outputFormat->setRenderComments(true);
+ $afterSpace = ' after-space ';
+ $this->outputFormat->setSpaceAfterBlocks($afterSpace);
+
+ $commentText1 = 'I am a teapot.';
+ $comment1 = new Comment($commentText1);
+ $commentText2 = 'But I am not.';
+ $comment2 = new Comment($commentText2);
+ $commentable = $this->createMock(Commentable::class);
+ $commentable->method('getComments')->willReturn([$comment1, $comment2]);
+
+ $result = $this->subject->comments($commentable);
+
+ self::assertStringContainsString('/*' . $commentText2 . '*/' . $afterSpace, $result);
+ }
+
+ /**
+ * @test
+ */
+ public function commentsWithCommentableWithTwoCommentsSeparatesCommentsBySpaceBetweenBlocks(): void
+ {
+ $this->outputFormat->setRenderComments(true);
+ $betweenSpace = ' between-space ';
+ $this->outputFormat->setSpaceBetweenBlocks($betweenSpace);
+
+ $commentText1 = 'I am a teapot.';
+ $comment1 = new Comment($commentText1);
+ $commentText2 = 'But I am not.';
+ $comment2 = new Comment($commentText2);
+ $commentable = $this->createMock(Commentable::class);
+ $commentable->method('getComments')->willReturn([$comment1, $comment2]);
+
+ $result = $this->subject->comments($commentable);
+
+ $expected = '/*' . $commentText1 . '*/' . $betweenSpace . '/*' . $commentText2 . '*/';
+ self::assertStringContainsString($expected, $result);
+ }
+
+ /**
+ * @test
+ */
+ public function commentsWithCommentableWithMoreThanTwoCommentsPutsSpaceAfterBlocksAfterLastRenderedComment(): void
+ {
+ $this->outputFormat->setRenderComments(true);
+ $afterSpace = ' after-space ';
+ $this->outputFormat->setSpaceAfterBlocks($afterSpace);
+
+ $commentText1 = 'I am a teapot.';
+ $comment1 = new Comment($commentText1);
+ $commentText2 = 'But I am not.';
+ $comment2 = new Comment($commentText2);
+ $commentText3 = 'So what am I then?';
+ $comment3 = new Comment($commentText3);
+ $commentable = $this->createMock(Commentable::class);
+ $commentable->method('getComments')->willReturn([$comment1, $comment2, $comment3]);
+
+ $result = $this->subject->comments($commentable);
+
+ self::assertStringContainsString('/*' . $commentText3 . '*/' . $afterSpace, $result);
+ }
+
+ /**
+ * @test
+ */
+ public function commentsWithCommentableWithMoreThanTwoCommentsSeparatesCommentsBySpaceBetweenBlocks(): void
+ {
+ $this->outputFormat->setRenderComments(true);
+ $betweenSpace = ' between-space ';
+ $this->outputFormat->setSpaceBetweenBlocks($betweenSpace);
+
+ $commentText1 = 'I am a teapot.';
+ $comment1 = new Comment($commentText1);
+ $commentText2 = 'But I am not.';
+ $comment2 = new Comment($commentText2);
+ $commentText3 = 'So what am I then?';
+ $comment3 = new Comment($commentText3);
+ $commentable = $this->createMock(Commentable::class);
+ $commentable->method('getComments')->willReturn([$comment1, $comment2, $comment3]);
+
+ $result = $this->subject->comments($commentable);
+
+ $expected = '/*' . $commentText1 . '*/'
+ . $betweenSpace . '/*' . $commentText2 . '*/'
+ . $betweenSpace . '/*' . $commentText3 . '*/';
+ self::assertStringContainsString($expected, $result);
+ }
+}
diff --git a/tests/Unit/Parsing/OutputExceptionTest.php b/tests/Unit/Parsing/OutputExceptionTest.php
new file mode 100644
index 000000000..73db4ebd5
--- /dev/null
+++ b/tests/Unit/Parsing/OutputExceptionTest.php
@@ -0,0 +1,76 @@
+getMessage());
+ }
+
+ /**
+ * @test
+ */
+ public function getLineNumberByDefaultReturnsNull(): void
+ {
+ $subject = new OutputException('foo');
+
+ self::assertNull($subject->getLineNumber());
+ }
+
+ /**
+ * @test
+ */
+ public function getLineNumberReturnsLineNumberProvidedToConstructor(): void
+ {
+ $lineNumber = 42;
+ $subject = new OutputException('foo', $lineNumber);
+
+ self::assertSame($lineNumber, $subject->getLineNumber());
+ }
+
+ /**
+ * @test
+ */
+ public function getMessageWithLineNumberProvidedIncludesLineNumber(): void
+ {
+ $lineNumber = 17;
+ $exception = new OutputException('foo', $lineNumber);
+
+ self::assertStringContainsString(' [line no: ' . $lineNumber . ']', $exception->getMessage());
+ }
+
+ /**
+ * @test
+ */
+ public function canBeThrown(): void
+ {
+ $this->expectException(OutputException::class);
+
+ throw new OutputException('foo');
+ }
+}
diff --git a/tests/Unit/Parsing/SourceExceptionTest.php b/tests/Unit/Parsing/SourceExceptionTest.php
new file mode 100644
index 000000000..923f6c490
--- /dev/null
+++ b/tests/Unit/Parsing/SourceExceptionTest.php
@@ -0,0 +1,78 @@
+getMessage());
+ }
+
+ /**
+ * @test
+ */
+ public function getLineNumberByDefaultReturnsNull(): void
+ {
+ $subject = new SourceException('foo');
+
+ self::assertNull($subject->getLineNumber());
+ }
+
+ /**
+ * @test
+ */
+ public function getLineNumberReturnsLineNumberProvidedToConstructor(): void
+ {
+ $lineNumber = 42;
+ $subject = new SourceException('foo', $lineNumber);
+
+ self::assertSame($lineNumber, $subject->getLineNumber());
+ }
+
+ /**
+ * @test
+ */
+ public function getMessageWithLineNumberProvidedIncludesLineNumber(): void
+ {
+ $lineNumber = 17;
+ $exception = new SourceException('foo', $lineNumber);
+
+ self::assertStringContainsString(' [line no: ' . $lineNumber . ']', $exception->getMessage());
+ }
+
+ /**
+ * @test
+ */
+ public function getMessageWithLineNumberProvidedIncludesMessage(): void
+ {
+ $message = 'There is no flatware.';
+ $exception = new SourceException($message, 17);
+
+ self::assertStringContainsString($message, $exception->getMessage());
+ }
+
+ /**
+ * @test
+ */
+ public function canBeThrown(): void
+ {
+ $this->expectException(SourceException::class);
+
+ throw new SourceException('foo');
+ }
+}
diff --git a/tests/Unit/Parsing/UnexpectedEOFExceptionTest.php b/tests/Unit/Parsing/UnexpectedEOFExceptionTest.php
new file mode 100644
index 000000000..b3c2ffcc9
--- /dev/null
+++ b/tests/Unit/Parsing/UnexpectedEOFExceptionTest.php
@@ -0,0 +1,178 @@
+getLineNumber());
+ }
+
+ /**
+ * @test
+ */
+ public function getLineNumberReturnsLineNumberProvidedToConstructor(): void
+ {
+ $lineNumber = 42;
+ $subject = new UnexpectedEOFException('expected', 'found', 'literal', $lineNumber);
+
+ self::assertSame($lineNumber, $subject->getLineNumber());
+ }
+
+ /**
+ * @test
+ */
+ public function getMessageWithLineNumberProvidedIncludesLineNumber(): void
+ {
+ $lineNumber = 17;
+ $exception = new UnexpectedEOFException('expected', 'found', 'literal', $lineNumber);
+
+ self::assertStringContainsString(' [line no: ' . $lineNumber . ']', $exception->getMessage());
+ }
+
+ /**
+ * @test
+ */
+ public function canBeThrown(): void
+ {
+ $this->expectException(UnexpectedEOFException::class);
+
+ throw new UnexpectedEOFException('expected', 'found');
+ }
+
+ /**
+ * @test
+ */
+ public function messageByDefaultRefersToTokenNotFound(): void
+ {
+ $expected = 'tea';
+ $found = 'coffee';
+
+ $exception = new UnexpectedEOFException($expected, $found);
+
+ $expectedMessage = 'Token “' . $expected . '” (literal) not found. Got “' . $found . '”.';
+ self::assertStringContainsString($expectedMessage, $exception->getMessage());
+ }
+
+ /**
+ * @test
+ */
+ public function messageForInvalidMatchTypeRefersToTokenNotFound(): void
+ {
+ $expected = 'tea';
+ $found = 'coffee';
+
+ // @phpstan-ignore-next-line argument.type We're explicitly testing with an invalid value here.
+ $exception = new UnexpectedEOFException($expected, $found, 'coding');
+
+ $expectedMessage = 'Token “' . $expected . '” (coding) not found. Got “' . $found . '”.';
+ self::assertStringContainsString($expectedMessage, $exception->getMessage());
+ }
+
+ /**
+ * @test
+ */
+ public function messageForLiteralMatchTypeRefersToTokenNotFound(): void
+ {
+ $expected = 'tea';
+ $found = 'coffee';
+
+ $exception = new UnexpectedEOFException($expected, $found, 'literal');
+
+ $expectedMessage = 'Token “' . $expected . '” (literal) not found. Got “' . $found . '”.';
+ self::assertStringContainsString($expectedMessage, $exception->getMessage());
+ }
+
+ /**
+ * @test
+ */
+ public function messageForSearchMatchTypeRefersToNoResults(): void
+ {
+ $expected = 'tea';
+ $found = 'coffee';
+
+ $exception = new UnexpectedEOFException($expected, $found, 'search');
+
+ $expectedMessage = 'Search for “' . $expected . '” returned no results. Context: “' . $found . '”.';
+ self::assertStringContainsString($expectedMessage, $exception->getMessage());
+ }
+
+ /**
+ * @test
+ */
+ public function messageForCountMatchTypeRefersToNumberOfCharacters(): void
+ {
+ $expected = 'tea';
+ $found = 'coffee';
+
+ $exception = new UnexpectedEOFException($expected, $found, 'count');
+
+ $expectedMessage = 'Next token was expected to have ' . $expected . ' chars. Context: “' . $found . '”.';
+ self::assertStringContainsString($expectedMessage, $exception->getMessage());
+ }
+
+ /**
+ * @test
+ */
+ public function messageForIdentifierMatchTypeRefersToIdentifier(): void
+ {
+ $expected = 'tea';
+ $found = 'coffee';
+
+ $exception = new UnexpectedEOFException($expected, $found, 'identifier');
+
+ $expectedMessage = 'Identifier expected. Got “' . $found . '”';
+ self::assertStringContainsString($expectedMessage, $exception->getMessage());
+ }
+
+ /**
+ * @test
+ */
+ public function messageForCustomMatchTypeMentionsExpectedAndFound(): void
+ {
+ $expected = 'tea';
+ $found = 'coffee';
+
+ $exception = new UnexpectedEOFException($expected, $found, 'custom');
+
+ $expectedMessage = $expected . ' ' . $found;
+ self::assertStringContainsString($expectedMessage, $exception->getMessage());
+ }
+
+ /**
+ * @test
+ */
+ public function messageForCustomMatchTypeTrimsMessage(): void
+ {
+ $expected = 'tea';
+ $found = 'coffee';
+
+ $exception = new UnexpectedEOFException(' ' . $expected, $found . ' ', 'custom');
+
+ $expectedMessage = $expected . ' ' . $found;
+ self::assertStringContainsString($expectedMessage, $exception->getMessage());
+ }
+}
diff --git a/tests/Unit/Parsing/UnexpectedTokenExceptionTest.php b/tests/Unit/Parsing/UnexpectedTokenExceptionTest.php
new file mode 100644
index 000000000..6adb3d7e0
--- /dev/null
+++ b/tests/Unit/Parsing/UnexpectedTokenExceptionTest.php
@@ -0,0 +1,178 @@
+getLineNumber());
+ }
+
+ /**
+ * @test
+ */
+ public function getLineNumberReturnsLineNumberProvidedToConstructor(): void
+ {
+ $lineNumber = 42;
+ $subject = new UnexpectedTokenException('expected', 'found', 'literal', $lineNumber);
+
+ self::assertSame($lineNumber, $subject->getLineNumber());
+ }
+
+ /**
+ * @test
+ */
+ public function getMessageWithLineNumberProvidedIncludesLineNumber(): void
+ {
+ $lineNumber = 17;
+ $exception = new UnexpectedTokenException('expected', 'found', 'literal', $lineNumber);
+
+ self::assertStringContainsString(' [line no: ' . $lineNumber . ']', $exception->getMessage());
+ }
+
+ /**
+ * @test
+ */
+ public function canBeThrown(): void
+ {
+ $this->expectException(UnexpectedTokenException::class);
+
+ throw new UnexpectedTokenException('expected', 'found');
+ }
+
+ /**
+ * @test
+ */
+ public function messageByDefaultRefersToTokenNotFound(): void
+ {
+ $expected = 'tea';
+ $found = 'coffee';
+
+ $exception = new UnexpectedTokenException($expected, $found);
+
+ $expectedMessage = 'Token “' . $expected . '” (literal) not found. Got “' . $found . '”.';
+ self::assertStringContainsString($expectedMessage, $exception->getMessage());
+ }
+
+ /**
+ * @test
+ */
+ public function messageForInvalidMatchTypeRefersToTokenNotFound(): void
+ {
+ $expected = 'tea';
+ $found = 'coffee';
+
+ // @phpstan-ignore-next-line argument.type We're explicitly testing with an invalid value here.
+ $exception = new UnexpectedTokenException($expected, $found, 'coding');
+
+ $expectedMessage = 'Token “' . $expected . '” (coding) not found. Got “' . $found . '”.';
+ self::assertStringContainsString($expectedMessage, $exception->getMessage());
+ }
+
+ /**
+ * @test
+ */
+ public function messageForLiteralMatchTypeRefersToTokenNotFound(): void
+ {
+ $expected = 'tea';
+ $found = 'coffee';
+
+ $exception = new UnexpectedTokenException($expected, $found, 'literal');
+
+ $expectedMessage = 'Token “' . $expected . '” (literal) not found. Got “' . $found . '”.';
+ self::assertStringContainsString($expectedMessage, $exception->getMessage());
+ }
+
+ /**
+ * @test
+ */
+ public function messageForSearchMatchTypeRefersToNoResults(): void
+ {
+ $expected = 'tea';
+ $found = 'coffee';
+
+ $exception = new UnexpectedTokenException($expected, $found, 'search');
+
+ $expectedMessage = 'Search for “' . $expected . '” returned no results. Context: “' . $found . '”.';
+ self::assertStringContainsString($expectedMessage, $exception->getMessage());
+ }
+
+ /**
+ * @test
+ */
+ public function messageForCountMatchTypeRefersToNumberOfCharacters(): void
+ {
+ $expected = 'tea';
+ $found = 'coffee';
+
+ $exception = new UnexpectedTokenException($expected, $found, 'count');
+
+ $expectedMessage = 'Next token was expected to have ' . $expected . ' chars. Context: “' . $found . '”.';
+ self::assertStringContainsString($expectedMessage, $exception->getMessage());
+ }
+
+ /**
+ * @test
+ */
+ public function messageForIdentifierMatchTypeRefersToIdentifier(): void
+ {
+ $expected = 'tea';
+ $found = 'coffee';
+
+ $exception = new UnexpectedTokenException($expected, $found, 'identifier');
+
+ $expectedMessage = 'Identifier expected. Got “' . $found . '”';
+ self::assertStringContainsString($expectedMessage, $exception->getMessage());
+ }
+
+ /**
+ * @test
+ */
+ public function messageForCustomMatchTypeMentionsExpectedAndFound(): void
+ {
+ $expected = 'tea';
+ $found = 'coffee';
+
+ $exception = new UnexpectedTokenException($expected, $found, 'custom');
+
+ $expectedMessage = $expected . ' ' . $found;
+ self::assertStringContainsString($expectedMessage, $exception->getMessage());
+ }
+
+ /**
+ * @test
+ */
+ public function messageForCustomMatchTypeTrimsMessage(): void
+ {
+ $expected = 'tea';
+ $found = 'coffee';
+
+ $exception = new UnexpectedTokenException(' ' . $expected, $found . ' ', 'custom');
+
+ $expectedMessage = $expected . ' ' . $found;
+ self::assertStringContainsString($expectedMessage, $exception->getMessage());
+ }
+}
diff --git a/tests/Unit/Position/Fixtures/ConcretePosition.php b/tests/Unit/Position/Fixtures/ConcretePosition.php
new file mode 100644
index 000000000..0db387065
--- /dev/null
+++ b/tests/Unit/Position/Fixtures/ConcretePosition.php
@@ -0,0 +1,13 @@
+subject = new ConcretePosition();
+ }
+
+ /**
+ * @test
+ */
+ public function getLineNumberInitiallyReturnsNull(): void
+ {
+ self::assertNull($this->subject->getLineNumber());
+ }
+
+ /**
+ * @test
+ */
+ public function getColumnNumberInitiallyReturnsNull(): void
+ {
+ self::assertNull($this->subject->getColumnNumber());
+ }
+
+ /**
+ * @return array}>
+ */
+ public function provideLineNumber(): array
+ {
+ return [
+ 'line 1' => [1],
+ 'line 42' => [42],
+ ];
+ }
+
+ /**
+ * @test
+ *
+ * @param int<1, max> $lineNumber
+ *
+ * @dataProvider provideLineNumber
+ */
+ public function setPositionOnVirginSetsLineNumber(int $lineNumber): void
+ {
+ $this->subject->setPosition($lineNumber);
+
+ self::assertSame($lineNumber, $this->subject->getLineNumber());
+ }
+
+ /**
+ * @test
+ *
+ * @param int<1, max> $lineNumber
+ *
+ * @dataProvider provideLineNumber
+ */
+ public function setPositionSetsNewLineNumber(int $lineNumber): void
+ {
+ $this->subject->setPosition(99);
+
+ $this->subject->setPosition($lineNumber);
+
+ self::assertSame($lineNumber, $this->subject->getLineNumber());
+ }
+
+ /**
+ * @test
+ */
+ public function setPositionWithNullClearsLineNumber(): void
+ {
+ $this->subject->setPosition(99);
+
+ $this->subject->setPosition(null);
+
+ self::assertNull($this->subject->getLineNumber());
+ }
+
+ /**
+ * @return array}>
+ */
+ public function provideColumnNumber(): array
+ {
+ return [
+ 'column 0' => [0],
+ 'column 14' => [14],
+ 'column 39' => [39],
+ ];
+ }
+
+ /**
+ * @test
+ *
+ * @param int<0, max> $columnNumber
+ *
+ * @dataProvider provideColumnNumber
+ */
+ public function setPositionOnVirginSetsColumnNumber(int $columnNumber): void
+ {
+ $this->subject->setPosition(1, $columnNumber);
+
+ self::assertSame($columnNumber, $this->subject->getColumnNumber());
+ }
+
+ /**
+ * @test
+ *
+ * @dataProvider provideColumnNumber
+ */
+ public function setPositionSetsNewColumnNumber(int $columnNumber): void
+ {
+ $this->subject->setPosition(1, 99);
+
+ $this->subject->setPosition(2, $columnNumber);
+
+ self::assertSame($columnNumber, $this->subject->getColumnNumber());
+ }
+
+ /**
+ * @test
+ */
+ public function setPositionWithoutColumnNumberClearsColumnNumber(): void
+ {
+ $this->subject->setPosition(1, 99);
+
+ $this->subject->setPosition(2);
+
+ self::assertNull($this->subject->getColumnNumber());
+ }
+
+ /**
+ * @test
+ */
+ public function setPositionWithNullForColumnNumberClearsColumnNumber(): void
+ {
+ $this->subject->setPosition(1, 99);
+
+ $this->subject->setPosition(2, null);
+
+ self::assertNull($this->subject->getColumnNumber());
+ }
+
+ /**
+ * @return DataProvider, 1: int<0, max>}>
+ */
+ public function provideLineAndColumnNumber(): DataProvider
+ {
+ return DataProvider::cross($this->provideLineNumber(), $this->provideColumnNumber());
+ }
+
+ /**
+ * @test
+ *
+ * @dataProvider provideLineAndColumnNumber
+ */
+ public function setPositionOnVirginSetsLineAndColumnNumber(int $lineNumber, int $columnNumber): void
+ {
+ $this->subject->setPosition($lineNumber, $columnNumber);
+
+ self::assertSame($lineNumber, $this->subject->getLineNumber());
+ self::assertSame($columnNumber, $this->subject->getColumnNumber());
+ }
+
+ /**
+ * @test
+ *
+ * @dataProvider provideLineAndColumnNumber
+ */
+ public function setPositionSetsNewLineAndColumnNumber(int $lineNumber, int $columnNumber): void
+ {
+ $this->subject->setPosition(98, 99);
+
+ $this->subject->setPosition($lineNumber, $columnNumber);
+
+ self::assertSame($lineNumber, $this->subject->getLineNumber());
+ self::assertSame($columnNumber, $this->subject->getColumnNumber());
+ }
+}
diff --git a/tests/Unit/Property/CSSNamespaceTest.php b/tests/Unit/Property/CSSNamespaceTest.php
new file mode 100644
index 000000000..2e4d99222
--- /dev/null
+++ b/tests/Unit/Property/CSSNamespaceTest.php
@@ -0,0 +1,34 @@
+subject = new CSSNamespace(new CSSString('http://www.w3.org/2000/svg'));
+ }
+
+ /**
+ * @test
+ */
+ public function implementsCSSListItem(): void
+ {
+ self::assertInstanceOf(CSSListItem::class, $this->subject);
+ }
+}
diff --git a/tests/Unit/Property/CharsetTest.php b/tests/Unit/Property/CharsetTest.php
new file mode 100644
index 000000000..e0645f5ef
--- /dev/null
+++ b/tests/Unit/Property/CharsetTest.php
@@ -0,0 +1,34 @@
+subject = new Charset(new CSSString('UTF-8'));
+ }
+
+ /**
+ * @test
+ */
+ public function implementsCSSListItem(): void
+ {
+ self::assertInstanceOf(CSSListItem::class, $this->subject);
+ }
+}
diff --git a/tests/Unit/Property/ImportTest.php b/tests/Unit/Property/ImportTest.php
new file mode 100644
index 000000000..4ec028e3f
--- /dev/null
+++ b/tests/Unit/Property/ImportTest.php
@@ -0,0 +1,35 @@
+subject = new Import(new URL(new CSSString('https://example.org/')), null);
+ }
+
+ /**
+ * @test
+ */
+ public function implementsCSSListItem(): void
+ {
+ self::assertInstanceOf(CSSListItem::class, $this->subject);
+ }
+}
diff --git a/tests/Unit/Property/Selector/SpecificityCalculatorTest.php b/tests/Unit/Property/Selector/SpecificityCalculatorTest.php
new file mode 100644
index 000000000..088bd5179
--- /dev/null
+++ b/tests/Unit/Property/Selector/SpecificityCalculatorTest.php
@@ -0,0 +1,94 @@
+}>
+ */
+ public static function provideSelectorsAndSpecificities(): array
+ {
+ return [
+ 'element' => ['a', 1],
+ 'element and descendant with pseudo-selector' => ['ol li::before', 3],
+ 'class' => ['.highlighted', 10],
+ 'element with class' => ['li.green', 11],
+ 'class with pseudo-selector' => ['.help:hover', 20],
+ 'ID' => ['#file', 100],
+ 'ID and descendant class' => ['#test .help', 110],
+ ];
+ }
+
+ /**
+ * @test
+ *
+ * @param non-empty-string $selector
+ * @param int<0, max> $expectedSpecificity
+ *
+ * @dataProvider provideSelectorsAndSpecificities
+ */
+ public function calculateReturnsSpecificityForProvidedSelector(
+ string $selector,
+ int $expectedSpecificity
+ ): void {
+ self::assertSame($expectedSpecificity, SpecificityCalculator::calculate($selector));
+ }
+
+ /**
+ * @test
+ *
+ * @param non-empty-string $selector
+ * @param int<0, max> $expectedSpecificity
+ *
+ * @dataProvider provideSelectorsAndSpecificities
+ */
+ public function calculateAfterClearingCacheReturnsSpecificityForProvidedSelector(
+ string $selector,
+ int $expectedSpecificity
+ ): void {
+ SpecificityCalculator::clearCache();
+
+ self::assertSame($expectedSpecificity, SpecificityCalculator::calculate($selector));
+ }
+
+ /**
+ * @test
+ */
+ public function calculateCalledTwoTimesReturnsSameSpecificityForProvidedSelector(): void
+ {
+ $selector = '#test .help';
+
+ $firstResult = SpecificityCalculator::calculate($selector);
+ $secondResult = SpecificityCalculator::calculate($selector);
+
+ self::assertSame($firstResult, $secondResult);
+ }
+
+ /**
+ * @test
+ */
+ public function calculateCalledReturnsSameSpecificityForProvidedSelectorBeforeAndAfterClearingCache(): void
+ {
+ $selector = '#test .help';
+
+ $firstResult = SpecificityCalculator::calculate($selector);
+ SpecificityCalculator::clearCache();
+ $secondResult = SpecificityCalculator::calculate($selector);
+
+ self::assertSame($firstResult, $secondResult);
+ }
+}
diff --git a/tests/Unit/Property/SelectorTest.php b/tests/Unit/Property/SelectorTest.php
new file mode 100644
index 000000000..e53e8274d
--- /dev/null
+++ b/tests/Unit/Property/SelectorTest.php
@@ -0,0 +1,144 @@
+getSelector());
+ }
+
+ /**
+ * @test
+ */
+ public function setSelectorOverwritesSelectorProvidedToConstructor(): void
+ {
+ $subject = new Selector('a');
+
+ $selector = 'input';
+ $subject->setSelector($selector);
+
+ self::assertSame($selector, $subject->getSelector());
+ }
+
+ /**
+ * @return array}>
+ */
+ public static function provideSelectorsAndSpecificities(): array
+ {
+ return [
+ 'element' => ['a', 1],
+ 'element and descendant with pseudo-selector' => ['ol li::before', 3],
+ 'class' => ['.highlighted', 10],
+ 'element with class' => ['li.green', 11],
+ 'class with pseudo-selector' => ['.help:hover', 20],
+ 'ID' => ['#file', 100],
+ 'ID and descendant class' => ['#test .help', 110],
+ '`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],
+ ];
+ }
+
+ /**
+ * @test
+ *
+ * @param non-empty-string $selector
+ * @param int<0, max> $expectedSpecificity
+ *
+ * @dataProvider provideSelectorsAndSpecificities
+ */
+ public function getSpecificityByDefaultReturnsSpecificityOfSelectorProvidedToConstructor(
+ string $selector,
+ int $expectedSpecificity
+ ): void {
+ $subject = new Selector($selector);
+
+ self::assertSame($expectedSpecificity, $subject->getSpecificity());
+ }
+
+ /**
+ * @test
+ *
+ * @param non-empty-string $selector
+ * @param int<0, max> $expectedSpecificity
+ *
+ * @dataProvider provideSelectorsAndSpecificities
+ */
+ public function getSpecificityReturnsSpecificityOfSelectorLastProvidedViaSetSelector(
+ string $selector,
+ int $expectedSpecificity
+ ): void {
+ $subject = new Selector('p');
+
+ $subject->setSelector($selector);
+
+ self::assertSame($expectedSpecificity, $subject->getSpecificity());
+ }
+
+ /**
+ * @test
+ *
+ * @dataProvider provideSelectorsAndSpecificities
+ */
+ public function isValidForValidSelectorReturnsTrue(string $selector): void
+ {
+ self::assertTrue(Selector::isValid($selector));
+ }
+
+ /**
+ * @return array
+ */
+ public static function provideInvalidSelectors(): array
+ {
+ return [
+ // This is currently broken.
+ // 'empty string' => [''],
+ 'percent sign' => ['%'],
+ // This is currently broken.
+ // 'hash only' => ['#'],
+ // This is currently broken.
+ // 'dot only' => ['.'],
+ 'slash' => ['/'],
+ 'less-than sign' => ['<'],
+ // This is currently broken.
+ // 'whitespace only' => [" \t\n\r"],
+ ];
+ }
+
+ /**
+ * @test
+ *
+ * @dataProvider provideInvalidSelectors
+ */
+ public function isValidForInvalidSelectorReturnsFalse(string $selector): void
+ {
+ self::assertFalse(Selector::isValid($selector));
+ }
+}
diff --git a/tests/Unit/Rule/RuleTest.php b/tests/Unit/Rule/RuleTest.php
new file mode 100644
index 000000000..008bcfc18
--- /dev/null
+++ b/tests/Unit/Rule/RuleTest.php
@@ -0,0 +1,73 @@
+}>
+ */
+ public static function provideRulesAndExpectedParsedValueListTypes(): array
+ {
+ return [
+ 'src (e.g. in @font-face)' => [
+ "
+ src: url('../fonts/open-sans-italic-300.woff2') format('woff2'),
+ url('../fonts/open-sans-italic-300.ttf') format('truetype');
+ ",
+ [RuleValueList::class, RuleValueList::class],
+ ],
+ ];
+ }
+
+ /**
+ * @test
+ *
+ * @param list $expectedTypeClassnames
+ *
+ * @dataProvider provideRulesAndExpectedParsedValueListTypes
+ */
+ public function parsesValuesIntoExpectedTypeList(string $rule, array $expectedTypeClassnames): void
+ {
+ $subject = Rule::parse(new ParserState($rule, Settings::create()));
+
+ $value = $subject->getValue();
+ self::assertInstanceOf(ValueList::class, $value);
+
+ $actualClassnames = \array_map(
+ /**
+ * @param Value|string $component
+ */
+ static function ($component): string {
+ return \is_string($component) ? 'string' : \get_class($component);
+ },
+ $value->getListComponents()
+ );
+
+ self::assertSame($expectedTypeClassnames, $actualClassnames);
+ }
+}
diff --git a/tests/Unit/RuleSet/AtRuleSetTest.php b/tests/Unit/RuleSet/AtRuleSetTest.php
new file mode 100644
index 000000000..0e3e0c974
--- /dev/null
+++ b/tests/Unit/RuleSet/AtRuleSetTest.php
@@ -0,0 +1,33 @@
+subject = new AtRuleSet('supports');
+ }
+
+ /**
+ * @test
+ */
+ public function implementsCSSListItem(): void
+ {
+ self::assertInstanceOf(CSSListItem::class, $this->subject);
+ }
+}
diff --git a/tests/Unit/RuleSet/DeclarationBlockTest.php b/tests/Unit/RuleSet/DeclarationBlockTest.php
new file mode 100644
index 000000000..4b20e9fc8
--- /dev/null
+++ b/tests/Unit/RuleSet/DeclarationBlockTest.php
@@ -0,0 +1,281 @@
+subject = new DeclarationBlock();
+ }
+
+ /**
+ * @test
+ */
+ public function implementsCSSElement(): void
+ {
+ self::assertInstanceOf(CSSElement::class, $this->subject);
+ }
+
+ /**
+ * @test
+ */
+ public function implementsCSSListItem(): void
+ {
+ self::assertInstanceOf(CSSListItem::class, $this->subject);
+ }
+
+ /**
+ * @test
+ */
+ public function implementsPositionable(): void
+ {
+ self::assertInstanceOf(Positionable::class, $this->subject);
+ }
+
+ /**
+ * @test
+ */
+ public function getLineNumberByDefaultReturnsNull(): void
+ {
+ $result = $this->subject->getLineNumber();
+
+ self::assertNull($result);
+ }
+
+ /**
+ * @return array|null}>
+ */
+ public function provideLineNumber(): array
+ {
+ return [
+ 'null' => [null],
+ 'line 1' => [1],
+ 'line 42' => [42],
+ ];
+ }
+
+ /**
+ * @test
+ *
+ * @param int<1, max>|null $lineNumber
+ *
+ * @dataProvider provideLineNumber
+ */
+ public function getLineNumberReturnsLineNumberPassedToConstructor(?int $lineNumber): void
+ {
+ $subject = new DeclarationBlock($lineNumber);
+
+ $result = $subject->getLineNumber();
+
+ self::assertSame($lineNumber, $result);
+ }
+
+ /**
+ * @return array
+ */
+ public static function provideSelector(): array
+ {
+ return [
+ 'type' => ['body'],
+ 'class' => ['.teapot'],
+ 'type & class' => ['img.teapot'],
+ 'id' => ['#my-mug'],
+ 'type & id' => ['h2#my-mug'],
+ 'pseudo-class' => [':hover'],
+ 'type & pseudo-class' => ['a:hover'],
+ '`not`' => [':not(#your-mug)'],
+ '`not` with multiple arguments' => [':not(#your-mug, .their-mug)'],
+ 'pseudo-element' => ['::before'],
+ 'attribute with `"`' => ['[alt="{}()[]\\"\',"]'],
+ 'attribute with `\'`' => ['[alt=\'{}()[]"\\\',\']'],
+ ];
+ }
+
+ /**
+ * @test
+ *
+ * @param non-empty-string $selector
+ *
+ * @dataProvider provideSelector
+ */
+ public function parsesSingleSelector(string $selector): void
+ {
+ $subject = DeclarationBlock::parse(new ParserState($selector . ' {}', Settings::create()));
+
+ self::assertInstanceOf(DeclarationBlock::class, $subject);
+ self::assertSame([$selector], self::getSelectorsAsStrings($subject));
+ }
+
+ /**
+ * @return DataProvider
+ */
+ public static function provideTwoSelectors(): DataProvider
+ {
+ return DataProvider::cross(self::provideSelector(), self::provideSelector());
+ }
+
+ /**
+ * @test
+ *
+ * @param non-empty-string $firstSelector
+ * @param non-empty-string $secondSelector
+ *
+ * @dataProvider provideTwoSelectors
+ */
+ public function parsesTwoCommaSeparatedSelectors(string $firstSelector, string $secondSelector): void
+ {
+ $joinedSelectors = $firstSelector . ', ' . $secondSelector;
+
+ $subject = DeclarationBlock::parse(new ParserState($joinedSelectors . ' {}', Settings::create()));
+
+ self::assertInstanceOf(DeclarationBlock::class, $subject);
+ self::assertSame([$firstSelector, $secondSelector], self::getSelectorsAsStrings($subject));
+ }
+
+ /**
+ * @return array
+ */
+ public static function provideInvalidSelector(): 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))'],
+ ];
+ }
+
+ /**
+ * @test
+ *
+ * @param non-empty-string $selector
+ *
+ * @dataProvider provideInvalidSelector
+ */
+ public function parseSkipsBlockWithInvalidSelector(string $selector): void
+ {
+ static $nextCss = ' .next {}';
+ $css = $selector . ' {}' . $nextCss;
+ $parserState = new ParserState($css, Settings::create());
+
+ $subject = DeclarationBlock::parse($parserState);
+
+ self::assertNull($subject);
+ self::assertTrue($parserState->comes($nextCss));
+ }
+
+ /**
+ * @return array
+ */
+ private static function getSelectorsAsStrings(DeclarationBlock $declarationBlock): array
+ {
+ return \array_map(
+ static function (Selector $selectorObject): string {
+ return $selectorObject->getSelector();
+ },
+ $declarationBlock->getSelectors()
+ );
+ }
+
+ /**
+ * @test
+ */
+ public function getRuleSetOnVirginReturnsARuleSet(): void
+ {
+ $result = $this->subject->getRuleSet();
+
+ self::assertInstanceOf(RuleSet::class, $result);
+ }
+
+ /**
+ * @test
+ */
+ public function getRuleSetAfterRulesSetReturnsARuleSet(): void
+ {
+ $this->subject->setRules([new Rule('color')]);
+
+ $result = $this->subject->getRuleSet();
+
+ self::assertInstanceOf(RuleSet::class, $result);
+ }
+
+ /**
+ * @test
+ */
+ public function getRuleSetOnVirginReturnsObjectWithoutRules(): void
+ {
+ $result = $this->subject->getRuleSet();
+
+ self::assertSame([], $result->getRules());
+ }
+
+ /**
+ * @test
+ *
+ * @param list $propertyNamesToSet
+ *
+ * @dataProvider providePropertyNames
+ */
+ public function getRuleSetReturnsObjectWithRulesSet(array $propertyNamesToSet): void
+ {
+ $rules = self::createRulesFromPropertyNames($propertyNamesToSet);
+ $this->subject->setRules($rules);
+
+ $result = $this->subject->getRuleSet();
+
+ self::assertSame($rules, $result->getRules());
+ }
+
+ /**
+ * @test
+ */
+ public function getRuleSetByDefaultReturnsObjectWithNullLineNumber(): void
+ {
+ $result = $this->subject->getRuleSet();
+
+ self::assertNull($result->getLineNumber());
+ }
+
+ /**
+ * @test
+ *
+ * @param int<1, max>|null $lineNumber
+ *
+ * @dataProvider provideLineNumber
+ */
+ public function getRuleSetReturnsObjectWithLineNumberPassedToConstructor(?int $lineNumber): void
+ {
+ $subject = new DeclarationBlock($lineNumber);
+
+ $result = $subject->getRuleSet();
+
+ self::assertSame($lineNumber, $result->getLineNumber());
+ }
+}
diff --git a/tests/Unit/RuleSet/RuleContainerTest.php b/tests/Unit/RuleSet/RuleContainerTest.php
new file mode 100644
index 000000000..bcb641a22
--- /dev/null
+++ b/tests/Unit/RuleSet/RuleContainerTest.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 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