diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index b23f7449..865d202d 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -1,8 +1,10 @@
# https://help.github.com/en/categories/automating-your-workflow-with-github-actions
on:
- pull_request:
push:
+ branches:
+ - main
+ pull_request:
schedule:
- cron: '3 3 * * 1'
@@ -14,7 +16,7 @@ jobs:
runs-on: ubuntu-22.04
strategy:
matrix:
- php-version: [ '5.6', '7.0', '7.1', '7.2', '7.3', '7.4', '8.0', '8.1', '8.2' ]
+ php-version: [ '5.6', '7.0', '7.1', '7.2', '7.3', '7.4', '8.0', '8.1', '8.2', '8.3', '8.4' ]
steps:
- name: Checkout
@@ -39,11 +41,7 @@ jobs:
strategy:
fail-fast: false
matrix:
- php-version: [ '5.6', '7.0', '7.1', '7.2', '7.3' ]
- coverage: [ 'none' ]
- include:
- - php-version: '7.4'
- coverage: xdebug
+ php-version: [ '5.6', '7.0', '7.1', '7.2', '7.3', '7.4', '8.0', '8.1', '8.2', '8.3', '8.4' ]
steps:
- name: Checkout
@@ -55,61 +53,6 @@ jobs:
php-version: ${{ matrix.php-version }}
ini-values: error_reporting=E_ALL
tools: composer:v2
- coverage: "${{ matrix.coverage }}"
-
- - name: Show the Composer configuration
- run: composer config --global --list
-
- - name: Cache dependencies installed with composer
- uses: actions/cache@v4
- with:
- path: ~/.cache/composer
- key: php${{ matrix.php-version }}-composer-${{ hashFiles('**/composer.json') }}
- restore-keys: |
- php${{ matrix.php-version }}-composer-
-
- - name: Install Composer dependencies
- run: |
- composer update --with-dependencies --no-progress;
- composer show;
-
- - name: Run Tests
- run: ./vendor/bin/phpunit --coverage-clover build/coverage/xml
-
- - name: Upload coverage results to Codacy
- env:
- CODACY_PROJECT_TOKEN: ${{ secrets.CODACY_PROJECT_TOKEN }}
- if: "${{ matrix.coverage != 'none' && env.CODACY_PROJECT_TOKEN != '' }}"
- run: |
- ./vendor/bin/codacycoverage clover build/coverage/xml
-
- static-analysis:
- name: Static Analysis
-
- runs-on: ubuntu-22.04
-
- needs: [ php-lint ]
-
- strategy:
- fail-fast: false
- matrix:
- include:
- - command: sniffer
- php-version: '7.4'
- - command: fixer
- php-version: '7.4'
- - command: stan
- php-version: '7.4'
-
- steps:
- - name: Checkout
- uses: actions/checkout@v4
-
- - name: Install PHP
- uses: shivammathur/setup-php@v2
- with:
- php-version: ${{ matrix.php-version }}
- tools: "composer:v2, phive"
coverage: none
- name: Show the Composer configuration
@@ -128,9 +71,5 @@ jobs:
composer update --with-dependencies --no-progress;
composer show;
- - name: Install development tools
- run: |
- phive --no-progress install --trust-gpg-keys BBAB5DF0A0D6672989CF1869E82B2FB314E9906E,A972B9ABB95D0B760B51442231C7E470E2138192,D32680D5957DC7116BE29C14CF1A108D0E7AE720
-
- - name: Run Command
- run: composer ci:php:${{ matrix.command }}
+ - name: Run Tests
+ run: ./vendor/bin/phpunit
diff --git a/.gitignore b/.gitignore
index c1747f26..acf0d9d8 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,6 +1,7 @@
/.phive/*
/.php-cs-fixer.cache
/.php_cs.cache
+/.phpunit.result.cache
/composer.lock
/phpstan.neon
/vendor/
diff --git a/.phive/phars.xml b/.phive/phars.xml
deleted file mode 100644
index d353fbf9..00000000
--- a/.phive/phars.xml
+++ /dev/null
@@ -1,7 +0,0 @@
-
-
-
-
-
-
-
diff --git a/CHANGELOG.md b/CHANGELOG.md
index c70ef0a1..74afb6a6 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,14 +7,144 @@ This project adheres to [Semantic Versioning](https://semver.org/).
### 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)
+
### Removed
### 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 (#675, #701, #746, #751)
+
+### Changed
+
+- Mark parsing-internal classes and methods as `@internal` (#711)
+- Block installations on unsupported higher PHP versions (#691)
+
+### Deprecated
+
+- Deprecate the expansion of shorthand properties (#719)
+- Deprecate `Parser::setCharset()` and `Parser::getCharset()` (#703)
+
+### Fixed
+
+- Fix type errors in PHP strict mode (#695)
+
+## 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` (#500)
+
+### 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
@@ -37,7 +167,8 @@ This project adheres to [Semantic Versioning](https://semver.org/).
* 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*
@@ -52,7 +183,10 @@ This project adheres to [Semantic Versioning](https://semver.org/).
* 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
@@ -64,11 +198,16 @@ This project adheres to [Semantic Versioning](https://semver.org/).
## 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*
@@ -76,16 +215,20 @@ This project adheres to [Semantic Versioning](https://semver.org/).
* 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*
@@ -97,7 +240,8 @@ This project adheres to [Semantic Versioning](https://semver.org/).
### 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)
@@ -107,7 +251,8 @@ This project adheres to [Semantic Versioning](https://semver.org/).
## 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*
@@ -124,7 +269,8 @@ This project adheres to [Semantic Versioning](https://semver.org/).
### 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)
@@ -138,22 +284,27 @@ This project adheres to [Semantic Versioning](https://semver.org/).
### 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*
@@ -168,13 +319,15 @@ This project adheres to [Semantic Versioning](https://semver.org/).
## 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*
@@ -192,7 +345,9 @@ This project adheres to [Semantic Versioning](https://semver.org/).
## 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*
@@ -229,18 +384,22 @@ This project adheres to [Semantic Versioning](https://semver.org/).
### 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)
@@ -249,10 +408,18 @@ This project adheres to [Semantic Versioning](https://semver.org/).
### 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)
@@ -260,8 +427,13 @@ This project adheres to [Semantic Versioning](https://semver.org/).
### 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/composer.json b/composer.json
index 6b7c9607..e3caf0cc 100644
--- a/composer.json
+++ b/composer.json
@@ -12,15 +12,23 @@
"authors": [
{
"name": "Raphael Schweikert"
+ },
+ {
+ "name": "Oliver Klee",
+ "email": "github@oliverklee.de"
+ },
+ {
+ "name": "Jake Hotson",
+ "email": "jake.github@qzdesign.co.uk"
}
],
"require": {
- "php": ">=5.6.20",
+ "php": "^5.6.20 || ^7.0.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0",
"ext-iconv": "*"
},
"require-dev": {
- "phpunit/phpunit": "^5.7.27",
- "codacy/coverage": "^1.4.3"
+ "phpunit/phpunit": "5.7.27 || 6.5.14 || 7.5.20 || 8.5.41",
+ "rawr/cross-data-providers": "^2.0.0"
},
"suggest": {
"ext-mbstring": "for parsing UTF-8 CSS"
@@ -37,38 +45,7 @@
},
"extra": {
"branch-alias": {
- "dev-master": "9.0.x-dev"
+ "dev-main": "9.0.x-dev"
}
- },
- "scripts": {
- "ci": [
- "@ci:static"
- ],
- "ci:php:fixer": "@php ./.phive/php-cs-fixer.phar --config=config/php-cs-fixer.php fix --dry-run -v --show-progress=dots bin src tests",
- "ci:php:sniffer": "@php ./.phive/phpcs.phar --standard=config/phpcs.xml bin src tests",
- "ci:php:stan": "@php ./.phive/phpstan.phar --configuration=config/phpstan.neon",
- "ci:static": [
- "@ci:php:fixer",
- "@ci:php:sniffer",
- "@ci:php:stan"
- ],
- "fix:php": [
- "@fix:php:fixer",
- "@fix:php:sniffer"
- ],
- "fix:php:fixer": "@php ./.phive/php-cs-fixer.phar --config=config/php-cs-fixer.php fix bin src tests",
- "fix:php:sniffer": "@php ./.phive/phpcbf.phar --standard=config/phpcs.xml bin src tests",
- "phpstan:baseline": "@php ./.phive/phpstan.phar --configuration=config/phpstan.neon --generate-baseline=config/phpstan-baseline.neon"
- },
- "scripts-descriptions": {
- "ci": "Runs all dynamic and static code checks (i.e. currently, only the static checks).",
- "ci:php:fixer": "Checks the code style with PHP CS Fixer.",
- "ci:php:sniffer": "Checks the code style with PHP_CodeSniffer.",
- "ci:php:stan": "Checks the types with PHPStan.",
- "ci:static": "Runs all static code analysis checks for the code.",
- "fix:php": "Autofixes all autofixable issues in the PHP code.",
- "fix:php:fixer": "Fixes autofixable issues found by PHP CS Fixer.",
- "fix:php:sniffer": "Fixes autofixable issues found by PHP_CodeSniffer.",
- "phpstand:baseline": "Updates the PHPStan baseline file to match the code."
}
}
diff --git a/config/php-cs-fixer.php b/config/php-cs-fixer.php
deleted file mode 100644
index 88a9a692..00000000
--- a/config/php-cs-fixer.php
+++ /dev/null
@@ -1,34 +0,0 @@
-setRiskyAllowed(true)
- ->setRules(
- [
- '@PSR12' => true,
- // Disable constant visibility from the PSR12 rule set as this would break compatibility with PHP < 7.1.
- 'visibility_required' => ['elements' => ['property', 'method']],
-
- '@PHPUnit50Migration:risky' => true,
- '@PHPUnit52Migration:risky' => true,
- '@PHPUnit54Migration:risky' => true,
- '@PHPUnit55Migration:risky' => true,
- '@PHPUnit56Migration:risky' => true,
- '@PHPUnit57Migration:risky' => true,
-
- 'php_unit_construct' => true,
- 'php_unit_dedicate_assert' => ['target' => '5.6'],
- 'php_unit_expectation' => ['target' => '5.6'],
- 'php_unit_fqcn_annotation' => true,
- 'php_unit_method_casing' => true,
- 'php_unit_mock' => ['target' => '5.5'],
- 'php_unit_mock_short_will_return' => true,
- 'php_unit_namespaced' => ['target' => '5.7'],
- 'php_unit_set_up_tear_down_visibility' => true,
- 'php_unit_test_annotation' => ['style' => 'annotation'],
- 'php_unit_test_case_static_method_calls' => ['call_type' => 'self'],
- ]
- );
diff --git a/config/phpcs.xml b/config/phpcs.xml
deleted file mode 100644
index 14473bb2..00000000
--- a/config/phpcs.xml
+++ /dev/null
@@ -1,17 +0,0 @@
-
-
-
- This standard requires PHP_CodeSniffer >= 3.6.0.
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/config/phpstan-baseline.neon b/config/phpstan-baseline.neon
deleted file mode 100644
index b730548c..00000000
--- a/config/phpstan-baseline.neon
+++ /dev/null
@@ -1,22 +0,0 @@
-parameters:
- ignoreErrors:
- -
- message: "#^Call to an undefined method Sabberworm\\\\CSS\\\\OutputFormat\\:\\:setIndentation\\(\\)\\.$#"
- count: 2
- path: ../src/OutputFormat.php
-
- -
- message: "#^Class Sabberworm\\\\CSS\\\\Value\\\\Size constructor invoked with 5 parameters, 1\\-4 required\\.$#"
- count: 2
- path: ../src/RuleSet/DeclarationBlock.php
-
- -
- message: "#^Variable \\$oRule might not be defined\\.$#"
- count: 2
- path: ../src/RuleSet/DeclarationBlock.php
-
- -
- message: "#^Variable \\$oVal might not be defined\\.$#"
- count: 1
- path: ../src/Value/CalcFunction.php
-
diff --git a/config/phpstan.neon b/config/phpstan.neon
deleted file mode 100644
index 3d7611a6..00000000
--- a/config/phpstan.neon
+++ /dev/null
@@ -1,18 +0,0 @@
-includes:
- - phpstan-baseline.neon
-
-parameters:
- parallel:
- # Don't be overly greedy on machines with more CPU's to be a good neighbor especially on CI
- maximumNumberOfProcesses: 5
-
- level: 1
-
- scanDirectories:
- - %currentWorkingDirectory%/bin/
- - %currentWorkingDirectory%/src/
- - %currentWorkingDirectory%/tests/
- paths:
- - %currentWorkingDirectory%/bin/
- - %currentWorkingDirectory%/src/
- - %currentWorkingDirectory%/tests/
diff --git a/phpunit.xml b/phpunit.xml
index 5f3dd458..249dd48b 100644
--- a/phpunit.xml
+++ b/phpunit.xml
@@ -1,6 +1,15 @@
+ xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/5.7/phpunit.xsd"
+ beStrictAboutChangesToGlobalState="true"
+ beStrictAboutOutputDuringTests="true"
+ beStrictAboutTodoAnnotatedTests="true"
+ cacheResult="false"
+ colors="true"
+ convertDeprecationsToExceptions="true"
+ forceCoversAnnotation="true"
+ verbose="true"
+>
tests
diff --git a/src/CSSElement.php b/src/CSSElement.php
new file mode 100644
index 00000000..944aabe2
--- /dev/null
+++ b/src/CSSElement.php
@@ -0,0 +1,17 @@
+comments($this);
$sResult .= $oOutputFormat->sBeforeAtRuleBlock;
diff --git a/src/CSSList/CSSBlockList.php b/src/CSSList/CSSBlockList.php
index fce7913e..aa0c67fc 100644
--- a/src/CSSList/CSSBlockList.php
+++ b/src/CSSList/CSSBlockList.php
@@ -2,6 +2,7 @@
namespace Sabberworm\CSS\CSSList;
+use Sabberworm\CSS\CSSElement;
use Sabberworm\CSS\Property\Selector;
use Sabberworm\CSS\Rule\Rule;
use Sabberworm\CSS\RuleSet\DeclarationBlock;
@@ -59,7 +60,53 @@ protected function allRuleSets(array &$aResult)
}
/**
- * @param CSSList|Rule|RuleSet|Value $oElement
+ * Returns all `Value` objects found recursively in `Rule`s in the tree.
+ *
+ * @param CSSElement|string|null $element
+ * This is the `CSSList` or `RuleSet` to start the search from (defaults to the whole document).
+ * If a string is given, it is used as a rule name filter.
+ * Passing a string for this parameter is deprecated in version 8.9.0, and will not work from v9.0;
+ * use the following parameter to pass a rule name filter instead.
+ * @param string|bool|null $ruleSearchPatternOrSearchInFunctionArguments
+ * 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).
+ * If a Boolean is provided, it is treated as the `$searchInFunctionArguments` argument.
+ * Passing a Boolean for this parameter is deprecated in version 8.9.0, and will not work from v9.0;
+ * use the `$searchInFunctionArguments` parameter instead.
+ * @param bool $searchInFunctionArguments whether to also return Value objects used as Function arguments.
+ *
+ * @return array
+ *
+ * @see RuleSet->getRules()
+ */
+ public function getAllValues(
+ $element = null,
+ $ruleSearchPatternOrSearchInFunctionArguments = null,
+ $searchInFunctionArguments = false
+ ) {
+ if (\is_bool($ruleSearchPatternOrSearchInFunctionArguments)) {
+ $searchInFunctionArguments = $ruleSearchPatternOrSearchInFunctionArguments;
+ $searchString = null;
+ } else {
+ $searchString = $ruleSearchPatternOrSearchInFunctionArguments;
+ }
+
+ if ($element === null) {
+ $element = $this;
+ } elseif (\is_string($element)) {
+ $searchString = $element;
+ $element = $this;
+ }
+
+ $result = [];
+ $this->allValues($element, $result, $searchString, $searchInFunctionArguments);
+ return $result;
+ }
+
+ /**
+ * @param CSSElement|string $oElement
* @param array $aResult
* @param string|null $sSearchString
* @param bool $bSearchInFunctionArguments
diff --git a/src/CSSList/CSSList.php b/src/CSSList/CSSList.php
index dcd8c331..18d926e1 100644
--- a/src/CSSList/CSSList.php
+++ b/src/CSSList/CSSList.php
@@ -4,11 +4,14 @@
use Sabberworm\CSS\Comment\Comment;
use Sabberworm\CSS\Comment\Commentable;
+use Sabberworm\CSS\CSSElement;
use Sabberworm\CSS\OutputFormat;
use Sabberworm\CSS\Parsing\ParserState;
use Sabberworm\CSS\Parsing\SourceException;
use Sabberworm\CSS\Parsing\UnexpectedEOFException;
use Sabberworm\CSS\Parsing\UnexpectedTokenException;
+use Sabberworm\CSS\Position\Position;
+use Sabberworm\CSS\Position\Positionable;
use Sabberworm\CSS\Property\AtRule;
use Sabberworm\CSS\Property\Charset;
use Sabberworm\CSS\Property\CSSNamespace;
@@ -29,23 +32,24 @@
*
* It can also contain `Import` and `Charset` objects stemming from at-rules.
*/
-abstract class CSSList implements Renderable, Commentable
+abstract class CSSList implements Commentable, CSSElement, Positionable
{
+ use Position;
+
/**
* @var array
+ *
+ * @internal since 8.8.0
*/
protected $aComments;
/**
* @var array
+ *
+ * @internal since 8.8.0
*/
protected $aContents;
- /**
- * @var int
- */
- protected $iLineNo;
-
/**
* @param int $iLineNo
*/
@@ -53,7 +57,7 @@ public function __construct($iLineNo = 0)
{
$this->aComments = [];
$this->aContents = [];
- $this->iLineNo = $iLineNo;
+ $this->setPosition($iLineNo);
}
/**
@@ -61,6 +65,8 @@ public function __construct($iLineNo = 0)
*
* @throws UnexpectedTokenException
* @throws SourceException
+ *
+ * @internal since V8.8.0
*/
public static function parseList(ParserState $oParserState, CSSList $oList)
{
@@ -131,18 +137,15 @@ private static function parseListItem(ParserState $oParserState, CSSList $oList)
}
return $oAtRule;
} elseif ($oParserState->comes('}')) {
- if (!$oParserState->getSettings()->bLenientParsing) {
- throw new UnexpectedTokenException('CSS selector', '}', 'identifier', $oParserState->currentLine());
- } else {
- if ($bIsRoot) {
- if ($oParserState->getSettings()->bLenientParsing) {
- return DeclarationBlock::parse($oParserState);
- } else {
- throw new SourceException("Unopened {", $oParserState->currentLine());
- }
+ if ($bIsRoot) {
+ if ($oParserState->getSettings()->bLenientParsing) {
+ return DeclarationBlock::parse($oParserState);
} else {
- return null;
+ throw new SourceException("Unopened {", $oParserState->currentLine());
}
+ } else {
+ // End of list
+ return null;
}
} else {
return DeclarationBlock::parse($oParserState, $oList);
@@ -253,14 +256,6 @@ private static function identifierIs($sIdentifier, $sMatch)
?: preg_match("/^(-\\w+-)?$sMatch$/i", $sIdentifier) === 1;
}
- /**
- * @return int
- */
- public function getLineNo()
- {
- return $this->iLineNo;
- }
-
/**
* Prepends an item to the list of contents.
*
@@ -299,6 +294,22 @@ public function splice($iOffset, $iLength = null, $mReplacement = null)
array_splice($this->aContents, $iOffset, $iLength, $mReplacement);
}
+ /**
+ * 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)
+ {
+ if (in_array($sibling, $this->aContents, true)) {
+ $this->replace($sibling, [$item, $sibling]);
+ } else {
+ $this->append($item);
+ }
+ }
+
/**
* Removes an item from the CSS list.
*
@@ -395,6 +406,8 @@ public function removeDeclarationBlockBySelector($mSelector, $bRemoveAll = false
/**
* @return string
+ *
+ * @deprecated in V8.8.0, will be removed in V9.0.0. Use `render` instead.
*/
public function __toString()
{
diff --git a/src/CSSList/Document.php b/src/CSSList/Document.php
index bad99831..ed4b09b3 100644
--- a/src/CSSList/Document.php
+++ b/src/CSSList/Document.php
@@ -8,7 +8,6 @@
use Sabberworm\CSS\Property\Selector;
use Sabberworm\CSS\RuleSet\DeclarationBlock;
use Sabberworm\CSS\RuleSet\RuleSet;
-use Sabberworm\CSS\Value\Value;
/**
* This class represents the root of a parsed CSS file. It contains all top-level CSS contents: mostly declaration
@@ -28,6 +27,8 @@ public function __construct($iLineNo = 0)
* @return Document
*
* @throws SourceException
+ *
+ * @internal since V8.8.0
*/
public static function parse(ParserState $oParserState)
{
@@ -75,33 +76,6 @@ public function getAllRuleSets()
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;
- }
-
/**
* Returns all `Selector` objects with the requested specificity found recursively in the tree.
*
@@ -128,6 +102,8 @@ public function getSelectorsBySpecificity($sSpecificitySearch = null)
* Expands all shorthand properties to their long value.
*
* @return void
+ *
+ * @deprecated since 8.7.0, will be removed without substitution in version 9.0 in #511
*/
public function expandShorthands()
{
@@ -140,6 +116,8 @@ public function expandShorthands()
* Create shorthands properties whenever possible.
*
* @return void
+ *
+ * @deprecated since 8.7.0, will be removed without substitution in version 9.0 in #511
*/
public function createShorthands()
{
@@ -155,7 +133,7 @@ public function createShorthands()
*
* @return string
*/
- public function render(OutputFormat $oOutputFormat = null)
+ public function render($oOutputFormat = null)
{
if ($oOutputFormat === null) {
$oOutputFormat = new OutputFormat();
diff --git a/src/CSSList/KeyFrame.php b/src/CSSList/KeyFrame.php
index caef7b3d..618308a7 100644
--- a/src/CSSList/KeyFrame.php
+++ b/src/CSSList/KeyFrame.php
@@ -61,6 +61,8 @@ public function getAnimationName()
/**
* @return string
+ *
+ * @deprecated in V8.8.0, will be removed in V9.0.0. Use `render` instead.
*/
public function __toString()
{
@@ -68,9 +70,11 @@ public function __toString()
}
/**
+ * @param OutputFormat|null $oOutputFormat
+ *
* @return string
*/
- public function render(OutputFormat $oOutputFormat)
+ public function render($oOutputFormat)
{
$sResult = $oOutputFormat->comments($this);
$sResult .= "@{$this->vendorKeyFrame} {$this->animationName}{$oOutputFormat->spaceBeforeOpeningBrace()}{";
diff --git a/src/Comment/Comment.php b/src/Comment/Comment.php
index 6128d749..fb571b40 100644
--- a/src/Comment/Comment.php
+++ b/src/Comment/Comment.php
@@ -4,16 +4,17 @@
use Sabberworm\CSS\OutputFormat;
use Sabberworm\CSS\Renderable;
+use Sabberworm\CSS\Position\Position;
+use Sabberworm\CSS\Position\Positionable;
-class Comment implements Renderable
+class Comment implements Positionable, Renderable
{
- /**
- * @var int
- */
- protected $iLineNo;
+ use Position;
/**
* @var string
+ *
+ * @internal since 8.8.0
*/
protected $sComment;
@@ -24,7 +25,7 @@ class Comment implements Renderable
public function __construct($sComment = '', $iLineNo = 0)
{
$this->sComment = $sComment;
- $this->iLineNo = $iLineNo;
+ $this->setPosition($iLineNo);
}
/**
@@ -35,14 +36,6 @@ public function getComment()
return $this->sComment;
}
- /**
- * @return int
- */
- public function getLineNo()
- {
- return $this->iLineNo;
- }
-
/**
* @param string $sComment
*
@@ -55,6 +48,8 @@ public function setComment($sComment)
/**
* @return string
+ *
+ * @deprecated in V8.8.0, will be removed in V9.0.0. Use `render` instead.
*/
public function __toString()
{
@@ -62,9 +57,11 @@ public function __toString()
}
/**
+ * @param OutputFormat|null $oOutputFormat
+ *
* @return string
*/
- public function render(OutputFormat $oOutputFormat)
+ public function render($oOutputFormat)
{
return '/*' . $this->sComment . '*/';
}
diff --git a/src/OutputFormat.php b/src/OutputFormat.php
index 96f26e14..9778e274 100644
--- a/src/OutputFormat.php
+++ b/src/OutputFormat.php
@@ -3,7 +3,7 @@
namespace Sabberworm\CSS;
/**
- * Class OutputFormat
+ * Extending this class is deprecated in version 8.8.0; it will be made `final` in version 9.0.0.
*
* @method OutputFormat setSemicolonAfterLastRule(bool $bSemicolonAfterLastRule) Set whether semicolons are added after
* last rule.
@@ -14,13 +14,17 @@ class OutputFormat
* Value format: `"` means double-quote, `'` means single-quote
*
* @var string
+ *
+ * @internal since 8.8.0, will be made private in 9.0.0
*/
public $sStringQuotingType = '"';
/**
* Output RGB colors in hash notation if possible
*
- * @var string
+ * @var bool
+ *
+ * @internal since 8.8.0, will be made private in 9.0.0
*/
public $bRGBHashNotation = true;
@@ -30,6 +34,8 @@ class OutputFormat
* Semicolon after the last rule of a declaration block can be omitted. To do that, set this false.
*
* @var bool
+ *
+ * @internal since 8.8.0, will be made private in 9.0.0
*/
public $bSemicolonAfterLastRule = true;
@@ -38,36 +44,52 @@ class OutputFormat
* Note that these strings are not sanity-checked: the value should only consist of whitespace
* Any newline character will be indented according to the current level.
* The triples (After, Before, Between) can be set using a wildcard (e.g. `$oFormat->set('Space*Rules', "\n");`)
+ *
+ * @var string
+ *
+ * @internal since 8.8.0, will be made private in 9.0.0
*/
public $sSpaceAfterRuleName = ' ';
/**
* @var string
+ *
+ * @internal since 8.8.0, will be made private in 9.0.0
*/
public $sSpaceBeforeRules = '';
/**
* @var string
+ *
+ * @internal since 8.8.0, will be made private in 9.0.0
*/
public $sSpaceAfterRules = '';
/**
* @var string
+ *
+ * @internal since 8.8.0, will be made private in 9.0.0
*/
public $sSpaceBetweenRules = '';
/**
* @var string
+ *
+ * @internal since 8.8.0, will be made private in 9.0.0
*/
public $sSpaceBeforeBlocks = '';
/**
* @var string
+ *
+ * @internal since 8.8.0, will be made private in 9.0.0
*/
public $sSpaceAfterBlocks = '';
/**
* @var string
+ *
+ * @internal since 8.8.0, will be made private in 9.0.0
*/
public $sSpaceBetweenBlocks = "\n";
@@ -75,11 +97,15 @@ class OutputFormat
* Content injected in and around at-rule blocks.
*
* @var string
+ *
+ * @internal since 8.8.0, will be made private in 9.0.0
*/
public $sBeforeAtRuleBlock = '';
/**
* @var string
+ *
+ * @internal since 8.8.0, will be made private in 9.0.0
*/
public $sAfterAtRuleBlock = '';
@@ -87,28 +113,64 @@ class OutputFormat
* This is what’s printed before and after the comma if a declaration block contains multiple selectors.
*
* @var string
+ *
+ * @internal since 8.8.0, will be made private in 9.0.0
*/
public $sSpaceBeforeSelectorSeparator = '';
/**
* @var string
+ *
+ * @internal since 8.8.0, will be made private in 9.0.0
*/
public $sSpaceAfterSelectorSeparator = ' ';
/**
- * 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
+ * `array` is deprecated in version 8.8.0, and will be removed in version 9.0.0.
+ * To set the spacing for specific separators, use {@see $aSpaceBeforeListArgumentSeparators} instead.
+ *
+ * @var string|array
+ *
+ * @internal since 8.8.0, will be made private in 9.0.0
*/
public $sSpaceBeforeListArgumentSeparator = '';
/**
- * @var string
+ * Keys are separators (e.g. `,`). Values are the space sequence to insert, or an empty string.
+ *
+ * @var array
+ *
+ * @internal since 8.8.0, will be made private in 9.0.0
+ */
+ public $aSpaceBeforeListArgumentSeparators = [];
+
+ /**
+ * This is what’s inserted after the separator in value lists, by default.
+ *
+ * `array` is deprecated in version 8.8.0, and will be removed in version 9.0.0.
+ * To set the spacing for specific separators, use {@see $aSpaceAfterListArgumentSeparators} instead.
+ *
+ * @var string|array
+ *
+ * @internal since 8.8.0, will be made private in 9.0.0
*/
public $sSpaceAfterListArgumentSeparator = '';
+ /**
+ * Keys are separators (e.g. `,`). Values are the space sequence to insert, or an empty string.
+ *
+ * @var array
+ *
+ * @internal since 8.8.0, will be made private in 9.0.0
+ */
+ public $aSpaceAfterListArgumentSeparators = [];
+
/**
* @var string
+ *
+ * @internal since 8.8.0, will be made private in 9.0.0
*/
public $sSpaceBeforeOpeningBrace = ' ';
@@ -116,16 +178,22 @@ class OutputFormat
* Content injected in and around declaration blocks.
*
* @var string
+ *
+ * @internal since 8.8.0, will be made private in 9.0.0
*/
public $sBeforeDeclarationBlock = '';
/**
* @var string
+ *
+ * @internal since 8.8.0, will be made private in 9.0.0
*/
public $sAfterDeclarationBlockSelectors = '';
/**
* @var string
+ *
+ * @internal since 8.8.0, will be made private in 9.0.0
*/
public $sAfterDeclarationBlock = '';
@@ -133,6 +201,8 @@ class OutputFormat
* Indentation character(s) per level. Only applicable if newlines are used in any of the spacing settings.
*
* @var string
+ *
+ * @internal since 8.8.0, will be made private in 9.0.0
*/
public $sIndentation = "\t";
@@ -140,6 +210,8 @@ class OutputFormat
* Output exceptions.
*
* @var bool
+ *
+ * @internal since 8.8.0, will be made private in 9.0.0
*/
public $bIgnoreExceptions = false;
@@ -147,6 +219,8 @@ class OutputFormat
* Render comments for lists and RuleSets
*
* @var bool
+ *
+ * @internal since 8.8.0, will be made private in 9.0.0
*/
public $bRenderComments = false;
@@ -165,6 +239,9 @@ class OutputFormat
*/
private $iIndentationLevel = 0;
+ /**
+ * @internal since V8.8.0. Use the factory methods `create()`, `createCompact()`, or `createPretty()` instead.
+ */
public function __construct()
{
}
@@ -173,6 +250,8 @@ public function __construct()
* @param string $sName
*
* @return string|null
+ *
+ * @deprecated since 8.8.0, will be removed in 9.0.0. Use specific getters instead.
*/
public function get($sName)
{
@@ -191,6 +270,8 @@ public function get($sName)
* @param mixed $mValue
*
* @return self|false
+ *
+ * @deprecated since 8.8.0, will be removed in 9.0.0. Use specific setters instead.
*/
public function set($aNames, $mValue)
{
@@ -237,6 +318,7 @@ public function __call($sMethodName, array $aArguments)
} elseif (strpos($sMethodName, 'get') === 0) {
return $this->get(substr($sMethodName, 3));
} elseif (method_exists(OutputFormatter::class, $sMethodName)) {
+ // @deprecated since 8.8.0, will be removed in 9.0.0. Call the method on the formatter directly instead.
return call_user_func_array([$this->getFormatter(), $sMethodName], $aArguments);
} else {
throw new \Exception('Unknown OutputFormat method called: ' . $sMethodName);
@@ -265,6 +347,8 @@ public function indentWithSpaces($iNumber = 2)
/**
* @return OutputFormat
+ *
+ * @internal since V8.8.0
*/
public function nextLevel()
{
@@ -286,17 +370,22 @@ public function beLenient()
/**
* @return OutputFormatter
+ *
+ * @internal since 8.8.0
*/
public function getFormatter()
{
if ($this->oFormatter === null) {
$this->oFormatter = new OutputFormatter($this);
}
+
return $this->oFormatter;
}
/**
* @return int
+ *
+ * @deprecated #869 since version V8.8.0, will be removed in V9.0.0. Use `getIndentationLevel()` instead.
*/
public function level()
{
@@ -341,7 +430,7 @@ public static function createPretty()
$format->set('Space*Rules', "\n")
->set('Space*Blocks', "\n")
->setSpaceBetweenBlocks("\n\n")
- ->set('SpaceAfterListArgumentSeparator', ['default' => '', ',' => ' '])
+ ->set('SpaceAfterListArgumentSeparators', [',' => ' '])
->setRenderComments(true);
return $format;
}
diff --git a/src/OutputFormatter.php b/src/OutputFormatter.php
index 7418494c..a436ee3b 100644
--- a/src/OutputFormatter.php
+++ b/src/OutputFormatter.php
@@ -5,6 +5,9 @@
use Sabberworm\CSS\Comment\Commentable;
use Sabberworm\CSS\Parsing\OutputException;
+/**
+ * @internal since 8.8.0
+ */
class OutputFormatter
{
/**
@@ -117,6 +120,11 @@ public function spaceAfterSelectorSeparator()
*/
public function spaceBeforeListArgumentSeparator($sSeparator)
{
+ $spaceForSeparator = $this->oFormat->getSpaceBeforeListArgumentSeparators();
+ if (isset($spaceForSeparator[$sSeparator])) {
+ return $spaceForSeparator[$sSeparator];
+ }
+
return $this->space('BeforeListArgumentSeparator', $sSeparator);
}
@@ -127,6 +135,11 @@ public function spaceBeforeListArgumentSeparator($sSeparator)
*/
public function spaceAfterListArgumentSeparator($sSeparator)
{
+ $spaceForSeparator = $this->oFormat->getSpaceAfterListArgumentSeparators();
+ if (isset($spaceForSeparator[$sSeparator])) {
+ return $spaceForSeparator[$sSeparator];
+ }
+
return $this->space('AfterListArgumentSeparator', $sSeparator);
}
@@ -215,6 +228,7 @@ public function removeLastSemicolon($sString)
/**
*
* @param array $aComments
+ *
* @return string
*/
public function comments(Commentable $oCommentable)
@@ -249,6 +263,6 @@ private function prepareSpace($sSpaceString)
*/
private function indent()
{
- return str_repeat($this->oFormat->sIndentation, $this->oFormat->level());
+ return str_repeat($this->oFormat->sIndentation, $this->oFormat->getIndentationLevel());
}
}
diff --git a/src/Parser.php b/src/Parser.php
index e582cfab..f60fc086 100644
--- a/src/Parser.php
+++ b/src/Parser.php
@@ -21,7 +21,7 @@ class Parser
* @param Settings|null $oParserSettings
* @param int $iLineNo the line number (starting from 1, not from 0)
*/
- public function __construct($sText, Settings $oParserSettings = null, $iLineNo = 1)
+ public function __construct($sText, $oParserSettings = null, $iLineNo = 1)
{
if ($oParserSettings === null) {
$oParserSettings = Settings::create();
@@ -35,6 +35,8 @@ public function __construct($sText, Settings $oParserSettings = null, $iLineNo =
* @param string $sCharset
*
* @return void
+ *
+ * @deprecated since 8.7.0, will be removed in version 9.0.0 with #687
*/
public function setCharset($sCharset)
{
@@ -45,6 +47,8 @@ public function setCharset($sCharset)
* Returns the charset that is used if the CSS does not contain an `@charset` declaration.
*
* @return void
+ *
+ * @deprecated since 8.7.0, will be removed in version 9.0.0 with #687
*/
public function getCharset()
{
diff --git a/src/Parsing/Anchor.php b/src/Parsing/Anchor.php
index 93789e26..a42893da 100644
--- a/src/Parsing/Anchor.php
+++ b/src/Parsing/Anchor.php
@@ -2,6 +2,9 @@
namespace Sabberworm\CSS\Parsing;
+/**
+ * @internal since 8.7.0
+ */
class Anchor
{
/**
diff --git a/src/Parsing/ParserState.php b/src/Parsing/ParserState.php
index 7a99f327..574f60f3 100644
--- a/src/Parsing/ParserState.php
+++ b/src/Parsing/ParserState.php
@@ -5,10 +5,15 @@
use Sabberworm\CSS\Comment\Comment;
use Sabberworm\CSS\Settings;
+/**
+ * @internal since 8.7.0
+ */
class ParserState
{
/**
* @var null
+ *
+ * @internal since 8.5.2
*/
const EOF = null;
@@ -136,6 +141,8 @@ public function setPosition($iPosition)
* @return string
*
* @throws UnexpectedTokenException
+ *
+ * @internal since V8.8.0
*/
public function parseIdentifier($bIgnoreCase = true)
{
@@ -167,6 +174,8 @@ public function parseIdentifier($bIgnoreCase = true)
*
* @throws UnexpectedEOFException
* @throws UnexpectedTokenException
+ *
+ * @internal since V8.8.0
*/
public function parseCharacter($bIsForIdentifier)
{
diff --git a/src/Parsing/SourceException.php b/src/Parsing/SourceException.php
index 1ca668a9..1aa27b43 100644
--- a/src/Parsing/SourceException.php
+++ b/src/Parsing/SourceException.php
@@ -2,12 +2,12 @@
namespace Sabberworm\CSS\Parsing;
-class SourceException extends \Exception
+use Sabberworm\CSS\Position\Position;
+use Sabberworm\CSS\Position\Positionable;
+
+class SourceException extends \Exception implements Positionable
{
- /**
- * @var int
- */
- private $iLineNo;
+ use Position;
/**
* @param string $sMessage
@@ -15,18 +15,10 @@ class SourceException extends \Exception
*/
public function __construct($sMessage, $iLineNo = 0)
{
- $this->iLineNo = $iLineNo;
+ $this->setPosition($iLineNo);
if (!empty($iLineNo)) {
$sMessage .= " [line no: $iLineNo]";
}
parent::__construct($sMessage);
}
-
- /**
- * @return int
- */
- public function getLineNo()
- {
- return $this->iLineNo;
- }
}
diff --git a/src/Position/Position.php b/src/Position/Position.php
new file mode 100644
index 00000000..1c4d0df0
--- /dev/null
+++ b/src/Position/Position.php
@@ -0,0 +1,72 @@
+|null
+ */
+ protected $lineNumber;
+
+ /**
+ * @var int<0, max>|null
+ */
+ protected $columnNumber;
+
+ /**
+ * @return int<1, max>|null
+ */
+ public function getLineNumber()
+ {
+ return $this->lineNumber;
+ }
+
+ /**
+ * @return int<0, max>
+ */
+ public function getLineNo()
+ {
+ $lineNumber = $this->getLineNumber();
+
+ return $lineNumber !== null ? $lineNumber : 0;
+ }
+
+ /**
+ * @return int<0, max>|null
+ */
+ public function getColumnNumber()
+ {
+ return $this->columnNumber;
+ }
+
+ /**
+ * @return int<0, max>
+ */
+ public function getColNo()
+ {
+ $columnNumber = $this->getColumnNumber();
+
+ return $columnNumber !== null ? $columnNumber : 0;
+ }
+
+ /**
+ * @param int<0, max>|null $lineNumber
+ * @param int<0, max>|null $columnNumber
+ */
+ public function setPosition($lineNumber, $columnNumber = null)
+ {
+ // The conditional is for backwards compatibility (backcompat); `0` will not be allowed in future.
+ $this->lineNumber = $lineNumber !== 0 ? $lineNumber : null;
+ $this->columnNumber = $columnNumber;
+ }
+}
diff --git a/src/Position/Positionable.php b/src/Position/Positionable.php
new file mode 100644
index 00000000..4539c425
--- /dev/null
+++ b/src/Position/Positionable.php
@@ -0,0 +1,45 @@
+|null
+ */
+ public function getLineNumber();
+
+ /**
+ * @return int<0, max>
+ *
+ * @deprecated in version 8.9.0, will be removed in v9.0. Use `getLineNumber()` instead.
+ */
+ public function getLineNo();
+
+ /**
+ * @return int<0, max>|null
+ */
+ public function getColumnNumber();
+
+ /**
+ * @return int<0, max>
+ *
+ * @deprecated in version 8.9.0, will be removed in v9.0. Use `getColumnNumber()` instead.
+ */
+ public function getColNo();
+
+ /**
+ * @param int<0, max>|null $lineNumber
+ * Providing zero for this parameter is deprecated in version 8.9.0, and will not be supported from v9.0.
+ * Use `null` instead when no line number is available.
+ * @param int<0, max>|null $columnNumber
+ */
+ public function setPosition($lineNumber, $columnNumber = null);
+}
diff --git a/src/Property/AtRule.php b/src/Property/AtRule.php
index 9536ff5e..d946a904 100644
--- a/src/Property/AtRule.php
+++ b/src/Property/AtRule.php
@@ -12,6 +12,8 @@ interface AtRule extends Renderable, Commentable
* we’re whitelisting the block rules and have anything else be treated as a set rule.
*
* @var string
+ *
+ * @internal since 8.5.2
*/
const BLOCK_RULES = 'media/document/supports/region-style/font-feature-values';
@@ -19,6 +21,8 @@ interface AtRule extends Renderable, Commentable
* … and more font-specific ones (to be used inside font-feature-values)
*
* @var string
+ *
+ * @internal since 8.5.2
*/
const SET_RULES = 'font-face/counter-style/page/swash/styleset/annotation';
diff --git a/src/Property/CSSNamespace.php b/src/Property/CSSNamespace.php
index 0d7eb496..188d3581 100644
--- a/src/Property/CSSNamespace.php
+++ b/src/Property/CSSNamespace.php
@@ -4,12 +4,16 @@
use Sabberworm\CSS\Comment\Comment;
use Sabberworm\CSS\OutputFormat;
+use Sabberworm\CSS\Position\Position;
+use Sabberworm\CSS\Position\Positionable;
/**
* `CSSNamespace` represents an `@namespace` rule.
*/
-class CSSNamespace implements AtRule
+class CSSNamespace implements AtRule, Positionable
{
+ use Position;
+
/**
* @var string
*/
@@ -27,6 +31,8 @@ class CSSNamespace implements AtRule
/**
* @var array
+ *
+ * @internal since 8.8.0
*/
protected $aComments;
@@ -39,20 +45,14 @@ public function __construct($mUrl, $sPrefix = null, $iLineNo = 0)
{
$this->mUrl = $mUrl;
$this->sPrefix = $sPrefix;
- $this->iLineNo = $iLineNo;
+ $this->setPosition($iLineNo);
$this->aComments = [];
}
- /**
- * @return int
- */
- public function getLineNo()
- {
- return $this->iLineNo;
- }
-
/**
* @return string
+ *
+ * @deprecated in V8.8.0, will be removed in V9.0.0. Use `render` instead.
*/
public function __toString()
{
@@ -60,9 +60,11 @@ public function __toString()
}
/**
+ * @param OutputFormat|null $oOutputFormat
+ *
* @return string
*/
- public function render(OutputFormat $oOutputFormat)
+ public function render($oOutputFormat)
{
return '@namespace ' . ($this->sPrefix === null ? '' : $this->sPrefix . ' ')
. $this->mUrl->render($oOutputFormat) . ';';
diff --git a/src/Property/Charset.php b/src/Property/Charset.php
index 26e1b250..1ebff3f3 100644
--- a/src/Property/Charset.php
+++ b/src/Property/Charset.php
@@ -4,6 +4,8 @@
use Sabberworm\CSS\Comment\Comment;
use Sabberworm\CSS\OutputFormat;
+use Sabberworm\CSS\Position\Position;
+use Sabberworm\CSS\Position\Positionable;
use Sabberworm\CSS\Value\CSSString;
/**
@@ -14,8 +16,10 @@
* - May only appear at the very top of a Document’s contents.
* - Must not appear more than once.
*/
-class Charset implements AtRule
+class Charset implements AtRule, Positionable
{
+ use Position;
+
/**
* @var CSSString
*/
@@ -23,11 +27,15 @@ class Charset implements AtRule
/**
* @var int
+ *
+ * @internal since 8.8.0
*/
protected $iLineNo;
/**
* @var array
+ *
+ * @internal since 8.8.0
*/
protected $aComments;
@@ -38,18 +46,10 @@ class Charset implements AtRule
public function __construct(CSSString $oCharset, $iLineNo = 0)
{
$this->oCharset = $oCharset;
- $this->iLineNo = $iLineNo;
+ $this->setPosition($iLineNo);
$this->aComments = [];
}
- /**
- * @return int
- */
- public function getLineNo()
- {
- return $this->iLineNo;
- }
-
/**
* @param string|CSSString $oCharset
*
@@ -71,6 +71,8 @@ public function getCharset()
/**
* @return string
+ *
+ * @deprecated in V8.8.0, will be removed in V9.0.0. Use `render` instead.
*/
public function __toString()
{
@@ -78,9 +80,11 @@ public function __toString()
}
/**
+ * @param OutputFormat|null $oOutputFormat
+ *
* @return string
*/
- public function render(OutputFormat $oOutputFormat)
+ public function render($oOutputFormat)
{
return "{$oOutputFormat->comments($this)}@charset {$this->oCharset->render($oOutputFormat)};";
}
diff --git a/src/Property/Import.php b/src/Property/Import.php
index d715a7a0..5b474493 100644
--- a/src/Property/Import.php
+++ b/src/Property/Import.php
@@ -4,13 +4,17 @@
use Sabberworm\CSS\Comment\Comment;
use Sabberworm\CSS\OutputFormat;
+use Sabberworm\CSS\Position\Position;
+use Sabberworm\CSS\Position\Positionable;
use Sabberworm\CSS\Value\URL;
/**
* Class representing an `@import` rule.
*/
-class Import implements AtRule
+class Import implements AtRule, Positionable
{
+ use Position;
+
/**
* @var URL
*/
@@ -21,13 +25,10 @@ class Import implements AtRule
*/
private $sMediaQuery;
- /**
- * @var int
- */
- protected $iLineNo;
-
/**
* @var array
+ *
+ * @internal since 8.8.0
*/
protected $aComments;
@@ -40,18 +41,10 @@ public function __construct(URL $oLocation, $sMediaQuery, $iLineNo = 0)
{
$this->oLocation = $oLocation;
$this->sMediaQuery = $sMediaQuery;
- $this->iLineNo = $iLineNo;
+ $this->setPosition($iLineNo);
$this->aComments = [];
}
- /**
- * @return int
- */
- public function getLineNo()
- {
- return $this->iLineNo;
- }
-
/**
* @param URL $oLocation
*
@@ -72,6 +65,8 @@ public function getLocation()
/**
* @return string
+ *
+ * @deprecated in V8.8.0, will be removed in V9.0.0. Use `render` instead.
*/
public function __toString()
{
@@ -79,9 +74,11 @@ public function __toString()
}
/**
+ * @param OutputFormat|null $oOutputFormat
+ *
* @return string
*/
- public function render(OutputFormat $oOutputFormat)
+ public function render($oOutputFormat)
{
return $oOutputFormat->comments($this) . "@import " . $this->oLocation->render($oOutputFormat)
. ($this->sMediaQuery === null ? '' : ' ' . $this->sMediaQuery) . ';';
diff --git a/src/Property/KeyframeSelector.php b/src/Property/KeyframeSelector.php
index 14ea5ebb..2aff8d24 100644
--- a/src/Property/KeyframeSelector.php
+++ b/src/Property/KeyframeSelector.php
@@ -8,6 +8,8 @@ class KeyframeSelector extends Selector
* regexp for specificity calculations
*
* @var string
+ *
+ * @internal since 8.5.2
*/
const SELECTOR_VALIDATION_RX = '/
^(
diff --git a/src/Property/Selector.php b/src/Property/Selector.php
index 70c9b2fd..a0611688 100644
--- a/src/Property/Selector.php
+++ b/src/Property/Selector.php
@@ -12,6 +12,8 @@ class Selector
* regexp for specificity calculations
*
* @var string
+ *
+ * @internal
*/
const NON_ID_ATTRIBUTES_AND_PSEUDO_CLASSES_RX = '/
(\.[\w]+) # classes
@@ -36,6 +38,8 @@ class Selector
* regexp for specificity calculations
*
* @var string
+ *
+ * @internal
*/
const ELEMENTS_AND_PSEUDO_ELEMENTS_RX = '/
((^|[\s\+\>\~]+)[\w]+ # elements
@@ -49,6 +53,8 @@ class Selector
* regexp for specificity calculations
*
* @var string
+ *
+ * @internal since 8.5.2
*/
const SELECTOR_VALIDATION_RX = '/
^(
@@ -74,6 +80,8 @@ class Selector
* @param string $sSelector
*
* @return bool
+ *
+ * @internal since V8.8.0
*/
public static function isValid($sSelector)
{
@@ -82,7 +90,7 @@ public static function isValid($sSelector)
/**
* @param string $sSelector
- * @param bool $bCalculateSpecificity
+ * @param bool $bCalculateSpecificity @deprecated since V8.8.0, will be removed in V9.0.0
*/
public function __construct($sSelector, $bCalculateSpecificity = false)
{
@@ -113,6 +121,8 @@ public function setSelector($sSelector)
/**
* @return string
+ *
+ * @deprecated in V8.8.0, will be removed in V9.0.0. Use `render` instead.
*/
public function __toString()
{
diff --git a/src/Renderable.php b/src/Renderable.php
index dc1bff3c..1f1b475d 100644
--- a/src/Renderable.php
+++ b/src/Renderable.php
@@ -6,13 +6,17 @@ interface Renderable
{
/**
* @return string
+ *
+ * @deprecated in V8.8.0, will be removed in V9.0.0. Use `render` instead.
*/
public function __toString();
/**
+ * @param OutputFormat|null $oOutputFormat
+ *
* @return string
*/
- public function render(OutputFormat $oOutputFormat);
+ public function render($oOutputFormat);
/**
* @return int
diff --git a/src/Rule/Rule.php b/src/Rule/Rule.php
index fc00c880..a34018a8 100644
--- a/src/Rule/Rule.php
+++ b/src/Rule/Rule.php
@@ -4,11 +4,13 @@
use Sabberworm\CSS\Comment\Comment;
use Sabberworm\CSS\Comment\Commentable;
+use Sabberworm\CSS\CSSElement;
use Sabberworm\CSS\OutputFormat;
use Sabberworm\CSS\Parsing\ParserState;
use Sabberworm\CSS\Parsing\UnexpectedEOFException;
use Sabberworm\CSS\Parsing\UnexpectedTokenException;
-use Sabberworm\CSS\Renderable;
+use Sabberworm\CSS\Position\Position;
+use Sabberworm\CSS\Position\Positionable;
use Sabberworm\CSS\Value\RuleValueList;
use Sabberworm\CSS\Value\Value;
@@ -17,8 +19,10 @@
*
* In CSS, `Rule`s are expressed as follows: “key: value[0][0] value[0][1], value[1][0] value[1][1];”
*/
-class Rule implements Renderable, Commentable
+class Rule implements Commentable, CSSElement, Positionable
{
+ use Position;
+
/**
* @var string
*/
@@ -39,18 +43,10 @@ class Rule implements Renderable, Commentable
*/
private $aIeHack;
- /**
- * @var int
- */
- protected $iLineNo;
-
- /**
- * @var int
- */
- protected $iColNo;
-
/**
* @var array
+ *
+ * @internal since 8.8.0
*/
protected $aComments;
@@ -65,20 +61,23 @@ public function __construct($sRule, $iLineNo = 0, $iColNo = 0)
$this->mValue = null;
$this->bIsImportant = false;
$this->aIeHack = [];
- $this->iLineNo = $iLineNo;
- $this->iColNo = $iColNo;
+ $this->setPosition($iLineNo, $iColNo);
$this->aComments = [];
}
/**
+ * @param array $commentsBeforeRule
+ *
* @return Rule
*
* @throws UnexpectedEOFException
* @throws UnexpectedTokenException
+ *
+ * @internal since V8.8.0
*/
- public static function parse(ParserState $oParserState)
+ public static function parse(ParserState $oParserState, $commentsBeforeRule = [])
{
- $aComments = $oParserState->consumeWhiteSpace();
+ $aComments = \array_merge($commentsBeforeRule, $oParserState->consumeWhiteSpace());
$oRule = new Rule(
$oParserState->parseIdentifier(!$oParserState->comes("--")),
$oParserState->currentLine(),
@@ -107,50 +106,31 @@ public static function parse(ParserState $oParserState)
while ($oParserState->comes(';')) {
$oParserState->consume(';');
}
- $oParserState->consumeWhiteSpace();
return $oRule;
}
/**
+ * 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 string $sRule
*
- * @return array
+ * @return list
*/
private static function listDelimiterForRule($sRule)
{
if (preg_match('/^font($|-)/', $sRule)) {
return [',', '/', ' '];
}
- return [',', ' ', '/'];
- }
-
- /**
- * @return int
- */
- public function getLineNo()
- {
- return $this->iLineNo;
- }
- /**
- * @return int
- */
- public function getColNo()
- {
- return $this->iColNo;
- }
-
- /**
- * @param int $iLine
- * @param int $iColumn
- *
- * @return void
- */
- public function setPosition($iLine, $iColumn)
- {
- $this->iColNo = $iColumn;
- $this->iLineNo = $iLine;
+ switch ($sRule) {
+ case 'src':
+ return [' ', ','];
+ default:
+ return [',', ' ', '/'];
+ }
}
/**
@@ -278,7 +258,7 @@ public function addValue($mValue, $sType = ' ')
}
if (!$this->mValue instanceof RuleValueList || $this->mValue->getListSeparator() !== $sType) {
$mCurrentValue = $this->mValue;
- $this->mValue = new RuleValueList($sType, $this->iLineNo);
+ $this->mValue = new RuleValueList($sType, $this->getLineNumber());
if ($mCurrentValue) {
$this->mValue->addListComponent($mCurrentValue);
}
@@ -292,6 +272,8 @@ public function addValue($mValue, $sType = ' ')
* @param int $iModifier
*
* @return void
+ *
+ * @deprecated since V8.8.0, will be removed in V9.0
*/
public function addIeHack($iModifier)
{
@@ -302,6 +284,8 @@ public function addIeHack($iModifier)
* @param array $aModifiers
*
* @return void
+ *
+ * @deprecated since V8.8.0, will be removed in V9.0
*/
public function setIeHack(array $aModifiers)
{
@@ -310,6 +294,8 @@ public function setIeHack(array $aModifiers)
/**
* @return array
+ *
+ * @deprecated since V8.8.0, will be removed in V9.0
*/
public function getIeHack()
{
@@ -336,6 +322,8 @@ public function getIsImportant()
/**
* @return string
+ *
+ * @deprecated in V8.8.0, will be removed in V9.0.0. Use `render` instead.
*/
public function __toString()
{
@@ -343,9 +331,11 @@ public function __toString()
}
/**
+ * @param OutputFormat|null $oOutputFormat
+ *
* @return string
*/
- public function render(OutputFormat $oOutputFormat)
+ public function render($oOutputFormat)
{
$sResult = "{$oOutputFormat->comments($this)}{$this->sRule}:{$oOutputFormat->spaceAfterRuleName()}";
if ($this->mValue instanceof Value) { // Can also be a ValueList
diff --git a/src/RuleSet/AtRuleSet.php b/src/RuleSet/AtRuleSet.php
index aab6d799..e687cb96 100644
--- a/src/RuleSet/AtRuleSet.php
+++ b/src/RuleSet/AtRuleSet.php
@@ -53,6 +53,8 @@ public function atRuleArgs()
/**
* @return string
+ *
+ * @deprecated in V8.8.0, will be removed in V9.0.0. Use `render` instead.
*/
public function __toString()
{
@@ -60,9 +62,11 @@ public function __toString()
}
/**
+ * @param OutputFormat|null $oOutputFormat
+ *
* @return string
*/
- public function render(OutputFormat $oOutputFormat)
+ public function render($oOutputFormat)
{
$sResult = $oOutputFormat->comments($this);
$sArgs = $this->sArgs;
diff --git a/src/RuleSet/DeclarationBlock.php b/src/RuleSet/DeclarationBlock.php
index de487bc1..9cc14ebf 100644
--- a/src/RuleSet/DeclarationBlock.php
+++ b/src/RuleSet/DeclarationBlock.php
@@ -49,6 +49,8 @@ public function __construct($iLineNo = 0)
*
* @throws UnexpectedTokenException
* @throws UnexpectedEOFException
+ *
+ * @internal since V8.8.0
*/
public static function parse(ParserState $oParserState, $oList = null)
{
@@ -181,6 +183,8 @@ public function getSelectors()
* Splits shorthand declarations (e.g. `margin` or `font`) into their constituent parts.
*
* @return void
+ *
+ * @deprecated since 8.7.0, will be removed without substitution in version 9.0 in #511
*/
public function expandShorthands()
{
@@ -196,6 +200,8 @@ public function expandShorthands()
* Creates shorthand declarations (e.g. `margin` or `font`) whenever possible.
*
* @return void
+ *
+ * @deprecated since 8.7.0, will be removed without substitution in version 9.0 in #511
*/
public function createShorthands()
{
@@ -215,6 +221,8 @@ public function createShorthands()
* Multiple borders are not yet supported as of 3.
*
* @return void
+ *
+ * @deprecated since 8.7.0, will be removed without substitution in version 9.0 in #511
*/
public function expandBorderShorthand()
{
@@ -276,6 +284,8 @@ public function expandBorderShorthand()
* Handles `margin`, `padding`, `border-color`, `border-style` and `border-width`.
*
* @return void
+ *
+ * @deprecated since 8.7.0, will be removed without substitution in version 9.0 in #511
*/
public function expandDimensionsShorthand()
{
@@ -336,6 +346,8 @@ public function expandDimensionsShorthand()
* into their constituent parts.
*
* @return void
+ *
+ * @deprecated since 8.7.0, will be removed without substitution in version 9.0 in #511
*/
public function expandFontShorthand()
{
@@ -406,6 +418,8 @@ public function expandFontShorthand()
* @see http://www.w3.org/TR/21/colors.html#propdef-background
*
* @return void
+ *
+ * @deprecated since 8.7.0, will be removed without substitution in version 9.0 in #511
*/
public function expandBackgroundShorthand()
{
@@ -420,8 +434,8 @@ public function expandBackgroundShorthand()
'background-repeat' => ['repeat'],
'background-attachment' => ['scroll'],
'background-position' => [
- new Size(0, '%', null, false, $this->iLineNo),
- new Size(0, '%', null, false, $this->iLineNo),
+ new Size(0, '%', false, $this->getLineNo()),
+ new Size(0, '%', false, $this->getLineNo()),
],
];
$mRuleValue = $oRule->getValue();
@@ -478,6 +492,8 @@ public function expandBackgroundShorthand()
/**
* @return void
+ *
+ * @deprecated since 8.7.0, will be removed without substitution in version 9.0 in #511
*/
public function expandListStyleShorthand()
{
@@ -561,10 +577,13 @@ public function expandListStyleShorthand()
* @param string $sShorthand
*
* @return void
+ *
+ * @deprecated since 8.7.0, will be removed without substitution in version 9.0 in #511
*/
public function createShorthandProperties(array $aProperties, $sShorthand)
{
$aRules = $this->getRulesAssoc();
+ $oRule = null;
$aNewValues = [];
foreach ($aProperties as $sProperty) {
if (!isset($aRules[$sProperty])) {
@@ -585,7 +604,7 @@ public function createShorthandProperties(array $aProperties, $sShorthand)
$this->removeRule($sProperty);
}
}
- if (count($aNewValues)) {
+ if ($aNewValues !== [] && $oRule instanceof Rule) {
$oNewRule = new Rule($sShorthand, $oRule->getLineNo(), $oRule->getColNo());
foreach ($aNewValues as $mValue) {
$oNewRule->addValue($mValue);
@@ -596,6 +615,8 @@ public function createShorthandProperties(array $aProperties, $sShorthand)
/**
* @return void
+ *
+ * @deprecated since 8.7.0, will be removed without substitution in version 9.0 in #511
*/
public function createBackgroundShorthand()
{
@@ -611,6 +632,8 @@ public function createBackgroundShorthand()
/**
* @return void
+ *
+ * @deprecated since 8.7.0, will be removed without substitution in version 9.0 in #511
*/
public function createListStyleShorthand()
{
@@ -628,6 +651,8 @@ public function createListStyleShorthand()
* Should be run after `create_dimensions_shorthand`!
*
* @return void
+ *
+ * @deprecated since 8.7.0, will be removed without substitution in version 9.0 in #511
*/
public function createBorderShorthand()
{
@@ -645,6 +670,8 @@ public function createBorderShorthand()
* and converts them into shorthand CSS properties.
*
* @return void
+ *
+ * @deprecated since 8.7.0, will be removed without substitution in version 9.0 in #511
*/
public function createDimensionsShorthand()
{
@@ -719,6 +746,8 @@ public function createDimensionsShorthand()
* At least `font-size` AND `font-family` must be present in order to create a shorthand declaration.
*
* @return void
+ *
+ * @deprecated since 8.7.0, will be removed without substitution in version 9.0 in #511
*/
public function createFontShorthand()
{
@@ -772,7 +801,7 @@ public function createFontShorthand()
$aLHValues = $mRuleValue->getListComponents();
}
if ($aLHValues[0] !== 'normal') {
- $val = new RuleValueList('/', $this->iLineNo);
+ $val = new RuleValueList('/', $this->getLineNo());
$val->addListComponent($aFSValues[0]);
$val->addListComponent($aLHValues[0]);
$oNewRule->addValue($val);
@@ -788,7 +817,7 @@ public function createFontShorthand()
} else {
$aFFValues = $mRuleValue->getListComponents();
}
- $oFFValue = new RuleValueList(',', $this->iLineNo);
+ $oFFValue = new RuleValueList(',', $this->getLineNo());
$oFFValue->setListComponents($aFFValues);
$oNewRule->addValue($oFFValue);
@@ -802,6 +831,8 @@ public function createFontShorthand()
* @return string
*
* @throws OutputException
+ *
+ * @deprecated in V8.8.0, will be removed in V9.0.0. Use `render` instead.
*/
public function __toString()
{
@@ -809,16 +840,21 @@ public function __toString()
}
/**
+ * @param OutputFormat|null $oOutputFormat
+ *
* @return string
*
* @throws OutputException
*/
- public function render(OutputFormat $oOutputFormat)
+ public function render($oOutputFormat)
{
$sResult = $oOutputFormat->comments($this);
if (count($this->aSelectors) === 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(
diff --git a/src/RuleSet/RuleSet.php b/src/RuleSet/RuleSet.php
index adb9be92..0110f50e 100644
--- a/src/RuleSet/RuleSet.php
+++ b/src/RuleSet/RuleSet.php
@@ -4,10 +4,13 @@
use Sabberworm\CSS\Comment\Comment;
use Sabberworm\CSS\Comment\Commentable;
+use Sabberworm\CSS\CSSElement;
use Sabberworm\CSS\OutputFormat;
use Sabberworm\CSS\Parsing\ParserState;
use Sabberworm\CSS\Parsing\UnexpectedEOFException;
use Sabberworm\CSS\Parsing\UnexpectedTokenException;
+use Sabberworm\CSS\Position\Position;
+use Sabberworm\CSS\Position\Positionable;
use Sabberworm\CSS\Renderable;
use Sabberworm\CSS\Rule\Rule;
@@ -20,20 +23,22 @@
* If you want to manipulate a `RuleSet`, use the methods `addRule(Rule $rule)`, `getRules()` and `removeRule($rule)`
* (which accepts either a `Rule` or a rule name; optionally suffixed by a dash to remove all related rules).
*/
-abstract class RuleSet implements Renderable, Commentable
+abstract class RuleSet implements CSSElement, Commentable, Positionable
{
- /**
- * @var array
- */
- private $aRules;
+ use Position;
/**
- * @var int
+ * the rules in this rule set, using the property name as the key,
+ * with potentially multiple rules per property name.
+ *
+ * @var array, Rule>>
*/
- protected $iLineNo;
+ private $aRules;
/**
* @var array
+ *
+ * @internal since 8.8.0
*/
protected $aComments;
@@ -43,7 +48,7 @@ abstract class RuleSet implements Renderable, Commentable
public function __construct($iLineNo = 0)
{
$this->aRules = [];
- $this->iLineNo = $iLineNo;
+ $this->setPosition($iLineNo);
$this->aComments = [];
}
@@ -52,17 +57,23 @@ public function __construct($iLineNo = 0)
*
* @throws UnexpectedTokenException
* @throws UnexpectedEOFException
+ *
+ * @internal since V8.8.0
*/
public static function parseRuleSet(ParserState $oParserState, RuleSet $oRuleSet)
{
while ($oParserState->comes(';')) {
$oParserState->consume(';');
}
- while (!$oParserState->comes('}')) {
+ while (true) {
+ $commentsBeforeRule = $oParserState->consumeWhiteSpace();
+ if ($oParserState->comes('}')) {
+ break;
+ }
$oRule = null;
if ($oParserState->getSettings()->bLenientParsing) {
try {
- $oRule = Rule::parse($oParserState);
+ $oRule = Rule::parse($oParserState, $commentsBeforeRule);
} catch (UnexpectedTokenException $e) {
try {
$sConsume = $oParserState->consumeUntil(["\n", ";", '}'], true);
@@ -80,7 +91,7 @@ public static function parseRuleSet(ParserState $oParserState, RuleSet $oRuleSet
}
}
} else {
- $oRule = Rule::parse($oParserState);
+ $oRule = Rule::parse($oParserState, $commentsBeforeRule);
}
if ($oRule) {
$oRuleSet->addRule($oRule);
@@ -89,20 +100,12 @@ public static function parseRuleSet(ParserState $oParserState, RuleSet $oRuleSet
$oParserState->consume('}');
}
- /**
- * @return int
- */
- public function getLineNo()
- {
- return $this->iLineNo;
- }
-
/**
* @param Rule|null $oSibling
*
* @return void
*/
- public function addRule(Rule $oRule, Rule $oSibling = null)
+ public function addRule(Rule $oRule, $oSibling = null)
{
$sRule = $oRule->getRule();
if (!isset($this->aRules[$sRule])) {
@@ -118,14 +121,19 @@ public function addRule(Rule $oRule, Rule $oSibling = null)
$oRule->setPosition($oSibling->getLineNo(), $oSibling->getColNo() - 1);
}
}
- if ($oRule->getLineNo() === 0 && $oRule->getColNo() === 0) {
+ if ($oRule->getLineNumber() === null) {
//this node is added manually, give it the next best line
+ $columnNumber = $oRule->getColNo();
$rules = $this->getRules();
$pos = count($rules);
if ($pos > 0) {
$last = $rules[$pos - 1];
- $oRule->setPosition($last->getLineNo() + 1, 0);
+ $oRule->setPosition($last->getLineNo() + 1, $columnNumber);
+ } else {
+ $oRule->setPosition(1, $columnNumber);
}
+ } elseif ($oRule->getColumnNumber() === null) {
+ $oRule->setPosition($oRule->getLineNumber(), 0);
}
array_splice($this->aRules[$sRule], $iPosition, 0, [$oRule]);
@@ -143,7 +151,8 @@ public function addRule(Rule $oRule, Rule $oSibling = null)
* 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())`.
+ * Passing a `Rule` for this parameter is deprecated in version 8.9.0, and will not work from v9.0.
+ * Call `getRules($rule->getRule())` instead.
*
* @return array
*/
@@ -202,7 +211,9 @@ public function setRules(array $aRules)
* @param Rule|string|null $mRule $mRule
* 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.
+ * Passing a `Rule` for this parameter is deprecated in version 8.9.0, and will not work from v9.0.
+ * Call `getRulesAssoc($rule->getRule())` instead.
*
* @return array
*/
@@ -217,20 +228,12 @@ public function getRulesAssoc($mRule = null)
}
/**
- * 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.
*
* @param Rule|string|null $mRule
- * pattern to remove. If $mRule is null, all rules are removed. 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.
- *
- * @return void
+ * `Rule` to remove.
+ * Passing a `string` or `null` is deprecated in version 8.9.0, and will no longer work from v9.0.
+ * Use `removeMatchingRules()` or `removeAllRules()` instead.
*/
public function removeRule($mRule)
{
@@ -244,24 +247,48 @@ public function removeRule($mRule)
unset($this->aRules[$sRule][$iKey]);
}
}
+ } elseif ($mRule !== null) {
+ $this->removeMatchingRules($mRule);
} 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]);
- }
+ $this->removeAllRules();
+ }
+ }
+
+ /**
+ * Removes rules by property name or search pattern.
+ *
+ * @param string $searchPattern
+ * pattern to remove.
+ * If the pattern ends in a dash,
+ * all rules starting with the pattern are removed as well as one matching the pattern with the dash
+ * excluded.
+ */
+ public function removeMatchingRules($searchPattern)
+ {
+ foreach ($this->aRules 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->aRules[$propertyName]);
}
}
}
+ public function removeAllRules()
+ {
+ $this->aRules = [];
+ }
+
/**
* @return string
+ *
+ * @deprecated in V8.8.0, will be removed in V9.0.0. Use `render` instead.
*/
public function __toString()
{
@@ -276,22 +303,20 @@ protected function renderRules(OutputFormat $oOutputFormat)
$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;
+ foreach ($this->getRules() 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;
}
if (!$bIsFirst) {
diff --git a/src/Settings.php b/src/Settings.php
index 79d99803..8d4bd468 100644
--- a/src/Settings.php
+++ b/src/Settings.php
@@ -16,6 +16,8 @@ class Settings
* and `mb_strpos` functions. Otherwise, the normal (ASCII-Only) functions will be used.
*
* @var bool
+ *
+ * @internal since 8.8.0, will be made private in 9.0.0
*/
public $bMultibyteSupport;
@@ -23,6 +25,8 @@ class Settings
* The default charset for the CSS if no `@charset` declaration is found. Defaults to utf-8.
*
* @var string
+ *
+ * @internal since 8.8.0, will be made private in 9.0.0
*/
public $sDefaultCharset = 'utf-8';
@@ -30,6 +34,8 @@ class Settings
* Whether the parser silently ignore invalid rules instead of choking on them.
*
* @var bool
+ *
+ * @internal since 8.8.0, will be made private in 9.0.0
*/
public $bLenientParsing = true;
diff --git a/src/Value/CSSFunction.php b/src/Value/CSSFunction.php
index 300dc3ec..703f6658 100644
--- a/src/Value/CSSFunction.php
+++ b/src/Value/CSSFunction.php
@@ -4,6 +4,9 @@
use Sabberworm\CSS\OutputFormat;
use Sabberworm\CSS\Parsing\ParserState;
+use Sabberworm\CSS\Parsing\SourceException;
+use Sabberworm\CSS\Parsing\UnexpectedEOFException;
+use Sabberworm\CSS\Parsing\UnexpectedTokenException;
/**
* A `CSSFunction` represents a special kind of value that also contains a function name and where the values are the
@@ -13,6 +16,8 @@ class CSSFunction extends ValueList
{
/**
* @var string
+ *
+ * @internal since 8.8.0
*/
protected $sName;
@@ -29,7 +34,7 @@ public function __construct($sName, $aArguments, $sSeparator = ',', $iLineNo = 0
$aArguments = $aArguments->getListComponents();
}
$this->sName = $sName;
- $this->iLineNo = $iLineNo;
+ $this->setPosition($iLineNo); // TODO: redundant?
parent::__construct($aArguments, $sSeparator, $iLineNo);
}
@@ -42,6 +47,8 @@ public function __construct($sName, $aArguments, $sSeparator = ',', $iLineNo = 0
* @throws SourceException
* @throws UnexpectedEOFException
* @throws UnexpectedTokenException
+ *
+ * @internal since V8.8.0
*/
public static function parse(ParserState $oParserState, $bIgnoreCase = false)
{
@@ -81,6 +88,8 @@ public function getArguments()
/**
* @return string
+ *
+ * @deprecated in V8.8.0, will be removed in V9.0.0. Use `render` instead.
*/
public function __toString()
{
@@ -88,9 +97,11 @@ public function __toString()
}
/**
+ * @param OutputFormat|null $oOutputFormat
+ *
* @return string
*/
- public function render(OutputFormat $oOutputFormat)
+ public function render($oOutputFormat)
{
$aArguments = parent::render($oOutputFormat);
return "{$this->sName}({$aArguments})";
diff --git a/src/Value/CSSString.php b/src/Value/CSSString.php
index da498d41..a6a705b5 100644
--- a/src/Value/CSSString.php
+++ b/src/Value/CSSString.php
@@ -36,6 +36,8 @@ public function __construct($sString, $iLineNo = 0)
* @throws SourceException
* @throws UnexpectedEOFException
* @throws UnexpectedTokenException
+ *
+ * @internal since V8.8.0
*/
public static function parse(ParserState $oParserState)
{
@@ -92,6 +94,8 @@ public function getString()
/**
* @return string
+ *
+ * @deprecated in V8.8.0, will be removed in V9.0.0. Use `render` instead.
*/
public function __toString()
{
@@ -99,9 +103,11 @@ public function __toString()
}
/**
+ * @param OutputFormat|null $oOutputFormat
+ *
* @return string
*/
- public function render(OutputFormat $oOutputFormat)
+ public function render($oOutputFormat)
{
$sString = addslashes($this->sString);
$sString = str_replace("\n", '\A', $sString);
diff --git a/src/Value/CalcFunction.php b/src/Value/CalcFunction.php
index 5ffd071f..c3ed0a08 100644
--- a/src/Value/CalcFunction.php
+++ b/src/Value/CalcFunction.php
@@ -6,15 +6,22 @@
use Sabberworm\CSS\Parsing\UnexpectedEOFException;
use Sabberworm\CSS\Parsing\UnexpectedTokenException;
+/**
+ * Support for `-webkit-calc` and `-moz-calc` is deprecated in version 8.8.0, and will be removed in version 9.0.0.
+ */
class CalcFunction extends CSSFunction
{
/**
* @var int
+ *
+ * @internal
*/
const T_OPERAND = 1;
/**
* @var int
+ *
+ * @internal
*/
const T_OPERATOR = 2;
@@ -26,6 +33,8 @@ class CalcFunction extends CSSFunction
*
* @throws UnexpectedTokenException
* @throws UnexpectedEOFException
+ *
+ * @internal since V8.8.0
*/
public static function parse(ParserState $oParserState, $bIgnoreCase = false)
{
@@ -87,7 +96,7 @@ public static function parse(ParserState $oParserState, $bIgnoreCase = false)
sprintf(
'Next token was expected to be an operand of type %s. Instead "%s" was found.',
implode(', ', $aOperators),
- $oVal
+ $oParserState->peek()
),
'',
'custom',
diff --git a/src/Value/CalcRuleValueList.php b/src/Value/CalcRuleValueList.php
index 7dbd26a1..17fbe7cf 100644
--- a/src/Value/CalcRuleValueList.php
+++ b/src/Value/CalcRuleValueList.php
@@ -15,9 +15,11 @@ public function __construct($iLineNo = 0)
}
/**
+ * @param OutputFormat|null $oOutputFormat
+ *
* @return string
*/
- public function render(OutputFormat $oOutputFormat)
+ public function render($oOutputFormat)
{
return $oOutputFormat->implode(' ', $this->aComponents);
}
diff --git a/src/Value/Color.php b/src/Value/Color.php
index 1cf00cce..d4b7caf0 100644
--- a/src/Value/Color.php
+++ b/src/Value/Color.php
@@ -30,6 +30,8 @@ public function __construct(array $aColor, $iLineNo = 0)
*
* @throws UnexpectedEOFException
* @throws UnexpectedTokenException
+ *
+ * @internal since V8.8.0
*/
public static function parse(ParserState $oParserState, $bIgnoreCase = false)
{
@@ -56,12 +58,19 @@ public static function parse(ParserState $oParserState, $bIgnoreCase = false)
$oParserState->currentLine()
),
];
- } else {
+ } 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()),
];
+ } else {
+ throw new UnexpectedTokenException(
+ 'Invalid hex color value',
+ $sValue,
+ 'custom',
+ $oParserState->currentLine()
+ );
}
} else {
$sColorMode = $oParserState->parseIdentifier(true);
@@ -146,6 +155,8 @@ public function getColorDescription()
/**
* @return string
+ *
+ * @deprecated in V8.8.0, will be removed in V9.0.0. Use `render` instead.
*/
public function __toString()
{
@@ -153,9 +164,11 @@ public function __toString()
}
/**
+ * @param OutputFormat|null $oOutputFormat
+ *
* @return string
*/
- public function render(OutputFormat $oOutputFormat)
+ public function render($oOutputFormat)
{
// Shorthand RGB color values
if ($oOutputFormat->getRGBHashNotation() && implode('', array_keys($this->aComponents)) === 'rgb') {
diff --git a/src/Value/LineName.php b/src/Value/LineName.php
index e231ce38..effc827c 100644
--- a/src/Value/LineName.php
+++ b/src/Value/LineName.php
@@ -23,6 +23,8 @@ public function __construct(array $aComponents = [], $iLineNo = 0)
*
* @throws UnexpectedTokenException
* @throws UnexpectedEOFException
+ *
+ * @internal since V8.8.0
*/
public static function parse(ParserState $oParserState)
{
@@ -49,6 +51,8 @@ public static function parse(ParserState $oParserState)
/**
* @return string
+ *
+ * @deprecated in V8.8.0, will be removed in V9.0.0. Use `render` instead.
*/
public function __toString()
{
@@ -56,9 +60,11 @@ public function __toString()
}
/**
+ * @param OutputFormat|null $oOutputFormat
+ *
* @return string
*/
- public function render(OutputFormat $oOutputFormat)
+ public function render($oOutputFormat)
{
return '[' . parent::render(OutputFormat::createCompact()) . ']';
}
diff --git a/src/Value/Size.php b/src/Value/Size.php
index 36a32381..98876973 100644
--- a/src/Value/Size.php
+++ b/src/Value/Size.php
@@ -16,16 +16,38 @@ class Size extends PrimitiveValue
* vh/vw/vm(ax)/vmin/rem are absolute insofar as they don’t scale to the immediate parent (only the viewport)
*
* @var array
- */
- const ABSOLUTE_SIZE_UNITS = ['px', 'cm', 'mm', 'mozmm', 'in', 'pt', 'pc', 'vh', 'vw', 'vmin', 'vmax', 'rem'];
+ *
+ * @internal
+ */
+ const ABSOLUTE_SIZE_UNITS = [
+ 'px',
+ 'pt',
+ 'pc',
+ 'cm',
+ 'mm',
+ 'mozmm',
+ 'in',
+ 'vh',
+ 'dvh',
+ 'svh',
+ 'lvh',
+ 'vw',
+ 'vmin',
+ 'vmax',
+ 'rem',
+ ];
/**
* @var array
+ *
+ * @internal
*/
const RELATIVE_SIZE_UNITS = ['%', 'em', 'ex', 'ch', 'fr'];
/**
* @var array
+ *
+ * @internal
*/
const NON_SIZE_UNITS = ['deg', 'grad', 'rad', 's', 'ms', 'turn', 'Hz', 'kHz'];
@@ -70,6 +92,8 @@ public function __construct($fSize, $sUnit = null, $bIsColorComponent = false, $
*
* @throws UnexpectedEOFException
* @throws UnexpectedTokenException
+ *
+ * @internal since V8.8.0
*/
public static function parse(ParserState $oParserState, $bIsColorComponent = false)
{
@@ -198,6 +222,8 @@ public function isRelative()
/**
* @return string
+ *
+ * @deprecated in V8.8.0, will be removed in V9.0.0. Use `render` instead.
*/
public function __toString()
{
@@ -205,14 +231,16 @@ public function __toString()
}
/**
+ * @param OutputFormat|null $oOutputFormat
+ *
* @return string
*/
- public function render(OutputFormat $oOutputFormat)
+ public function render($oOutputFormat)
{
$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;
+ ? preg_replace("/$sPoint?0+$/", "", sprintf("%f", $this->fSize)) : (string)$this->fSize;
return preg_replace(["/$sPoint/", "/^(-?)0\./"], ['.', '$1.'], $sSize)
. ($this->sUnit === null ? '' : $this->sUnit);
}
diff --git a/src/Value/URL.php b/src/Value/URL.php
index cdb911c3..1f2a0af7 100644
--- a/src/Value/URL.php
+++ b/src/Value/URL.php
@@ -33,6 +33,8 @@ public function __construct(CSSString $oURL, $iLineNo = 0)
* @throws SourceException
* @throws UnexpectedEOFException
* @throws UnexpectedTokenException
+ *
+ * @internal since V8.8.0
*/
public static function parse(ParserState $oParserState)
{
@@ -79,6 +81,8 @@ public function getURL()
/**
* @return string
+ *
+ * @deprecated in V8.8.0, will be removed in V9.0.0. Use `render` instead.
*/
public function __toString()
{
@@ -86,9 +90,11 @@ public function __toString()
}
/**
+ * @param OutputFormat|null $oOutputFormat
+ *
* @return string
*/
- public function render(OutputFormat $oOutputFormat)
+ public function render($oOutputFormat)
{
return "url({$this->oURL->render($oOutputFormat)})";
}
diff --git a/src/Value/Value.php b/src/Value/Value.php
index a920396b..3025566f 100644
--- a/src/Value/Value.php
+++ b/src/Value/Value.php
@@ -2,29 +2,28 @@
namespace Sabberworm\CSS\Value;
+use Sabberworm\CSS\CSSElement;
use Sabberworm\CSS\Parsing\ParserState;
use Sabberworm\CSS\Parsing\SourceException;
use Sabberworm\CSS\Parsing\UnexpectedEOFException;
use Sabberworm\CSS\Parsing\UnexpectedTokenException;
-use Sabberworm\CSS\Renderable;
+use Sabberworm\CSS\Position\Position;
+use Sabberworm\CSS\Position\Positionable;
/**
* Abstract base class for specific classes of CSS values: `Size`, `Color`, `CSSString` and `URL`, and another
* abstract subclass `ValueList`.
*/
-abstract class Value implements Renderable
+abstract class Value implements CSSElement, Positionable
{
- /**
- * @var int
- */
- protected $iLineNo;
+ use Position;
/**
* @param int $iLineNo
*/
public function __construct($iLineNo = 0)
{
- $this->iLineNo = $iLineNo;
+ $this->setPosition($iLineNo);
}
/**
@@ -34,6 +33,8 @@ public function __construct($iLineNo = 0)
*
* @throws UnexpectedTokenException
* @throws UnexpectedEOFException
+ *
+ * @internal since V8.8.0
*/
public static function parseValue(ParserState $oParserState, array $aListDelimiters = [])
{
@@ -43,9 +44,9 @@ public static function parseValue(ParserState $oParserState, array $aListDelimit
//Build a list of delimiters and parsed values
while (
!($oParserState->comes('}') || $oParserState->comes(';') || $oParserState->comes('!')
- || $oParserState->comes(')')
- || $oParserState->comes('\\')
- || $oParserState->isEnd())
+ || $oParserState->comes(')')
+ || $oParserState->comes('\\')
+ || $oParserState->isEnd())
) {
if (count($aStack) > 0) {
$bFoundDelimiter = false;
@@ -67,23 +68,30 @@ public static function parseValue(ParserState $oParserState, array $aListDelimit
}
// Convert the list to list objects
foreach ($aListDelimiters as $sDelimiter) {
- if (count($aStack) === 1) {
+ $iStackLength = count($aStack);
+ if ($iStackLength === 1) {
return $aStack[0];
}
- $iStartPosition = null;
- while (($iStartPosition = array_search($sDelimiter, $aStack, true)) !== false) {
+ $aNewStack = [];
+ for ($iStartPosition = 0; $iStartPosition < $iStackLength; ++$iStartPosition) {
+ if ($iStartPosition === ($iStackLength - 1) || $sDelimiter !== $aStack[$iStartPosition + 1]) {
+ $aNewStack[] = $aStack[$iStartPosition];
+ continue;
+ }
$iLength = 2; //Number of elements to be joined
- for ($i = $iStartPosition + 2; $i < count($aStack); $i += 2, ++$iLength) {
+ for ($i = $iStartPosition + 3; $i < $iStackLength; $i += 2, ++$iLength) {
if ($sDelimiter !== $aStack[$i]) {
break;
}
}
$oList = new RuleValueList($sDelimiter, $oParserState->currentLine());
- for ($i = $iStartPosition - 1; $i - $iStartPosition + 1 < $iLength * 2; $i += 2) {
+ for ($i = $iStartPosition; $i - $iStartPosition < $iLength * 2; $i += 2) {
$oList->addListComponent($aStack[$i]);
}
- array_splice($aStack, $iStartPosition - 1, $iLength * 2 - 1, [$oList]);
+ $aNewStack[] = $oList;
+ $iStartPosition += $iLength * 2 - 2;
}
+ $aStack = $aNewStack;
}
if (!isset($aStack[0])) {
throw new UnexpectedTokenException(
@@ -103,6 +111,8 @@ public static function parseValue(ParserState $oParserState, array $aListDelimit
*
* @throws UnexpectedEOFException
* @throws UnexpectedTokenException
+ *
+ * @internal since V8.8.0
*/
public static function parseIdentifierOrFunction(ParserState $oParserState, $bIgnoreCase = false)
{
@@ -133,6 +143,8 @@ public static function parseIdentifierOrFunction(ParserState $oParserState, $bIg
* @throws UnexpectedEOFException
* @throws UnexpectedTokenException
* @throws SourceException
+ *
+ * @internal since V8.8.0
*/
public static function parsePrimitiveValue(ParserState $oParserState)
{
@@ -156,7 +168,16 @@ public static function parsePrimitiveValue(ParserState $oParserState)
} elseif ($oParserState->comes("U+")) {
$oValue = self::parseUnicodeRangeValue($oParserState);
} else {
- $oValue = self::parseIdentifierOrFunction($oParserState);
+ $sNextChar = $oParserState->peek(1);
+ try {
+ $oValue = self::parseIdentifierOrFunction($oParserState);
+ } catch (UnexpectedTokenException $e) {
+ if (\in_array($sNextChar, ['+', '-', '*', '/'], true)) {
+ $oValue = $oParserState->consume(1);
+ } else {
+ throw $e;
+ }
+ }
}
$oParserState->consumeWhiteSpace();
return $oValue;
@@ -194,12 +215,4 @@ private static function parseUnicodeRangeValue(ParserState $oParserState)
} while (strlen($sRange) < $iCodepointMaxLength && preg_match("/[A-Fa-f0-9\?-]/", $oParserState->peek()));
return "U+{$sRange}";
}
-
- /**
- * @return int
- */
- public function getLineNo()
- {
- return $this->iLineNo;
- }
}
diff --git a/src/Value/ValueList.php b/src/Value/ValueList.php
index a93acc7b..61962258 100644
--- a/src/Value/ValueList.php
+++ b/src/Value/ValueList.php
@@ -14,11 +14,15 @@ abstract class ValueList extends Value
{
/**
* @var array
+ *
+ * @internal since 8.8.0
*/
protected $aComponents;
/**
* @var string
+ *
+ * @internal since 8.8.0
*/
protected $sSeparator;
@@ -86,6 +90,8 @@ public function setListSeparator($sSeparator)
/**
* @return string
+ *
+ * @deprecated in V8.8.0, will be removed in V9.0.0. Use `render` instead.
*/
public function __toString()
{
@@ -93,9 +99,11 @@ public function __toString()
}
/**
+ * @param OutputFormat|null $oOutputFormat
+ *
* @return string
*/
- public function render(OutputFormat $oOutputFormat)
+ public function render($oOutputFormat)
{
return $oOutputFormat->implode(
$oOutputFormat->spaceBeforeListArgumentSeparator($this->sSeparator) . $this->sSeparator
diff --git a/tests/CSSList/AtRuleBlockListTest.php b/tests/CSSList/AtRuleBlockListTest.php
index 48e6e578..030c9874 100644
--- a/tests/CSSList/AtRuleBlockListTest.php
+++ b/tests/CSSList/AtRuleBlockListTest.php
@@ -7,12 +7,48 @@
use Sabberworm\CSS\CSSList\AtRuleBlockList;
use Sabberworm\CSS\Parser;
use Sabberworm\CSS\Renderable;
+use Sabberworm\CSS\Settings;
/**
* @covers \Sabberworm\CSS\CSSList\AtRuleBlockList
*/
-class AtRuleBlockListTest extends TestCase
+final class AtRuleBlockListTest extends TestCase
{
+ /**
+ * @return array
+ */
+ public static function provideMinWidthMediaRule()
+ {
+ return [
+ 'without spaces around arguments' => ['@media(min-width: 768px){.class{color:red}}'],
+ 'with spaces around arguments' => ['@media (min-width: 768px) {.class{color:red}}'],
+ ];
+ }
+
+ /**
+ * @return array
+ */
+ public static function provideSyntacticlyCorrectAtRule()
+ {
+ 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;
+ }
+ }
+ ',
+ ],
+ ];
+ }
+
/**
* @test
*/
@@ -43,23 +79,12 @@ public function implementsCommentable()
self::assertInstanceOf(Commentable::class, $subject);
}
- /**
- * @return array>
- */
- public function mediaRuleDataProvider()
- {
- return [
- 'without spaces around arguments' => ['@media(min-width: 768px){.class{color:red}}'],
- 'with spaces around arguments' => ['@media (min-width: 768px) {.class{color:red}}'],
- ];
- }
-
/**
* @test
*
* @param string $css
*
- * @dataProvider mediaRuleDataProvider
+ * @dataProvider provideMinWidthMediaRule
*/
public function parsesRuleNameOfMediaQueries($css)
{
@@ -74,7 +99,7 @@ public function parsesRuleNameOfMediaQueries($css)
*
* @param string $css
*
- * @dataProvider mediaRuleDataProvider
+ * @dataProvider provideMinWidthMediaRule
*/
public function parsesArgumentsOfMediaQueries($css)
{
@@ -83,4 +108,19 @@ public function parsesArgumentsOfMediaQueries($css)
self::assertSame('(min-width: 768px)', $atRuleBlockList->atRuleArgs());
}
+
+ /**
+ * @test
+ *
+ * @param string $css
+ *
+ * @dataProvider provideMinWidthMediaRule
+ * @dataProvider provideSyntacticlyCorrectAtRule
+ */
+ public function parsesSyntacticlyCorrectAtRuleInStrictMode($css)
+ {
+ $contents = (new Parser($css, Settings::create()->beStrict()))->parse()->getContents();
+
+ self::assertNotEmpty($contents, 'Failing CSS: `' . $css . '`');
+ }
}
diff --git a/tests/CSSList/DocumentTest.php b/tests/CSSList/DocumentTest.php
index a727400b..70a09c53 100644
--- a/tests/CSSList/DocumentTest.php
+++ b/tests/CSSList/DocumentTest.php
@@ -11,14 +11,14 @@
/**
* @covers \Sabberworm\CSS\CSSList\Document
*/
-class DocumentTest extends TestCase
+final class DocumentTest extends TestCase
{
/**
* @var Document
*/
private $subject;
- protected function setUp()
+ private function setUpTestcase()
{
$this->subject = new Document();
}
@@ -28,6 +28,8 @@ protected function setUp()
*/
public function implementsRenderable()
{
+ $this->setUpTestcase();
+
self::assertInstanceOf(Renderable::class, $this->subject);
}
@@ -36,6 +38,8 @@ public function implementsRenderable()
*/
public function implementsCommentable()
{
+ $this->setUpTestcase();
+
self::assertInstanceOf(Commentable::class, $this->subject);
}
@@ -44,13 +48,15 @@ public function implementsCommentable()
*/
public function getContentsInitiallyReturnsEmptyArray()
{
+ $this->setUpTestcase();
+
self::assertSame([], $this->subject->getContents());
}
/**
* @return array>>
*/
- public function contentsDataProvider()
+ public static function contentsDataProvider()
{
return [
'empty array' => [[]],
@@ -68,6 +74,8 @@ public function contentsDataProvider()
*/
public function setContentsSetsContents(array $contents)
{
+ $this->setUpTestcase();
+
$this->subject->setContents($contents);
self::assertSame($contents, $this->subject->getContents());
@@ -78,6 +86,8 @@ public function setContentsSetsContents(array $contents)
*/
public function setContentsReplacesContentsSetInPreviousCall()
{
+ $this->setUpTestcase();
+
$contents2 = [new DeclarationBlock()];
$this->subject->setContents([new DeclarationBlock()]);
@@ -85,4 +95,63 @@ public function setContentsReplacesContentsSetInPreviousCall()
self::assertSame($contents2, $this->subject->getContents());
}
+
+ /**
+ * @test
+ */
+ public function insertContentBeforeInsertsContentBeforeSibbling()
+ {
+ $this->setUpTestcase();
+
+ $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()
+ {
+ $this->setUpTestcase();
+
+ $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
index 080d5f94..b29c1018 100644
--- a/tests/CSSList/KeyFrameTest.php
+++ b/tests/CSSList/KeyFrameTest.php
@@ -11,14 +11,14 @@
/**
* @covers \Sabberworm\CSS\CSSList\KeyFrame
*/
-class KeyFrameTest extends TestCase
+final class KeyFrameTest extends TestCase
{
/**
* @var KeyFrame
*/
protected $subject;
- protected function setUp()
+ private function setUpTestcase()
{
$this->subject = new KeyFrame();
}
@@ -28,6 +28,8 @@ protected function setUp()
*/
public function implementsAtRule()
{
+ $this->setUpTestcase();
+
self::assertInstanceOf(AtRule::class, $this->subject);
}
@@ -36,6 +38,8 @@ public function implementsAtRule()
*/
public function implementsRenderable()
{
+ $this->setUpTestcase();
+
self::assertInstanceOf(Renderable::class, $this->subject);
}
@@ -44,6 +48,8 @@ public function implementsRenderable()
*/
public function implementsCommentable()
{
+ $this->setUpTestcase();
+
self::assertInstanceOf(Commentable::class, $this->subject);
}
}
diff --git a/tests/Comment/CommentTest.php b/tests/Comment/CommentTest.php
index 29385f01..d26da963 100644
--- a/tests/Comment/CommentTest.php
+++ b/tests/Comment/CommentTest.php
@@ -14,7 +14,7 @@
* @covers \Sabberworm\CSS\OutputFormat
* @covers \Sabberworm\CSS\OutputFormatter
*/
-class CommentTest extends TestCase
+final class CommentTest extends TestCase
{
/**
* @test
@@ -140,11 +140,11 @@ public function keepCommentsInOutput()
', $oCss->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;}}',
+ . ' * 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))
);
}
@@ -170,8 +170,8 @@ public function stripCommentsFromOutput()
', $oCss->render(OutputFormat::createPretty()->setRenderComments(false)));
self::assertSame(
'@import url("some/url.css") screen;'
- . '.foo,#bar{background-color:#000;}'
- . '@media screen{#foo.bar{position:absolute;}}',
+ . '.foo,#bar{background-color:#000;}'
+ . '@media screen{#foo.bar{position:absolute;}}',
$oCss->render(OutputFormat::createCompact())
);
}
diff --git a/tests/Functional/RuleSet/DeclarationBlockTest.php b/tests/Functional/RuleSet/DeclarationBlockTest.php
new file mode 100644
index 00000000..5273b5b9
--- /dev/null
+++ b/tests/Functional/RuleSet/DeclarationBlockTest.php
@@ -0,0 +1,41 @@
+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::assertContains($expectedRendering, $declarationBlock->render(new OutputFormat()));
+ }
+}
diff --git a/tests/OutputFormatTest.php b/tests/OutputFormatTest.php
index 0de39123..2fb6df99 100644
--- a/tests/OutputFormatTest.php
+++ b/tests/OutputFormatTest.php
@@ -11,7 +11,7 @@
/**
* @covers \Sabberworm\CSS\OutputFormat
*/
-class OutputFormatTest extends TestCase
+final class OutputFormatTest extends TestCase
{
/**
* @var string
@@ -43,7 +43,7 @@ class OutputFormatTest extends TestCase
*/
private $oDocument;
- protected function setUp()
+ private function setUpTestcase()
{
$this->oParser = new Parser(self::TEST_CSS);
$this->oDocument = $this->oParser->parse();
@@ -54,6 +54,8 @@ protected function setUp()
*/
public function plain()
{
+ $this->setUpTestcase();
+
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;}}',
@@ -66,6 +68,8 @@ public function plain()
*/
public function compact()
{
+ $this->setUpTestcase();
+
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;}}',
@@ -78,6 +82,8 @@ public function compact()
*/
public function pretty()
{
+ $this->setUpTestcase();
+
self::assertSame(self::TEST_CSS, $this->oDocument->render(OutputFormat::createPretty()));
}
@@ -86,6 +92,8 @@ public function pretty()
*/
public function spaceAfterListArgumentSeparator()
{
+ $this->setUpTestcase();
+
self::assertSame(
'.main, .test {font: italic normal bold 16px/ 1.2 '
. '"Helvetica", Verdana, sans-serif;background: white;}'
@@ -96,9 +104,14 @@ public function spaceAfterListArgumentSeparator()
/**
* @test
+ *
+ * @deprecated since version 8.8.0; will be removed in version 9.0.
+ * Use `setSpaceAfterListArgumentSeparators()` to set different spacing per separator.
*/
- public function spaceAfterListArgumentSeparatorComplex()
+ public function spaceAfterListArgumentSeparatorComplexDeprecated()
{
+ $this->setUpTestcase();
+
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;}}",
@@ -111,11 +124,35 @@ public function spaceAfterListArgumentSeparatorComplex()
);
}
+ /**
+ * @test
+ */
+ public function spaceAfterListArgumentSeparatorComplex()
+ {
+ $this->setUpTestcase();
+
+ 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(' ')
+ ->setSpaceAfterListArgumentSeparators([
+ ',' => "\t",
+ '/' => '',
+ ' ' => '',
+ ])
+ )
+ );
+ }
+
/**
* @test
*/
public function spaceAfterSelectorSeparator()
{
+ $this->setUpTestcase();
+
self::assertSame(
'.main,
.test {font: italic normal bold 16px/1.2 "Helvetica",Verdana,sans-serif;background: white;}
@@ -129,6 +166,8 @@ public function spaceAfterSelectorSeparator()
*/
public function stringQuotingType()
{
+ $this->setUpTestcase();
+
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;}}',
@@ -141,6 +180,8 @@ public function stringQuotingType()
*/
public function rGBHashNotation()
{
+ $this->setUpTestcase();
+
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);}}',
@@ -153,6 +194,8 @@ public function rGBHashNotation()
*/
public function semicolonAfterLastRule()
{
+ $this->setUpTestcase();
+
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}}',
@@ -165,6 +208,8 @@ public function semicolonAfterLastRule()
*/
public function spaceAfterRuleName()
{
+ $this->setUpTestcase();
+
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;}}',
@@ -177,6 +222,8 @@ public function spaceAfterRuleName()
*/
public function spaceRules()
{
+ $this->setUpTestcase();
+
self::assertSame('.main, .test {
font: italic normal bold 16px/1.2 "Helvetica",Verdana,sans-serif;
background: white;
@@ -193,6 +240,8 @@ public function spaceRules()
*/
public function spaceBlocks()
{
+ $this->setUpTestcase();
+
self::assertSame('
.main, .test {font: italic normal bold 16px/1.2 "Helvetica",Verdana,sans-serif;background: white;}
@media screen {
@@ -206,6 +255,8 @@ public function spaceBlocks()
*/
public function spaceBoth()
{
+ $this->setUpTestcase();
+
self::assertSame('
.main, .test {
font: italic normal bold 16px/1.2 "Helvetica",Verdana,sans-serif;
@@ -226,6 +277,8 @@ public function spaceBoth()
*/
public function spaceBetweenBlocks()
{
+ $this->setUpTestcase();
+
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;}}',
@@ -238,6 +291,8 @@ public function spaceBetweenBlocks()
*/
public function indentation()
{
+ $this->setUpTestcase();
+
self::assertSame('
.main, .test {
font: italic normal bold 16px/1.2 "Helvetica",Verdana,sans-serif;
@@ -261,6 +316,8 @@ public function indentation()
*/
public function spaceBeforeBraces()
{
+ $this->setUpTestcase();
+
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;}}',
@@ -273,6 +330,8 @@ public function spaceBeforeBraces()
*/
public function ignoreExceptionsOff()
{
+ $this->setUpTestcase();
+
$this->expectException(OutputException::class);
$aBlocks = $this->oDocument->getAllDeclarationBlocks();
@@ -292,6 +351,8 @@ public function ignoreExceptionsOff()
*/
public function ignoreExceptionsOn()
{
+ $this->setUpTestcase();
+
$aBlocks = $this->oDocument->getAllDeclarationBlocks();
$oFirstBlock = $aBlocks[0];
$oFirstBlock->removeSelector('.main');
diff --git a/tests/ParserTest.php b/tests/ParserTest.php
index 74449ee2..7c9778ab 100644
--- a/tests/ParserTest.php
+++ b/tests/ParserTest.php
@@ -19,6 +19,7 @@
use Sabberworm\CSS\RuleSet\DeclarationBlock;
use Sabberworm\CSS\RuleSet\RuleSet;
use Sabberworm\CSS\Settings;
+use Sabberworm\CSS\Tests\RuleSet\DeclarationBlockTest;
use Sabberworm\CSS\Value\Color;
use Sabberworm\CSS\Value\Size;
use Sabberworm\CSS\Value\URL;
@@ -35,7 +36,7 @@
* @covers \Sabberworm\CSS\Value\Size::parse
* @covers \Sabberworm\CSS\Value\URL::parse
*/
-class ParserTest extends TestCase
+final class ParserTest extends TestCase
{
/**
* @test
@@ -146,6 +147,8 @@ public function colorParsing()
'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);
}
}
foreach ($oDoc->getAllValues('color') as $sColor) {
@@ -277,7 +280,7 @@ public function specificity()
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));
+ self::assertEquals([new Selector('ol li::before', true)], $oDoc->getSelectorsBySpecificity('3'));
}
/**
@@ -509,7 +512,7 @@ public function expandShorthands()
. '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());
+ DeclarationBlockTest::assertDeclarationBlockEquals($sExpected, $oDoc->render());
}
/**
@@ -526,7 +529,7 @@ public function createShorthands()
$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());
+ DeclarationBlockTest::assertDeclarationBlockEquals($sExpected, $oDoc->render());
}
/**
@@ -1160,17 +1163,72 @@ public function commentExtracting()
/**
* @test
*/
- public function flatCommentExtracting()
+ public function flatCommentExtractingOneComment()
{
$parser = new Parser('div {/*Find Me!*/left:10px; text-align:left;}');
$doc = $parser->parse();
+
$contents = $doc->getContents();
$divRules = $contents[0]->getRules();
$comments = $divRules[0]->getComments();
+
self::assertCount(1, $comments);
self::assertSame("Find Me!", $comments[0]->getComment());
}
+ /**
+ * @test
+ */
+ public function flatCommentExtractingTwoConjoinedCommentsForOneRule()
+ {
+ $parser = new Parser('div {/*Find Me!*//*Find Me Too!*/left:10px; text-align:left;}');
+ $document = $parser->parse();
+
+ $contents = $document->getContents();
+ $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()
+ {
+ $parser = new Parser('div { /*Find Me!*/ /*Find Me Too!*/ left:10px; text-align:left;}');
+ $document = $parser->parse();
+
+ $contents = $document->getContents();
+ $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()
+ {
+ $parser = new Parser('div {/*Find Me!*/left:10px; /*Find Me Too!*/text-align:left;}');
+ $doc = $parser->parse();
+
+ $contents = $doc->getContents();
+ $divRules = $contents[0]->getRules();
+ $rule1Comments = $divRules[0]->getComments();
+ $rule2Comments = $divRules[1]->getComments();
+
+ self::assertCount(1, $rule1Comments);
+ self::assertCount(1, $rule2Comments);
+ self::assertEquals('Find Me!', $rule1Comments[0]->getComment());
+ self::assertEquals('Find Me Too!', $rule2Comments[0]->getComment());
+ }
+
/**
* @test
*/
diff --git a/tests/RuleSet/DeclarationBlockTest.php b/tests/RuleSet/DeclarationBlockTest.php
index 49526952..d4cf31b6 100644
--- a/tests/RuleSet/DeclarationBlockTest.php
+++ b/tests/RuleSet/DeclarationBlockTest.php
@@ -3,14 +3,16 @@
namespace Sabberworm\CSS\Tests\RuleSet;
use PHPUnit\Framework\TestCase;
+use Sabberworm\CSS\OutputFormat;
use Sabberworm\CSS\Parser;
use Sabberworm\CSS\Rule\Rule;
+use Sabberworm\CSS\Settings as ParserSettings;
use Sabberworm\CSS\Value\Size;
/**
* @covers \Sabberworm\CSS\RuleSet\DeclarationBlock
*/
-class DeclarationBlockTest extends TestCase
+final class DeclarationBlockTest extends TestCase
{
/**
* @param string $sCss
@@ -27,13 +29,13 @@ public function expandBorderShorthand($sCss, $sExpected)
foreach ($oDoc->getAllDeclarationBlocks() as $oDeclaration) {
$oDeclaration->expandBorderShorthand();
}
- self::assertSame(trim((string)$oDoc), $sExpected);
+ self::assertDeclarationBlockEquals(trim((string)$oDoc), $sExpected);
}
/**
* @return array>
*/
- public function expandBorderShorthandProvider()
+ public static function expandBorderShorthandProvider()
{
return [
['body{ border: 2px solid #000 }', 'body {border-width: 2px;border-style: solid;border-color: #000;}'],
@@ -60,13 +62,13 @@ public function expandFontShorthand($sCss, $sExpected)
foreach ($oDoc->getAllDeclarationBlocks() as $oDeclaration) {
$oDeclaration->expandFontShorthand();
}
- self::assertSame(trim((string)$oDoc), $sExpected);
+ self::assertDeclarationBlockEquals(trim((string)$oDoc), $sExpected);
}
/**
* @return array>
*/
- public function expandFontShorthandProvider()
+ public static function expandFontShorthandProvider()
{
return [
[
@@ -116,13 +118,13 @@ public function expandBackgroundShorthand($sCss, $sExpected)
foreach ($oDoc->getAllDeclarationBlocks() as $oDeclaration) {
$oDeclaration->expandBackgroundShorthand();
}
- self::assertSame(trim((string)$oDoc), $sExpected);
+ self::assertDeclarationBlockEquals(trim((string)$oDoc), $sExpected);
}
/**
* @return array>
*/
- public function expandBackgroundShorthandProvider()
+ public static function expandBackgroundShorthandProvider()
{
return [
['body {border: 1px;}', 'body {border: 1px;}'],
@@ -169,13 +171,13 @@ public function expandDimensionsShorthand($sCss, $sExpected)
foreach ($oDoc->getAllDeclarationBlocks() as $oDeclaration) {
$oDeclaration->expandDimensionsShorthand();
}
- self::assertSame(trim((string)$oDoc), $sExpected);
+ self::assertDeclarationBlockEquals(trim((string)$oDoc), $sExpected);
}
/**
* @return array>
*/
- public function expandDimensionsShorthandProvider()
+ public static function expandDimensionsShorthandProvider()
{
return [
['body {border: 1px;}', 'body {border: 1px;}'],
@@ -213,7 +215,7 @@ public function createBorderShorthand($sCss, $sExpected)
/**
* @return array>
*/
- public function createBorderShorthandProvider()
+ public static function createBorderShorthandProvider()
{
return [
['body {border-width: 2px;border-style: solid;border-color: #000;}', 'body {border: 2px solid #000;}'],
@@ -244,7 +246,7 @@ public function createFontShorthand($sCss, $sExpected)
/**
* @return array>
*/
- public function createFontShorthandProvider()
+ public static function createFontShorthandProvider()
{
return [
['body {font-size: 12px; font-family: serif}', 'body {font: 12px serif;}'],
@@ -287,7 +289,7 @@ public function createDimensionsShorthand($sCss, $sExpected)
/**
* @return array>
*/
- public function createDimensionsShorthandProvider()
+ public static function createDimensionsShorthandProvider()
{
return [
['body {border: 1px;}', 'body {border: 1px;}'],
@@ -325,7 +327,7 @@ public function createBackgroundShorthand($sCss, $sExpected)
/**
* @return array>
*/
- public function createBackgroundShorthandProvider()
+ public static function createBackgroundShorthandProvider()
{
return [
['body {border: 1px;}', 'body {border: 1px;}'],
@@ -450,4 +452,103 @@ public function orderOfElementsMatchingOriginalOrderAfterExpandingShorthands()
array_map('strval', $oDeclaration->getRulesAssoc())
);
}
+
+ /**
+ * @return array
+ */
+ public static function declarationBlocksWithCommentsProvider()
+ {
+ return [
+ '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
+ *
+ * @param string $cssWithComments
+ * @param string $cssWithoutComments
+ *
+ * @dataProvider declarationBlocksWithCommentsProvider
+ */
+ public function canRemoveCommentsFromRulesUsingLenientParsing(
+ $cssWithComments,
+ $cssWithoutComments
+ ) {
+ $parserSettings = ParserSettings::create()->withLenientParsing(true);
+ $document = (new Parser($cssWithComments, $parserSettings))->parse();
+
+ $outputFormat = (new OutputFormat())->setRenderComments(false);
+ $renderedDocument = $document->render($outputFormat);
+
+ self::assertSame($cssWithoutComments, $renderedDocument);
+ }
+
+ /**
+ * @test
+ *
+ * @param string $cssWithComments
+ * @param string $cssWithoutComments
+ *
+ * @dataProvider declarationBlocksWithCommentsProvider
+ */
+ public function canRemoveCommentsFromRulesUsingStrictParsing(
+ $cssWithComments,
+ $cssWithoutComments
+ ) {
+ $parserSettings = ParserSettings::create()->withLenientParsing(false);
+ $document = (new Parser($cssWithComments, $parserSettings))->parse();
+
+ $outputFormat = (new OutputFormat())->setRenderComments(false);
+ $renderedDocument = $document->render($outputFormat);
+
+ self::assertSame($cssWithoutComments, $renderedDocument);
+ }
+
+ /**
+ * Asserts two declaration blocks are equivalent, allowing the rules to be in any order.
+ *
+ * @param string $expected
+ * @param string $actual
+ */
+ public static function assertDeclarationBlockEquals($expected, $actual)
+ {
+ $normalizedExpected = self::sortRulesInDeclarationBlock($expected);
+ $normalizedActual = self::sortRulesInDeclarationBlock($actual);
+
+ self::assertSame($normalizedExpected, $normalizedActual);
+ }
+
+ /**
+ * Sorts the rules within a declaration block by property name.
+ *
+ * @param string $declarationBlock
+ *
+ * @return string
+ */
+ private static function sortRulesInDeclarationBlock($declarationBlock)
+ {
+ // Match everything between `{` and `}`.
+ return \preg_replace_callback(
+ '/(?<=\\{)[^\\}]*+(?=\\})/',
+ [self::class, 'sortDeclarationBlockRules'],
+ $declarationBlock
+ );
+ }
+
+ /**
+ * Sorts rules from within a declaration block by property name.
+ *
+ * @param array{0: string} $rulesMatches
+ * This method is intended as a callback for `preg_replace_callback`.
+ *
+ * @return string
+ */
+ private static function sortDeclarationBlockRules($rulesMatches)
+ {
+ $rules = \explode(';', $rulesMatches[0]);
+ \sort($rules);
+ return \implode(';', $rules);
+ }
}
diff --git a/tests/RuleSet/LenientParsingTest.php b/tests/RuleSet/LenientParsingTest.php
index 5f5f224a..54d63e5b 100644
--- a/tests/RuleSet/LenientParsingTest.php
+++ b/tests/RuleSet/LenientParsingTest.php
@@ -19,7 +19,7 @@
* @covers \Sabberworm\CSS\Value\Size::parse
* @covers \Sabberworm\CSS\Value\URL::parse
*/
-class LenientParsingTest extends TestCase
+final class LenientParsingTest extends TestCase
{
/**
* @test
diff --git a/tests/Unit/CSSList/CSSBlockListTest.php b/tests/Unit/CSSList/CSSBlockListTest.php
new file mode 100644
index 00000000..85a2e9e7
--- /dev/null
+++ b/tests/Unit/CSSList/CSSBlockListTest.php
@@ -0,0 +1,279 @@
+getAllValues());
+ }
+
+ /**
+ * @test
+ *
+ * @return void
+ */
+ public function getAllValuesReturnsOneValueDirectlySetAsContent()
+ {
+ $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
+ *
+ * @return void
+ */
+ public function getAllValuesReturnsMultipleValuesDirectlySetAsContentInOneDeclarationBlock()
+ {
+ $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
+ *
+ * @return void
+ */
+ public function getAllValuesReturnsMultipleValuesDirectlySetAsContentInMultipleDeclarationBlocks()
+ {
+ $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
+ *
+ * @return void
+ */
+ public function getAllValuesReturnsValuesWithinAtRuleBlockList()
+ {
+ $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
+ *
+ * @return void
+ */
+ public function getAllValuesWithElementProvidedReturnsOnlyValuesWithinThatElement()
+ {
+ $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
+ *
+ * @return void
+ */
+ public function getAllValuesWithSearchStringProvidedReturnsOnlyValuesFromMatchingRules()
+ {
+ $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('font-');
+
+ self::assertSame([$value1], $result);
+ }
+
+ /**
+ * @test
+ *
+ * @return void
+ */
+ public function getAllValuesWithSearchStringProvidedInNewMethodSignatureReturnsOnlyValuesFromMatchingRules()
+ {
+ $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
+ *
+ * @return void
+ */
+ public function getAllValuesByDefaultDoesNotReturnValuesInFunctionArguments()
+ {
+ $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
+ *
+ * @return void
+ */
+ public function getAllValuesWithSearchInFunctionArgumentsReturnsValuesInFunctionArguments()
+ {
+ $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, true);
+
+ self::assertSame([$value1, $value2], $result);
+ }
+
+ /**
+ * @test
+ *
+ * @return void
+ */
+ public function getAllValuesWithSearchInFunctionArgumentsInNewMethodSignatureReturnsValuesInFunctionArguments()
+ {
+ $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 00000000..d7fa79cb
--- /dev/null
+++ b/tests/Unit/CSSList/CSSListTest.php
@@ -0,0 +1,27 @@
+subject = new ConcretePosition();
+ }
+
+ /**
+ * @test
+ */
+ public function getLineNumberInitiallyReturnsNull()
+ {
+ $this->doSetUp();
+
+ self::assertNull($this->subject->getLineNumber());
+ }
+
+ /**
+ * @test
+ */
+ public function getColumnNumberInitiallyReturnsNull()
+ {
+ $this->doSetUp();
+
+ self::assertNull($this->subject->getColumnNumber());
+ }
+
+ /**
+ * @return array}>
+ */
+ public function provideLineNumber()
+ {
+ return [
+ 'line 1' => [1],
+ 'line 42' => [42],
+ ];
+ }
+
+ /**
+ * @test
+ *
+ * @param int<1, max> $lineNumber
+ *
+ * @dataProvider provideLineNumber
+ */
+ public function setPositionOnVirginSetsLineNumber($lineNumber)
+ {
+ $this->doSetUp();
+
+ $this->subject->setPosition($lineNumber);
+
+ self::assertSame($lineNumber, $this->subject->getLineNumber());
+ }
+
+ /**
+ * @test
+ *
+ * @param int<1, max> $lineNumber
+ *
+ * @dataProvider provideLineNumber
+ */
+ public function setPositionSetsNewLineNumber($lineNumber)
+ {
+ $this->doSetUp();
+
+ $this->subject->setPosition(99);
+
+ $this->subject->setPosition($lineNumber);
+
+ self::assertSame($lineNumber, $this->subject->getLineNumber());
+ }
+
+ /**
+ * @test
+ */
+ public function setPositionWithNullClearsLineNumber()
+ {
+ $this->doSetUp();
+
+ $this->subject->setPosition(99);
+
+ $this->subject->setPosition(null);
+
+ self::assertNull($this->subject->getLineNumber());
+ }
+
+ /**
+ * @return array}>
+ */
+ public function provideColumnNumber()
+ {
+ return [
+ 'column 0' => [0],
+ 'column 14' => [14],
+ 'column 39' => [39],
+ ];
+ }
+
+ /**
+ * @test
+ *
+ * @param int<0, max> $columnNumber
+ *
+ * @dataProvider provideColumnNumber
+ */
+ public function setPositionOnVirginSetsColumnNumber($columnNumber)
+ {
+ $this->doSetUp();
+
+ $this->subject->setPosition(1, $columnNumber);
+
+ self::assertSame($columnNumber, $this->subject->getColumnNumber());
+ }
+
+ /**
+ * @test
+ *
+ * @param int $columnNumber
+ *
+ * @dataProvider provideColumnNumber
+ */
+ public function setPositionSetsNewColumnNumber($columnNumber)
+ {
+ $this->doSetUp();
+
+ $this->subject->setPosition(1, 99);
+
+ $this->subject->setPosition(2, $columnNumber);
+
+ self::assertSame($columnNumber, $this->subject->getColumnNumber());
+ }
+
+ /**
+ * @test
+ */
+ public function setPositionWithoutColumnNumberClearsColumnNumber()
+ {
+ $this->doSetUp();
+
+ $this->subject->setPosition(1, 99);
+
+ $this->subject->setPosition(2);
+
+ self::assertNull($this->subject->getColumnNumber());
+ }
+
+ /**
+ * @test
+ */
+ public function setPositionWithNullForColumnNumberClearsColumnNumber()
+ {
+ $this->doSetUp();
+
+ $this->subject->setPosition(1, 99);
+
+ $this->subject->setPosition(2, null);
+
+ self::assertNull($this->subject->getColumnNumber());
+ }
+
+ /**
+ * @return array, 1: int<0, max>}>
+ */
+ public function provideLineAndColumnNumber()
+ {
+ if (!\class_exists(DataProviders::class)) {
+ self::markTestSkipped('`DataProviders` class is not available');
+ return [];
+ }
+
+ return DataProviders::cross($this->provideLineNumber(), $this->provideColumnNumber());
+ }
+
+ /**
+ * @test
+ *
+ * @param int $lineNumber
+ * @param int $columnNumber
+ *
+ * @dataProvider provideLineAndColumnNumber
+ */
+ public function setPositionOnVirginSetsLineAndColumnNumber($lineNumber, $columnNumber)
+ {
+ $this->doSetUp();
+
+ $this->subject->setPosition($lineNumber, $columnNumber);
+
+ self::assertSame($lineNumber, $this->subject->getLineNumber());
+ self::assertSame($columnNumber, $this->subject->getColumnNumber());
+ }
+
+ /**
+ * @test
+ *
+ * @param int $lineNumber
+ * @param int $columnNumber
+ *
+ * @dataProvider provideLineAndColumnNumber
+ */
+ public function setPositionSetsNewLineAndColumnNumber($lineNumber, $columnNumber)
+ {
+ $this->doSetUp();
+
+ $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/Rule/RuleTest.php b/tests/Unit/Rule/RuleTest.php
new file mode 100644
index 00000000..8a2f0e6d
--- /dev/null
+++ b/tests/Unit/Rule/RuleTest.php
@@ -0,0 +1,75 @@
+}>
+ */
+ public static function provideRulesAndExpectedParsedValueListTypes()
+ {
+ 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 string $rule
+ * @param list $expectedTypeClassnames
+ *
+ * @dataProvider provideRulesAndExpectedParsedValueListTypes
+ */
+ public function parsesValuesIntoExpectedTypeList($rule, array $expectedTypeClassnames)
+ {
+ $subject = Rule::parse(new ParserState($rule, Settings::create()));
+
+ $value = $subject->getValue();
+ self::assertInstanceOf(ValueList::class, $value);
+
+ $actualClassnames = \array_map(
+ /**
+ * @param Value|string $component
+ * @return string
+ */
+ static function ($component) {
+ return \is_string($component) ? 'string' : \get_class($component);
+ },
+ $value->getListComponents()
+ );
+
+ self::assertSame($expectedTypeClassnames, $actualClassnames);
+ }
+}
diff --git a/tests/Unit/RuleSet/Fixtures/ConcreteRuleSet.php b/tests/Unit/RuleSet/Fixtures/ConcreteRuleSet.php
new file mode 100644
index 00000000..0aa96669
--- /dev/null
+++ b/tests/Unit/RuleSet/Fixtures/ConcreteRuleSet.php
@@ -0,0 +1,21 @@
+}>
+ */
+ public static function providePropertyNamesToBeSetInitially()
+ {
+ return [
+ 'no properties' => [[]],
+ 'one property' => [['color']],
+ 'two different properties' => [['color', 'display']],
+ 'two of the same property' => [['color', 'color']],
+ ];
+ }
+
+ /**
+ * @return array
+ */
+ public static function providePropertyNameToAdd()
+ {
+ 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 array, 1: string}>
+ */
+ public static function provideInitialPropertyNamesAndPropertyNameToAdd()
+ {
+ if (!\class_exists(DataProviders::class)) {
+ self::markTestSkipped('`DataProviders` class is not available');
+ return [];
+ }
+
+ return DataProviders::cross(self::providePropertyNamesToBeSetInitially(), self::providePropertyNameToAdd());
+ }
+
+ /**
+ * @test
+ *
+ * @param list $initialPropertyNames
+ *
+ * @dataProvider provideInitialPropertyNamesAndPropertyNameToAdd
+ */
+ public function addRuleWithoutSiblingAddsRuleAfterInitialRulesAndSetsValidLineAndColumnNumbers(
+ array $initialPropertyNames,
+ string $propertyNameToAdd
+ ) {
+ $subject = new ConcreteRuleSet();
+ $ruleToAdd = new Rule($propertyNameToAdd);
+ self::setRulesFromPropertyNames($subject, $initialPropertyNames);
+
+ $subject->addRule($ruleToAdd);
+
+ $rules = $subject->getRules();
+ self::assertSame($ruleToAdd, \end($rules));
+ self::assertInternalType('int', $ruleToAdd->getLineNumber(), 'line number not set');
+ self::assertGreaterThanOrEqual(1, $ruleToAdd->getLineNumber(), 'line number not valid');
+ self::assertInternalType('int', $ruleToAdd->getColumnNumber(), 'column number not set');
+ self::assertGreaterThanOrEqual(0, $ruleToAdd->getColumnNumber(), 'column number not valid');
+ }
+
+ /**
+ * @test
+ *
+ * @dataProvider provideInitialPropertyNamesAndPropertyNameToAdd
+ *
+ * @param list $initialPropertyNames
+ */
+ public function addRuleWithOnlyLineNumberAddsRuleAndSetsColumnNumberPreservingLineNumber(
+ array $initialPropertyNames,
+ string $propertyNameToAdd
+ ) {
+ $subject = new ConcreteRuleSet();
+ $ruleToAdd = new Rule($propertyNameToAdd);
+ $ruleToAdd->setPosition(42);
+ self::setRulesFromPropertyNames($subject, $initialPropertyNames);
+
+ $subject->addRule($ruleToAdd);
+
+ self::assertContains($ruleToAdd, $subject->getRules());
+ self::assertInternalType('int', $ruleToAdd->getColumnNumber(), 'column number not set');
+ self::assertGreaterThanOrEqual(0, $ruleToAdd->getColumnNumber(), 'column number not valid');
+ self::assertSame(42, $ruleToAdd->getLineNumber(), 'line number not preserved');
+ }
+
+ /**
+ * @test
+ *
+ * @dataProvider provideInitialPropertyNamesAndPropertyNameToAdd
+ *
+ * @param list $initialPropertyNames
+ */
+ public function addRuleWithOnlyColumnNumberAddsRuleAfterInitialRulesAndSetsLineNumberPreservingColumnNumber(
+ array $initialPropertyNames,
+ string $propertyNameToAdd
+ ) {
+ $subject = new ConcreteRuleSet();
+ $ruleToAdd = new Rule($propertyNameToAdd);
+ $ruleToAdd->setPosition(null, 42);
+ self::setRulesFromPropertyNames($subject, $initialPropertyNames);
+
+ $subject->addRule($ruleToAdd);
+
+ $rules = $subject->getRules();
+ self::assertSame($ruleToAdd, \end($rules));
+ self::assertInternalType('int', $ruleToAdd->getLineNumber(), 'line number not set');
+ self::assertGreaterThanOrEqual(1, $ruleToAdd->getLineNumber(), 'line number not valid');
+ self::assertSame(42, $ruleToAdd->getColumnNumber(), 'column number not preserved');
+ }
+
+ /**
+ * @test
+ *
+ * @dataProvider provideInitialPropertyNamesAndPropertyNameToAdd
+ *
+ * @param list $initialPropertyNames
+ */
+ public function addRuleWithCompletePositionAddsRuleAndPreservesPosition(
+ array $initialPropertyNames,
+ string $propertyNameToAdd
+ ) {
+ $subject = new ConcreteRuleSet();
+ $ruleToAdd = new Rule($propertyNameToAdd);
+ $ruleToAdd->setPosition(42, 64);
+ self::setRulesFromPropertyNames($subject, $initialPropertyNames);
+
+ $subject->addRule($ruleToAdd);
+
+ self::assertContains($ruleToAdd, $subject->getRules());
+ self::assertSame(42, $ruleToAdd->getLineNumber(), 'line number not preserved');
+ self::assertSame(64, $ruleToAdd->getColumnNumber(), 'column number not preserved');
+ }
+
+ /**
+ * @return array, 1: string, 2: list}>
+ */
+ public static function providePropertyNamesAndPropertyNameToRemoveAndExpectedRemainingPropertyNames()
+ {
+ 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
+ * @param string $propertyNameToRemove
+ * @param list $expectedRemainingPropertyNames
+ *
+ * @dataProvider providePropertyNamesAndPropertyNameToRemoveAndExpectedRemainingPropertyNames
+ */
+ public function removeMatchingRulesRemovesRulesByPropertyNameAndKeepsOthers(
+ array $initialPropertyNames,
+ $propertyNameToRemove,
+ array $expectedRemainingPropertyNames
+ ) {
+ $subject = new ConcreteRuleSet();
+ self::setRulesFromPropertyNames($subject, $initialPropertyNames);
+
+ $subject->removeMatchingRules($propertyNameToRemove);
+
+ $remainingRules = $subject->getRulesAssoc();
+ self::assertArrayNotHasKey($propertyNameToRemove, $remainingRules);
+ foreach ($expectedRemainingPropertyNames as $expectedPropertyName) {
+ self::assertArrayHasKey($expectedPropertyName, $remainingRules);
+ }
+ }
+
+ /**
+ * @return array, 1: string, 2: list}>
+ */
+ public static function providePropertyNamesAndPropertyNamePrefixToRemoveAndExpectedRemainingPropertyNames()
+ {
+ 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
+ * @param string $propertyNamePrefix
+ * @param list $expectedRemainingPropertyNames
+ *
+ * @dataProvider providePropertyNamesAndPropertyNamePrefixToRemoveAndExpectedRemainingPropertyNames
+ */
+ public function removeMatchingRulesRemovesRulesByPropertyNamePrefixAndKeepsOthers(
+ array $initialPropertyNames,
+ $propertyNamePrefix,
+ array $expectedRemainingPropertyNames
+ ) {
+ $propertyNamePrefixWithHyphen = $propertyNamePrefix . '-';
+ $subject = new ConcreteRuleSet();
+ self::setRulesFromPropertyNames($subject, $initialPropertyNames);
+
+ $subject->removeMatchingRules($propertyNamePrefixWithHyphen);
+
+ $remainingRules = $subject->getRulesAssoc();
+ self::assertArrayNotHasKey($propertyNamePrefix, $remainingRules);
+ foreach (\array_keys($remainingRules) as $remainingPropertyName) {
+ self::assertStringStartsNotWith($propertyNamePrefixWithHyphen, $remainingPropertyName);
+ }
+ foreach ($expectedRemainingPropertyNames as $expectedPropertyName) {
+ self::assertArrayHasKey($expectedPropertyName, $remainingRules);
+ }
+ }
+
+ /**
+ * @test
+ *
+ * @param list $propertyNamesToRemove
+ *
+ * @dataProvider providePropertyNamesToBeSetInitially
+ */
+ public function removeAllRulesRemovesAllRules(array $propertyNamesToRemove)
+ {
+ $subject = new ConcreteRuleSet();
+ self::setRulesFromPropertyNames($subject, $propertyNamesToRemove);
+
+ $subject->removeAllRules();
+
+ self::assertSame([], $subject->getRules());
+ }
+
+ /**
+ * @param list $propertyNames
+ */
+ private static function setRulesFromPropertyNames(RuleSet $subject, array $propertyNames)
+ {
+ $subject->setRules(\array_map(
+ function ($propertyName) {
+ return new Rule($propertyName);
+ },
+ $propertyNames
+ ));
+ }
+}
diff --git a/tests/Unit/Value/Fixtures/ConcreteValue.php b/tests/Unit/Value/Fixtures/ConcreteValue.php
new file mode 100644
index 00000000..e47e2da5
--- /dev/null
+++ b/tests/Unit/Value/Fixtures/ConcreteValue.php
@@ -0,0 +1,29 @@
+render(new OutputFormat());
+ }
+}
diff --git a/tests/Unit/Value/ValueTest.php b/tests/Unit/Value/ValueTest.php
new file mode 100644
index 00000000..d9ce5a4f
--- /dev/null
+++ b/tests/Unit/Value/ValueTest.php
@@ -0,0 +1,28 @@
+subject = new ConcretePosition();
+ }
+
+ /**
+ * @return array}>
+ */
+ public function provideLineNumber()
+ {
+ return [
+ 'line 1' => [1],
+ 'line 42' => [42],
+ ];
+ }
+
+ /**
+ * @return array}>
+ */
+ public function provideColumnNumber()
+ {
+ return [
+ 'column 0' => [0],
+ 'column 14' => [14],
+ 'column 39' => [39],
+ ];
+ }
+
+ /**
+ * @test
+ */
+ public function getLineNoInitiallyReturnsZero()
+ {
+ $this->doSetUp();
+
+ self::assertSame(0, $this->subject->getLineNo());
+ }
+
+ /**
+ * @test
+ *
+ * @paarm int $lineNumber
+ *
+ * @dataProvider provideLineNumber
+ */
+ public function getLineNoReturnsLineNumberSet($lineNumber)
+ {
+ $this->doSetUp();
+
+ $this->subject->setPosition($lineNumber);
+
+ self::assertSame($lineNumber, $this->subject->getLineNo());
+ }
+
+ /**
+ * @test
+ */
+ public function getLineNoReturnsZeroAfterLineNumberCleared()
+ {
+ $this->doSetUp();
+
+ $this->subject->setPosition(99);
+
+ $this->subject->setPosition(null);
+
+ self::assertSame(0, $this->subject->getLineNo());
+ }
+
+ /**
+ * @test
+ */
+ public function getColNoInitiallyReturnsZero()
+ {
+ $this->doSetUp();
+
+ self::assertSame(0, $this->subject->getColNo());
+ }
+
+ /**
+ * @test
+ *
+ * @param int $columnNumber
+ *
+ * @dataProvider provideColumnNumber
+ */
+ public function getColNoReturnsColumnNumberSet($columnNumber)
+ {
+ $this->doSetUp();
+
+ $this->subject->setPosition(1, $columnNumber);
+
+ self::assertSame($columnNumber, $this->subject->getColNo());
+ }
+
+ /**
+ * @test
+ */
+ public function getColNoReturnsZeroAfterColumnNumberCleared()
+ {
+ $this->doSetUp();
+
+ $this->subject->setPosition(1, 99);
+
+ $this->subject->setPosition(2);
+
+ self::assertSame(0, $this->subject->getColNo());
+ }
+
+ /**
+ * @test
+ */
+ public function setPositionWithZeroClearsLineNumber()
+ {
+ $this->doSetUp();
+
+ $this->subject->setPosition(99);
+
+ $this->subject->setPosition(0);
+
+ self::assertNull($this->subject->getLineNumber());
+ }
+
+ /**
+ * @test
+ */
+ public function getLineNoAfterSetPositionWithZeroReturnsZero()
+ {
+ $this->doSetUp();
+
+ $this->subject->setPosition(99);
+
+ $this->subject->setPosition(0);
+
+ self::assertSame(0, $this->subject->getLineNo());
+ }
+}
diff --git a/tests/Value/CalcRuleValueListTest.php b/tests/Value/CalcRuleValueListTest.php
index 0a2c5304..0ce279fd 100644
--- a/tests/Value/CalcRuleValueListTest.php
+++ b/tests/Value/CalcRuleValueListTest.php
@@ -9,7 +9,7 @@
/**
* @covers \Sabberworm\CSS\Value\CalcRuleValueList
*/
-class CalcRuleValueListTest extends TestCase
+final class CalcRuleValueListTest extends TestCase
{
/**
* @test
diff --git a/tests/Value/SizeTest.php b/tests/Value/SizeTest.php
new file mode 100644
index 00000000..6470148f
--- /dev/null
+++ b/tests/Value/SizeTest.php
@@ -0,0 +1,73 @@
+
+ */
+ public static function provideUnit()
+ {
+ $units = [
+ 'px',
+ 'pt',
+ 'pc',
+ 'cm',
+ 'mm',
+ 'mozmm',
+ 'in',
+ 'vh',
+ 'dvh',
+ 'svh',
+ 'lvh',
+ 'vw',
+ 'vmin',
+ 'vmax',
+ 'rem',
+ '%',
+ 'em',
+ 'ex',
+ 'ch',
+ 'fr',
+ 'deg',
+ 'grad',
+ 'rad',
+ 's',
+ 'ms',
+ 'turn',
+ 'Hz',
+ 'kHz',
+ ];
+
+ return \array_combine(
+ $units,
+ \array_map(
+ function ($unit) {
+ return [$unit];
+ },
+ $units
+ )
+ );
+ }
+
+ /**
+ * @test
+ *
+ * @dataProvider provideUnit
+ */
+ public function parsesUnit($unit)
+ {
+ $subject = Size::parse(new ParserState('1' . $unit, Settings::create()));
+
+ self::assertSame($unit, $subject->getUnit());
+ }
+}
diff --git a/tests/Value/ValueTest.php b/tests/Value/ValueTest.php
new file mode 100644
index 00000000..ac259466
--- /dev/null
+++ b/tests/Value/ValueTest.php
@@ -0,0 +1,107 @@
+
+ */
+ public static function provideArithmeticOperator()
+ {
+ $units = ['+', '-', '*', '/'];
+
+ return \array_combine(
+ $units,
+ \array_map(
+ function ($unit) {
+ return [$unit];
+ },
+ $units
+ )
+ );
+ }
+
+ /**
+ * @test
+ *
+ * @dataProvider provideArithmeticOperator
+ */
+ public function parsesArithmeticInFunctions($operator)
+ {
+ $subject = Value::parseValue(new ParserState('max(300px, 50vh ' . $operator . ' 10px);', Settings::create()));
+
+ self::assertSame('max(300px,50vh ' . $operator . ' 10px)', (string) $subject);
+ }
+
+ /**
+ * @return array
+ * The first datum is a template for the parser (using `sprintf` insertion marker `%s` for some expression).
+ * The second is for the expected result, which may have whitespace and trailing semicolon removed.
+ */
+ public static function provideCssFunctionTemplates()
+ {
+ return [
+ 'calc' => [
+ 'to be parsed' => 'calc(%s);',
+ 'expected' => 'calc(%s)',
+ ],
+ 'max' => [
+ 'to be parsed' => 'max(300px, %s);',
+ 'expected' => 'max(300px,%s)',
+ ],
+ ];
+ }
+
+ /**
+ * @test
+ *
+ * @dataProvider provideCssFunctionTemplates
+ */
+ public function parsesArithmeticWithMultipleOperatorsInFunctions(
+ $parserTemplate,
+ $expectedResultTemplate
+ ) {
+ static $expression = '300px + 10% + 10vw';
+
+ $subject = Value::parseValue(new ParserState(\sprintf($parserTemplate, $expression), Settings::create()));
+
+ self::assertSame(\sprintf($expectedResultTemplate, $expression), (string) $subject);
+ }
+
+ /**
+ * @return array
+ */
+ public static function provideMalformedLengthOperands()
+ {
+ return [
+ 'LHS missing number' => ['vh', '10px'],
+ 'RHS missing number' => ['50vh', 'px'],
+ 'LHS missing unit' => ['50', '10px'],
+ 'RHS missing unit' => ['50vh', '10'],
+ ];
+ }
+
+ /**
+ * @test
+ *
+ * @dataProvider provideMalformedLengthOperands
+ */
+ public function parsesArithmeticWithMalformedOperandsInFunctions($leftOperand, $rightOperand)
+ {
+ $subject = Value::parseValue(new ParserState(
+ 'max(300px, ' . $leftOperand . ' + ' . $rightOperand . ');',
+ Settings::create()
+ ));
+
+ self::assertSame('max(300px,' . $leftOperand . ' + ' . $rightOperand . ')', (string) $subject);
+ }
+}
diff --git a/tests/fixtures/colortest.css b/tests/fixtures/colortest.css
index 1c89cf41..f834aa77 100644
--- a/tests/fixtures/colortest.css
+++ b/tests/fixtures/colortest.css
@@ -9,6 +9,7 @@
#yours {
background-color: hsl(220, 10%, 220%);
background-color: hsla(220, 10%, 220%, 0.3);
+ outline-color: #22;
}
#variables {