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 {