diff --git a/.gitattributes b/.gitattributes index b6b5061f..82a4f0b9 100644 --- a/.gitattributes +++ b/.gitattributes @@ -3,7 +3,10 @@ /.github/ export-ignore /.gitignore export-ignore /.phive/ export-ignore +/CODE_OF_CONDUCT.md export-ignore +/CONTRIBUTING.md export-ignore /bin/ export-ignore /config/ export-ignore +/docs/ export-ignore /phpunit.xml export-ignore /tests/ export-ignore diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 9026f584..4f6aacc9 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -2,15 +2,26 @@ version: 2 updates: - - package-ecosystem: "github-actions" - directory: "/" - schedule: - interval: "daily" + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "daily" + commit-message: + prefix: "[Dependabot] " + milestone: 1 - - package-ecosystem: "composer" - directory: "/" - schedule: - interval: "daily" - allow: - - dependency-type: "development" - versioning-strategy: "increase" + - package-ecosystem: "composer" + directory: "/" + schedule: + interval: "daily" + allow: + - dependency-type: "development" + ignore: + - dependency-name: "phpstan/*" + - dependency-name: "phpunit/phpunit" + versions: [ ">= 9.0.0" ] + - dependency-name: "rector/rector" + versioning-strategy: "increase" + commit-message: + prefix: "[Dependabot] " + milestone: 1 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 951992d2..14624a48 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,136 +1,146 @@ # https://help.github.com/en/categories/automating-your-workflow-with-github-actions on: - pull_request: - push: - schedule: - - cron: '3 3 * * 1' + push: + branches: + - main + pull_request: + schedule: + - cron: '3 3 * * 1' name: CI jobs: - php-lint: - name: PHP Lint - 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' ] - - steps: - - name: Checkout - uses: actions/checkout@v3 - - - name: Install PHP - uses: shivammathur/setup-php@v2 - with: - php-version: ${{ matrix.php-version }} - coverage: none - - - name: PHP Lint - run: find src tests -name '*.php' -print0 | xargs -0 -n 1 -P 4 php -l - - unit-tests: - name: Unit tests - - runs-on: ubuntu-22.04 - - needs: [ php-lint ] - - 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 - - steps: - - name: Checkout - uses: actions/checkout@v3 - - - name: Install PHP - uses: shivammathur/setup-php@v2 - with: - 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@v3 - 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@v3 - - - 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 - run: composer config --global --list - - - name: Cache dependencies installed with composer - uses: actions/cache@v3 - 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: Install development tools - run: | - phive --no-progress install --trust-gpg-keys BBAB5DF0A0D6672989CF1869E82B2FB314E9906E,A972B9ABB95D0B760B51442231C7E470E2138192,D32680D5957DC7116BE29C14CF1A108D0E7AE720 - - - name: Run Command - run: composer ci:php:${{ matrix.command }} + php-lint: + name: PHP Lint + runs-on: ubuntu-22.04 + strategy: + matrix: + php-version: [ '7.2', '7.3', '7.4', '8.0', '8.1', '8.2', '8.3', '8.4' ] + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php-version }} + ini-file: development + tools: composer:v2 + coverage: none + + - 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: PHP Lint + run: composer ci:php:lint + + unit-tests: + name: Unit tests + + runs-on: ubuntu-22.04 + + needs: [ php-lint ] + + strategy: + fail-fast: false + matrix: + php-version: [ '7.2', '7.3', '7.4', '8.0', '8.1', '8.2', '8.3', '8.4' ] + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php-version }} + ini-file: development + tools: composer:v2 + coverage: none + + - 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 + + static-analysis: + name: Static Analysis + + runs-on: ubuntu-22.04 + + needs: [ php-lint ] + + strategy: + fail-fast: false + matrix: + command: + - composer:normalize + - php:fixer + - php:stan + - php:rector + php-version: + - '8.3' + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php-version }} + ini-file: development + tools: "composer:v2, phive" + coverage: none + + - 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: Install development tools + run: | + phive --no-progress install --trust-gpg-keys 0FDE18AE1D09E19F60F6B1CBC00543248C87FB13,BBAB5DF0A0D6672989CF1869E82B2FB314E9906E + + - name: Run Command + run: composer ci:${{ matrix.command }} diff --git a/.github/workflows/codecoverage.yml b/.github/workflows/codecoverage.yml new file mode 100644 index 00000000..dff38197 --- /dev/null +++ b/.github/workflows/codecoverage.yml @@ -0,0 +1,64 @@ +# https://help.github.com/en/categories/automating-your-workflow-with-github-actions + +on: + push: + branches: + - main + pull_request: + +name: Code coverage + +jobs: + code-coverage: + name: Code coverage + + runs-on: ubuntu-22.04 + + strategy: + fail-fast: false + matrix: + 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 }} + ini-file: development + tools: composer:v2 + coverage: xdebug + + - name: Show the Composer version + run: composer --version + + - 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: composer ci:tests:coverage + + - name: Show generated coverage files + run: ls -lah + + - name: Upload coverage results to Coveralls + uses: coverallsapp/github-action@v2 + env: + github-token: ${{ secrets.GITHUB_TOKEN }} + file: coverage.xml diff --git a/.gitignore b/.gitignore index c1747f26..8bdbea99 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,9 @@ /.phive/* /.php-cs-fixer.cache /.php_cs.cache +/.phpunit.result.cache /composer.lock +/coverage.xml /phpstan.neon /vendor/ !/.phive/phars.xml diff --git a/.phive/phars.xml b/.phive/phars.xml index d353fbf9..6af30ed5 100644 --- a/.phive/phars.xml +++ b/.phive/phars.xml @@ -1,7 +1,5 @@ - - - - + + diff --git a/CHANGELOG.md b/CHANGELOG.md index a23fd0e6..e603df7f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,222 @@ -# Revision History +# Changelog + +All notable changes to this project will be documented in this file. +This project adheres to [Semantic Versioning](https://semver.org/). + +Please also have a look at our +[API and deprecation policy](docs/API-and-deprecation-policy.md). + +## x.y.z + +### Added + +- Interface `RuleContainer` for `RuleSet` `Rule` manipulation methods (#1256) +- `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) +- `Positionable` interface for CSS items that may have a position + (line and perhaps column number) in the parsed CSS (#1221) +- Partial support for CSS Color Module Level 4: + - `rgb` and `rgba`, and `hsl` and `hsla` are now aliases (#797} + - Parse color functions that use the "modern" syntax (#800) + - Render RGB functions with "modern" syntax when required (#840) + - Support `none` as color function component value (#859) +- Add a class diagram to the README (#482) +- Add more tests (#449) + +### Changed + +- `setPosition()` (in `Rule` and other classes) now has fluent interface, + returning itself (#1259) +- `RuleSet::removeRule()` now only allows `Rule` as the parameter + (implementing classes are `AtRuleSet` and `DeclarationBlock`); + use `removeMatchingRules()` or `removeAllRules()` for other functions (#1255) +- `RuleSet::getRules()` and `getRulesAssoc()` now only allow `string` or `null` + as the parameter (implementing classes are `AtRuleSet` and `DeclarationBlock`) + (#1253) +- 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) +- Initialize `KeyFrame` properties to sensible defaults (#1146) +- Make `OutputFormat` `final` (#1128) +- Make `Selector` a `Renderable` (#1017) +- Only allow `string` for some `OutputFormat` properties (#885) +- Use more native type declarations and strict mode + (#641, #772, #774, #778, #804, #841, #873, #875, #891, #922, #923, #933, #958, + #964, #967, #1000, #1044, #1134, #1136, #1137, #1139, #1140, #1141, #1145, + #1162, #1163, #1166, #1172, #1174, #1178, #1179, #1181, #1183, #1184, #1186, + #1187, #1190, #1192, #1193, #1203) +- Add visibility to all class/interface constants (#469) + +### Deprecated + +- 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 + +- Passing a string as the first argument to `getAllValues()` is no longer + supported and will not work; + the search pattern should now be passed as the second argument (#1243) +- Passing a Boolean as the second argument to `getAllValues()` is no longer + supported and will not work; the flag for searching in function arguments + should now be passed as the third argument (#1243) +- Remove `__toString()` (#1046) +- Drop magic method forwarding in `OutputFormat` (#898) +- Drop `atRuleArgs()` from the `AtRule` interface (#1141) +- Remove `OutputFormat::get()` and `::set()` (#1108, #1110) +- Drop special support for vendor prefixes (#1083) +- Remove the IE hack in `Rule` (#995) +- Drop `getLineNo()` from the `Renderable` interface (#1038) +- Remove `OutputFormat::level()` (#874) +- Remove expansion of shorthand properties (#838) +- Remove `Parser::setCharset/getCharset` (#808) +- Remove `Rule::getValues()` (#582) +- Remove `Rule::setValues()` (#562) +- Remove `Document::getAllSelectors()` (#561) +- Remove `DeclarationBlock::getSelector()` (#559) +- Remove `DeclarationBlock::setSelector()` (#560) +- Drop support for PHP < 7.2 (#420) + +### Fixed + +- Don't render `rgb` colors with percentage values using hex notation (#803) + +### Documentation + +- Add an API and deprecation policy (#720) + +@ziegenberg is a new contributor to this release and did a lot of the heavy +lifting. Thanks! :heart: + +## 8.8.0: Bug fixes and deprecations + +### Added + +- `OutputFormat` properties for space around specific list separators (#880) + +### Changed + +- Mark the `OutputFormat` constructor as `@internal` (#1131) +- Mark `OutputFormatter` as `@internal` (#896) +- Mark `Selector::isValid()` as `@internal` (#1037) +- Mark parsing-related methods of most CSS elements as `@internal` (#908) +- Mark `OutputFormat::nextLevel()` as `@internal` (#901) +- Make all non-private properties `@internal` (#886) + +### Deprecated + +- Deprecate extending `OutputFormat` (#1131) +- Deprecate `OutputFormat::get()` and `::set()` (#1107) +- Deprecate support for `-webkit-calc` and `-moz-calc` (#1086) +- Deprecate magic method forwarding from `OutputFormat` to `OutputFormatter` + (#894) +- Deprecate `__toString()` (#1006) +- Deprecate greedy calculation of selector specificity (#1018) +- Deprecate the IE hack in `Rule` (#993, #1003) +- `OutputFormat` properties for space around list separators as an array (#880) +- Deprecate `OutputFormat::level()` (#870) + +### Fixed + +- Include comments for all rules in declaration block (#1169) +- Render rules in line and column number order (#1059) +- Create `Size` with correct types in `expandBackgroundShorthand` (#814) +- Parse `@font-face` `src` property as comma-delimited list (#794) + +## 8.7.0: Add support for PHP 8.4 + +### Added + +- Add support for PHP 8.4 (#643, #657) + +### Changed + +- Mark parsing-internal classes and methods as `@internal` (#674) +- Block installations on unsupported higher PHP versions (#691) + +### Deprecated + +- Deprecate the expansion of shorthand properties + (#578, #580, #579, #577, #576, #575, #574, #573, #572, #571, #570, #569, #566, + #567, #558, #714) +- Deprecate `Parser::setCharset()` and `Parser::getCharset()` (#688) + +### Fixed + +- Fix type errors in PHP strict mode (#664) + +## 8.6.0 + +### Added + +- Support arithmetic operators in CSS function arguments (#607) +- Add support for inserting an item in a CSS list (#545) +- Add support for the `dvh`, `lvh` and `svh` length units (#415) + +### Changed + +- Improve performance of `Value::parseValue` with many delimiters by refactoring + to remove `array_search()` (#413) + +## 8.5.2 + +### Changed + +- Mark all class constants as `@internal` (#472) + +### Fixed + +- Fix undefined local variable in `CalcFunction::parse()` (#593) + +## 8.5.1 + +### Fixed + +- Fix PHP notice caused by parsing invalid color values having less than + 6 characters (#485) +- Fix (regression) failure to parse at-rules with strict parsing (#456) + +## 8.5.0 + +### Added + +- Add a method to get an import's media queries (#384) +- Add more unit tests (#381, #382) + +### Fixed + +- Retain CSSList and Rule comments when rendering CSS (#351) +- Replace invalid `turns` unit with `turn` (#350) +- Also allow string values for rules (#348) +- Fix invalid calc parsing (#169) +- Handle scientific notation when parsing sizes (#179) +- Fix PHP 8.1 compatibility in `ParserState::strsplit()` (#344) ## 8.4.0 @@ -6,7 +224,8 @@ * 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* @@ -21,7 +240,10 @@ * 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 @@ -33,11 +255,16 @@ ## 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* @@ -45,16 +272,20 @@ * 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* @@ -66,7 +297,8 @@ ### 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) @@ -76,7 +308,8 @@ ## 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* @@ -93,7 +326,8 @@ ### 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) @@ -107,22 +341,27 @@ ### 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* @@ -137,13 +376,15 @@ ## 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* @@ -161,7 +402,9 @@ ## 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* @@ -198,18 +441,22 @@ ### 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) @@ -218,10 +465,18 @@ ### 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) @@ -229,8 +484,13 @@ ### Backwards-incompatible changes -* `Sabberworm\CSS\RuleSet->getRules()` returns an index-based array instead of an associative array. Use `Sabberworm\CSS\RuleSet->getRulesAssoc()` (which eliminates duplicate rules and lets the later rule of the same name win). -* `Sabberworm\CSS\RuleSet->removeRule()` works as it did before except when passed an instance of `Sabberworm\CSS\Rule\Rule`, in which case it would only remove the exact rule given instead of all the rules of the same type. To get the old behaviour, use `Sabberworm\CSS\RuleSet->removeRule($oRule->getRule()`; +* `Sabberworm\CSS\RuleSet->getRules()` returns an index-based array instead of + an associative array. Use `Sabberworm\CSS\RuleSet->getRulesAssoc()` (which + eliminates duplicate rules and lets the later rule of the same name win). +* `Sabberworm\CSS\RuleSet->removeRule()` works as it did before except when + passed an instance of `Sabberworm\CSS\Rule\Rule`, in which case it would only + remove the exact rule given instead of all the rules of the same type. To get + the old behaviour, use `Sabberworm\CSS\RuleSet->removeRule($oRule->getRule()`; ## 1.0 diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 00000000..d50e40b4 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,119 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, caste, color, religion, or sexual +identity and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the overall + community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or advances of + any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email address, + without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official email address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +(coc-github at myintervals dot com). +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series of +actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or permanent +ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within the +community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org/), +version 2.1, available at +https://www.contributor-covenant.org/version/2/1/code_of_conduct.html. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..1d0085f3 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,196 @@ +# Contributing to PHP-CSS-Parser + +Those that wish to contribute bug fixes, new features, refactorings and +clean-up to PHP-CSS-Parser are more than welcome. + +When you contribute, please take the following things into account: + +## Contributor Code of Conduct + +Please note that this project is released with a +[Contributor Code of Conduct](../CODE_OF_CONDUCT.md). By participating in this +project, you agree to abide by its terms. + +## General workflow + +This is the workflow for contributing changes to this project:: + +1. [Fork the Git repository](https://docs.github.com/en/get-started/exploring-projects-on-github/contributing-to-a-project). +1. Clone your forked repository locally and install the development + dependencies. +1. Create a local branch for your changes. +1. Add unit tests for your changes. + These tests should fail without your changes. +1. Add your changes. Your added unit tests now should pass, and no other tests + should be broken. Check that your changes follow the same coding style as the + rest of the project. +1. Add a changelog entry, newest on top. +1. Commit and push your changes. +1. [Create a pull request](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/about-pull-requests) + for your changes. +1. Check that the CI build is green. (If it is not, fix the problems listed.) + Please note that for first-time contributors, you will need to wait for a + maintainer to allow your CI build to run. +1. Wait for a review by the maintainers. +1. Polish your changes as needed until they are ready to be merged. + +## About code reviews + +After you have submitted a pull request, the maintainers will review your +changes. This will probably result in quite a few comments on ways to improve +your pull request. This project receives contributions from developers around +the world, so we need the code to be the most consistent, readable, and +maintainable that it can be. + +Please do not feel frustrated by this - instead please view this both as our +contribution to your pull request as well as a way to learn more about +improving code quality. + +If you would like to know whether an idea would fit in the general strategy of +this project or would like to get feedback on the best architecture for your +ideas, we propose you open a ticket first and discuss your ideas there +first before investing a lot of time in writing code. + +## Install the development dependencies + +To install the most important development dependencies, please run the following +command: + +```bash +composer install +``` + +We also have some optional development dependencies that require higher PHP +versions than the lowest PHP version this project supports. Hence they are not +installed by default. + +To install these, you will need to have [PHIVE](https://phar.io/) installed. +You can then run the following command: + +```bash +phive install +``` + +## Unit-test your changes + +Please cover all changes with unit tests and make sure that your code does not +break any existing tests. We will only merge pull requests that include full +code coverage of the fixed bugs and the new features. + +To run the existing PHPUnit tests, run this command: + +```bash +composer ci:tests:unit +``` + +## Coding Style + +Please use the same coding style +([PER 2.0](https://www.php-fig.org/per/coding-style/)) as the rest of the code. +Indentation is four spaces. + +We will only merge pull requests that follow the project's coding style. + +Please check your code with the provided static code analysis tools: + +```bash +composer ci:static +``` + +Please make your code clean, well-readable and easy to understand. + +If you add new methods or fields, please add proper PHPDoc for the new +methods/fields. Please use grammatically correct, complete sentences in the +code documentation. + +You can autoformat your code using the following command: + +```bash +composer fix +``` + +## Git commits + +Commit message should have a <= 50-character summary, optionally followed by a +blank line and a more in depth description of 79 characters per line. + +Please use grammatically correct, complete sentences in the commit messages. + +Also, please prefix the subject line of the commit message with either +`[FEATURE]`, `[TASK]`, `[BUGFIX]` OR `[CLEANUP]`. This makes it faster to see +what a commit is about. + +## Creating pull requests (PRs) + +When you create a pull request, please +[make your PR editable](https://github.com/blog/2247-improving-collaboration-with-forks). + +## Rebasing + +If other PRs have been merged during the time between your initial PR creation +and final approval, it may be required that you rebase your changes against the +latest `main` branch. + +There are potential pitfalls here if you follow the suggestions from `git`, +which could leave your branch in an unrecoverable mess, +and you having to start over with a new branch and new PR. + +The procedure below is tried and tested, and will help you avoid frustration. + +To rebase a feature branch to the latest `main`: + +1. Make sure that your local copy of the repository has the most up-to-date + revisions of `main` (this is important, otherwise you may end up rebasing to + an older base point): + ```bash + git switch main + git pull + ``` +1. Switch to the (feature) branch to be rebased and make sure your copy is up to + date: + ```bash + git switch feature/something-cool + git pull + ``` +1. Consider taking a copy of the folder tree at this stage; this may help when + resolving conflicts in the next step. +1. Begin the rebasing process + ```bash + git rebase main + ``` +1. Resolve the conflicts in the reported files. (This will typically require + reversing the order of the new entries in `CHANGELOG.md`.) You may use a + folder `diff` against the copy taken at step 3 to assist, but bear in mind + that at this stage `git` is partway through rebasing, so some files will have + been merged and include the latest changes from `main`, whilst others might + not. In any case, you should ignore changes to files not reported as having + conflicts. + + If there were no conflicts, skip this and the next step. +1. Mark the conflicting files as resolved and continue the rebase + ```bash + git add . + git rebase --continue + ``` + (You can alternatively use more specific wildcards or specify individual + files with a full relative path.) + + If there were no conflicts reported in the previous step, skip this step. + + If there are more conflicts to resolve, repeat the previous step then this + step again. +1. Force-push the rebased (feature) branch to the remote repository + ```bash + git push --force + ``` + The `--force` option is important. Without it, you'll get an error with a + hint suggesting a `git pull` is required: + ``` + hint: Updates were rejected because the tip of your current branch is behind + hint: its remote counterpart. Integrate the remote changes (e.g. + hint: 'git pull ...') before pushing again. + hint: See the 'Note about fast-forwards' in 'git push --help' for details. + ``` + ***DO NOT*** follow the hint and execute `git pull`. This will result in the + set of all commits on the feature branch being duplicated, and the "patching + base" not being moved at all. diff --git a/README.md b/README.md index 90428cbf..9ecdc3e7 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ # PHP CSS Parser -[![Build Status](https://github.com/sabberworm/PHP-CSS-Parser/workflows/CI/badge.svg?branch=master)](https://github.com/sabberworm/PHP-CSS-Parser/actions/) +[![Build Status](https://github.com/MyIntervals/PHP-CSS-Parser/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/MyIntervals/PHP-CSS-Parser/actions/) +[![Coverage Status](https://coveralls.io/repos/github/MyIntervals/PHP-CSS-Parser/badge.svg?branch=main)](https://coveralls.io/github/MyIntervals/PHP-CSS-Parser?branch=main) A Parser for CSS Files written in PHP. Allows extraction of CSS files into a data structure, manipulation of said structure and output as (optimized) CSS. @@ -158,7 +159,7 @@ foreach($cssDocument->getAllRuleSets() as $oRuleSet) { // Note that the added dash will make this remove all rules starting with // `font-` (like `font-size`, `font-weight`, etc.) as well as a potential // `font` rule. - $oRuleSet->removeRule('font-'); + $oRuleSet->removeRule('font-'); $oRuleSet->removeRule('cursor'); } ``` @@ -220,44 +221,44 @@ html, body { ```php class Sabberworm\CSS\CSSList\Document#4 (2) { - protected $aContents => + protected $contents => array(4) { [0] => class Sabberworm\CSS\Property\Charset#6 (2) { - private $sCharset => + private $charset => class Sabberworm\CSS\Value\CSSString#5 (2) { - private $sString => + private $string => string(5) "utf-8" - protected $iLineNo => + protected $lineNumber => int(1) } - protected $iLineNo => + protected $lineNumber => int(1) } [1] => class Sabberworm\CSS\RuleSet\AtRuleSet#7 (4) { - private $sType => + private $type => string(9) "font-face" - private $sArgs => + private $arguments => string(0) "" - private $aRules => + private $rules => array(2) { 'font-family' => array(1) { [0] => class Sabberworm\CSS\Rule\Rule#8 (4) { - private $sRule => + private $rule => string(11) "font-family" - private $mValue => + private $value => class Sabberworm\CSS\Value\CSSString#9 (2) { - private $sString => + private $string => string(10) "CrassRoots" - protected $iLineNo => + protected $lineNumber => int(4) } - private $bIsImportant => + private $isImportant => bool(false) - protected $iLineNo => + protected $lineNumber => int(4) } } @@ -265,76 +266,76 @@ class Sabberworm\CSS\CSSList\Document#4 (2) { array(1) { [0] => class Sabberworm\CSS\Rule\Rule#10 (4) { - private $sRule => + private $rule => string(3) "src" - private $mValue => + private $value => class Sabberworm\CSS\Value\URL#11 (2) { - private $oURL => + private $url => class Sabberworm\CSS\Value\CSSString#12 (2) { - private $sString => + private $string => string(15) "../media/cr.ttf" - protected $iLineNo => + protected $lineNumber => int(5) } - protected $iLineNo => + protected $lineNumber => int(5) } - private $bIsImportant => + private $isImportant => bool(false) - protected $iLineNo => + protected $lineNumber => int(5) } } } - protected $iLineNo => + protected $lineNumber => int(3) } [2] => class Sabberworm\CSS\RuleSet\DeclarationBlock#13 (3) { - private $aSelectors => + private $selectors => array(2) { [0] => class Sabberworm\CSS\Property\Selector#14 (2) { - private $sSelector => + private $selector => string(4) "html" - private $iSpecificity => + private $specificity => NULL } [1] => class Sabberworm\CSS\Property\Selector#15 (2) { - private $sSelector => + private $selector => string(4) "body" - private $iSpecificity => + private $specificity => NULL } } - private $aRules => + private $rules => array(1) { 'font-size' => array(1) { [0] => class Sabberworm\CSS\Rule\Rule#16 (4) { - private $sRule => + private $rule => string(9) "font-size" - private $mValue => + private $value => class Sabberworm\CSS\Value\Size#17 (4) { - private $fSize => + private $size => double(1.6) - private $sUnit => + private $unit => string(2) "em" - private $bIsColorComponent => + private $isColorComponent => bool(false) - protected $iLineNo => + protected $lineNumber => int(9) } - private $bIsImportant => + private $isImportant => bool(false) - protected $iLineNo => + protected $lineNumber => int(9) } } } - protected $iLineNo => + protected $lineNumber => int(8) } [3] => @@ -343,96 +344,96 @@ class Sabberworm\CSS\CSSList\Document#4 (2) { string(9) "keyframes" private $animationName => string(6) "mymove" - protected $aContents => + protected $contents => array(2) { [0] => class Sabberworm\CSS\RuleSet\DeclarationBlock#19 (3) { - private $aSelectors => + private $selectors => array(1) { [0] => class Sabberworm\CSS\Property\Selector#20 (2) { - private $sSelector => + private $selector => string(4) "from" - private $iSpecificity => + private $specificity => NULL } } - private $aRules => + private $rules => array(1) { 'top' => array(1) { [0] => class Sabberworm\CSS\Rule\Rule#21 (4) { - private $sRule => + private $rule => string(3) "top" - private $mValue => + private $value => class Sabberworm\CSS\Value\Size#22 (4) { - private $fSize => + private $size => double(0) - private $sUnit => + private $unit => string(2) "px" - private $bIsColorComponent => + private $isColorComponent => bool(false) - protected $iLineNo => + protected $lineNumber => int(13) } - private $bIsImportant => + private $isImportant => bool(false) - protected $iLineNo => + protected $lineNumber => int(13) } } } - protected $iLineNo => + protected $lineNumber => int(13) } [1] => class Sabberworm\CSS\RuleSet\DeclarationBlock#23 (3) { - private $aSelectors => + private $selectors => array(1) { [0] => class Sabberworm\CSS\Property\Selector#24 (2) { - private $sSelector => + private $selector => string(2) "to" - private $iSpecificity => + private $specificity => NULL } } - private $aRules => + private $rules => array(1) { 'top' => array(1) { [0] => class Sabberworm\CSS\Rule\Rule#25 (4) { - private $sRule => + private $rule => string(3) "top" - private $mValue => + private $value => class Sabberworm\CSS\Value\Size#26 (4) { - private $fSize => + private $size => double(200) - private $sUnit => + private $unit => string(2) "px" - private $bIsColorComponent => + private $isColorComponent => bool(false) - protected $iLineNo => + protected $lineNumber => int(14) } - private $bIsImportant => + private $isImportant => bool(false) - protected $iLineNo => + protected $lineNumber => int(14) } } } - protected $iLineNo => + protected $lineNumber => int(14) } } - protected $iLineNo => + protected $lineNumber => int(12) } } - protected $iLineNo => + protected $lineNumber => int(1) } @@ -466,85 +467,85 @@ html, body {font-size: 1.6em;} ```php class Sabberworm\CSS\CSSList\Document#4 (2) { - protected $aContents => + protected $contents => array(1) { [0] => class Sabberworm\CSS\RuleSet\DeclarationBlock#5 (3) { - private $aSelectors => + private $selectors => array(1) { [0] => class Sabberworm\CSS\Property\Selector#6 (2) { - private $sSelector => + private $selector => string(7) "#header" - private $iSpecificity => + private $specificity => NULL } } - private $aRules => + private $rules => array(3) { 'margin' => array(1) { [0] => class Sabberworm\CSS\Rule\Rule#7 (4) { - private $sRule => + private $rule => string(6) "margin" - private $mValue => + private $value => class Sabberworm\CSS\Value\RuleValueList#12 (3) { - protected $aComponents => + protected $components => array(4) { [0] => class Sabberworm\CSS\Value\Size#8 (4) { - private $fSize => + private $size => double(10) - private $sUnit => + private $unit => string(2) "px" - private $bIsColorComponent => + private $isColorComponent => bool(false) - protected $iLineNo => + protected $lineNumber => int(2) } [1] => class Sabberworm\CSS\Value\Size#9 (4) { - private $fSize => + private $size => double(2) - private $sUnit => + private $unit => string(2) "em" - private $bIsColorComponent => + private $isColorComponent => bool(false) - protected $iLineNo => + protected $lineNumber => int(2) } [2] => class Sabberworm\CSS\Value\Size#10 (4) { - private $fSize => + private $size => double(1) - private $sUnit => + private $unit => string(2) "cm" - private $bIsColorComponent => + private $isColorComponent => bool(false) - protected $iLineNo => + protected $lineNumber => int(2) } [3] => class Sabberworm\CSS\Value\Size#11 (4) { - private $fSize => + private $size => double(2) - private $sUnit => + private $unit => string(1) "%" - private $bIsColorComponent => + private $isColorComponent => bool(false) - protected $iLineNo => + protected $lineNumber => int(2) } } - protected $sSeparator => + protected $separator => string(1) " " - protected $iLineNo => + protected $lineNumber => int(2) } - private $bIsImportant => + private $isImportant => bool(false) - protected $iLineNo => + protected $lineNumber => int(2) } } @@ -552,11 +553,11 @@ class Sabberworm\CSS\CSSList\Document#4 (2) { array(1) { [0] => class Sabberworm\CSS\Rule\Rule#13 (4) { - private $sRule => + private $rule => string(11) "font-family" - private $mValue => + private $value => class Sabberworm\CSS\Value\RuleValueList#15 (3) { - protected $aComponents => + protected $components => array(4) { [0] => string(7) "Verdana" @@ -564,9 +565,9 @@ class Sabberworm\CSS\CSSList\Document#4 (2) { string(9) "Helvetica" [2] => class Sabberworm\CSS\Value\CSSString#14 (2) { - private $sString => + private $string => string(9) "Gill Sans" - protected $iLineNo => + protected $lineNumber => int(3) } [3] => @@ -574,12 +575,12 @@ class Sabberworm\CSS\CSSList\Document#4 (2) { } protected $sSeparator => string(1) "," - protected $iLineNo => + protected $lineNumber => int(3) } - private $bIsImportant => + private $isImportant => bool(false) - protected $iLineNo => + protected $lineNumber => int(3) } } @@ -587,22 +588,22 @@ class Sabberworm\CSS\CSSList\Document#4 (2) { array(1) { [0] => class Sabberworm\CSS\Rule\Rule#16 (4) { - private $sRule => + private $rule => string(5) "color" - private $mValue => + private $value => string(3) "red" - private $bIsImportant => + private $isImportant => bool(true) - protected $iLineNo => + protected $lineNumber => int(4) } } } - protected $iLineNo => + protected $lineNumber => int(1) } } - protected $iLineNo => + protected $lineNumber => int(1) } @@ -615,6 +616,207 @@ class Sabberworm\CSS\CSSList\Document#4 (2) { #header {margin: 10px 2em 1cm 2%;font-family: Verdana,Helvetica,"Gill Sans",sans-serif;color: red !important;} ``` +## Class diagram + +```mermaid +classDiagram + direction LR + + %% Start of the part originally generated from the PHP code using tasuku43/mermaid-class-diagram + + class CSSElement { + <> + } + class Renderable { + <> + } + class Positionable { + <> + } + class CSSListItem { + <> + } + class RuleContainer { + <> + } + class DeclarationBlock { + } + class RuleSet { + <> + } + class AtRuleSet { + } + class KeyframeSelector { + } + class AtRule { + <> + } + class Charset { + } + class Import { + } + class Selector { + } + class CSSNamespace { + } + class Settings { + } + class Rule { + } + class Parser { + } + class OutputFormatter { + } + class OutputFormat { + } + class OutputException { + } + class UnexpectedEOFException { + } + class SourceException { + } + class UnexpectedTokenException { + } + class ParserState { + } + class Anchor { + } + class CSSBlockList { + <> + } + class Document { + } + class CSSList { + <> + } + class KeyFrame { + } + class AtRuleBlockList { + } + class Color { + } + class URL { + } + class CalcRuleValueList { + } + class ValueList { + <> + } + class CalcFunction { + } + class LineName { + } + class Value { + <> + } + class Size { + } + class CSSString { + } + class PrimitiveValue { + <> + } + class CSSFunction { + } + class RuleValueList { + } + class Commentable { + <> + } + class Comment { + } + + RuleSet <|-- DeclarationBlock: inheritance + Renderable <|-- CSSElement: inheritance + Renderable <|-- CSSListItem: inheritance + Commentable <|-- CSSListItem: inheritance + Positionable <|.. RuleSet: realization + CSSElement <|.. RuleSet: realization + CSSListItem <|.. RuleSet: realization + RuleContainer <|.. RuleSet: realization + RuleSet <|-- AtRuleSet: inheritance + AtRule <|.. AtRuleSet: realization + Renderable <|.. Selector: realization + Selector <|-- KeyframeSelector: inheritance + CSSListItem <|-- AtRule: inheritance + Positionable <|.. Charset: realization + AtRule <|.. Charset: realization + Positionable <|.. Import: realization + AtRule <|.. Import: realization + Positionable <|.. CSSNamespace: realization + AtRule <|.. CSSNamespace: realization + CSSElement <|.. Rule: realization + Positionable <|.. Rule: realization + Commentable <|.. Rule: realization + SourceException <|-- OutputException: inheritance + UnexpectedTokenException <|-- UnexpectedEOFException: inheritance + Exception <|-- SourceException: inheritance + Positionable <|.. SourceException: realization + SourceException <|-- UnexpectedTokenException: inheritance + CSSList <|-- CSSBlockList: inheritance + CSSBlockList <|-- Document: inheritance + CSSElement <|.. CSSList: realization + Positionable <|.. CSSList: realization + CSSListItem <|.. CSSList: realization + CSSList <|-- KeyFrame: inheritance + AtRule <|.. KeyFrame: realization + CSSBlockList <|-- AtRuleBlockList: inheritance + AtRule <|.. AtRuleBlockList: realization + CSSFunction <|-- Color: inheritance + PrimitiveValue <|-- URL: inheritance + RuleValueList <|-- CalcRuleValueList: inheritance + Value <|-- ValueList: inheritance + CSSFunction <|-- CalcFunction: inheritance + ValueList <|-- LineName: inheritance + CSSElement <|.. Value: realization + Positionable <|.. Value: realization + PrimitiveValue <|-- Size: inheritance + PrimitiveValue <|-- CSSString: inheritance + Value <|-- PrimitiveValue: inheritance + ValueList <|-- CSSFunction: inheritance + ValueList <|-- RuleValueList: inheritance + Renderable <|.. Comment: realization + Positionable <|.. Comment: realization + + %% end of the generated part + + + Anchor --> "1" ParserState : parserState + CSSList --> "*" CSSList : contents + CSSList --> "*" Charset : contents + CSSList --> "*" Comment : comments + CSSList --> "*" Import : contents + CSSList --> "*" RuleSet : contents + CSSNamespace --> "*" Comment : comments + Charset --> "*" Comment : comments + Charset --> "1" CSSString : charset + DeclarationBlock --> "*" Selector : selectors + Import --> "*" Comment : comments + OutputFormat --> "1" OutputFormat : nextLevelFormat + OutputFormat --> "1" OutputFormatter : outputFormatter + OutputFormatter --> "1" OutputFormat : outputFormat + Parser --> "1" ParserState : parserState + ParserState --> "1" Settings : parserSettings + Rule --> "*" Comment : comments + Rule --> "1" RuleValueList : value + RuleSet --> "*" Comment : comments + RuleSet --> "*" Rule : rules + URL --> "1" CSSString : url + ValueList --> "*" Value : components +``` + +## API and deprecation policy + +Please have a look at our +[API and deprecation policy](docs/API-and-deprecation-policy.md). + +## Contributing + +Contributions in the form of bug reports, feature requests, or pull requests are +more than welcome. :pray: Please have a look at our +[contribution guidelines](CONTRIBUTING.md) to learn more about how to +contribute to PHP-CSS-Parser. + ## Contributors/Thanks to * [oliverklee](https://github.com/oliverklee) for lots of refactorings, code modernizations and CI integrations @@ -628,10 +830,12 @@ class Sabberworm\CSS\CSSList\Document#4 (2) { * [docteurklein](https://github.com/docteurklein) for output formatting and `CSSList->remove()` inspiration. * [nicolopignatelli](https://github.com/nicolopignatelli) for PSR-0 compatibility. * [diegoembarcadero](https://github.com/diegoembarcadero) for keyframe at-rule parsing. -* [goetas](https://github.com/goetas) for @namespace at-rule support. +* [goetas](https://github.com/goetas) for `@namespace` at-rule support. +* [ziegenberg](https://github.com/ziegenberg) for general housekeeping and cleanup. * [View full list](https://github.com/sabberworm/PHP-CSS-Parser/contributors) ## Misc -* Legacy Support: The latest pre-PSR-0 version of this project can be checked with the `0.9.0` tag. -* Running Tests: To run all unit tests for this project, run `composer install` to install phpunit and use `./vendor/bin/phpunit`. +### Legacy Support + +The latest pre-PSR-0 version of this project can be checked with the `0.9.0` tag. diff --git a/bin/quickdump.php b/bin/quickdump.php index f371ab1b..c759d028 100755 --- a/bin/quickdump.php +++ b/bin/quickdump.php @@ -1,13 +1,15 @@ #!/usr/bin/env php parse(); @@ -15,7 +17,7 @@ print $sSource; echo "\n```\n\n" . '#### Structure (`var_dump()`)' . "\n\n```php\n"; -var_dump($oDoc); +\var_dump($oDoc); echo "\n```\n\n" . '#### Output (`render()`)' . "\n\n```css\n"; print $oDoc->render(); diff --git a/composer.json b/composer.json index a1f5677b..e4ea9c43 100644 --- a/composer.json +++ b/composer.json @@ -1,26 +1,41 @@ { "name": "sabberworm/php-css-parser", - "type": "library", "description": "Parser for CSS Files written in PHP", + "license": "MIT", + "type": "library", "keywords": [ "parser", "css", "stylesheet" ], - "homepage": "https://www.sabberworm.com/blog/2010/6/10/php-css-parser", - "license": "MIT", "authors": [ { "name": "Raphael Schweikert" + }, + { + "name": "Oliver Klee", + "email": "github@oliverklee.de" + }, + { + "name": "Jake Hotson", + "email": "jake.github@qzdesign.co.uk" } ], + "homepage": "https://www.sabberworm.com/blog/2010/6/10/php-css-parser", "require": { - "php": ">=5.6.20", + "php": "^7.2.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" + "php-parallel-lint/php-parallel-lint": "1.4.0", + "phpstan/extension-installer": "1.4.3", + "phpstan/phpstan": "1.12.16 || 2.1.2", + "phpstan/phpstan-phpunit": "1.4.2 || 2.0.4", + "phpstan/phpstan-strict-rules": "1.6.2 || 2.0.3", + "phpunit/phpunit": "8.5.42", + "rawr/phpunit-data-provider": "3.3.1", + "rector/rector": "1.2.10 || 2.0.7", + "rector/type-perfect": "1.0.0 || 2.0.2" }, "suggest": { "ext-mbstring": "for parsing UTF-8 CSS" @@ -35,35 +50,77 @@ "Sabberworm\\CSS\\Tests\\": "tests/" } }, + "config": { + "allow-plugins": { + "phpstan/extension-installer": true + }, + "preferred-install": { + "*": "dist" + }, + "sort-packages": true + }, + "extra": { + "branch-alias": { + "dev-main": "9.0.x-dev" + } + }, "scripts": { "ci": [ - "@ci:static" + "@ci:static", + "@ci:dynamic" ], - "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:composer:normalize": "\"./.phive/composer-normalize\" --dry-run", + "ci:dynamic": [ + "@ci:tests" + ], + "ci:php:fixer": "\"./.phive/php-cs-fixer\" --config=config/php-cs-fixer.php fix --dry-run -v --show-progress=dots --diff bin src tests config", + "ci:php:lint": "parallel-lint src tests config bin", + "ci:php:rector": "rector --no-progress-bar --dry-run --config=config/rector.php", + "ci:php:stan": "phpstan --no-progress --configuration=config/phpstan.neon", "ci:static": [ + "@ci:composer:normalize", "@ci:php:fixer", - "@ci:php:sniffer", + "@ci:php:lint", + "@ci:php:rector", "@ci:php:stan" ], + "ci:tests": [ + "@ci:tests:unit" + ], + "ci:tests:coverage": "phpunit --do-not-cache-result --coverage-clover=coverage.xml", + "ci:tests:sof": "phpunit --stop-on-failure --do-not-cache-result", + "ci:tests:unit": "phpunit --do-not-cache-result", + "fix": [ + "@fix:php" + ], + "fix:composer:normalize": "\"./.phive/composer-normalize\" --no-check-lock", "fix:php": [ - "@fix:php:fixer", - "@fix:php:sniffer" + "@fix:composer:normalize", + "@fix:php:rector", + "@fix:php:fixer" ], - "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" + "fix:php:fixer": "\"./.phive/php-cs-fixer\" --config=config/php-cs-fixer.php fix bin src tests", + "fix:php:rector": "rector --config=config/rector.php", + "phpstan:baseline": "phpstan --configuration=config/phpstan.neon --generate-baseline=config/phpstan-baseline.neon --allow-empty-baseline" }, "scripts-descriptions": { - "ci": "Runs all dynamic and static code checks (i.e. currently, only the static checks).", + "ci": "Runs all dynamic and static code checks.", + "ci:composer:normalize": "Checks the formatting and structure of the composer.json.", + "ci:dynamic": "Runs all dynamic code checks (i.e., currently, the unit tests).", "ci:php:fixer": "Checks the code style with PHP CS Fixer.", - "ci:php:sniffer": "Checks the code style with PHP_CodeSniffer.", + "ci:php:lint": "Checks the syntax of the PHP code.", + "ci:php:rector": "Checks the code for possible code updates and refactoring.", "ci:php:stan": "Checks the types with PHPStan.", "ci:static": "Runs all static code analysis checks for the code.", + "ci:tests": "Runs all dynamic tests (i.e., currently, the unit tests).", + "ci:tests:coverage": "Runs the unit tests with code coverage.", + "ci:tests:sof": "Runs the unit tests and stops at the first failure.", + "ci:tests:unit": "Runs all unit tests.", + "fix": "Runs all fixers", + "fix:composer:normalize": "Reformats and sorts the composer.json file.", "fix:php": "Autofixes all autofixable issues in the PHP code.", "fix:php:fixer": "Fixes autofixable issues found by PHP CS Fixer.", - "fix:php:sniffer": "Fixes autofixable issues found by PHP_CodeSniffer.", - "phpstand:baseline": "Updates the PHPStan baseline file to match the code." + "fix:php:rector": "Fixes autofixable issues found by Rector.", + "phpstan:baseline": "Updates the PHPStan baseline file to match the code." } } diff --git a/config/php-cs-fixer.php b/config/php-cs-fixer.php index 88a9a692..96b39bbb 100644 --- a/config/php-cs-fixer.php +++ b/config/php-cs-fixer.php @@ -1,16 +1,13 @@ 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']], + '@PER-CS2.0' => true, + '@PER-CS2.0:risky' => true, '@PHPUnit50Migration:risky' => true, '@PHPUnit52Migration:risky' => true, @@ -18,17 +15,89 @@ '@PHPUnit55Migration:risky' => true, '@PHPUnit56Migration:risky' => true, '@PHPUnit57Migration:risky' => true, + '@PHPUnit60Migration:risky' => true, + '@PHPUnit75Migration:risky' => true, + '@PHPUnit84Migration:risky' => true, + // overwrite the PER2 defaults to restore compatibility with PHP 7.x + 'trailing_comma_in_multiline' => ['elements' => ['arrays']], + + // casing + 'magic_constant_casing' => true, + 'native_function_casing' => true, + + // cast notation + 'modernize_types_casting' => true, + 'no_short_bool_cast' => true, + + // class notation + 'no_php4_constructor' => true, + + // comment + 'no_empty_comment' => true, + + // control structure + 'yoda_style' => ['equal' => false, 'identical' => false, 'less_and_greater' => false], + + // function notation + 'native_function_invocation' => ['include' => ['@all']], + 'nullable_type_declaration_for_default_null_value' => true, + + // import + 'no_unused_imports' => true, + + // language construct + 'combine_consecutive_issets' => true, + 'combine_consecutive_unsets' => true, + 'dir_constant' => true, + 'is_null' => true, + 'nullable_type_declaration' => true, + + // namespace notation + 'no_leading_namespace_whitespace' => true, + + // operator + 'standardize_not_equals' => true, + 'ternary_to_null_coalescing' => true, + + // PHP tag + 'linebreak_after_opening_tag' => true, + + // PHPUnit 'php_unit_construct' => true, - 'php_unit_dedicate_assert' => ['target' => '5.6'], - 'php_unit_expectation' => ['target' => '5.6'], + 'php_unit_dedicate_assert' => ['target' => 'newest'], + 'php_unit_expectation' => ['target' => 'newest'], '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'], + + // PHPDoc + 'no_blank_lines_after_phpdoc' => true, + 'no_empty_phpdoc' => true, + 'phpdoc_indent' => true, + 'phpdoc_no_package' => true, + 'phpdoc_trim' => true, + 'phpdoc_types_order' => ['null_adjustment' => 'always_last', 'sort_algorithm' => 'none'], + + // return notation + 'no_useless_return' => true, + + // semicolon + 'no_empty_statement' => true, + 'no_singleline_whitespace_before_semicolons' => true, + 'semicolon_after_instruction' => true, + + // strict + 'declare_strict_types' => true, + 'strict_param' => true, + + // string notation + 'single_quote' => true, + 'string_implicit_backslashes' => ['single_quoted' => 'escape'], + + // whitespace + 'statement_indentation' => false, ] ); diff --git a/config/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 index b730548c..9be2ebd7 100644 --- a/config/phpstan-baseline.neon +++ b/config/phpstan-baseline.neon @@ -1,22 +1,85 @@ parameters: ignoreErrors: - - message: "#^Call to an undefined method Sabberworm\\\\CSS\\\\OutputFormat\\:\\:setIndentation\\(\\)\\.$#" - count: 2 - path: ../src/OutputFormat.php + message: '#^Only booleans are allowed in an if condition, string given\.$#' + identifier: if.condNotBoolean + count: 1 + path: ../src/CSSList/AtRuleBlockList.php - - message: "#^Class Sabberworm\\\\CSS\\\\Value\\\\Size constructor invoked with 5 parameters, 1\\-4 required\\.$#" - count: 2 - path: ../src/RuleSet/DeclarationBlock.php + message: '#^Loose comparison via "\!\=" is not allowed\.$#' + identifier: notEqual.notAllowed + count: 1 + path: ../src/CSSList/CSSList.php + + - + message: '#^Loose comparison via "\=\=" is not allowed\.$#' + identifier: equal.notAllowed + count: 1 + path: ../src/CSSList/CSSList.php + + - + message: '#^Parameters should have "Sabberworm\\CSS\\CSSList\\CSSListItem\|array" types as the only types passed to this method$#' + identifier: typePerfect.narrowPublicClassMethodParamType + count: 1 + path: ../src/CSSList/CSSList.php + + - + message: '#^Short ternary operator is not allowed\. Use null coalesce operator if applicable or consider using long ternary\.$#' + identifier: ternary.shortNotAllowed + count: 1 + path: ../src/CSSList/CSSList.php - - message: "#^Variable \\$oRule might not be defined\\.$#" - count: 2 + message: '#^Parameters should have "string\|null" types as the only types passed to this method$#' + identifier: typePerfect.narrowPublicClassMethodParamType + count: 1 + path: ../src/CSSList/Document.php + + - + message: '#^Only booleans are allowed in an if condition, Sabberworm\\CSS\\Value\\RuleValueList\|string\|null given\.$#' + identifier: if.condNotBoolean + count: 1 + path: ../src/Rule/Rule.php + + - + message: '#^Loose comparison via "\!\=" is not allowed\.$#' + identifier: notEqual.notAllowed + count: 1 path: ../src/RuleSet/DeclarationBlock.php - - message: "#^Variable \\$oVal might not be defined\\.$#" + message: '#^Parameters should have "string" types as the only types passed to this method$#' + identifier: typePerfect.narrowPublicClassMethodParamType count: 1 + path: ../src/RuleSet/DeclarationBlock.php + + - + message: '#^Loose comparison via "\!\=" is not allowed\.$#' + identifier: notEqual.notAllowed + count: 3 path: ../src/Value/CalcFunction.php + - + message: '#^Cannot call method getSize\(\) on Sabberworm\\CSS\\Value\\Value\|string\.$#' + identifier: method.nonObject + count: 3 + path: ../src/Value/Color.php + + - + message: '#^Loose comparison via "\=\=" is not allowed\.$#' + identifier: equal.notAllowed + count: 3 + path: ../src/Value/Color.php + + - + message: '#^Loose comparison via "\!\=" is not allowed\.$#' + identifier: notEqual.notAllowed + count: 1 + path: ../src/Value/Size.php + + - + message: '#^Parameters should have "float" types as the only types passed to this method$#' + identifier: typePerfect.narrowPublicClassMethodParamType + count: 1 + path: ../src/Value/Size.php diff --git a/config/phpstan.neon b/config/phpstan.neon index 3d7611a6..6a410ac2 100644 --- a/config/phpstan.neon +++ b/config/phpstan.neon @@ -6,13 +6,18 @@ parameters: # Don't be overly greedy on machines with more CPU's to be a good neighbor especially on CI maximumNumberOfProcesses: 5 - level: 1 + phpVersion: 70200 + + level: 3 - scanDirectories: - - %currentWorkingDirectory%/bin/ - - %currentWorkingDirectory%/src/ - - %currentWorkingDirectory%/tests/ paths: - %currentWorkingDirectory%/bin/ - %currentWorkingDirectory%/src/ - %currentWorkingDirectory%/tests/ + + type_perfect: + no_mixed_property: true + no_mixed_caller: true + null_over_false: true + narrow_param: true + narrow_return: true diff --git a/config/rector.php b/config/rector.php new file mode 100644 index 00000000..57c98235 --- /dev/null +++ b/config/rector.php @@ -0,0 +1,41 @@ +withPaths( + [ + __DIR__ . '/../src', + __DIR__ . '/../tests', + ] + ) + ->withSets([ + // Rector sets + + LevelSetList::UP_TO_PHP_72, + + // SetList::CODE_QUALITY, + // SetList::CODING_STYLE, + // SetList::DEAD_CODE, + // SetList::EARLY_RETURN, + // SetList::INSTANCEOF, + // SetList::NAMING, + // SetList::PRIVATIZATION, + SetList::STRICT_BOOLEANS, + SetList::TYPE_DECLARATION, + + // PHPUnit sets + + PHPUnitSetList::PHPUNIT_80, + // PHPUnitSetList::PHPUNIT_CODE_QUALITY, + ]) + ->withRules([ + AddVoidReturnTypeWhereNoReturnRector::class, + ]) + ->withImportNames(true, true, false); diff --git a/docs/API-and-deprecation-policy.md b/docs/API-and-deprecation-policy.md new file mode 100644 index 00000000..57e2acec --- /dev/null +++ b/docs/API-and-deprecation-policy.md @@ -0,0 +1,52 @@ +# API and Deprecation Policy + +## API Policy + +The code in this library is intended to be called by other projects. It is not +intended to be extended. If you want to extend any classes, you're on your own, +and your code might break with any new release of this library. + +Any classes, methods and properties that are `public` and not marked as +`@internal` are considered to be part of the API. Those methods will continue +working in a compatible way over minor and bug-fix releases according +to [Semantic Versioning](https://semver.org/), though we might change the native +type declarations in a way that could break subclasses. + +Any classes, methods and properties that are `protected` or `private` are _not_ +considered part of the API. Please do not rely on them. If you do, you're on +your own. + +Any code that is marked as `@internal` is subject to change or removal without +notice. Please do not call it. There be dragons. + +If a class is marked as `@internal`, all properties and methods of this class +are by definition considered to be internal as well. + +When we change some code from public to `@internal` in a release, the first +release that might change that code in a breaking way will be the next major +release after that. This will allow you to change your code accordingly. We'll +also add since which version the code is internal. + +For example, we might mark some code as `@internal` in version 8.7.0. The first +version that possibly changes this code in a breaking way will then be version +9.0.0. + +Before you upgrade your code to the next major version of this library, please +update to the latest release of the previous major version and make sure that +your code does not reference any code that is marked as `@internal`. + +## Deprecation Policy + +Code that we plan to remove is marked as `@deprecated`. In the corresponding +annotation, we also note in which release the code will be removed. + +When we mark some code as `@deprecated` in a release, we'll usually remove it in +the next major release. We'll also add since which version the code is +deprecated. + +For example, when we mark some code as `@deprecated` in version 8.7.0, we'll +remove it in version 9.0.0 (or sometimes a later major release). + +Before you upgrade your code to the next major version of this library, please +update to the latest release of the previous major version and make sure that +your code does not reference any code that is marked as `@deprecated`. diff --git a/docs/release-checklist.md b/docs/release-checklist.md new file mode 100644 index 00000000..48d50b7e --- /dev/null +++ b/docs/release-checklist.md @@ -0,0 +1,15 @@ +# Steps to release a new version + +1. In the [composer.json](../composer.json), update the `branch-alias` entry to + point to the release _after_ the upcoming release. +1. In the [CHANGELOG.md](../CHANGELOG.md), create a new section with subheadings + for changes _after_ the upcoming release, set the version number for the + upcoming release, and remove any empty sections. +1. Update the target milestone in the Dependabot configuration. +1. Create a pull request "Prepare release of version x.y.z" with those changes. +1. Have the pull request reviewed and merged. +1. Tag the new release. +1. In the + [Releases tab](https://github.com/MyIntervals/PHP-CSS-Parser/releases), + create a new release and copy the change log entries to the new release. +1. Post about the new release on social media. diff --git a/phpunit.xml b/phpunit.xml index 5f3dd458..aab1f10c 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -1,6 +1,15 @@ + xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/8.5/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 @@ + $lineNumber */ - public function __construct($sType, $sArgs = '', $iLineNo = 0) + public function __construct(string $type, string $arguments = '', int $lineNumber = 0) { - parent::__construct($iLineNo); - $this->sType = $sType; - $this->sArgs = $sArgs; + parent::__construct($lineNumber); + $this->type = $type; + $this->arguments = $arguments; } /** - * @return string + * @return non-empty-string */ - public function atRuleName() + public function atRuleName(): string { - return $this->sType; + return $this->type; } - /** - * @return string - */ - public function atRuleArgs() + public function atRuleArgs(): string { - return $this->sArgs; + return $this->arguments; } /** - * @return string + * @return non-empty-string */ - public function __toString() + public function render(OutputFormat $outputFormat): string { - return $this->render(new OutputFormat()); - } - - /** - * @return string - */ - public function render(OutputFormat $oOutputFormat) - { - $sResult = $oOutputFormat->comments($this); - $sResult .= $oOutputFormat->sBeforeAtRuleBlock; - $sArgs = $this->sArgs; - if ($sArgs) { - $sArgs = ' ' . $sArgs; + $formatter = $outputFormat->getFormatter(); + $result = $formatter->comments($this); + $result .= $outputFormat->getContentBeforeAtRuleBlock(); + $arguments = $this->arguments; + if ($arguments) { + $arguments = ' ' . $arguments; } - $sResult .= "@{$this->sType}$sArgs{$oOutputFormat->spaceBeforeOpeningBrace()}{"; - $sResult .= $this->renderListContents($oOutputFormat); - $sResult .= '}'; - $sResult .= $oOutputFormat->sAfterAtRuleBlock; - return $sResult; + $result .= "@{$this->type}$arguments{$formatter->spaceBeforeOpeningBrace()}{"; + $result .= $this->renderListContents($outputFormat); + $result .= '}'; + $result .= $outputFormat->getContentAfterAtRuleBlock(); + return $result; } - /** - * @return bool - */ - public function isRootList() + public function isRootList(): bool { return false; } diff --git a/src/CSSList/CSSBlockList.php b/src/CSSList/CSSBlockList.php index fce7913e..2dfd284f 100644 --- a/src/CSSList/CSSBlockList.php +++ b/src/CSSList/CSSBlockList.php @@ -1,10 +1,14 @@ $aResult + * Gets all `DeclarationBlock` objects recursively, no matter how deeply nested the selectors are. * - * @return void + * @return list */ - protected function allDeclarationBlocks(array &$aResult) + public function getAllDeclarationBlocks(): array { - foreach ($this->aContents as $mContent) { - if ($mContent instanceof DeclarationBlock) { - $aResult[] = $mContent; - } elseif ($mContent instanceof CSSBlockList) { - $mContent->allDeclarationBlocks($aResult); + $result = []; + + foreach ($this->contents as $item) { + if ($item instanceof DeclarationBlock) { + $result[] = $item; + } elseif ($item instanceof CSSBlockList) { + $result = \array_merge($result, $item->getAllDeclarationBlocks()); } } + + return $result; } /** - * @param array $aResult + * Returns all `RuleSet` objects recursively found in the tree, no matter how deeply nested the rule sets are. * - * @return void + * @return list */ - protected function allRuleSets(array &$aResult) + public function getAllRuleSets(): array { - foreach ($this->aContents as $mContent) { - if ($mContent instanceof RuleSet) { - $aResult[] = $mContent; - } elseif ($mContent instanceof CSSBlockList) { - $mContent->allRuleSets($aResult); + $result = []; + + foreach ($this->contents as $item) { + if ($item instanceof RuleSet) { + $result[] = $item; + } elseif ($item instanceof CSSBlockList) { + $result = \array_merge($result, $item->getAllRuleSets()); } } + + return $result; } /** - * @param CSSList|Rule|RuleSet|Value $oElement - * @param array $aResult - * @param string|null $sSearchString - * @param bool $bSearchInFunctionArguments + * Returns all `Value` objects found recursively in `Rule`s in the tree. + * + * @param CSSElement|null $element + * This is the `CSSList` or `RuleSet` to start the search from (defaults to the whole document). + * @param string|null $ruleSearchPattern + * This allows filtering rules by property name + * (e.g. if "color" is passed, only `Value`s from `color` properties will be returned, + * or if "font-" is provided, `Value`s from all font rules, like `font-size`, and including `font` itself, + * will be returned). + * @param bool $searchInFunctionArguments whether to also return `Value` objects used as `CSSFunction` arguments. * - * @return void + * @return list + * + * @see RuleSet->getRules() */ - protected function allValues($oElement, array &$aResult, $sSearchString = null, $bSearchInFunctionArguments = false) - { - if ($oElement instanceof CSSBlockList) { - foreach ($oElement->getContents() as $oContent) { - $this->allValues($oContent, $aResult, $sSearchString, $bSearchInFunctionArguments); + public function getAllValues( + ?CSSElement $element = null, + ?string $ruleSearchPattern = null, + bool $searchInFunctionArguments = false + ): array { + $element = $element ?? $this; + + $result = []; + if ($element instanceof CSSBlockList) { + foreach ($element->getContents() as $contentItem) { + // Statement at-rules are skipped since they do not contain values. + if ($contentItem instanceof CSSElement) { + $result = \array_merge( + $result, + $this->getAllValues($contentItem, $ruleSearchPattern, $searchInFunctionArguments) + ); + } + } + } elseif ($element instanceof RuleContainer) { + foreach ($element->getRules($ruleSearchPattern) as $rule) { + $result = \array_merge( + $result, + $this->getAllValues($rule, $ruleSearchPattern, $searchInFunctionArguments) + ); } - } elseif ($oElement instanceof RuleSet) { - foreach ($oElement->getRules($sSearchString) as $oRule) { - $this->allValues($oRule, $aResult, $sSearchString, $bSearchInFunctionArguments); + } elseif ($element instanceof Rule) { + $value = $element->getValue(); + // `string` values are discarded. + if ($value instanceof CSSElement) { + $result = \array_merge( + $result, + $this->getAllValues($value, $ruleSearchPattern, $searchInFunctionArguments) + ); } - } elseif ($oElement instanceof Rule) { - $this->allValues($oElement->getValue(), $aResult, $sSearchString, $bSearchInFunctionArguments); - } elseif ($oElement instanceof ValueList) { - if ($bSearchInFunctionArguments || !($oElement instanceof CSSFunction)) { - foreach ($oElement->getListComponents() as $mComponent) { - $this->allValues($mComponent, $aResult, $sSearchString, $bSearchInFunctionArguments); + } elseif ($element instanceof ValueList) { + if ($searchInFunctionArguments || !($element instanceof CSSFunction)) { + foreach ($element->getListComponents() as $component) { + // `string` components are discarded. + if ($component instanceof CSSElement) { + $result = \array_merge( + $result, + $this->getAllValues($component, $ruleSearchPattern, $searchInFunctionArguments) + ); + } } } - } else { - // Non-List `Value` or `CSSString` (CSS identifier) - $aResult[] = $oElement; + } elseif ($element instanceof Value) { + $result[] = $element; } + + return $result; } /** - * @param array $aResult - * @param string|null $sSpecificitySearch - * - * @return void + * @return list */ - protected function allSelectors(array &$aResult, $sSpecificitySearch = null) + protected function getAllSelectors(?string $specificitySearch = null): array { - /** @var array $aDeclarationBlocks */ - $aDeclarationBlocks = []; - $this->allDeclarationBlocks($aDeclarationBlocks); - foreach ($aDeclarationBlocks as $oBlock) { - foreach ($oBlock->getSelectors() as $oSelector) { - if ($sSpecificitySearch === null) { - $aResult[] = $oSelector; + $result = []; + + foreach ($this->getAllDeclarationBlocks() as $declarationBlock) { + foreach ($declarationBlock->getSelectors() as $selector) { + if ($specificitySearch === null) { + $result[] = $selector; } else { - $sComparator = '==='; - $aSpecificitySearch = explode(' ', $sSpecificitySearch); - $iTargetSpecificity = $aSpecificitySearch[0]; - if (count($aSpecificitySearch) > 1) { - $sComparator = $aSpecificitySearch[0]; - $iTargetSpecificity = $aSpecificitySearch[1]; + $comparator = '==='; + $expressionParts = \explode(' ', $specificitySearch); + $targetSpecificity = $expressionParts[0]; + if (\count($expressionParts) > 1) { + $comparator = $expressionParts[0]; + $targetSpecificity = $expressionParts[1]; } - $iTargetSpecificity = (int)$iTargetSpecificity; - $iSelectorSpecificity = $oSelector->getSpecificity(); - $bMatches = false; - switch ($sComparator) { + $targetSpecificity = (int) $targetSpecificity; + $selectorSpecificity = $selector->getSpecificity(); + $comparatorMatched = false; + switch ($comparator) { case '<=': - $bMatches = $iSelectorSpecificity <= $iTargetSpecificity; + $comparatorMatched = $selectorSpecificity <= $targetSpecificity; break; case '<': - $bMatches = $iSelectorSpecificity < $iTargetSpecificity; + $comparatorMatched = $selectorSpecificity < $targetSpecificity; break; case '>=': - $bMatches = $iSelectorSpecificity >= $iTargetSpecificity; + $comparatorMatched = $selectorSpecificity >= $targetSpecificity; break; case '>': - $bMatches = $iSelectorSpecificity > $iTargetSpecificity; + $comparatorMatched = $selectorSpecificity > $targetSpecificity; break; default: - $bMatches = $iSelectorSpecificity === $iTargetSpecificity; + $comparatorMatched = $selectorSpecificity === $targetSpecificity; break; } - if ($bMatches) { - $aResult[] = $oSelector; + if ($comparatorMatched) { + $result[] = $selector; } } } } + + return $result; } } diff --git a/src/CSSList/CSSList.php b/src/CSSList/CSSList.php index dcd8c331..1942d8c9 100644 --- a/src/CSSList/CSSList.php +++ b/src/CSSList/CSSList.php @@ -1,20 +1,23 @@ - */ - protected $aComments; + use CommentContainer; + use Position; /** - * @var array - */ - protected $aContents; - - /** - * @var int + * @var array, CSSListItem> + * + * @internal since 8.8.0 */ - protected $iLineNo; + protected $contents = []; /** - * @param int $iLineNo + * @param int<0, max> $lineNumber */ - public function __construct($iLineNo = 0) + public function __construct(int $lineNumber = 0) { - $this->aComments = []; - $this->aContents = []; - $this->iLineNo = $iLineNo; + $this->setPosition($lineNumber); } /** - * @return void - * * @throws UnexpectedTokenException * @throws SourceException + * + * @internal since V8.8.0 */ - public static function parseList(ParserState $oParserState, CSSList $oList) + public static function parseList(ParserState $parserState, CSSList $list): void { - $bIsRoot = $oList instanceof Document; - if (is_string($oParserState)) { - $oParserState = new ParserState($oParserState, Settings::create()); + $isRoot = $list instanceof Document; + if (\is_string($parserState)) { + $parserState = new ParserState($parserState, Settings::create()); } - $bLenientParsing = $oParserState->getSettings()->bLenientParsing; - $aComments = []; - while (!$oParserState->isEnd()) { - $aComments = array_merge($aComments, $oParserState->consumeWhiteSpace()); - $oListItem = null; - if ($bLenientParsing) { + $usesLenientParsing = $parserState->getSettings()->usesLenientParsing(); + $comments = []; + while (!$parserState->isEnd()) { + $comments = \array_merge($comments, $parserState->consumeWhiteSpace()); + $listItem = null; + if ($usesLenientParsing) { try { - $oListItem = self::parseListItem($oParserState, $oList); + $listItem = self::parseListItem($parserState, $list); } catch (UnexpectedTokenException $e) { - $oListItem = false; + $listItem = false; } } else { - $oListItem = self::parseListItem($oParserState, $oList); + $listItem = self::parseListItem($parserState, $list); } - if ($oListItem === null) { + if ($listItem === null) { // List parsing finished return; } - if ($oListItem) { - $oListItem->addComments($aComments); - $oList->append($oListItem); + if ($listItem) { + $listItem->addComments($comments); + $list->append($listItem); } - $aComments = $oParserState->consumeWhiteSpace(); + $comments = $parserState->consumeWhiteSpace(); } - $oList->addComments($aComments); - if (!$bIsRoot && !$bLenientParsing) { - throw new SourceException("Unexpected end of document", $oParserState->currentLine()); + $list->addComments($comments); + if (!$isRoot && !$usesLenientParsing) { + throw new SourceException('Unexpected end of document', $parserState->currentLine()); } } /** - * @return AtRuleBlockList|KeyFrame|Charset|CSSNamespace|Import|AtRuleSet|DeclarationBlock|null|false + * @return CSSListItem|false|null + * If `null` is returned, it means the end of the list has been reached. + * If `false` is returned, it means an invalid item has been encountered, + * but parsing of the next item should proceed. * * @throws SourceException * @throws UnexpectedEOFException * @throws UnexpectedTokenException */ - private static function parseListItem(ParserState $oParserState, CSSList $oList) + private static function parseListItem(ParserState $parserState, CSSList $list) { - $bIsRoot = $oList instanceof Document; - if ($oParserState->comes('@')) { - $oAtRule = self::parseAtRule($oParserState); - if ($oAtRule instanceof Charset) { - if (!$bIsRoot) { + $isRoot = $list instanceof Document; + if ($parserState->comes('@')) { + $atRule = self::parseAtRule($parserState); + if ($atRule instanceof Charset) { + if (!$isRoot) { throw new UnexpectedTokenException( '@charset may only occur in root document', '', 'custom', - $oParserState->currentLine() + $parserState->currentLine() ); } - if (count($oList->getContents()) > 0) { + if (\count($list->getContents()) > 0) { throw new UnexpectedTokenException( '@charset must be the first parseable token in a document', '', 'custom', - $oParserState->currentLine() + $parserState->currentLine() ); } - $oParserState->setCharset($oAtRule->getCharset()); + $parserState->setCharset($atRule->getCharset()); } - return $oAtRule; - } elseif ($oParserState->comes('}')) { - if (!$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()); - } + return $atRule; + } elseif ($parserState->comes('}')) { + if ($isRoot) { + if ($parserState->getSettings()->usesLenientParsing()) { + return DeclarationBlock::parse($parserState) ?? false; } else { - return null; + throw new SourceException('Unopened {', $parserState->currentLine()); } + } else { + // End of list + return null; } } else { - return DeclarationBlock::parse($oParserState, $oList); + return DeclarationBlock::parse($parserState, $list) ?? false; } } /** - * @param ParserState $oParserState - * - * @return AtRuleBlockList|KeyFrame|Charset|CSSNamespace|Import|AtRuleSet|null - * * @throws SourceException * @throws UnexpectedTokenException * @throws UnexpectedEOFException */ - private static function parseAtRule(ParserState $oParserState) + private static function parseAtRule(ParserState $parserState): ?CSSListItem { - $oParserState->consume('@'); - $sIdentifier = $oParserState->parseIdentifier(); - $iIdentifierLineNum = $oParserState->currentLine(); - $oParserState->consumeWhiteSpace(); - if ($sIdentifier === 'import') { - $oLocation = URL::parse($oParserState); - $oParserState->consumeWhiteSpace(); - $sMediaQuery = null; - if (!$oParserState->comes(';')) { - $sMediaQuery = trim($oParserState->consumeUntil([';', ParserState::EOF])); + $parserState->consume('@'); + $identifier = $parserState->parseIdentifier(); + $identifierLineNumber = $parserState->currentLine(); + $parserState->consumeWhiteSpace(); + if ($identifier === 'import') { + $location = URL::parse($parserState); + $parserState->consumeWhiteSpace(); + $mediaQuery = null; + if (!$parserState->comes(';')) { + $mediaQuery = \trim($parserState->consumeUntil([';', ParserState::EOF])); + if ($mediaQuery === '') { + $mediaQuery = null; + } } - $oParserState->consumeUntil([';', ParserState::EOF], true, true); - return new Import($oLocation, $sMediaQuery ?: null, $iIdentifierLineNum); - } elseif ($sIdentifier === 'charset') { - $oCharsetString = CSSString::parse($oParserState); - $oParserState->consumeWhiteSpace(); - $oParserState->consumeUntil([';', ParserState::EOF], true, true); - return new Charset($oCharsetString, $iIdentifierLineNum); - } elseif (self::identifierIs($sIdentifier, 'keyframes')) { - $oResult = new KeyFrame($iIdentifierLineNum); - $oResult->setVendorKeyFrame($sIdentifier); - $oResult->setAnimationName(trim($oParserState->consumeUntil('{', false, true))); - CSSList::parseList($oParserState, $oResult); - if ($oParserState->comes('}')) { - $oParserState->consume('}'); + $parserState->consumeUntil([';', ParserState::EOF], true, true); + return new Import($location, $mediaQuery, $identifierLineNumber); + } elseif ($identifier === 'charset') { + $charsetString = CSSString::parse($parserState); + $parserState->consumeWhiteSpace(); + $parserState->consumeUntil([';', ParserState::EOF], true, true); + return new Charset($charsetString, $identifierLineNumber); + } elseif (self::identifierIs($identifier, 'keyframes')) { + $result = new KeyFrame($identifierLineNumber); + $result->setVendorKeyFrame($identifier); + $result->setAnimationName(\trim($parserState->consumeUntil('{', false, true))); + CSSList::parseList($parserState, $result); + if ($parserState->comes('}')) { + $parserState->consume('}'); } - return $oResult; - } elseif ($sIdentifier === 'namespace') { - $sPrefix = null; - $mUrl = Value::parsePrimitiveValue($oParserState); - if (!$oParserState->comes(';')) { - $sPrefix = $mUrl; - $mUrl = Value::parsePrimitiveValue($oParserState); + return $result; + } elseif ($identifier === 'namespace') { + $prefix = null; + $url = Value::parsePrimitiveValue($parserState); + if (!$parserState->comes(';')) { + $prefix = $url; + $url = Value::parsePrimitiveValue($parserState); } - $oParserState->consumeUntil([';', ParserState::EOF], true, true); - if ($sPrefix !== null && !is_string($sPrefix)) { - throw new UnexpectedTokenException('Wrong namespace prefix', $sPrefix, 'custom', $iIdentifierLineNum); + $parserState->consumeUntil([';', ParserState::EOF], true, true); + if ($prefix !== null && !\is_string($prefix)) { + throw new UnexpectedTokenException('Wrong namespace prefix', $prefix, 'custom', $identifierLineNumber); } - if (!($mUrl instanceof CSSString || $mUrl instanceof URL)) { + if (!($url instanceof CSSString || $url instanceof URL)) { throw new UnexpectedTokenException( 'Wrong namespace url of invalid type', - $mUrl, + $url, 'custom', - $iIdentifierLineNum + $identifierLineNumber ); } - return new CSSNamespace($mUrl, $sPrefix, $iIdentifierLineNum); + return new CSSNamespace($url, $prefix, $identifierLineNumber); } else { // Unknown other at rule (font-face or such) - $sArgs = trim($oParserState->consumeUntil('{', false, true)); - if (substr_count($sArgs, "(") != substr_count($sArgs, ")")) { - if ($oParserState->getSettings()->bLenientParsing) { + $arguments = \trim($parserState->consumeUntil('{', false, true)); + if (\substr_count($arguments, '(') != \substr_count($arguments, ')')) { + if ($parserState->getSettings()->usesLenientParsing()) { return null; } else { - throw new SourceException("Unmatched brace count in media query", $oParserState->currentLine()); + throw new SourceException('Unmatched brace count in media query', $parserState->currentLine()); } } - $bUseRuleSet = true; - foreach (explode('/', AtRule::BLOCK_RULES) as $sBlockRuleName) { - if (self::identifierIs($sIdentifier, $sBlockRuleName)) { - $bUseRuleSet = false; + $useRuleSet = true; + foreach (\explode('/', AtRule::BLOCK_RULES) as $blockRuleName) { + if (self::identifierIs($identifier, $blockRuleName)) { + $useRuleSet = false; break; } } - if ($bUseRuleSet) { - $oAtRule = new AtRuleSet($sIdentifier, $sArgs, $iIdentifierLineNum); - RuleSet::parseRuleSet($oParserState, $oAtRule); + if ($useRuleSet) { + $atRule = new AtRuleSet($identifier, $arguments, $identifierLineNumber); + RuleSet::parseRuleSet($parserState, $atRule); } else { - $oAtRule = new AtRuleBlockList($sIdentifier, $sArgs, $iIdentifierLineNum); - CSSList::parseList($oParserState, $oAtRule); - if ($oParserState->comes('}')) { - $oParserState->consume('}'); + $atRule = new AtRuleBlockList($identifier, $arguments, $identifierLineNumber); + CSSList::parseList($parserState, $atRule); + if ($parserState->comes('}')) { + $parserState->consume('}'); } } - return $oAtRule; + return $atRule; } } /** * Tests an identifier for a given value. Since identifiers are all keywords, they can be vendor-prefixed. * We need to check for these versions too. - * - * @param string $sIdentifier - * @param string $sMatch - * - * @return bool */ - private static function identifierIs($sIdentifier, $sMatch) + private static function identifierIs(string $identifier, string $match): bool { - return (strcasecmp($sIdentifier, $sMatch) === 0) - ?: preg_match("/^(-\\w+-)?$sMatch$/i", $sIdentifier) === 1; + return (\strcasecmp($identifier, $match) === 0) + ?: \preg_match("/^(-\\w+-)?$match$/i", $identifier) === 1; } /** - * @return int + * Prepends an item to the list of contents. */ - public function getLineNo() + public function prepend(CSSListItem $item): void { - return $this->iLineNo; + \array_unshift($this->contents, $item); } /** - * Prepends an item to the list of contents. - * - * @param RuleSet|CSSList|Import|Charset $oItem - * - * @return void + * Appends an item to the list of contents. */ - public function prepend($oItem) + public function append(CSSListItem $item): void { - array_unshift($this->aContents, $oItem); + $this->contents[] = $item; } /** - * Appends an item to the list of contents. - * - * @param RuleSet|CSSList|Import|Charset $oItem + * Splices the list of contents. * - * @return void + * @param array $replacement */ - public function append($oItem) + public function splice(int $offset, ?int $length = null, ?array $replacement = null): void { - $this->aContents[] = $oItem; + \array_splice($this->contents, $offset, $length, $replacement); } /** - * Splices the list of contents. - * - * @param int $iOffset - * @param int $iLength - * @param array $mReplacement - * - * @return void + * Inserts an item in the CSS list before its sibling. If the desired sibling cannot be found, + * the item is appended at the end. */ - public function splice($iOffset, $iLength = null, $mReplacement = null) + public function insertBefore(CSSListItem $item, CSSListItem $sibling): void { - array_splice($this->aContents, $iOffset, $iLength, $mReplacement); + if (\in_array($sibling, $this->contents, true)) { + $this->replace($sibling, [$item, $sibling]); + } else { + $this->append($item); + } } /** * Removes an item from the CSS list. * - * @param RuleSet|Import|Charset|CSSList $oItemToRemove - * May be a RuleSet (most likely a DeclarationBlock), a Import, - * a Charset or another CSSList (most likely a MediaQuery) + * @param CSSListItem $itemToRemove + * May be a `RuleSet` (most likely a `DeclarationBlock`), an `Import`, + * a `Charset` or another `CSSList` (most likely a `MediaQuery`) * * @return bool whether the item was removed */ - public function remove($oItemToRemove) + public function remove(CSSListItem $itemToRemove): bool { - $iKey = array_search($oItemToRemove, $this->aContents, true); - if ($iKey !== false) { - unset($this->aContents[$iKey]); + $key = \array_search($itemToRemove, $this->contents, true); + if ($key !== false) { + unset($this->contents[$key]); return true; } + return false; } /** * Replaces an item from the CSS list. * - * @param RuleSet|Import|Charset|CSSList $oOldItem + * @param CSSListItem $oldItem * May be a `RuleSet` (most likely a `DeclarationBlock`), an `Import`, a `Charset` * or another `CSSList` (most likely a `MediaQuery`) - * - * @return bool + * @param CSSListItem|array $newItem */ - public function replace($oOldItem, $mNewItem) + public function replace(CSSListItem $oldItem, $newItem): bool { - $iKey = array_search($oOldItem, $this->aContents, true); - if ($iKey !== false) { - if (is_array($mNewItem)) { - array_splice($this->aContents, $iKey, 1, $mNewItem); + $key = \array_search($oldItem, $this->contents, true); + if ($key !== false) { + if (\is_array($newItem)) { + \array_splice($this->contents, $key, 1, $newItem); } else { - array_splice($this->aContents, $iKey, 1, [$mNewItem]); + \array_splice($this->contents, $key, 1, [$newItem]); } return true; } + return false; } /** - * @param array $aContents + * @param array $contents */ - public function setContents(array $aContents) + public function setContents(array $contents): void { - $this->aContents = []; - foreach ($aContents as $content) { + $this->contents = []; + foreach ($contents as $content) { $this->append($content); } } @@ -355,129 +342,88 @@ public function setContents(array $aContents) /** * Removes a declaration block from the CSS list if it matches all given selectors. * - * @param DeclarationBlock|array|string $mSelector the selectors to match - * @param bool $bRemoveAll whether to stop at the first declaration block found or remove all blocks - * - * @return void + * @param DeclarationBlock|array|string $selectors the selectors to match + * @param bool $removeAll whether to stop at the first declaration block found or remove all blocks */ - public function removeDeclarationBlockBySelector($mSelector, $bRemoveAll = false) + public function removeDeclarationBlockBySelector($selectors, bool $removeAll = false): void { - if ($mSelector instanceof DeclarationBlock) { - $mSelector = $mSelector->getSelectors(); + if ($selectors instanceof DeclarationBlock) { + $selectors = $selectors->getSelectors(); } - if (!is_array($mSelector)) { - $mSelector = explode(',', $mSelector); + if (!\is_array($selectors)) { + $selectors = \explode(',', $selectors); } - foreach ($mSelector as $iKey => &$mSel) { - if (!($mSel instanceof Selector)) { - if (!Selector::isValid($mSel)) { + foreach ($selectors as $key => &$selector) { + if (!($selector instanceof Selector)) { + if (!Selector::isValid($selector)) { throw new UnexpectedTokenException( "Selector did not match '" . Selector::SELECTOR_VALIDATION_RX . "'.", - $mSel, - "custom" + $selector, + 'custom' ); } - $mSel = new Selector($mSel); + $selector = new Selector($selector); } } - foreach ($this->aContents as $iKey => $mItem) { - if (!($mItem instanceof DeclarationBlock)) { + foreach ($this->contents as $key => $item) { + if (!($item instanceof DeclarationBlock)) { continue; } - if ($mItem->getSelectors() == $mSelector) { - unset($this->aContents[$iKey]); - if (!$bRemoveAll) { + if ($item->getSelectors() == $selectors) { + unset($this->contents[$key]); + if (!$removeAll) { return; } } } } - /** - * @return string - */ - public function __toString() + protected function renderListContents(OutputFormat $outputFormat): string { - return $this->render(new OutputFormat()); - } - - /** - * @return string - */ - protected function renderListContents(OutputFormat $oOutputFormat) - { - $sResult = ''; - $bIsFirst = true; - $oNextLevel = $oOutputFormat; + $result = ''; + $isFirst = true; + $nextLevelFormat = $outputFormat; if (!$this->isRootList()) { - $oNextLevel = $oOutputFormat->nextLevel(); + $nextLevelFormat = $outputFormat->nextLevel(); } - foreach ($this->aContents as $oContent) { - $sRendered = $oOutputFormat->safely(function () use ($oNextLevel, $oContent) { - return $oContent->render($oNextLevel); + $nextLevelFormatter = $nextLevelFormat->getFormatter(); + $formatter = $outputFormat->getFormatter(); + foreach ($this->contents as $listItem) { + $renderedCss = $formatter->safely(static function () use ($nextLevelFormat, $listItem): string { + return $listItem->render($nextLevelFormat); }); - if ($sRendered === null) { + if ($renderedCss === null) { continue; } - if ($bIsFirst) { - $bIsFirst = false; - $sResult .= $oNextLevel->spaceBeforeBlocks(); + if ($isFirst) { + $isFirst = false; + $result .= $nextLevelFormatter->spaceBeforeBlocks(); } else { - $sResult .= $oNextLevel->spaceBetweenBlocks(); + $result .= $nextLevelFormatter->spaceBetweenBlocks(); } - $sResult .= $sRendered; + $result .= $renderedCss; } - if (!$bIsFirst) { + if (!$isFirst) { // Had some output - $sResult .= $oOutputFormat->spaceAfterBlocks(); + $result .= $formatter->spaceAfterBlocks(); } - return $sResult; + return $result; } /** * Return true if the list can not be further outdented. Only important when rendering. - * - * @return bool */ - abstract public function isRootList(); + abstract public function isRootList(): bool; /** * Returns the stored items. * - * @return array - */ - public function getContents() - { - return $this->aContents; - } - - /** - * @param array $aComments - * - * @return void - */ - public function addComments(array $aComments) - { - $this->aComments = array_merge($this->aComments, $aComments); - } - - /** - * @return array - */ - public function getComments() - { - return $this->aComments; - } - - /** - * @param array $aComments - * - * @return void + * @return array, CSSListItem> */ - public function setComments(array $aComments) + public function getContents(): array { - $this->aComments = $aComments; + return $this->contents; } } diff --git a/src/CSSList/CSSListItem.php b/src/CSSList/CSSListItem.php new file mode 100644 index 00000000..3cf2509b --- /dev/null +++ b/src/CSSList/CSSListItem.php @@ -0,0 +1,18 @@ +currentLine()); - CSSList::parseList($oParserState, $oDocument); - return $oDocument; - } - - /** - * Gets all `DeclarationBlock` objects recursively, no matter how deeply nested the selectors are. - * Aliased as `getAllSelectors()`. * - * @return array + * @internal since V8.8.0 */ - public function getAllDeclarationBlocks() + public static function parse(ParserState $parserState): Document { - /** @var array $aResult */ - $aResult = []; - $this->allDeclarationBlocks($aResult); - return $aResult; - } + $document = new Document($parserState->currentLine()); + CSSList::parseList($parserState, $document); - /** - * Gets all `DeclarationBlock` objects recursively. - * - * @return array - * - * @deprecated will be removed in version 9.0; use `getAllDeclarationBlocks()` instead - */ - public function getAllSelectors() - { - return $this->getAllDeclarationBlocks(); - } - - /** - * Returns all `RuleSet` objects recursively found in the tree, no matter how deeply nested the rule sets are. - * - * @return array - */ - public function getAllRuleSets() - { - /** @var array $aResult */ - $aResult = []; - $this->allRuleSets($aResult); - return $aResult; - } - - /** - * Returns all `Value` objects found recursively in `Rule`s in the tree. - * - * @param CSSList|RuleSet|string $mElement - * the `CSSList` or `RuleSet` to start the search from (defaults to the whole document). - * If a string is given, it is used as rule name filter. - * @param bool $bSearchInFunctionArguments whether to also return Value objects used as Function arguments. - * - * @return array - * - * @see RuleSet->getRules() - */ - public function getAllValues($mElement = null, $bSearchInFunctionArguments = false) - { - $sSearchString = null; - if ($mElement === null) { - $mElement = $this; - } elseif (is_string($mElement)) { - $sSearchString = $mElement; - $mElement = $this; - } - /** @var array $aResult */ - $aResult = []; - $this->allValues($mElement, $aResult, $sSearchString, $bSearchInFunctionArguments); - return $aResult; + return $document; } /** @@ -108,65 +34,31 @@ public function getAllValues($mElement = null, $bSearchInFunctionArguments = fal * Note that this does not yield the full `DeclarationBlock` that the selector belongs to * (and, currently, there is no way to get to that). * - * @param string|null $sSpecificitySearch + * @param string|null $specificitySearch * An optional filter by specificity. * May contain a comparison operator and a number or just a number (defaults to "=="). * - * @return array - * @example `getSelectorsBySpecificity('>= 100')` - * - */ - public function getSelectorsBySpecificity($sSpecificitySearch = null) - { - /** @var array $aResult */ - $aResult = []; - $this->allSelectors($aResult, $sSpecificitySearch); - return $aResult; - } - - /** - * Expands all shorthand properties to their long value. - * - * @return void - */ - public function expandShorthands() - { - foreach ($this->getAllDeclarationBlocks() as $oDeclaration) { - $oDeclaration->expandShorthands(); - } - } - - /** - * Create shorthands properties whenever possible. + * @return list * - * @return void + * @example `getSelectorsBySpecificity('>= 100')` */ - public function createShorthands() + public function getSelectorsBySpecificity(?string $specificitySearch = null): array { - foreach ($this->getAllDeclarationBlocks() as $oDeclaration) { - $oDeclaration->createShorthands(); - } + return $this->getAllSelectors($specificitySearch); } /** * Overrides `render()` to make format argument optional. - * - * @param OutputFormat|null $oOutputFormat - * - * @return string */ - public function render(OutputFormat $oOutputFormat = null) + public function render(?OutputFormat $outputFormat = null): string { - if ($oOutputFormat === null) { - $oOutputFormat = new OutputFormat(); + if ($outputFormat === null) { + $outputFormat = new OutputFormat(); } - return $oOutputFormat->comments($this) . $this->renderListContents($oOutputFormat); + return $outputFormat->getFormatter()->comments($this) . $this->renderListContents($outputFormat); } - /** - * @return bool - */ - public function isRootList() + public function isRootList(): bool { return true; } diff --git a/src/CSSList/KeyFrame.php b/src/CSSList/KeyFrame.php index caef7b3d..e632d088 100644 --- a/src/CSSList/KeyFrame.php +++ b/src/CSSList/KeyFrame.php @@ -1,5 +1,7 @@ vendorKeyFrame = null; - $this->animationName = null; - } - - /** - * @param string $vendorKeyFrame - */ - public function setVendorKeyFrame($vendorKeyFrame) + public function setVendorKeyFrame(string $vendorKeyFrame): void { $this->vendorKeyFrame = $vendorKeyFrame; } /** - * @return string|null + * @return non-empty-string */ - public function getVendorKeyFrame() + public function getVendorKeyFrame(): string { return $this->vendorKeyFrame; } /** - * @param string $animationName + * @param non-empty-string $animationName */ - public function setAnimationName($animationName) + public function setAnimationName(string $animationName): void { $this->animationName = $animationName; } /** - * @return string|null + * @return non-empty-string */ - public function getAnimationName() + public function getAnimationName(): string { return $this->animationName; } /** - * @return string + * @return non-empty-string */ - public function __toString() + public function render(OutputFormat $outputFormat): string { - return $this->render(new OutputFormat()); + $formatter = $outputFormat->getFormatter(); + $result = $formatter->comments($this); + $result .= "@{$this->vendorKeyFrame} {$this->animationName}{$formatter->spaceBeforeOpeningBrace()}{"; + $result .= $this->renderListContents($outputFormat); + $result .= '}'; + return $result; } - /** - * @return string - */ - public function render(OutputFormat $oOutputFormat) - { - $sResult = $oOutputFormat->comments($this); - $sResult .= "@{$this->vendorKeyFrame} {$this->animationName}{$oOutputFormat->spaceBeforeOpeningBrace()}{"; - $sResult .= $this->renderListContents($oOutputFormat); - $sResult .= '}'; - return $sResult; - } - - /** - * @return bool - */ - public function isRootList() + public function isRootList(): bool { return false; } /** - * @return string|null + * @return non-empty-string */ - public function atRuleName() + public function atRuleName(): string { return $this->vendorKeyFrame; } /** - * @return string|null + * @return non-empty-string */ - public function atRuleArgs() + public function atRuleArgs(): string { return $this->animationName; } diff --git a/src/Comment/Comment.php b/src/Comment/Comment.php index 6128d749..7a56624c 100644 --- a/src/Comment/Comment.php +++ b/src/Comment/Comment.php @@ -1,71 +1,49 @@ sComment = $sComment; - $this->iLineNo = $iLineNo; - } - - /** - * @return string - */ - public function getComment() - { - return $this->sComment; - } + protected $commentText; /** - * @return int + * @param int<0, max> $lineNumber */ - public function getLineNo() + public function __construct(string $commentText = '', int $lineNumber = 0) { - return $this->iLineNo; + $this->commentText = $commentText; + $this->setPosition($lineNumber); } - /** - * @param string $sComment - * - * @return void - */ - public function setComment($sComment) + public function getComment(): string { - $this->sComment = $sComment; + return $this->commentText; } - /** - * @return string - */ - public function __toString() + public function setComment(string $commentText): void { - return $this->render(new OutputFormat()); + $this->commentText = $commentText; } /** - * @return string + * @return non-empty-string */ - public function render(OutputFormat $oOutputFormat) + public function render(OutputFormat $outputFormat): string { - return '/*' . $this->sComment . '*/'; + return '/*' . $this->commentText . '*/'; } } diff --git a/src/Comment/CommentContainer.php b/src/Comment/CommentContainer.php new file mode 100644 index 00000000..87f6ff46 --- /dev/null +++ b/src/Comment/CommentContainer.php @@ -0,0 +1,44 @@ + + */ + protected $comments = []; + + /** + * @param list $comments + */ + public function addComments(array $comments): void + { + $this->comments = \array_merge($this->comments, $comments); + } + + /** + * @return list + */ + public function getComments(): array + { + return $this->comments; + } + + /** + * @param list $comments + */ + public function setComments(array $comments): void + { + $this->comments = $comments; + } +} diff --git a/src/Comment/Commentable.php b/src/Comment/Commentable.php index 5e450bfb..5f28021d 100644 --- a/src/Comment/Commentable.php +++ b/src/Comment/Commentable.php @@ -1,25 +1,26 @@ $aComments - * - * @return void + * @param list $comments */ - public function addComments(array $aComments); + public function addComments(array $comments): void; /** - * @return array + * @return list */ - public function getComments(); + public function getComments(): array; /** - * @param array $aComments - * - * @return void + * @param list $comments */ - public function setComments(array $aComments); + public function setComments(array $comments): void; } diff --git a/src/OutputFormat.php b/src/OutputFormat.php index 96f26e14..6ad45aa4 100644 --- a/src/OutputFormat.php +++ b/src/OutputFormat.php @@ -1,28 +1,24 @@ set('Space*Rules', "\n");`) + * The triples (After, Before, Between) can be set using a wildcard + * (e.g. `$outputFormat->set('Space*Rules', "\n");`) + * + * @var string */ - public $sSpaceAfterRuleName = ' '; + private $spaceAfterRuleName = ' '; /** * @var string */ - public $sSpaceBeforeRules = ''; + private $spaceBeforeRules = ''; /** * @var string */ - public $sSpaceAfterRules = ''; + private $spaceAfterRules = ''; /** * @var string */ - public $sSpaceBetweenRules = ''; + private $spaceBetweenRules = ''; /** * @var string */ - public $sSpaceBeforeBlocks = ''; + private $spaceBeforeBlocks = ''; /** * @var string */ - public $sSpaceAfterBlocks = ''; + private $spaceAfterBlocks = ''; /** * @var string */ - public $sSpaceBetweenBlocks = "\n"; + private $spaceBetweenBlocks = "\n"; /** * Content injected in and around at-rule blocks. * * @var string */ - public $sBeforeAtRuleBlock = ''; + private $contentBeforeAtRuleBlock = ''; /** * @var string */ - public $sAfterAtRuleBlock = ''; + private $contentAfterAtRuleBlock = ''; /** * This is what’s printed before and after the comma if a declaration block contains multiple selectors. * * @var string */ - public $sSpaceBeforeSelectorSeparator = ''; + private $spaceBeforeSelectorSeparator = ''; /** * @var string */ - public $sSpaceAfterSelectorSeparator = ' '; + private $spaceAfterSelectorSeparator = ' '; /** - * This is what’s printed after the comma of value lists + * This is what’s inserted before the separator in value lists, by default. * * @var string */ - public $sSpaceBeforeListArgumentSeparator = ''; + private $spaceBeforeListArgumentSeparator = ''; + + /** + * Keys are separators (e.g. `,`). Values are the space sequence to insert, or an empty string. + * + * @var array + */ + private $spaceBeforeListArgumentSeparators = []; /** + * This is what’s inserted after the separator in value lists, by default. + * * @var string */ - public $sSpaceAfterListArgumentSeparator = ''; + private $spaceAfterListArgumentSeparator = ''; + + /** + * Keys are separators (e.g. `,`). Values are the space sequence to insert, or an empty string. + * + * @var array + */ + private $spaceAfterListArgumentSeparators = []; /** * @var string */ - public $sSpaceBeforeOpeningBrace = ' '; + private $spaceBeforeOpeningBrace = ' '; /** * Content injected in and around declaration blocks. * * @var string */ - public $sBeforeDeclarationBlock = ''; + private $contentBeforeDeclarationBlock = ''; /** * @var string */ - public $sAfterDeclarationBlockSelectors = ''; + private $contentAfterDeclarationBlockSelectors = ''; /** * @var string */ - public $sAfterDeclarationBlock = ''; + private $contentAfterDeclarationBlock = ''; /** * Indentation character(s) per level. Only applicable if newlines are used in any of the spacing settings. * * @var string */ - public $sIndentation = "\t"; + private $indentation = "\t"; /** * Output exceptions. * * @var bool */ - public $bIgnoreExceptions = false; + private $shouldIgnoreExceptions = false; /** * Render comments for lists and RuleSets * * @var bool */ - public $bRenderComments = false; + private $shouldRenderComments = false; /** * @var OutputFormatter|null */ - private $oFormatter = null; + private $outputFormatter; /** * @var OutputFormat|null */ - private $oNextLevelFormat = null; + private $nextLevelFormat; /** - * @var int + * @var int<0, max> */ - private $iIndentationLevel = 0; + private $indentationLevel = 0; - public function __construct() + /** + * @return non-empty-string + * + * @internal + */ + public function getStringQuotingType(): string { + return $this->stringQuotingType; } /** - * @param string $sName + * @param non-empty-string $quotingType * - * @return string|null + * @return $this fluent interface */ - public function get($sName) + public function setStringQuotingType(string $quotingType): self { - $aVarPrefixes = ['a', 's', 'm', 'b', 'f', 'o', 'c', 'i']; - foreach ($aVarPrefixes as $sPrefix) { - $sFieldName = $sPrefix . ucfirst($sName); - if (isset($this->$sFieldName)) { - return $this->$sFieldName; - } - } - return null; + $this->stringQuotingType = $quotingType; + + return $this; + } + + /** + * @internal + */ + public function usesRgbHashNotation(): bool + { + return $this->usesRgbHashNotation; + } + + /** + * @return $this fluent interface + */ + public function setRGBHashNotation(bool $usesRgbHashNotation): self + { + $this->usesRgbHashNotation = $usesRgbHashNotation; + + return $this; } /** - * @param array|string $aNames - * @param mixed $mValue + * @internal + */ + public function shouldRenderSemicolonAfterLastRule(): bool + { + return $this->renderSemicolonAfterLastRule; + } + + /** + * @return $this fluent interface + */ + public function setSemicolonAfterLastRule(bool $renderSemicolonAfterLastRule): self + { + $this->renderSemicolonAfterLastRule = $renderSemicolonAfterLastRule; + + return $this; + } + + /** + * @internal + */ + public function getSpaceAfterRuleName(): string + { + return $this->spaceAfterRuleName; + } + + /** + * @return $this fluent interface + */ + public function setSpaceAfterRuleName(string $whitespace): self + { + $this->spaceAfterRuleName = $whitespace; + + return $this; + } + + /** + * @internal + */ + public function getSpaceBeforeRules(): string + { + return $this->spaceBeforeRules; + } + + /** + * @return $this fluent interface + */ + public function setSpaceBeforeRules(string $whitespace): self + { + $this->spaceBeforeRules = $whitespace; + + return $this; + } + + /** + * @internal + */ + public function getSpaceAfterRules(): string + { + return $this->spaceAfterRules; + } + + /** + * @return $this fluent interface + */ + public function setSpaceAfterRules(string $whitespace): self + { + $this->spaceAfterRules = $whitespace; + + return $this; + } + + /** + * @internal + */ + public function getSpaceBetweenRules(): string + { + return $this->spaceBetweenRules; + } + + /** + * @return $this fluent interface + */ + public function setSpaceBetweenRules(string $whitespace): self + { + $this->spaceBetweenRules = $whitespace; + + return $this; + } + + /** + * @internal + */ + public function getSpaceBeforeBlocks(): string + { + return $this->spaceBeforeBlocks; + } + + /** + * @return $this fluent interface + */ + public function setSpaceBeforeBlocks(string $whitespace): self + { + $this->spaceBeforeBlocks = $whitespace; + + return $this; + } + + /** + * @internal + */ + public function getSpaceAfterBlocks(): string + { + return $this->spaceAfterBlocks; + } + + /** + * @return $this fluent interface + */ + public function setSpaceAfterBlocks(string $whitespace): self + { + $this->spaceAfterBlocks = $whitespace; + + return $this; + } + + /** + * @internal + */ + public function getSpaceBetweenBlocks(): string + { + return $this->spaceBetweenBlocks; + } + + /** + * @return $this fluent interface + */ + public function setSpaceBetweenBlocks(string $whitespace): self + { + $this->spaceBetweenBlocks = $whitespace; + + return $this; + } + + /** + * @internal + */ + public function getContentBeforeAtRuleBlock(): string + { + return $this->contentBeforeAtRuleBlock; + } + + /** + * @return $this fluent interface + */ + public function setBeforeAtRuleBlock(string $content): self + { + $this->contentBeforeAtRuleBlock = $content; + + return $this; + } + + /** + * @internal + */ + public function getContentAfterAtRuleBlock(): string + { + return $this->contentAfterAtRuleBlock; + } + + /** + * @return $this fluent interface + */ + public function setAfterAtRuleBlock(string $content): self + { + $this->contentAfterAtRuleBlock = $content; + + return $this; + } + + /** + * @internal + */ + public function getSpaceBeforeSelectorSeparator(): string + { + return $this->spaceBeforeSelectorSeparator; + } + + /** + * @return $this fluent interface + */ + public function setSpaceBeforeSelectorSeparator(string $whitespace): self + { + $this->spaceBeforeSelectorSeparator = $whitespace; + + return $this; + } + + /** + * @internal + */ + public function getSpaceAfterSelectorSeparator(): string + { + return $this->spaceAfterSelectorSeparator; + } + + /** + * @return $this fluent interface + */ + public function setSpaceAfterSelectorSeparator(string $whitespace): self + { + $this->spaceAfterSelectorSeparator = $whitespace; + + return $this; + } + + /** + * @internal + */ + public function getSpaceBeforeListArgumentSeparator(): string + { + return $this->spaceBeforeListArgumentSeparator; + } + + /** + * @return $this fluent interface + */ + public function setSpaceBeforeListArgumentSeparator(string $whitespace): self + { + $this->spaceBeforeListArgumentSeparator = $whitespace; + + return $this; + } + + /** + * @return array * - * @return self|false - */ - public function set($aNames, $mValue) - { - $aVarPrefixes = ['a', 's', 'm', 'b', 'f', 'o', 'c', 'i']; - if (is_string($aNames) && strpos($aNames, '*') !== false) { - $aNames = - [ - str_replace('*', 'Before', $aNames), - str_replace('*', 'Between', $aNames), - str_replace('*', 'After', $aNames), - ]; - } elseif (!is_array($aNames)) { - $aNames = [$aNames]; - } - foreach ($aVarPrefixes as $sPrefix) { - $bDidReplace = false; - foreach ($aNames as $sName) { - $sFieldName = $sPrefix . ucfirst($sName); - if (isset($this->$sFieldName)) { - $this->$sFieldName = $mValue; - $bDidReplace = true; - } - } - if ($bDidReplace) { - return $this; - } - } - // Break the chain so the user knows this option is invalid - return false; + * @internal + */ + public function getSpaceBeforeListArgumentSeparators(): array + { + return $this->spaceBeforeListArgumentSeparators; } /** - * @param string $sMethodName - * @param array $aArguments + * @param array $separatorSpaces * - * @return mixed + * @return $this fluent interface + */ + public function setSpaceBeforeListArgumentSeparators(array $separatorSpaces): self + { + $this->spaceBeforeListArgumentSeparators = $separatorSpaces; + + return $this; + } + + /** + * @internal + */ + public function getSpaceAfterListArgumentSeparator(): string + { + return $this->spaceAfterListArgumentSeparator; + } + + /** + * @return $this fluent interface + */ + public function setSpaceAfterListArgumentSeparator(string $whitespace): self + { + $this->spaceAfterListArgumentSeparator = $whitespace; + + return $this; + } + + /** + * @return array * - * @throws \Exception - */ - public function __call($sMethodName, array $aArguments) - { - if (strpos($sMethodName, 'set') === 0) { - return $this->set(substr($sMethodName, 3), $aArguments[0]); - } elseif (strpos($sMethodName, 'get') === 0) { - return $this->get(substr($sMethodName, 3)); - } elseif (method_exists(OutputFormatter::class, $sMethodName)) { - return call_user_func_array([$this->getFormatter(), $sMethodName], $aArguments); - } else { - throw new \Exception('Unknown OutputFormat method called: ' . $sMethodName); - } + * @internal + */ + public function getSpaceAfterListArgumentSeparators(): array + { + return $this->spaceAfterListArgumentSeparators; } /** - * @param int $iNumber + * @param array $separatorSpaces * - * @return self + * @return $this fluent interface + */ + public function setSpaceAfterListArgumentSeparators(array $separatorSpaces): self + { + $this->spaceAfterListArgumentSeparators = $separatorSpaces; + + return $this; + } + + /** + * @internal + */ + public function getSpaceBeforeOpeningBrace(): string + { + return $this->spaceBeforeOpeningBrace; + } + + /** + * @return $this fluent interface + */ + public function setSpaceBeforeOpeningBrace(string $whitespace): self + { + $this->spaceBeforeOpeningBrace = $whitespace; + + return $this; + } + + /** + * @internal + */ + public function getContentBeforeDeclarationBlock(): string + { + return $this->contentBeforeDeclarationBlock; + } + + /** + * @return $this fluent interface + */ + public function setBeforeDeclarationBlock(string $content): self + { + $this->contentBeforeDeclarationBlock = $content; + + return $this; + } + + /** + * @internal + */ + public function getContentAfterDeclarationBlockSelectors(): string + { + return $this->contentAfterDeclarationBlockSelectors; + } + + /** + * @return $this fluent interface + */ + public function setAfterDeclarationBlockSelectors(string $content): self + { + $this->contentAfterDeclarationBlockSelectors = $content; + + return $this; + } + + /** + * @internal + */ + public function getContentAfterDeclarationBlock(): string + { + return $this->contentAfterDeclarationBlock; + } + + /** + * @return $this fluent interface + */ + public function setAfterDeclarationBlock(string $content): self + { + $this->contentAfterDeclarationBlock = $content; + + return $this; + } + + /** + * @internal + */ + public function getIndentation(): string + { + return $this->indentation; + } + + /** + * @return $this fluent interface */ - public function indentWithTabs($iNumber = 1) + public function setIndentation(string $indentation): self { - return $this->setIndentation(str_repeat("\t", $iNumber)); + $this->indentation = $indentation; + + return $this; + } + + /** + * @internal + */ + public function shouldIgnoreExceptions(): bool + { + return $this->shouldIgnoreExceptions; + } + + /** + * @return $this fluent interface + */ + public function setIgnoreExceptions(bool $ignoreExceptions): self + { + $this->shouldIgnoreExceptions = $ignoreExceptions; + + return $this; + } + + /** + * @internal + */ + public function shouldRenderComments(): bool + { + return $this->shouldRenderComments; + } + + /** + * @return $this fluent interface + */ + public function setRenderComments(bool $renderComments): self + { + $this->shouldRenderComments = $renderComments; + + return $this; } /** - * @param int $iNumber + * @return int<0, max> * - * @return self + * @internal */ - public function indentWithSpaces($iNumber = 2) + public function getIndentationLevel(): int { - return $this->setIndentation(str_repeat(" ", $iNumber)); + return $this->indentationLevel; } /** - * @return OutputFormat + * @param int<1, max> $numberOfTabs + * + * @return $this fluent interface */ - public function nextLevel() + public function indentWithTabs(int $numberOfTabs = 1): self { - if ($this->oNextLevelFormat === null) { - $this->oNextLevelFormat = clone $this; - $this->oNextLevelFormat->iIndentationLevel++; - $this->oNextLevelFormat->oFormatter = null; - } - return $this->oNextLevelFormat; + return $this->setIndentation(\str_repeat("\t", $numberOfTabs)); } /** - * @return void + * @param int<1, max> $numberOfSpaces + * + * @return $this fluent interface */ - public function beLenient() + public function indentWithSpaces(int $numberOfSpaces = 2): self { - $this->bIgnoreExceptions = true; + return $this->setIndentation(\str_repeat(' ', $numberOfSpaces)); } /** - * @return OutputFormatter + * @internal since V8.8.0 */ - public function getFormatter() + public function nextLevel(): self { - if ($this->oFormatter === null) { - $this->oFormatter = new OutputFormatter($this); + if ($this->nextLevelFormat === null) { + $this->nextLevelFormat = clone $this; + $this->nextLevelFormat->indentationLevel++; + $this->nextLevelFormat->outputFormatter = null; } - return $this->oFormatter; + return $this->nextLevelFormat; + } + + public function beLenient(): void + { + $this->shouldIgnoreExceptions = true; } /** - * @return int + * @internal since 8.8.0 */ - public function level() + public function getFormatter(): OutputFormatter { - return $this->iIndentationLevel; + if ($this->outputFormatter === null) { + $this->outputFormatter = new OutputFormatter($this); + } + + return $this->outputFormatter; } /** * Creates an instance of this class without any particular formatting settings. - * - * @return self */ - public static function create() + public static function create(): self { return new OutputFormat(); } /** * Creates an instance of this class with a preset for compact formatting. - * - * @return self */ - public static function createCompact() + public static function createCompact(): self { $format = self::create(); - $format->set('Space*Rules', "") - ->set('Space*Blocks', "") + $format + ->setSpaceBeforeRules('') + ->setSpaceBetweenRules('') + ->setSpaceAfterRules('') + ->setSpaceBeforeBlocks('') + ->setSpaceBetweenBlocks('') + ->setSpaceAfterBlocks('') ->setSpaceAfterRuleName('') ->setSpaceBeforeOpeningBrace('') ->setSpaceAfterSelectorSeparator('') ->setRenderComments(false); + return $format; } /** * Creates an instance of this class with a preset for pretty formatting. - * - * @return self */ - public static function createPretty() + public static function createPretty(): self { $format = self::create(); - $format->set('Space*Rules', "\n") - ->set('Space*Blocks', "\n") + $format + ->setSpaceBeforeRules("\n") + ->setSpaceBetweenRules("\n") + ->setSpaceAfterRules("\n") + ->setSpaceBeforeBlocks("\n") ->setSpaceBetweenBlocks("\n\n") - ->set('SpaceAfterListArgumentSeparator', ['default' => '', ',' => ' ']) + ->setSpaceAfterBlocks("\n") + ->setSpaceAfterListArgumentSeparators([',' => ' ']) ->setRenderComments(true); + return $format; } } diff --git a/src/OutputFormatter.php b/src/OutputFormatter.php index 7418494c..09918c38 100644 --- a/src/OutputFormatter.php +++ b/src/OutputFormatter.php @@ -1,254 +1,235 @@ oFormat = $oFormat; + $this->outputFormat = $outputFormat; } /** - * @param string $sName - * @param string|null $sType + * @param non-empty-string $name * - * @return string - */ - public function space($sName, $sType = null) - { - $sSpaceString = $this->oFormat->get("Space$sName"); - // If $sSpaceString is an array, we have multiple values configured - // depending on the type of object the space applies to - if (is_array($sSpaceString)) { - if ($sType !== null && isset($sSpaceString[$sType])) { - $sSpaceString = $sSpaceString[$sType]; - } else { - $sSpaceString = reset($sSpaceString); - } + * @throws \InvalidArgumentException + */ + public function space(string $name): string + { + switch ($name) { + case 'AfterRuleName': + $spaceString = $this->outputFormat->getSpaceAfterRuleName(); + break; + case 'BeforeRules': + $spaceString = $this->outputFormat->getSpaceBeforeRules(); + break; + case 'AfterRules': + $spaceString = $this->outputFormat->getSpaceAfterRules(); + break; + case 'BetweenRules': + $spaceString = $this->outputFormat->getSpaceBetweenRules(); + break; + case 'BeforeBlocks': + $spaceString = $this->outputFormat->getSpaceBeforeBlocks(); + break; + case 'AfterBlocks': + $spaceString = $this->outputFormat->getSpaceAfterBlocks(); + break; + case 'BetweenBlocks': + $spaceString = $this->outputFormat->getSpaceBetweenBlocks(); + break; + case 'BeforeSelectorSeparator': + $spaceString = $this->outputFormat->getSpaceBeforeSelectorSeparator(); + break; + case 'AfterSelectorSeparator': + $spaceString = $this->outputFormat->getSpaceAfterSelectorSeparator(); + break; + case 'BeforeOpeningBrace': + $spaceString = $this->outputFormat->getSpaceBeforeOpeningBrace(); + break; + case 'BeforeListArgumentSeparator': + $spaceString = $this->outputFormat->getSpaceBeforeListArgumentSeparator(); + break; + case 'AfterListArgumentSeparator': + $spaceString = $this->outputFormat->getSpaceAfterListArgumentSeparator(); + break; + default: + throw new \InvalidArgumentException("Unknown space type: $name", 1740049248); } - return $this->prepareSpace($sSpaceString); + + return $this->prepareSpace($spaceString); } - /** - * @return string - */ - public function spaceAfterRuleName() + public function spaceAfterRuleName(): string { return $this->space('AfterRuleName'); } - /** - * @return string - */ - public function spaceBeforeRules() + public function spaceBeforeRules(): string { return $this->space('BeforeRules'); } - /** - * @return string - */ - public function spaceAfterRules() + public function spaceAfterRules(): string { return $this->space('AfterRules'); } - /** - * @return string - */ - public function spaceBetweenRules() + public function spaceBetweenRules(): string { return $this->space('BetweenRules'); } - /** - * @return string - */ - public function spaceBeforeBlocks() + public function spaceBeforeBlocks(): string { return $this->space('BeforeBlocks'); } - /** - * @return string - */ - public function spaceAfterBlocks() + public function spaceAfterBlocks(): string { return $this->space('AfterBlocks'); } - /** - * @return string - */ - public function spaceBetweenBlocks() + public function spaceBetweenBlocks(): string { return $this->space('BetweenBlocks'); } - /** - * @return string - */ - public function spaceBeforeSelectorSeparator() + public function spaceBeforeSelectorSeparator(): string { return $this->space('BeforeSelectorSeparator'); } - /** - * @return string - */ - public function spaceAfterSelectorSeparator() + public function spaceAfterSelectorSeparator(): string { return $this->space('AfterSelectorSeparator'); } /** - * @param string $sSeparator - * - * @return string + * @param non-empty-string $separator */ - public function spaceBeforeListArgumentSeparator($sSeparator) + public function spaceBeforeListArgumentSeparator(string $separator): string { - return $this->space('BeforeListArgumentSeparator', $sSeparator); + $spaceForSeparator = $this->outputFormat->getSpaceBeforeListArgumentSeparators(); + + return $spaceForSeparator[$separator] ?? $this->space('BeforeListArgumentSeparator'); } /** - * @param string $sSeparator - * - * @return string + * @param non-empty-string $separator */ - public function spaceAfterListArgumentSeparator($sSeparator) + public function spaceAfterListArgumentSeparator(string $separator): string { - return $this->space('AfterListArgumentSeparator', $sSeparator); + $spaceForSeparator = $this->outputFormat->getSpaceAfterListArgumentSeparators(); + + return $spaceForSeparator[$separator] ?? $this->space('AfterListArgumentSeparator'); } - /** - * @return string - */ - public function spaceBeforeOpeningBrace() + public function spaceBeforeOpeningBrace(): string { return $this->space('BeforeOpeningBrace'); } /** - * Runs the given code, either swallowing or passing exceptions, depending on the `bIgnoreExceptions` setting. - * - * @param string $cCode the name of the function to call - * - * @return string|null + * Runs the given code, either swallowing or passing exceptions, depending on the `ignoreExceptions` setting. */ - public function safely($cCode) + public function safely(callable $callable): ?string { - if ($this->oFormat->get('IgnoreExceptions')) { + if ($this->outputFormat->shouldIgnoreExceptions()) { // If output exceptions are ignored, run the code with exception guards try { - return $cCode(); + return $callable(); } catch (OutputException $e) { return null; } // Do nothing } else { // Run the code as-is - return $cCode(); + return $callable(); } } /** - * Clone of the `implode` function, but calls `render` with the current output format instead of `__toString()`. - * - * @param string $sSeparator - * @param array $aValues - * @param bool $bIncreaseLevel + * Clone of the `implode` function, but calls `render` with the current output format. * - * @return string + * @param array $values */ - public function implode($sSeparator, array $aValues, $bIncreaseLevel = false) + public function implode(string $separator, array $values, bool $increaseLevel = false): string { - $sResult = ''; - $oFormat = $this->oFormat; - if ($bIncreaseLevel) { - $oFormat = $oFormat->nextLevel(); + $result = ''; + $outputFormat = $this->outputFormat; + if ($increaseLevel) { + $outputFormat = $outputFormat->nextLevel(); } - $bIsFirst = true; - foreach ($aValues as $mValue) { - if ($bIsFirst) { - $bIsFirst = false; + $isFirst = true; + foreach ($values as $value) { + if ($isFirst) { + $isFirst = false; } else { - $sResult .= $sSeparator; + $result .= $separator; } - if ($mValue instanceof Renderable) { - $sResult .= $mValue->render($oFormat); + if ($value instanceof Renderable) { + $result .= $value->render($outputFormat); } else { - $sResult .= $mValue; + $result .= $value; } } - return $sResult; + return $result; } - /** - * @param string $sString - * - * @return string - */ - public function removeLastSemicolon($sString) + public function removeLastSemicolon(string $string): string { - if ($this->oFormat->get('SemicolonAfterLastRule')) { - return $sString; + if ($this->outputFormat->shouldRenderSemicolonAfterLastRule()) { + return $string; } - $sString = explode(';', $sString); - if (count($sString) < 2) { - return $sString[0]; + + $parts = \explode(';', $string); + if (\count($parts) < 2) { + return $parts[0]; } - $sLast = array_pop($sString); - $sNextToLast = array_pop($sString); - array_push($sString, $sNextToLast . $sLast); - return implode(';', $sString); + $lastPart = \array_pop($parts); + $nextToLastPart = \array_pop($parts); + \array_push($parts, $nextToLastPart . $lastPart); + + return \implode(';', $parts); } - /** - * - * @param array $aComments - * @return string - */ - public function comments(Commentable $oCommentable) + public function comments(Commentable $commentable): string { - if (!$this->oFormat->bRenderComments) { + if (!$this->outputFormat->shouldRenderComments()) { return ''; } - $sResult = ''; - $aComments = $oCommentable->getComments(); - $iLastCommentIndex = count($aComments) - 1; + $result = ''; + $comments = $commentable->getComments(); + $lastCommentIndex = \count($comments) - 1; - foreach ($aComments as $i => $oComment) { - $sResult .= $oComment->render($this->oFormat); - $sResult .= $i === $iLastCommentIndex ? $this->spaceAfterBlocks() : $this->spaceBetweenBlocks(); + foreach ($comments as $i => $comment) { + $result .= $comment->render($this->outputFormat); + $result .= $i === $lastCommentIndex ? $this->spaceAfterBlocks() : $this->spaceBetweenBlocks(); } - return $sResult; + return $result; } - /** - * @param string $sSpaceString - * - * @return string - */ - private function prepareSpace($sSpaceString) + private function prepareSpace(string $spaceString): string { - return str_replace("\n", "\n" . $this->indent(), $sSpaceString); + return \str_replace("\n", "\n" . $this->indent(), $spaceString); } - /** - * @return string - */ - private function indent() + private function indent(): string { - return str_repeat($this->oFormat->sIndentation, $this->oFormat->level()); + return \str_repeat($this->outputFormat->getIndentation(), $this->outputFormat->getIndentationLevel()); } } diff --git a/src/Parser.php b/src/Parser.php index e582cfab..b34a5107 100644 --- a/src/Parser.php +++ b/src/Parser.php @@ -1,5 +1,7 @@ $lineNumber the line number (starting from 1, not from 0) */ - public function __construct($sText, Settings $oParserSettings = null, $iLineNo = 1) + public function __construct(string $text, ?Settings $parserSettings = null, int $lineNumber = 1) { - if ($oParserSettings === null) { - $oParserSettings = Settings::create(); + if ($parserSettings === null) { + $parserSettings = Settings::create(); } - $this->oParserState = new ParserState($sText, $oParserSettings, $iLineNo); - } - - /** - * Sets the charset to be used if the CSS does not contain an `@charset` declaration. - * - * @param string $sCharset - * - * @return void - */ - public function setCharset($sCharset) - { - $this->oParserState->setCharset($sCharset); - } - - /** - * Returns the charset that is used if the CSS does not contain an `@charset` declaration. - * - * @return void - */ - public function getCharset() - { - // Note: The `return` statement is missing here. This is a bug that needs to be fixed. - $this->oParserState->getCharset(); + $this->parserState = new ParserState($text, $parserSettings, $lineNumber); } /** * Parses the CSS provided to the constructor and creates a `Document` from it. * - * @return Document - * * @throws SourceException */ - public function parse() + public function parse(): Document { - return Document::parse($this->oParserState); + return Document::parse($this->parserState); } } diff --git a/src/Parsing/Anchor.php b/src/Parsing/Anchor.php index 93789e26..c27f436a 100644 --- a/src/Parsing/Anchor.php +++ b/src/Parsing/Anchor.php @@ -1,34 +1,35 @@ */ - private $iPosition; + private $position; /** - * @var \Sabberworm\CSS\Parsing\ParserState + * @var ParserState */ - private $oParserState; + private $parserState; /** - * @param int $iPosition - * @param \Sabberworm\CSS\Parsing\ParserState $oParserState + * @param int<0, max> $position */ - public function __construct($iPosition, ParserState $oParserState) + public function __construct(int $position, ParserState $parserState) { - $this->iPosition = $iPosition; - $this->oParserState = $oParserState; + $this->position = $position; + $this->parserState = $parserState; } - /** - * @return void - */ - public function backtrack() + public function backtrack(): void { - $this->oParserState->setPosition($this->iPosition); + $this->parserState->setPosition($this->position); } } diff --git a/src/Parsing/OutputException.php b/src/Parsing/OutputException.php index 9bfbc75f..0a20dc96 100644 --- a/src/Parsing/OutputException.php +++ b/src/Parsing/OutputException.php @@ -1,18 +1,10 @@ */ - private $aText; + private $characters; /** - * @var int + * @var int<0, max> */ - private $iCurrentPosition; + private $currentPosition = 0; /** * will only be used if the CSS does not contain an `@charset` declaration * * @var string */ - private $sCharset; + private $charset; /** - * @var int + * @var int<1, max> $lineNumber */ - private $iLength; + private $lineNumber; /** - * @var int + * @param string $text the complete CSS as text (i.e., usually the contents of a CSS file) + * @param int<1, max> $lineNumber */ - private $iLineNo; - - /** - * @param string $sText the complete CSS as text (i.e., usually the contents of a CSS file) - * @param int $iLineNo - */ - public function __construct($sText, Settings $oParserSettings, $iLineNo = 1) + public function __construct(string $text, Settings $parserSettings, int $lineNumber = 1) { - $this->oParserSettings = $oParserSettings; - $this->sText = $sText; - $this->iCurrentPosition = 0; - $this->iLineNo = $iLineNo; - $this->setCharset($this->oParserSettings->sDefaultCharset); + $this->parserSettings = $parserSettings; + $this->text = $text; + $this->lineNumber = $lineNumber; + $this->setCharset($this->parserSettings->getDefaultCharset()); } /** * Sets the charset to be used if the CSS does not contain an `@charset` declaration. * - * @param string $sCharset - * - * @return void - */ - public function setCharset($sCharset) - { - $this->sCharset = $sCharset; - $this->aText = $this->strsplit($this->sText); - if (is_array($this->aText)) { - $this->iLength = count($this->aText); - } - } - - /** - * Returns the charset that is used if the CSS does not contain an `@charset` declaration. - * - * @return string + * @throws SourceException if the charset is UTF-8 and the content has invalid byte sequences */ - public function getCharset() + public function setCharset(string $charset): void { - return $this->sCharset; + $this->charset = $charset; + $this->characters = $this->strsplit($this->text); } /** - * @return int + * @return int<1, max> */ - public function currentLine() + public function currentLine(): int { - return $this->iLineNo; + return $this->lineNumber; } /** - * @return int + * @return int<0, max> */ - public function currentColumn() + public function currentColumn(): int { - return $this->iCurrentPosition; + return $this->currentPosition; } - /** - * @return Settings - */ - public function getSettings() + public function getSettings(): Settings { - return $this->oParserSettings; + return $this->parserSettings; } - /** - * @return \Sabberworm\CSS\Parsing\Anchor - */ - public function anchor() + public function anchor(): Anchor { - return new Anchor($this->iCurrentPosition, $this); + return new Anchor($this->currentPosition, $this); } /** - * @param int $iPosition - * - * @return void + * @param int<0, max> $position */ - public function setPosition($iPosition) + public function setPosition(int $position): void { - $this->iCurrentPosition = $iPosition; + $this->currentPosition = $position; } /** - * @param bool $bIgnoreCase - * - * @return string + * @return non-empty-string * * @throws UnexpectedTokenException */ - public function parseIdentifier($bIgnoreCase = true) + public function parseIdentifier(bool $ignoreCase = true): string { if ($this->isEnd()) { - throw new UnexpectedEOFException('', '', 'identifier', $this->iLineNo); + throw new UnexpectedEOFException('', '', 'identifier', $this->lineNumber); } - $sResult = $this->parseCharacter(true); - if ($sResult === null) { - throw new UnexpectedTokenException($sResult, $this->peek(5), 'identifier', $this->iLineNo); + $result = $this->parseCharacter(true); + if ($result === null) { + throw new UnexpectedTokenException('', $this->peek(5), 'identifier', $this->lineNumber); } - $sCharacter = null; - while (!$this->isEnd() && ($sCharacter = $this->parseCharacter(true)) !== null) { - if (preg_match('/[a-zA-Z0-9\x{00A0}-\x{FFFF}_-]/Sux', $sCharacter)) { - $sResult .= $sCharacter; + $character = null; + while (!$this->isEnd() && ($character = $this->parseCharacter(true)) !== null) { + if (\preg_match('/[a-zA-Z0-9\\x{00A0}-\\x{FFFF}_-]/Sux', $character)) { + $result .= $character; } else { - $sResult .= '\\' . $sCharacter; + $result .= '\\' . $character; } } - if ($bIgnoreCase) { - $sResult = $this->strtolower($sResult); + if ($ignoreCase) { + $result = $this->strtolower($result); } - return $sResult; + + return $result; } /** - * @param bool $bIsForIdentifier - * - * @return string|null - * * @throws UnexpectedEOFException * @throws UnexpectedTokenException */ - public function parseCharacter($bIsForIdentifier) + public function parseCharacter(bool $isForIdentifier): ?string { if ($this->peek() === '\\') { - if ( - $bIsForIdentifier && $this->oParserSettings->bLenientParsing - && ($this->comes('\0') || $this->comes('\9')) - ) { - // Non-strings can contain \0 or \9 which is an IE hack supported in lenient parsing. - return null; - } $this->consume('\\'); - if ($this->comes('\n') || $this->comes('\r')) { + if ($this->comes('\\n') || $this->comes('\\r')) { return ''; } - if (preg_match('/[0-9a-fA-F]/Su', $this->peek()) === 0) { + if (\preg_match('/[0-9a-fA-F]/Su', $this->peek()) === 0) { return $this->consume(1); } - $sUnicode = $this->consumeExpression('/^[0-9a-fA-F]{1,6}/u', 6); - if ($this->strlen($sUnicode) < 6) { + $hexCodePoint = $this->consumeExpression('/^[0-9a-fA-F]{1,6}/u', 6); + if ($this->strlen($hexCodePoint) < 6) { // Consume whitespace after incomplete unicode escape - if (preg_match('/\\s/isSu', $this->peek())) { - if ($this->comes('\r\n')) { + if (\preg_match('/\\s/isSu', $this->peek())) { + if ($this->comes('\\r\\n')) { $this->consume(2); } else { $this->consume(1); } } } - $iUnicode = intval($sUnicode, 16); - $sUtf32 = ""; + $codePoint = \intval($hexCodePoint, 16); + $utf32EncodedCharacter = ''; for ($i = 0; $i < 4; ++$i) { - $sUtf32 .= chr($iUnicode & 0xff); - $iUnicode = $iUnicode >> 8; + $utf32EncodedCharacter .= \chr($codePoint & 0xff); + $codePoint = $codePoint >> 8; } - return iconv('utf-32le', $this->sCharset, $sUtf32); + return \iconv('utf-32le', $this->charset, $utf32EncodedCharacter); } - if ($bIsForIdentifier) { - $peek = ord($this->peek()); + if ($isForIdentifier) { + $peek = \ord($this->peek()); // Ranges: a-z A-Z 0-9 - _ if ( ($peek >= 97 && $peek <= 122) @@ -220,116 +184,118 @@ public function parseCharacter($bIsForIdentifier) } else { return $this->consume(1); } + return null; } /** - * @return array|void + * @return list * * @throws UnexpectedEOFException * @throws UnexpectedTokenException */ - public function consumeWhiteSpace() + public function consumeWhiteSpace(): array { - $aComments = []; + $comments = []; do { - while (preg_match('/\\s/isSu', $this->peek()) === 1) { + while (\preg_match('/\\s/isSu', $this->peek()) === 1) { $this->consume(1); } - if ($this->oParserSettings->bLenientParsing) { + if ($this->parserSettings->usesLenientParsing()) { try { - $oComment = $this->consumeComment(); + $comment = $this->consumeComment(); } catch (UnexpectedEOFException $e) { - $this->iCurrentPosition = $this->iLength; - return $aComments; + $this->currentPosition = \count($this->characters); + break; } } else { - $oComment = $this->consumeComment(); + $comment = $this->consumeComment(); } - if ($oComment !== false) { - $aComments[] = $oComment; + if ($comment instanceof Comment) { + $comments[] = $comment; } - } while ($oComment !== false); - return $aComments; + } while ($comment instanceof Comment); + + return $comments; } /** - * @param string $sString - * @param bool $bCaseInsensitive - * - * @return bool + * @param non-empty-string $string */ - public function comes($sString, $bCaseInsensitive = false) + public function comes(string $string, bool $caseInsensitive = false): bool { - $sPeek = $this->peek(strlen($sString)); - return ($sPeek == '') - ? false - : $this->streql($sPeek, $sString, $bCaseInsensitive); + $peek = $this->peek(\strlen($string)); + + return ($peek !== '') && $this->streql($peek, $string, $caseInsensitive); } /** - * @param int $iLength - * @param int $iOffset - * - * @return string + * @param int<1, max> $length + * @param int<0, max> $offset */ - public function peek($iLength = 1, $iOffset = 0) + public function peek(int $length = 1, int $offset = 0): string { - $iOffset += $this->iCurrentPosition; - if ($iOffset >= $this->iLength) { + $offset += $this->currentPosition; + if ($offset >= \count($this->characters)) { return ''; } - return $this->substr($iOffset, $iLength); + + return $this->substr($offset, $length); } /** - * @param int $mValue - * - * @return string + * @param string|int<1, max> $value * * @throws UnexpectedEOFException * @throws UnexpectedTokenException */ - public function consume($mValue = 1) + public function consume($value = 1): string { - if (is_string($mValue)) { - $iLineCount = substr_count($mValue, "\n"); - $iLength = $this->strlen($mValue); - if (!$this->streql($this->substr($this->iCurrentPosition, $iLength), $mValue)) { - throw new UnexpectedTokenException($mValue, $this->peek(max($iLength, 5)), $this->iLineNo); + if (\is_string($value)) { + $numberOfLines = \substr_count($value, "\n"); + $length = $this->strlen($value); + if (!$this->streql($this->substr($this->currentPosition, $length), $value)) { + throw new UnexpectedTokenException( + $value, + $this->peek(\max($length, 5)), + 'literal', + $this->lineNumber + ); } - $this->iLineNo += $iLineCount; - $this->iCurrentPosition += $this->strlen($mValue); - return $mValue; + + $this->lineNumber += $numberOfLines; + $this->currentPosition += $this->strlen($value); + $result = $value; } else { - if ($this->iCurrentPosition + $mValue > $this->iLength) { - throw new UnexpectedEOFException($mValue, $this->peek(5), 'count', $this->iLineNo); + if ($this->currentPosition + $value > \count($this->characters)) { + throw new UnexpectedEOFException((string) $value, $this->peek(5), 'count', $this->lineNumber); } - $sResult = $this->substr($this->iCurrentPosition, $mValue); - $iLineCount = substr_count($sResult, "\n"); - $this->iLineNo += $iLineCount; - $this->iCurrentPosition += $mValue; - return $sResult; + + $result = $this->substr($this->currentPosition, $value); + $numberOfLines = \substr_count($result, "\n"); + $this->lineNumber += $numberOfLines; + $this->currentPosition += $value; } + + return $result; } /** - * @param string $mExpression - * @param int|null $iMaxLength - * - * @return string + * @param string $expression + * @param int<1, max>|null $maximumLength * * @throws UnexpectedEOFException * @throws UnexpectedTokenException */ - public function consumeExpression($mExpression, $iMaxLength = null) + public function consumeExpression(string $expression, ?int $maximumLength = null): string { - $aMatches = null; - $sInput = $iMaxLength !== null ? $this->peek($iMaxLength) : $this->inputLeft(); - if (preg_match($mExpression, $sInput, $aMatches, PREG_OFFSET_CAPTURE) === 1) { - return $this->consume($aMatches[0][0]); + $matches = null; + $input = ($maximumLength !== null) ? $this->peek($maximumLength) : $this->inputLeft(); + if (\preg_match($expression, $input, $matches, PREG_OFFSET_CAPTURE) !== 1) { + throw new UnexpectedTokenException($expression, $this->peek(5), 'expression', $this->lineNumber); } - throw new UnexpectedTokenException($mExpression, $this->peek(5), 'expression', $this->iLineNo); + + return $this->consume($matches[0][0]); } /** @@ -337,13 +303,14 @@ public function consumeExpression($mExpression, $iMaxLength = null) */ public function consumeComment() { - $mComment = false; + $lineNumber = $this->lineNumber; + $comment = null; + if ($this->comes('/*')) { - $iLineNo = $this->iLineNo; $this->consume(1); - $mComment = ''; + $comment = ''; while (($char = $this->consume(1)) !== '') { - $mComment .= $char; + $comment .= $char; if ($this->comes('*/')) { $this->consume(2); break; @@ -351,193 +318,147 @@ public function consumeComment() } } - if ($mComment !== false) { - // We skip the * which was included in the comment. - return new Comment(substr($mComment, 1), $iLineNo); - } - - return $mComment; + // We skip the * which was included in the comment. + return \is_string($comment) ? new Comment(\substr($comment, 1), $lineNumber) : false; } - /** - * @return bool - */ - public function isEnd() + public function isEnd(): bool { - return $this->iCurrentPosition >= $this->iLength; + return $this->currentPosition >= \count($this->characters); } /** - * @param array|string $aEnd - * @param string $bIncludeEnd - * @param string $consumeEnd + * @param list|string $stopCharacters * @param array $comments * - * @return string - * * @throws UnexpectedEOFException * @throws UnexpectedTokenException */ - public function consumeUntil($aEnd, $bIncludeEnd = false, $consumeEnd = false, array &$comments = []) - { - $aEnd = is_array($aEnd) ? $aEnd : [$aEnd]; - $out = ''; - $start = $this->iCurrentPosition; + public function consumeUntil( + $stopCharacters, + bool $includeEnd = false, + bool $consumeEnd = false, + array &$comments = [] + ): string { + $stopCharacters = \is_array($stopCharacters) ? $stopCharacters : [$stopCharacters]; + $consumedCharacters = ''; + $start = $this->currentPosition; while (!$this->isEnd()) { - $char = $this->consume(1); - if (in_array($char, $aEnd)) { - if ($bIncludeEnd) { - $out .= $char; + $character = $this->consume(1); + if (\in_array($character, $stopCharacters, true)) { + if ($includeEnd) { + $consumedCharacters .= $character; } elseif (!$consumeEnd) { - $this->iCurrentPosition -= $this->strlen($char); + $this->currentPosition -= $this->strlen($character); } - return $out; + return $consumedCharacters; } - $out .= $char; - if ($comment = $this->consumeComment()) { + $consumedCharacters .= $character; + $comment = $this->consumeComment(); + if ($comment instanceof Comment) { $comments[] = $comment; } } - if (in_array(self::EOF, $aEnd)) { - return $out; + if (\in_array(self::EOF, $stopCharacters, true)) { + return $consumedCharacters; } - $this->iCurrentPosition = $start; + $this->currentPosition = $start; throw new UnexpectedEOFException( - 'One of ("' . implode('","', $aEnd) . '")', + 'One of ("' . \implode('","', $stopCharacters) . '")', $this->peek(5), 'search', - $this->iLineNo + $this->lineNumber ); } - /** - * @return string - */ - private function inputLeft() + private function inputLeft(): string { - return $this->substr($this->iCurrentPosition, -1); + return $this->substr($this->currentPosition, -1); } - /** - * @param string $sString1 - * @param string $sString2 - * @param bool $bCaseInsensitive - * - * @return bool - */ - public function streql($sString1, $sString2, $bCaseInsensitive = true) + public function streql(string $string1, string $string2, bool $caseInsensitive = true): bool { - if ($bCaseInsensitive) { - return $this->strtolower($sString1) === $this->strtolower($sString2); - } else { - return $sString1 === $sString2; - } + return $caseInsensitive + ? ($this->strtolower($string1) === $this->strtolower($string2)) + : ($string1 === $string2); } /** - * @param int $iAmount - * - * @return void + * @param int<1, max> $numberOfCharacters */ - public function backtrack($iAmount) + public function backtrack(int $numberOfCharacters): void { - $this->iCurrentPosition -= $iAmount; + $this->currentPosition -= $numberOfCharacters; } /** - * @param string $sString - * - * @return int + * @return int<0, max> */ - public function strlen($sString) + public function strlen(string $string): int { - if ($this->oParserSettings->bMultibyteSupport) { - return mb_strlen($sString, $this->sCharset); - } else { - return strlen($sString); - } + return $this->parserSettings->hasMultibyteSupport() + ? \mb_strlen($string, $this->charset) + : \strlen($string); } /** - * @param int $iStart - * @param int $iLength - * - * @return string + * @param int<0, max> $offset */ - private function substr($iStart, $iLength) + private function substr(int $offset, int $length): string { - if ($iLength < 0) { - $iLength = $this->iLength - $iStart + $iLength; + if ($length < 0) { + $length = \count($this->characters) - $offset + $length; } - if ($iStart + $iLength > $this->iLength) { - $iLength = $this->iLength - $iStart; + if ($offset + $length > \count($this->characters)) { + $length = \count($this->characters) - $offset; } - $sResult = ''; - while ($iLength > 0) { - $sResult .= $this->aText[$iStart]; - $iStart++; - $iLength--; + $result = ''; + while ($length > 0) { + $result .= $this->characters[$offset]; + $offset++; + $length--; } - return $sResult; + + return $result; } /** - * @param string $sString - * - * @return string + * @return ($string is non-empty-string ? non-empty-string : string) */ - private function strtolower($sString) + private function strtolower(string $string): string { - if ($this->oParserSettings->bMultibyteSupport) { - return mb_strtolower($sString, $this->sCharset); - } else { - return strtolower($sString); - } + return $this->parserSettings->hasMultibyteSupport() + ? \mb_strtolower($string, $this->charset) + : \strtolower($string); } /** - * @param string $sString + * @return list * - * @return array + * @throws SourceException if the charset is UTF-8 and the string contains invalid byte sequences */ - private function strsplit($sString) + private function strsplit(string $string): array { - if ($this->oParserSettings->bMultibyteSupport) { - if ($this->streql($this->sCharset, 'utf-8')) { - return preg_split('//u', $sString, -1, PREG_SPLIT_NO_EMPTY); + if ($this->parserSettings->hasMultibyteSupport()) { + if ($this->streql($this->charset, 'utf-8')) { + $result = \preg_split('//u', $string, -1, PREG_SPLIT_NO_EMPTY); + if (!\is_array($result)) { + throw new SourceException('`preg_split` failed with error ' . \preg_last_error()); + } } else { - $iLength = mb_strlen($sString, $this->sCharset); - $aResult = []; - for ($i = 0; $i < $iLength; ++$i) { - $aResult[] = mb_substr($sString, $i, 1, $this->sCharset); + $length = \mb_strlen($string, $this->charset); + $result = []; + for ($i = 0; $i < $length; ++$i) { + $result[] = \mb_substr($string, $i, 1, $this->charset); } - return $aResult; } } else { - if ($sString === '') { - return []; - } else { - return str_split($sString); - } + $result = ($string !== '') ? \str_split($string) : []; } - } - /** - * @param string $sString - * @param string $sNeedle - * @param int $iOffset - * - * @return int|false - */ - private function strpos($sString, $sNeedle, $iOffset) - { - if ($this->oParserSettings->bMultibyteSupport) { - return mb_strpos($sString, $sNeedle, $iOffset, $this->sCharset); - } else { - return strpos($sString, $sNeedle, $iOffset); - } + return $result; } } diff --git a/src/Parsing/SourceException.php b/src/Parsing/SourceException.php index 1ca668a9..ca07cc48 100644 --- a/src/Parsing/SourceException.php +++ b/src/Parsing/SourceException.php @@ -1,32 +1,25 @@ $lineNumber */ - public function __construct($sMessage, $iLineNo = 0) + public function __construct(string $message, int $lineNumber = 0) { - $this->iLineNo = $iLineNo; - if (!empty($iLineNo)) { - $sMessage .= " [line no: $iLineNo]"; + $this->setPosition($lineNumber); + if ($lineNumber !== 0) { + $message .= " [line no: $lineNumber]"; } - parent::__construct($sMessage); - } - - /** - * @return int - */ - public function getLineNo() - { - return $this->iLineNo; + parent::__construct($message); } } diff --git a/src/Parsing/UnexpectedEOFException.php b/src/Parsing/UnexpectedEOFException.php index 368ec70c..17e2a215 100644 --- a/src/Parsing/UnexpectedEOFException.php +++ b/src/Parsing/UnexpectedEOFException.php @@ -1,5 +1,7 @@ $lineNumber */ - public function __construct($sExpected, $sFound, $sMatchType = 'literal', $iLineNo = 0) + public function __construct(string $expected, string $found, string $matchType = 'literal', int $lineNumber = 0) { - $this->sExpected = $sExpected; - $this->sFound = $sFound; - $this->sMatchType = $sMatchType; - $sMessage = "Token “{$sExpected}” ({$sMatchType}) not found. Got “{$sFound}”."; - if ($this->sMatchType === 'search') { - $sMessage = "Search for “{$sExpected}” returned no results. Context: “{$sFound}”."; - } elseif ($this->sMatchType === 'count') { - $sMessage = "Next token was expected to have {$sExpected} chars. Context: “{$sFound}”."; - } elseif ($this->sMatchType === 'identifier') { - $sMessage = "Identifier expected. Got “{$sFound}”"; - } elseif ($this->sMatchType === 'custom') { - $sMessage = trim("$sExpected $sFound"); + $message = "Token “{$expected}” ({$matchType}) not found. Got “{$found}”."; + if ($matchType === 'search') { + $message = "Search for “{$expected}” returned no results. Context: “{$found}”."; + } elseif ($matchType === 'count') { + $message = "Next token was expected to have {$expected} chars. Context: “{$found}”."; + } elseif ($matchType === 'identifier') { + $message = "Identifier expected. Got “{$found}”"; + } elseif ($matchType === 'custom') { + $message = \trim("$expected $found"); } - parent::__construct($sMessage, $iLineNo); + parent::__construct($message, $lineNumber); } } diff --git a/src/Position/Position.php b/src/Position/Position.php new file mode 100644 index 00000000..0771453b --- /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(): ?int + { + return $this->lineNumber; + } + + /** + * @return int<0, max> + */ + public function getLineNo(): int + { + return $this->getLineNumber() ?? 0; + } + + /** + * @return int<0, max>|null + */ + public function getColumnNumber(): ?int + { + return $this->columnNumber; + } + + /** + * @return int<0, max> + */ + public function getColNo(): int + { + return $this->getColumnNumber() ?? 0; + } + + /** + * @param int<0, max>|null $lineNumber + * @param int<0, max>|null $columnNumber + * + * @return $this fluent interface + */ + public function setPosition(?int $lineNumber, ?int $columnNumber = null): Positionable + { + // The conditional is for backwards compatibility (backcompat); `0` will not be allowed in future. + $this->lineNumber = $lineNumber !== 0 ? $lineNumber : null; + $this->columnNumber = $columnNumber; + + return $this; + } +} diff --git a/src/Position/Positionable.php b/src/Position/Positionable.php new file mode 100644 index 00000000..675fb55f --- /dev/null +++ b/src/Position/Positionable.php @@ -0,0 +1,47 @@ +|null + */ + public function getLineNumber(): ?int; + + /** + * @return int<0, max> + * + * @deprecated in version 8.9.0, will be removed in v9.0. Use `getLineNumber()` instead. + */ + public function getLineNo(): int; + + /** + * @return int<0, max>|null + */ + public function getColumnNumber(): ?int; + + /** + * @return int<0, max> + * + * @deprecated in version 8.9.0, will be removed in v9.0. Use `getColumnNumber()` instead. + */ + public function getColNo(): int; + + /** + * @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 + * + * @return $this fluent interface + */ + public function setPosition(?int $lineNumber, ?int $columnNumber = null): Positionable; +} diff --git a/src/Property/AtRule.php b/src/Property/AtRule.php index 9536ff5e..49a160a1 100644 --- a/src/Property/AtRule.php +++ b/src/Property/AtRule.php @@ -1,34 +1,29 @@ + * @var CSSString|URL */ - protected $aComments; + private $url; /** - * @param string $mUrl - * @param string|null $sPrefix - * @param int $iLineNo + * @var string|null */ - public function __construct($mUrl, $sPrefix = null, $iLineNo = 0) - { - $this->mUrl = $mUrl; - $this->sPrefix = $sPrefix; - $this->iLineNo = $iLineNo; - $this->aComments = []; - } + private $prefix; /** - * @return int + * @param CSSString|URL $url + * @param int<0, max> $lineNumber */ - public function getLineNo() + public function __construct($url, ?string $prefix = null, int $lineNumber = 0) { - return $this->iLineNo; + $this->url = $url; + $this->prefix = $prefix; + $this->setPosition($lineNumber); } /** - * @return string + * @return non-empty-string */ - public function __toString() + public function render(OutputFormat $outputFormat): string { - return $this->render(new OutputFormat()); + return '@namespace ' . ($this->prefix === null ? '' : $this->prefix . ' ') + . $this->url->render($outputFormat) . ';'; } /** - * @return string - */ - public function render(OutputFormat $oOutputFormat) - { - return '@namespace ' . ($this->sPrefix === null ? '' : $this->sPrefix . ' ') - . $this->mUrl->render($oOutputFormat) . ';'; - } - - /** - * @return string + * @return CSSString|URL */ public function getUrl() { - return $this->mUrl; + return $this->url; } - /** - * @return string|null - */ - public function getPrefix() + public function getPrefix(): ?string { - return $this->sPrefix; + return $this->prefix; } /** - * @param string $mUrl - * - * @return void + * @param CSSString|URL $url */ - public function setUrl($mUrl) + public function setUrl($url): void { - $this->mUrl = $mUrl; + $this->url = $url; } - /** - * @param string $sPrefix - * - * @return void - */ - public function setPrefix($sPrefix) + public function setPrefix(string $prefix): void { - $this->sPrefix = $sPrefix; + $this->prefix = $prefix; } /** - * @return string + * @return non-empty-string */ - public function atRuleName() + public function atRuleName(): string { return 'namespace'; } /** - * @return array + * @return array{0: CSSString|URL|non-empty-string, 1?: CSSString|URL} */ - public function atRuleArgs() + public function atRuleArgs(): array { - $aResult = [$this->mUrl]; - if ($this->sPrefix) { - array_unshift($aResult, $this->sPrefix); + $result = [$this->url]; + if (\is_string($this->prefix) && $this->prefix !== '') { + \array_unshift($result, $this->prefix); } - return $aResult; - } - - /** - * @param array $aComments - * - * @return void - */ - public function addComments(array $aComments) - { - $this->aComments = array_merge($this->aComments, $aComments); - } - - /** - * @return array - */ - public function getComments() - { - return $this->aComments; - } - - /** - * @param array $aComments - * - * @return void - */ - public function setComments(array $aComments) - { - $this->aComments = $aComments; + return $result; } } diff --git a/src/Property/Charset.php b/src/Property/Charset.php index 26e1b250..90e7d5fb 100644 --- a/src/Property/Charset.php +++ b/src/Property/Charset.php @@ -1,9 +1,13 @@ - */ - protected $aComments; + use CommentContainer; + use Position; /** - * @param CSSString $oCharset - * @param int $iLineNo + * @var CSSString */ - public function __construct(CSSString $oCharset, $iLineNo = 0) - { - $this->oCharset = $oCharset; - $this->iLineNo = $iLineNo; - $this->aComments = []; - } + private $charset; /** - * @return int + * @param int<0, max> $lineNumber */ - public function getLineNo() + public function __construct(CSSString $charset, int $lineNumber = 0) { - return $this->iLineNo; + $this->charset = $charset; + $this->setPosition($lineNumber); } /** - * @param string|CSSString $oCharset - * - * @return void + * @param string|CSSString $charset */ - public function setCharset($sCharset) + public function setCharset($charset): void { - $sCharset = $sCharset instanceof CSSString ? $sCharset : new CSSString($sCharset); - $this->oCharset = $sCharset; + $charset = $charset instanceof CSSString ? $charset : new CSSString($charset); + $this->charset = $charset; } - /** - * @return string - */ - public function getCharset() + public function getCharset(): string { - return $this->oCharset->getString(); + return $this->charset->getString(); } /** - * @return string + * @return non-empty-string */ - public function __toString() + public function render(OutputFormat $outputFormat): string { - return $this->render(new OutputFormat()); + return "{$outputFormat->getFormatter()->comments($this)}@charset {$this->charset->render($outputFormat)};"; } /** - * @return string + * @return non-empty-string */ - public function render(OutputFormat $oOutputFormat) - { - return "{$oOutputFormat->comments($this)}@charset {$this->oCharset->render($oOutputFormat)};"; - } - - /** - * @return string - */ - public function atRuleName() + public function atRuleName(): string { return 'charset'; } - /** - * @return string - */ - public function atRuleArgs() - { - return $this->oCharset; - } - - /** - * @param array $aComments - * - * @return void - */ - public function addComments(array $aComments) - { - $this->aComments = array_merge($this->aComments, $aComments); - } - - /** - * @return array - */ - public function getComments() - { - return $this->aComments; - } - - /** - * @param array $aComments - * - * @return void - */ - public function setComments(array $aComments) + public function atRuleArgs(): CSSString { - $this->aComments = $aComments; + return $this->charset; } } diff --git a/src/Property/Import.php b/src/Property/Import.php index d715a7a0..51c0e4ea 100644 --- a/src/Property/Import.php +++ b/src/Property/Import.php @@ -1,145 +1,85 @@ + * @var URL */ - protected $aComments; + private $location; /** - * @param URL $oLocation - * @param string $sMediaQuery - * @param int $iLineNo + * @var string|null */ - public function __construct(URL $oLocation, $sMediaQuery, $iLineNo = 0) - { - $this->oLocation = $oLocation; - $this->sMediaQuery = $sMediaQuery; - $this->iLineNo = $iLineNo; - $this->aComments = []; - } + private $mediaQuery; /** - * @return int + * @param int<0, max> $lineNumber */ - public function getLineNo() + public function __construct(URL $location, ?string $mediaQuery, int $lineNumber = 0) { - return $this->iLineNo; + $this->location = $location; + $this->mediaQuery = $mediaQuery; + $this->setPosition($lineNumber); } - /** - * @param URL $oLocation - * - * @return void - */ - public function setLocation($oLocation) - { - $this->oLocation = $oLocation; - } - - /** - * @return URL - */ - public function getLocation() + public function setLocation(URL $location): void { - return $this->oLocation; + $this->location = $location; } - /** - * @return string - */ - public function __toString() + public function getLocation(): URL { - return $this->render(new OutputFormat()); + return $this->location; } /** - * @return string + * @return non-empty-string */ - public function render(OutputFormat $oOutputFormat) + public function render(OutputFormat $outputFormat): string { - return $oOutputFormat->comments($this) . "@import " . $this->oLocation->render($oOutputFormat) - . ($this->sMediaQuery === null ? '' : ' ' . $this->sMediaQuery) . ';'; + return $outputFormat->getFormatter()->comments($this) . '@import ' . $this->location->render($outputFormat) + . ($this->mediaQuery === null ? '' : ' ' . $this->mediaQuery) . ';'; } /** - * @return string + * @return non-empty-string */ - public function atRuleName() + public function atRuleName(): string { return 'import'; } /** - * @return array + * @return array{0: URL, 1?: non-empty-string} */ - public function atRuleArgs() + public function atRuleArgs(): array { - $aResult = [$this->oLocation]; - if ($this->sMediaQuery) { - array_push($aResult, $this->sMediaQuery); + $result = [$this->location]; + if (\is_string($this->mediaQuery) && $this->mediaQuery !== '') { + $result[] = $this->mediaQuery; } - return $aResult; - } - /** - * @param array $aComments - * - * @return void - */ - public function addComments(array $aComments) - { - $this->aComments = array_merge($this->aComments, $aComments); + return $result; } - /** - * @return array - */ - public function getComments() - { - return $this->aComments; - } - - /** - * @param array $aComments - * - * @return void - */ - public function setComments(array $aComments) - { - $this->aComments = $aComments; - } - - /** - * @return string - */ - public function getMediaQuery() + public function getMediaQuery(): ?string { - return $this->sMediaQuery; + return $this->mediaQuery; } } diff --git a/src/Property/KeyframeSelector.php b/src/Property/KeyframeSelector.php index 14ea5ebb..2ab8ca97 100644 --- a/src/Property/KeyframeSelector.php +++ b/src/Property/KeyframeSelector.php @@ -1,5 +1,7 @@ ]* # any sequence of valid unescaped characters - (?:\\\\.)? # a single escaped character - (?:([\'"]).*?(?]* # any sequence of valid unescaped characters + (?:\\\\.)? # a single escaped character + (?:([\'"]).*?(?\~]+)[\w]+ # elements - | - \:{1,2}( # pseudo-elements - after|before|first-letter|first-line|selection - )) - /ix'; - - /** - * regexp for specificity calculations - * - * @var string - */ - const SELECTOR_VALIDATION_RX = '/ + public const SELECTOR_VALIDATION_RX = '/ ^( (?: - [a-zA-Z0-9\x{00A0}-\x{FFFF}_^$|*="\'~\[\]()\-\s\.:#+>]* # any sequence of valid unescaped characters - (?:\\\\.)? # a single escaped character - (?:([\'"]).*?(?]* # any sequence of valid unescaped characters + (?:\\\\.)? # a single escaped character + (?:([\'"]).*?(?setSelector($sSelector); - if ($bCalculateSpecificity) { - $this->getSpecificity(); - } + $this->setSelector($selector); } - /** - * @return string - */ - public function getSelector() + public function getSelector(): string { - return $this->sSelector; + return $this->selector; } - /** - * @param string $sSelector - * - * @return void - */ - public function setSelector($sSelector) + public function setSelector(string $selector): void { - $this->sSelector = trim($sSelector); - $this->iSpecificity = null; + $this->selector = \trim($selector); } /** - * @return string + * @return int<0, max> */ - public function __toString() + public function getSpecificity(): int { - return $this->getSelector(); + return SpecificityCalculator::calculate($this->selector); } - /** - * @return int - */ - public function getSpecificity() + public function render(OutputFormat $outputFormat): string { - if ($this->iSpecificity === null) { - $a = 0; - /// @todo should exclude \# as well as "#" - $aMatches = null; - $b = substr_count($this->sSelector, '#'); - $c = preg_match_all(self::NON_ID_ATTRIBUTES_AND_PSEUDO_CLASSES_RX, $this->sSelector, $aMatches); - $d = preg_match_all(self::ELEMENTS_AND_PSEUDO_ELEMENTS_RX, $this->sSelector, $aMatches); - $this->iSpecificity = ($a * 1000) + ($b * 100) + ($c * 10) + $d; - } - return $this->iSpecificity; + return $this->getSelector(); } } diff --git a/src/Property/Selector/SpecificityCalculator.php b/src/Property/Selector/SpecificityCalculator.php new file mode 100644 index 00000000..745f229d --- /dev/null +++ b/src/Property/Selector/SpecificityCalculator.php @@ -0,0 +1,85 @@ +\\~]+)[\\w]+ # elements + | + \\:{1,2}( # pseudo-elements + after|before|first-letter|first-line|selection + )) + /ix'; + + /** + * @var array> + */ + private static $cache = []; + + /** + * Calculates the specificity of the given CSS selector. + * + * @return int<0, max> + * + * @internal + */ + public static function calculate(string $selector): int + { + if (!isset(self::$cache[$selector])) { + $a = 0; + /// @todo should exclude \# as well as "#" + $matches = null; + $b = \substr_count($selector, '#'); + $c = \preg_match_all(self::NON_ID_ATTRIBUTES_AND_PSEUDO_CLASSES_RX, $selector, $matches); + $d = \preg_match_all(self::ELEMENTS_AND_PSEUDO_ELEMENTS_RX, $selector, $matches); + self::$cache[$selector] = ($a * 1000) + ($b * 100) + ($c * 10) + $d; + } + + return self::$cache[$selector]; + } + + /** + * Clears the cache in order to lower memory usage. + */ + public static function clearCache(): void + { + self::$cache = []; + } +} diff --git a/src/Renderable.php b/src/Renderable.php index dc1bff3c..9ebf9a9b 100644 --- a/src/Renderable.php +++ b/src/Renderable.php @@ -1,21 +1,10 @@ - */ - private $aIeHack; - - /** - * @var int - */ - protected $iLineNo; - - /** - * @var int - */ - protected $iColNo; - - /** - * @var array - */ - protected $aComments; + private $isImportant = false; /** - * @param string $sRule - * @param int $iLineNo - * @param int $iColNo + * @param non-empty-string $rule + * @param int<0, max> $lineNumber + * @param int<0, max> $columnNumber */ - public function __construct($sRule, $iLineNo = 0, $iColNo = 0) + public function __construct(string $rule, int $lineNumber = 0, int $columnNumber = 0) { - $this->sRule = $sRule; - $this->mValue = null; - $this->bIsImportant = false; - $this->aIeHack = []; - $this->iLineNo = $iLineNo; - $this->iColNo = $iColNo; - $this->aComments = []; + $this->rule = $rule; + $this->setPosition($lineNumber, $columnNumber); } /** - * @return Rule + * @param list $commentsBeforeRule * * @throws UnexpectedEOFException * @throws UnexpectedTokenException + * + * @internal since V8.8.0 */ - public static function parse(ParserState $oParserState) + public static function parse(ParserState $parserState, array $commentsBeforeRule = []): Rule { - $aComments = $oParserState->consumeWhiteSpace(); - $oRule = new Rule( - $oParserState->parseIdentifier(!$oParserState->comes("--")), - $oParserState->currentLine(), - $oParserState->currentColumn() + $comments = \array_merge($commentsBeforeRule, $parserState->consumeWhiteSpace()); + $rule = new Rule( + $parserState->parseIdentifier(!$parserState->comes('--')), + $parserState->currentLine(), + $parserState->currentColumn() ); - $oRule->setComments($aComments); - $oRule->addComments($oParserState->consumeWhiteSpace()); - $oParserState->consume(':'); - $oValue = Value::parseValue($oParserState, self::listDelimiterForRule($oRule->getRule())); - $oRule->setValue($oValue); - if ($oParserState->getSettings()->bLenientParsing) { - while ($oParserState->comes('\\')) { - $oParserState->consume('\\'); - $oRule->addIeHack($oParserState->consume()); - $oParserState->consumeWhiteSpace(); - } + $rule->setComments($comments); + $rule->addComments($parserState->consumeWhiteSpace()); + $parserState->consume(':'); + $value = Value::parseValue($parserState, self::listDelimiterForRule($rule->getRule())); + $rule->setValue($value); + $parserState->consumeWhiteSpace(); + if ($parserState->comes('!')) { + $parserState->consume('!'); + $parserState->consumeWhiteSpace(); + $parserState->consume('important'); + $rule->setIsImportant(true); } - $oParserState->consumeWhiteSpace(); - if ($oParserState->comes('!')) { - $oParserState->consume('!'); - $oParserState->consumeWhiteSpace(); - $oParserState->consume('important'); - $oRule->setIsImportant(true); + $parserState->consumeWhiteSpace(); + while ($parserState->comes(';')) { + $parserState->consume(';'); } - $oParserState->consumeWhiteSpace(); - while ($oParserState->comes(';')) { - $oParserState->consume(';'); - } - $oParserState->consumeWhiteSpace(); - return $oRule; + return $rule; } /** - * @param string $sRule + * Returns a list of delimiters (or separators). + * The first item is the innermost separator (or, put another way, the highest-precedence operator). + * The sequence continues to the outermost separator (or lowest-precedence operator). + * + * @param non-empty-string $rule * - * @return array + * @return list */ - private static function listDelimiterForRule($sRule) + private static function listDelimiterForRule(string $rule): array { - if (preg_match('/^font($|-)/', $sRule)) { + if (\preg_match('/^font($|-)/', $rule)) { return [',', '/', ' ']; } - return [',', ' ', '/']; - } - /** - * @return int - */ - public function getLineNo() - { - return $this->iLineNo; - } - - /** - * @return int - */ - public function getColNo() - { - return $this->iColNo; + switch ($rule) { + case 'src': + return [' ', ',']; + default: + return [',', ' ', '/']; + } } /** - * @param int $iLine - * @param int $iColumn - * - * @return void + * @param non-empty-string $rule */ - public function setPosition($iLine, $iColumn) + public function setRule(string $rule): void { - $this->iColNo = $iColumn; - $this->iLineNo = $iLine; + $this->rule = $rule; } /** - * @param string $sRule - * - * @return void + * @return non-empty-string */ - public function setRule($sRule) + public function getRule(): string { - $this->sRule = $sRule; - } - - /** - * @return string - */ - public function getRule() - { - return $this->sRule; + return $this->rule; } /** @@ -176,218 +133,66 @@ public function getRule() */ public function getValue() { - return $this->mValue; - } - - /** - * @param RuleValueList|string|null $mValue - * - * @return void - */ - public function setValue($mValue) - { - $this->mValue = $mValue; - } - - /** - * @param array> $aSpaceSeparatedValues - * - * @return RuleValueList - * - * @deprecated will be removed in version 9.0 - * Old-Style 2-dimensional array given. Retained for (some) backwards-compatibility. - * Use `setValue()` instead and wrap the value inside a RuleValueList if necessary. - */ - public function setValues(array $aSpaceSeparatedValues) - { - $oSpaceSeparatedList = null; - if (count($aSpaceSeparatedValues) > 1) { - $oSpaceSeparatedList = new RuleValueList(' ', $this->iLineNo); - } - foreach ($aSpaceSeparatedValues as $aCommaSeparatedValues) { - $oCommaSeparatedList = null; - if (count($aCommaSeparatedValues) > 1) { - $oCommaSeparatedList = new RuleValueList(',', $this->iLineNo); - } - foreach ($aCommaSeparatedValues as $mValue) { - if (!$oSpaceSeparatedList && !$oCommaSeparatedList) { - $this->mValue = $mValue; - return $mValue; - } - if ($oCommaSeparatedList) { - $oCommaSeparatedList->addListComponent($mValue); - } else { - $oSpaceSeparatedList->addListComponent($mValue); - } - } - if (!$oSpaceSeparatedList) { - $this->mValue = $oCommaSeparatedList; - return $oCommaSeparatedList; - } else { - $oSpaceSeparatedList->addListComponent($oCommaSeparatedList); - } - } - $this->mValue = $oSpaceSeparatedList; - return $oSpaceSeparatedList; + return $this->value; } /** - * @return array> - * - * @deprecated will be removed in version 9.0 - * Old-Style 2-dimensional array returned. Retained for (some) backwards-compatibility. - * Use `getValue()` instead and check for the existence of a (nested set of) ValueList object(s). + * @param RuleValueList|string|null $value */ - public function getValues() + public function setValue($value): void { - if (!$this->mValue instanceof RuleValueList) { - return [[$this->mValue]]; - } - if ($this->mValue->getListSeparator() === ',') { - return [$this->mValue->getListComponents()]; - } - $aResult = []; - foreach ($this->mValue->getListComponents() as $mValue) { - if (!$mValue instanceof RuleValueList || $mValue->getListSeparator() !== ',') { - $aResult[] = [$mValue]; - continue; - } - if ($this->mValue->getListSeparator() === ' ' || count($aResult) === 0) { - $aResult[] = []; - } - foreach ($mValue->getListComponents() as $mValue) { - $aResult[count($aResult) - 1][] = $mValue; - } - } - return $aResult; + $this->value = $value; } /** * Adds a value to the existing value. Value will be appended if a `RuleValueList` exists of the given type. * Otherwise, the existing value will be wrapped by one. * - * @param RuleValueList|array $mValue - * @param string $sType - * - * @return void + * @param RuleValueList|array $value */ - public function addValue($mValue, $sType = ' ') + public function addValue($value, string $type = ' '): void { - if (!is_array($mValue)) { - $mValue = [$mValue]; + if (!\is_array($value)) { + $value = [$value]; } - if (!$this->mValue instanceof RuleValueList || $this->mValue->getListSeparator() !== $sType) { - $mCurrentValue = $this->mValue; - $this->mValue = new RuleValueList($sType, $this->iLineNo); - if ($mCurrentValue) { - $this->mValue->addListComponent($mCurrentValue); + if (!($this->value instanceof RuleValueList) || $this->value->getListSeparator() !== $type) { + $currentValue = $this->value; + $this->value = new RuleValueList($type, $this->getLineNumber()); + if ($currentValue) { + $this->value->addListComponent($currentValue); } } - foreach ($mValue as $mValueItem) { - $this->mValue->addListComponent($mValueItem); + foreach ($value as $valueItem) { + $this->value->addListComponent($valueItem); } } - /** - * @param int $iModifier - * - * @return void - */ - public function addIeHack($iModifier) + public function setIsImportant(bool $isImportant): void { - $this->aIeHack[] = $iModifier; + $this->isImportant = $isImportant; } - /** - * @param array $aModifiers - * - * @return void - */ - public function setIeHack(array $aModifiers) + public function getIsImportant(): bool { - $this->aIeHack = $aModifiers; + return $this->isImportant; } /** - * @return array + * @return non-empty-string */ - public function getIeHack() + public function render(OutputFormat $outputFormat): string { - return $this->aIeHack; - } - - /** - * @param bool $bIsImportant - * - * @return void - */ - public function setIsImportant($bIsImportant) - { - $this->bIsImportant = $bIsImportant; - } - - /** - * @return bool - */ - public function getIsImportant() - { - return $this->bIsImportant; - } - - /** - * @return string - */ - public function __toString() - { - return $this->render(new OutputFormat()); - } - - /** - * @return string - */ - public function render(OutputFormat $oOutputFormat) - { - $sResult = "{$oOutputFormat->comments($this)}{$this->sRule}:{$oOutputFormat->spaceAfterRuleName()}"; - if ($this->mValue instanceof Value) { // Can also be a ValueList - $sResult .= $this->mValue->render($oOutputFormat); + $formatter = $outputFormat->getFormatter(); + $result = "{$formatter->comments($this)}{$this->rule}:{$formatter->spaceAfterRuleName()}"; + if ($this->value instanceof Value) { // Can also be a ValueList + $result .= $this->value->render($outputFormat); } else { - $sResult .= $this->mValue; - } - if (!empty($this->aIeHack)) { - $sResult .= ' \\' . implode('\\', $this->aIeHack); + $result .= $this->value; } - if ($this->bIsImportant) { - $sResult .= ' !important'; + if ($this->isImportant) { + $result .= ' !important'; } - $sResult .= ';'; - return $sResult; - } - - /** - * @param array $aComments - * - * @return void - */ - public function addComments(array $aComments) - { - $this->aComments = array_merge($this->aComments, $aComments); - } - - /** - * @return array - */ - public function getComments() - { - return $this->aComments; - } - - /** - * @param array $aComments - * - * @return void - */ - public function setComments(array $aComments) - { - $this->aComments = $aComments; + $result .= ';'; + return $result; } } diff --git a/src/RuleSet/AtRuleSet.php b/src/RuleSet/AtRuleSet.php index aab6d799..0fda9638 100644 --- a/src/RuleSet/AtRuleSet.php +++ b/src/RuleSet/AtRuleSet.php @@ -1,5 +1,7 @@ $lineNumber */ - public function __construct($sType, $sArgs = '', $iLineNo = 0) + public function __construct(string $type, string $arguments = '', int $lineNumber = 0) { - parent::__construct($iLineNo); - $this->sType = $sType; - $this->sArgs = $sArgs; + parent::__construct($lineNumber); + $this->type = $type; + $this->arguments = $arguments; } /** - * @return string + * @return non-empty-string */ - public function atRuleName() + public function atRuleName(): string { - return $this->sType; + return $this->type; } - /** - * @return string - */ - public function atRuleArgs() - { - return $this->sArgs; - } - - /** - * @return string - */ - public function __toString() + public function atRuleArgs(): string { - return $this->render(new OutputFormat()); + return $this->arguments; } /** - * @return string + * @return non-empty-string */ - public function render(OutputFormat $oOutputFormat) + public function render(OutputFormat $outputFormat): string { - $sResult = $oOutputFormat->comments($this); - $sArgs = $this->sArgs; - if ($sArgs) { - $sArgs = ' ' . $sArgs; + $formatter = $outputFormat->getFormatter(); + $result = $formatter->comments($this); + $arguments = $this->arguments; + if ($arguments !== '') { + $arguments = ' ' . $arguments; } - $sResult .= "@{$this->sType}$sArgs{$oOutputFormat->spaceBeforeOpeningBrace()}{"; - $sResult .= $this->renderRules($oOutputFormat); - $sResult .= '}'; - return $sResult; + $result .= "@{$this->type}$arguments{$formatter->spaceBeforeOpeningBrace()}{"; + $result .= $this->renderRules($outputFormat); + $result .= '}'; + return $result; } } diff --git a/src/RuleSet/DeclarationBlock.php b/src/RuleSet/DeclarationBlock.php index de487bc1..e4125797 100644 --- a/src/RuleSet/DeclarationBlock.php +++ b/src/RuleSet/DeclarationBlock.php @@ -1,5 +1,7 @@ + * @var array */ - private $aSelectors; + private $selectors = []; /** - * @param int $iLineNo - */ - public function __construct($iLineNo = 0) - { - parent::__construct($iLineNo); - $this->aSelectors = []; - } - - /** - * @param CSSList|null $oList - * - * @return DeclarationBlock|false - * * @throws UnexpectedTokenException * @throws UnexpectedEOFException + * + * @internal since V8.8.0 */ - public static function parse(ParserState $oParserState, $oList = null) + public static function parse(ParserState $parserState, ?CSSList $list = null): ?DeclarationBlock { - $aComments = []; - $oResult = new DeclarationBlock($oParserState->currentLine()); + $comments = []; + $result = new DeclarationBlock($parserState->currentLine()); try { - $aSelectorParts = []; - $sStringWrapperChar = false; + $selectorParts = []; do { - $aSelectorParts[] = $oParserState->consume(1) - . $oParserState->consumeUntil(['{', '}', '\'', '"'], false, false, $aComments); - if (in_array($oParserState->peek(), ['\'', '"']) && substr(end($aSelectorParts), -1) != "\\") { - if ($sStringWrapperChar === false) { - $sStringWrapperChar = $oParserState->peek(); - } elseif ($sStringWrapperChar == $oParserState->peek()) { - $sStringWrapperChar = false; + $selectorParts[] = $parserState->consume(1) + . $parserState->consumeUntil(['{', '}', '\'', '"'], false, false, $comments); + if (\in_array($parserState->peek(), ['\'', '"'], true) && \substr(\end($selectorParts), -1) != '\\') { + if (!isset($stringWrapperCharacter)) { + $stringWrapperCharacter = $parserState->peek(); + } elseif ($stringWrapperCharacter === $parserState->peek()) { + unset($stringWrapperCharacter); } } - } while (!in_array($oParserState->peek(), ['{', '}']) || $sStringWrapperChar !== false); - $oResult->setSelectors(implode('', $aSelectorParts), $oList); - if ($oParserState->comes('{')) { - $oParserState->consume(1); + } while (!\in_array($parserState->peek(), ['{', '}'], true) || isset($stringWrapperCharacter)); + $result->setSelectors(\implode('', $selectorParts), $list); + if ($parserState->comes('{')) { + $parserState->consume(1); } } catch (UnexpectedTokenException $e) { - if ($oParserState->getSettings()->bLenientParsing) { - if (!$oParserState->comes('}')) { - $oParserState->consumeUntil('}', false, true); + if ($parserState->getSettings()->usesLenientParsing()) { + if (!$parserState->comes('}')) { + $parserState->consumeUntil('}', false, true); } - return false; + return null; } else { throw $e; } } - $oResult->setComments($aComments); - RuleSet::parseRuleSet($oParserState, $oResult); - return $oResult; + $result->setComments($comments); + RuleSet::parseRuleSet($parserState, $result); + return $result; } /** - * @param array|string $mSelector - * @param CSSList|null $oList + * @param array|string $selectors * * @throws UnexpectedTokenException */ - public function setSelectors($mSelector, $oList = null) + public function setSelectors($selectors, ?CSSList $list = null): void { - if (is_array($mSelector)) { - $this->aSelectors = $mSelector; + if (\is_array($selectors)) { + $this->selectors = $selectors; } else { - $this->aSelectors = explode(',', $mSelector); + $this->selectors = \explode(',', $selectors); } - foreach ($this->aSelectors as $iKey => $mSelector) { - if (!($mSelector instanceof Selector)) { - if ($oList === null || !($oList instanceof KeyFrame)) { - if (!Selector::isValid($mSelector)) { + foreach ($this->selectors as $key => $selector) { + if (!($selector instanceof Selector)) { + if ($list === null || !($list instanceof KeyFrame)) { + if (!Selector::isValid($selector)) { throw new UnexpectedTokenException( "Selector did not match '" . Selector::SELECTOR_VALIDATION_RX . "'.", - $mSelector, - "custom" + $selectors, + 'custom' ); } - $this->aSelectors[$iKey] = new Selector($mSelector); + $this->selectors[$key] = new Selector($selector); } else { - if (!KeyframeSelector::isValid($mSelector)) { + if (!KeyframeSelector::isValid($selector)) { throw new UnexpectedTokenException( "Selector did not match '" . KeyframeSelector::SELECTOR_VALIDATION_RX . "'.", - $mSelector, - "custom" + $selector, + 'custom' ); } - $this->aSelectors[$iKey] = new KeyframeSelector($mSelector); + $this->selectors[$key] = new KeyframeSelector($selector); } } } @@ -128,18 +111,16 @@ public function setSelectors($mSelector, $oList = null) /** * Remove one of the selectors of the block. * - * @param Selector|string $mSelector - * - * @return bool + * @param Selector|string $selectorToRemove */ - public function removeSelector($mSelector) + public function removeSelector($selectorToRemove): bool { - if ($mSelector instanceof Selector) { - $mSelector = $mSelector->getSelector(); + if ($selectorToRemove instanceof Selector) { + $selectorToRemove = $selectorToRemove->getSelector(); } - foreach ($this->aSelectors as $iKey => $oSelector) { - if ($oSelector->getSelector() === $mSelector) { - unset($this->aSelectors[$iKey]); + foreach ($this->selectors as $key => $selector) { + if ($selector->getSelector() === $selectorToRemove) { + unset($this->selectors[$key]); return true; } } @@ -147,689 +128,40 @@ public function removeSelector($mSelector) } /** - * @return array - * - * @deprecated will be removed in version 9.0; use `getSelectors()` instead - */ - public function getSelector() - { - return $this->getSelectors(); - } - - /** - * @param Selector|string $mSelector - * @param CSSList|null $oList - * - * @return void - * - * @deprecated will be removed in version 9.0; use `setSelectors()` instead - */ - public function setSelector($mSelector, $oList = null) - { - $this->setSelectors($mSelector, $oList); - } - - /** - * @return array - */ - public function getSelectors() - { - return $this->aSelectors; - } - - /** - * Splits shorthand declarations (e.g. `margin` or `font`) into their constituent parts. - * - * @return void - */ - public function expandShorthands() - { - // border must be expanded before dimensions - $this->expandBorderShorthand(); - $this->expandDimensionsShorthand(); - $this->expandFontShorthand(); - $this->expandBackgroundShorthand(); - $this->expandListStyleShorthand(); - } - - /** - * Creates shorthand declarations (e.g. `margin` or `font`) whenever possible. - * - * @return void - */ - public function createShorthands() - { - $this->createBackgroundShorthand(); - $this->createDimensionsShorthand(); - // border must be shortened after dimensions - $this->createBorderShorthand(); - $this->createFontShorthand(); - $this->createListStyleShorthand(); - } - - /** - * Splits shorthand border declarations (e.g. `border: 1px red;`). - * - * Additional splitting happens in expandDimensionsShorthand. - * - * Multiple borders are not yet supported as of 3. - * - * @return void - */ - public function expandBorderShorthand() - { - $aBorderRules = [ - 'border', - 'border-left', - 'border-right', - 'border-top', - 'border-bottom', - ]; - $aBorderSizes = [ - 'thin', - 'medium', - 'thick', - ]; - $aRules = $this->getRulesAssoc(); - foreach ($aBorderRules as $sBorderRule) { - if (!isset($aRules[$sBorderRule])) { - continue; - } - $oRule = $aRules[$sBorderRule]; - $mRuleValue = $oRule->getValue(); - $aValues = []; - if (!$mRuleValue instanceof RuleValueList) { - $aValues[] = $mRuleValue; - } else { - $aValues = $mRuleValue->getListComponents(); - } - foreach ($aValues as $mValue) { - if ($mValue instanceof Value) { - $mNewValue = clone $mValue; - } else { - $mNewValue = $mValue; - } - if ($mValue instanceof Size) { - $sNewRuleName = $sBorderRule . "-width"; - } elseif ($mValue instanceof Color) { - $sNewRuleName = $sBorderRule . "-color"; - } else { - if (in_array($mValue, $aBorderSizes)) { - $sNewRuleName = $sBorderRule . "-width"; - } else { - $sNewRuleName = $sBorderRule . "-style"; - } - } - $oNewRule = new Rule($sNewRuleName, $oRule->getLineNo(), $oRule->getColNo()); - $oNewRule->setIsImportant($oRule->getIsImportant()); - $oNewRule->addValue([$mNewValue]); - $this->addRule($oNewRule); - } - $this->removeRule($sBorderRule); - } - } - - /** - * Splits shorthand dimensional declarations (e.g. `margin: 0px auto;`) - * into their constituent parts. - * - * Handles `margin`, `padding`, `border-color`, `border-style` and `border-width`. - * - * @return void + * @return array */ - public function expandDimensionsShorthand() + public function getSelectors(): array { - $aExpansions = [ - 'margin' => 'margin-%s', - 'padding' => 'padding-%s', - 'border-color' => 'border-%s-color', - 'border-style' => 'border-%s-style', - 'border-width' => 'border-%s-width', - ]; - $aRules = $this->getRulesAssoc(); - foreach ($aExpansions as $sProperty => $sExpanded) { - if (!isset($aRules[$sProperty])) { - continue; - } - $oRule = $aRules[$sProperty]; - $mRuleValue = $oRule->getValue(); - $aValues = []; - if (!$mRuleValue instanceof RuleValueList) { - $aValues[] = $mRuleValue; - } else { - $aValues = $mRuleValue->getListComponents(); - } - $top = $right = $bottom = $left = null; - switch (count($aValues)) { - case 1: - $top = $right = $bottom = $left = $aValues[0]; - break; - case 2: - $top = $bottom = $aValues[0]; - $left = $right = $aValues[1]; - break; - case 3: - $top = $aValues[0]; - $left = $right = $aValues[1]; - $bottom = $aValues[2]; - break; - case 4: - $top = $aValues[0]; - $right = $aValues[1]; - $bottom = $aValues[2]; - $left = $aValues[3]; - break; - } - foreach (['top', 'right', 'bottom', 'left'] as $sPosition) { - $oNewRule = new Rule(sprintf($sExpanded, $sPosition), $oRule->getLineNo(), $oRule->getColNo()); - $oNewRule->setIsImportant($oRule->getIsImportant()); - $oNewRule->addValue(${$sPosition}); - $this->addRule($oNewRule); - } - $this->removeRule($sProperty); - } + return $this->selectors; } /** - * Converts shorthand font declarations - * (e.g. `font: 300 italic 11px/14px verdana, helvetica, sans-serif;`) - * into their constituent parts. - * - * @return void - */ - public function expandFontShorthand() - { - $aRules = $this->getRulesAssoc(); - if (!isset($aRules['font'])) { - return; - } - $oRule = $aRules['font']; - // reset properties to 'normal' per http://www.w3.org/TR/21/fonts.html#font-shorthand - $aFontProperties = [ - 'font-style' => 'normal', - 'font-variant' => 'normal', - 'font-weight' => 'normal', - 'font-size' => 'normal', - 'line-height' => 'normal', - ]; - $mRuleValue = $oRule->getValue(); - $aValues = []; - if (!$mRuleValue instanceof RuleValueList) { - $aValues[] = $mRuleValue; - } else { - $aValues = $mRuleValue->getListComponents(); - } - foreach ($aValues as $mValue) { - if (!$mValue instanceof Value) { - $mValue = mb_strtolower($mValue); - } - if (in_array($mValue, ['normal', 'inherit'])) { - foreach (['font-style', 'font-weight', 'font-variant'] as $sProperty) { - if (!isset($aFontProperties[$sProperty])) { - $aFontProperties[$sProperty] = $mValue; - } - } - } elseif (in_array($mValue, ['italic', 'oblique'])) { - $aFontProperties['font-style'] = $mValue; - } elseif ($mValue == 'small-caps') { - $aFontProperties['font-variant'] = $mValue; - } elseif ( - in_array($mValue, ['bold', 'bolder', 'lighter']) - || ($mValue instanceof Size - && in_array($mValue->getSize(), range(100, 900, 100))) - ) { - $aFontProperties['font-weight'] = $mValue; - } elseif ($mValue instanceof RuleValueList && $mValue->getListSeparator() == '/') { - list($oSize, $oHeight) = $mValue->getListComponents(); - $aFontProperties['font-size'] = $oSize; - $aFontProperties['line-height'] = $oHeight; - } elseif ($mValue instanceof Size && $mValue->getUnit() !== null) { - $aFontProperties['font-size'] = $mValue; - } else { - $aFontProperties['font-family'] = $mValue; - } - } - foreach ($aFontProperties as $sProperty => $mValue) { - $oNewRule = new Rule($sProperty, $oRule->getLineNo(), $oRule->getColNo()); - $oNewRule->addValue($mValue); - $oNewRule->setIsImportant($oRule->getIsImportant()); - $this->addRule($oNewRule); - } - $this->removeRule('font'); - } - - /** - * Converts shorthand background declarations - * (e.g. `background: url("chess.png") gray 50% repeat fixed;`) - * into their constituent parts. - * - * @see http://www.w3.org/TR/21/colors.html#propdef-background - * - * @return void - */ - public function expandBackgroundShorthand() - { - $aRules = $this->getRulesAssoc(); - if (!isset($aRules['background'])) { - return; - } - $oRule = $aRules['background']; - $aBgProperties = [ - 'background-color' => ['transparent'], - 'background-image' => ['none'], - 'background-repeat' => ['repeat'], - 'background-attachment' => ['scroll'], - 'background-position' => [ - new Size(0, '%', null, false, $this->iLineNo), - new Size(0, '%', null, false, $this->iLineNo), - ], - ]; - $mRuleValue = $oRule->getValue(); - $aValues = []; - if (!$mRuleValue instanceof RuleValueList) { - $aValues[] = $mRuleValue; - } else { - $aValues = $mRuleValue->getListComponents(); - } - if (count($aValues) == 1 && $aValues[0] == 'inherit') { - foreach ($aBgProperties as $sProperty => $mValue) { - $oNewRule = new Rule($sProperty, $oRule->getLineNo(), $oRule->getColNo()); - $oNewRule->addValue('inherit'); - $oNewRule->setIsImportant($oRule->getIsImportant()); - $this->addRule($oNewRule); - } - $this->removeRule('background'); - return; - } - $iNumBgPos = 0; - foreach ($aValues as $mValue) { - if (!$mValue instanceof Value) { - $mValue = mb_strtolower($mValue); - } - if ($mValue instanceof URL) { - $aBgProperties['background-image'] = $mValue; - } elseif ($mValue instanceof Color) { - $aBgProperties['background-color'] = $mValue; - } elseif (in_array($mValue, ['scroll', 'fixed'])) { - $aBgProperties['background-attachment'] = $mValue; - } elseif (in_array($mValue, ['repeat', 'no-repeat', 'repeat-x', 'repeat-y'])) { - $aBgProperties['background-repeat'] = $mValue; - } elseif ( - in_array($mValue, ['left', 'center', 'right', 'top', 'bottom']) - || $mValue instanceof Size - ) { - if ($iNumBgPos == 0) { - $aBgProperties['background-position'][0] = $mValue; - $aBgProperties['background-position'][1] = 'center'; - } else { - $aBgProperties['background-position'][$iNumBgPos] = $mValue; - } - $iNumBgPos++; - } - } - foreach ($aBgProperties as $sProperty => $mValue) { - $oNewRule = new Rule($sProperty, $oRule->getLineNo(), $oRule->getColNo()); - $oNewRule->setIsImportant($oRule->getIsImportant()); - $oNewRule->addValue($mValue); - $this->addRule($oNewRule); - } - $this->removeRule('background'); - } - - /** - * @return void - */ - public function expandListStyleShorthand() - { - $aListProperties = [ - 'list-style-type' => 'disc', - 'list-style-position' => 'outside', - 'list-style-image' => 'none', - ]; - $aListStyleTypes = [ - 'none', - 'disc', - 'circle', - 'square', - 'decimal-leading-zero', - 'decimal', - 'lower-roman', - 'upper-roman', - 'lower-greek', - 'lower-alpha', - 'lower-latin', - 'upper-alpha', - 'upper-latin', - 'hebrew', - 'armenian', - 'georgian', - 'cjk-ideographic', - 'hiragana', - 'hira-gana-iroha', - 'katakana-iroha', - 'katakana', - ]; - $aListStylePositions = [ - 'inside', - 'outside', - ]; - $aRules = $this->getRulesAssoc(); - if (!isset($aRules['list-style'])) { - return; - } - $oRule = $aRules['list-style']; - $mRuleValue = $oRule->getValue(); - $aValues = []; - if (!$mRuleValue instanceof RuleValueList) { - $aValues[] = $mRuleValue; - } else { - $aValues = $mRuleValue->getListComponents(); - } - if (count($aValues) == 1 && $aValues[0] == 'inherit') { - foreach ($aListProperties as $sProperty => $mValue) { - $oNewRule = new Rule($sProperty, $oRule->getLineNo(), $oRule->getColNo()); - $oNewRule->addValue('inherit'); - $oNewRule->setIsImportant($oRule->getIsImportant()); - $this->addRule($oNewRule); - } - $this->removeRule('list-style'); - return; - } - foreach ($aValues as $mValue) { - if (!$mValue instanceof Value) { - $mValue = mb_strtolower($mValue); - } - if ($mValue instanceof Url) { - $aListProperties['list-style-image'] = $mValue; - } elseif (in_array($mValue, $aListStyleTypes)) { - $aListProperties['list-style-types'] = $mValue; - } elseif (in_array($mValue, $aListStylePositions)) { - $aListProperties['list-style-position'] = $mValue; - } - } - foreach ($aListProperties as $sProperty => $mValue) { - $oNewRule = new Rule($sProperty, $oRule->getLineNo(), $oRule->getColNo()); - $oNewRule->setIsImportant($oRule->getIsImportant()); - $oNewRule->addValue($mValue); - $this->addRule($oNewRule); - } - $this->removeRule('list-style'); - } - - /** - * @param array $aProperties - * @param string $sShorthand - * - * @return void - */ - public function createShorthandProperties(array $aProperties, $sShorthand) - { - $aRules = $this->getRulesAssoc(); - $aNewValues = []; - foreach ($aProperties as $sProperty) { - if (!isset($aRules[$sProperty])) { - continue; - } - $oRule = $aRules[$sProperty]; - if (!$oRule->getIsImportant()) { - $mRuleValue = $oRule->getValue(); - $aValues = []; - if (!$mRuleValue instanceof RuleValueList) { - $aValues[] = $mRuleValue; - } else { - $aValues = $mRuleValue->getListComponents(); - } - foreach ($aValues as $mValue) { - $aNewValues[] = $mValue; - } - $this->removeRule($sProperty); - } - } - if (count($aNewValues)) { - $oNewRule = new Rule($sShorthand, $oRule->getLineNo(), $oRule->getColNo()); - foreach ($aNewValues as $mValue) { - $oNewRule->addValue($mValue); - } - $this->addRule($oNewRule); - } - } - - /** - * @return void - */ - public function createBackgroundShorthand() - { - $aProperties = [ - 'background-color', - 'background-image', - 'background-repeat', - 'background-position', - 'background-attachment', - ]; - $this->createShorthandProperties($aProperties, 'background'); - } - - /** - * @return void - */ - public function createListStyleShorthand() - { - $aProperties = [ - 'list-style-type', - 'list-style-position', - 'list-style-image', - ]; - $this->createShorthandProperties($aProperties, 'list-style'); - } - - /** - * Combines `border-color`, `border-style` and `border-width` into `border`. - * - * Should be run after `create_dimensions_shorthand`! - * - * @return void - */ - public function createBorderShorthand() - { - $aProperties = [ - 'border-width', - 'border-style', - 'border-color', - ]; - $this->createShorthandProperties($aProperties, 'border'); - } - - /** - * Looks for long format CSS dimensional properties - * (margin, padding, border-color, border-style and border-width) - * and converts them into shorthand CSS properties. - * - * @return void - */ - public function createDimensionsShorthand() - { - $aPositions = ['top', 'right', 'bottom', 'left']; - $aExpansions = [ - 'margin' => 'margin-%s', - 'padding' => 'padding-%s', - 'border-color' => 'border-%s-color', - 'border-style' => 'border-%s-style', - 'border-width' => 'border-%s-width', - ]; - $aRules = $this->getRulesAssoc(); - foreach ($aExpansions as $sProperty => $sExpanded) { - $aFoldable = []; - foreach ($aRules as $sRuleName => $oRule) { - foreach ($aPositions as $sPosition) { - if ($sRuleName == sprintf($sExpanded, $sPosition)) { - $aFoldable[$sRuleName] = $oRule; - } - } - } - // All four dimensions must be present - if (count($aFoldable) == 4) { - $aValues = []; - foreach ($aPositions as $sPosition) { - $oRule = $aRules[sprintf($sExpanded, $sPosition)]; - $mRuleValue = $oRule->getValue(); - $aRuleValues = []; - if (!$mRuleValue instanceof RuleValueList) { - $aRuleValues[] = $mRuleValue; - } else { - $aRuleValues = $mRuleValue->getListComponents(); - } - $aValues[$sPosition] = $aRuleValues; - } - $oNewRule = new Rule($sProperty, $oRule->getLineNo(), $oRule->getColNo()); - if ((string)$aValues['left'][0] == (string)$aValues['right'][0]) { - if ((string)$aValues['top'][0] == (string)$aValues['bottom'][0]) { - if ((string)$aValues['top'][0] == (string)$aValues['left'][0]) { - // All 4 sides are equal - $oNewRule->addValue($aValues['top']); - } else { - // Top and bottom are equal, left and right are equal - $oNewRule->addValue($aValues['top']); - $oNewRule->addValue($aValues['left']); - } - } else { - // Only left and right are equal - $oNewRule->addValue($aValues['top']); - $oNewRule->addValue($aValues['left']); - $oNewRule->addValue($aValues['bottom']); - } - } else { - // No sides are equal - $oNewRule->addValue($aValues['top']); - $oNewRule->addValue($aValues['left']); - $oNewRule->addValue($aValues['bottom']); - $oNewRule->addValue($aValues['right']); - } - $this->addRule($oNewRule); - foreach ($aPositions as $sPosition) { - $this->removeRule(sprintf($sExpanded, $sPosition)); - } - } - } - } - - /** - * Looks for long format CSS font properties (e.g. `font-weight`) and - * tries to convert them into a shorthand CSS `font` property. - * - * At least `font-size` AND `font-family` must be present in order to create a shorthand declaration. - * - * @return void - */ - public function createFontShorthand() - { - $aFontProperties = [ - 'font-style', - 'font-variant', - 'font-weight', - 'font-size', - 'line-height', - 'font-family', - ]; - $aRules = $this->getRulesAssoc(); - if (!isset($aRules['font-size']) || !isset($aRules['font-family'])) { - return; - } - $oOldRule = isset($aRules['font-size']) ? $aRules['font-size'] : $aRules['font-family']; - $oNewRule = new Rule('font', $oOldRule->getLineNo(), $oOldRule->getColNo()); - unset($oOldRule); - foreach (['font-style', 'font-variant', 'font-weight'] as $sProperty) { - if (isset($aRules[$sProperty])) { - $oRule = $aRules[$sProperty]; - $mRuleValue = $oRule->getValue(); - $aValues = []; - if (!$mRuleValue instanceof RuleValueList) { - $aValues[] = $mRuleValue; - } else { - $aValues = $mRuleValue->getListComponents(); - } - if ($aValues[0] !== 'normal') { - $oNewRule->addValue($aValues[0]); - } - } - } - // Get the font-size value - $oRule = $aRules['font-size']; - $mRuleValue = $oRule->getValue(); - $aFSValues = []; - if (!$mRuleValue instanceof RuleValueList) { - $aFSValues[] = $mRuleValue; - } else { - $aFSValues = $mRuleValue->getListComponents(); - } - // But wait to know if we have line-height to add it - if (isset($aRules['line-height'])) { - $oRule = $aRules['line-height']; - $mRuleValue = $oRule->getValue(); - $aLHValues = []; - if (!$mRuleValue instanceof RuleValueList) { - $aLHValues[] = $mRuleValue; - } else { - $aLHValues = $mRuleValue->getListComponents(); - } - if ($aLHValues[0] !== 'normal') { - $val = new RuleValueList('/', $this->iLineNo); - $val->addListComponent($aFSValues[0]); - $val->addListComponent($aLHValues[0]); - $oNewRule->addValue($val); - } - } else { - $oNewRule->addValue($aFSValues[0]); - } - $oRule = $aRules['font-family']; - $mRuleValue = $oRule->getValue(); - $aFFValues = []; - if (!$mRuleValue instanceof RuleValueList) { - $aFFValues[] = $mRuleValue; - } else { - $aFFValues = $mRuleValue->getListComponents(); - } - $oFFValue = new RuleValueList(',', $this->iLineNo); - $oFFValue->setListComponents($aFFValues); - $oNewRule->addValue($oFFValue); - - $this->addRule($oNewRule); - foreach ($aFontProperties as $sProperty) { - $this->removeRule($sProperty); - } - } - - /** - * @return string - * - * @throws OutputException - */ - public function __toString() - { - return $this->render(new OutputFormat()); - } - - /** - * @return string + * @return non-empty-string * * @throws OutputException */ - public function render(OutputFormat $oOutputFormat) + public function render(OutputFormat $outputFormat): string { - $sResult = $oOutputFormat->comments($this); - if (count($this->aSelectors) === 0) { + $formatter = $outputFormat->getFormatter(); + $result = $formatter->comments($this); + if (\count($this->selectors) === 0) { // If all the selectors have been removed, this declaration block becomes invalid - throw new OutputException("Attempt to print declaration block with missing selector", $this->iLineNo); - } - $sResult .= $oOutputFormat->sBeforeDeclarationBlock; - $sResult .= $oOutputFormat->implode( - $oOutputFormat->spaceBeforeSelectorSeparator() . ',' . $oOutputFormat->spaceAfterSelectorSeparator(), - $this->aSelectors + throw new OutputException( + 'Attempt to print declaration block with missing selector', + $this->getLineNumber() + ); + } + $result .= $outputFormat->getContentBeforeDeclarationBlock(); + $result .= $formatter->implode( + $formatter->spaceBeforeSelectorSeparator() . ',' . $formatter->spaceAfterSelectorSeparator(), + $this->selectors ); - $sResult .= $oOutputFormat->sAfterDeclarationBlockSelectors; - $sResult .= $oOutputFormat->spaceBeforeOpeningBrace() . '{'; - $sResult .= $this->renderRules($oOutputFormat); - $sResult .= '}'; - $sResult .= $oOutputFormat->sAfterDeclarationBlock; - return $sResult; + $result .= $outputFormat->getContentAfterDeclarationBlockSelectors(); + $result .= $formatter->spaceBeforeOpeningBrace() . '{'; + $result .= $this->renderRules($outputFormat); + $result .= '}'; + $result .= $outputFormat->getContentAfterDeclarationBlock(); + + return $result; } } diff --git a/src/RuleSet/RuleContainer.php b/src/RuleSet/RuleContainer.php new file mode 100644 index 00000000..0c6c5936 --- /dev/null +++ b/src/RuleSet/RuleContainer.php @@ -0,0 +1,36 @@ + $rules + */ + public function setRules(array $rules): void; + + /** + * @return array, Rule> + */ + public function getRules(?string $searchPattern = null): array; + + /** + * @return array + */ + public function getRulesAssoc(?string $searchPattern = null): array; +} diff --git a/src/RuleSet/RuleSet.php b/src/RuleSet/RuleSet.php index adb9be92..1fc1295e 100644 --- a/src/RuleSet/RuleSet.php +++ b/src/RuleSet/RuleSet.php @@ -1,14 +1,18 @@ - */ - private $aRules; + use CommentContainer; + use Position; /** - * @var int - */ - protected $iLineNo; - - /** - * @var array + * the rules in this rule set, using the property name as the key, + * with potentially multiple rules per property name. + * + * @var array, Rule>> */ - protected $aComments; + private $rules = []; /** - * @param int $iLineNo + * @param int<0, max> $lineNumber */ - public function __construct($iLineNo = 0) + public function __construct(int $lineNumber = 0) { - $this->aRules = []; - $this->iLineNo = $iLineNo; - $this->aComments = []; + $this->setPosition($lineNumber); } /** - * @return void - * * @throws UnexpectedTokenException * @throws UnexpectedEOFException + * + * @internal since V8.8.0 */ - public static function parseRuleSet(ParserState $oParserState, RuleSet $oRuleSet) + public static function parseRuleSet(ParserState $parserState, RuleSet $ruleSet): void { - while ($oParserState->comes(';')) { - $oParserState->consume(';'); + while ($parserState->comes(';')) { + $parserState->consume(';'); } - while (!$oParserState->comes('}')) { - $oRule = null; - if ($oParserState->getSettings()->bLenientParsing) { + while (true) { + $commentsBeforeRule = $parserState->consumeWhiteSpace(); + if ($parserState->comes('}')) { + break; + } + $rule = null; + if ($parserState->getSettings()->usesLenientParsing()) { try { - $oRule = Rule::parse($oParserState); + $rule = Rule::parse($parserState, $commentsBeforeRule); } catch (UnexpectedTokenException $e) { try { - $sConsume = $oParserState->consumeUntil(["\n", ";", '}'], true); + $consumedText = $parserState->consumeUntil(["\n", ';', '}'], true); // We need to “unfind” the matches to the end of the ruleSet as this will be matched later - if ($oParserState->streql(substr($sConsume, -1), '}')) { - $oParserState->backtrack(1); + if ($parserState->streql(\substr($consumedText, -1), '}')) { + $parserState->backtrack(1); } else { - while ($oParserState->comes(';')) { - $oParserState->consume(';'); + while ($parserState->comes(';')) { + $parserState->consume(';'); } } } catch (UnexpectedTokenException $e) { @@ -80,113 +85,95 @@ public static function parseRuleSet(ParserState $oParserState, RuleSet $oRuleSet } } } else { - $oRule = Rule::parse($oParserState); + $rule = Rule::parse($parserState, $commentsBeforeRule); } - if ($oRule) { - $oRuleSet->addRule($oRule); + if ($rule instanceof Rule) { + $ruleSet->addRule($rule); } } - $oParserState->consume('}'); + $parserState->consume('}'); } - /** - * @return int - */ - public function getLineNo() + public function addRule(Rule $ruleToAdd, ?Rule $sibling = null): void { - return $this->iLineNo; - } - - /** - * @param Rule|null $oSibling - * - * @return void - */ - public function addRule(Rule $oRule, Rule $oSibling = null) - { - $sRule = $oRule->getRule(); - if (!isset($this->aRules[$sRule])) { - $this->aRules[$sRule] = []; + $propertyName = $ruleToAdd->getRule(); + if (!isset($this->rules[$propertyName])) { + $this->rules[$propertyName] = []; } - $iPosition = count($this->aRules[$sRule]); + $position = \count($this->rules[$propertyName]); - if ($oSibling !== null) { - $iSiblingPos = array_search($oSibling, $this->aRules[$sRule], true); - if ($iSiblingPos !== false) { - $iPosition = $iSiblingPos; - $oRule->setPosition($oSibling->getLineNo(), $oSibling->getColNo() - 1); + if ($sibling !== null) { + $siblingPosition = \array_search($sibling, $this->rules[$propertyName], true); + if ($siblingPosition !== false) { + $position = $siblingPosition; + $ruleToAdd->setPosition($sibling->getLineNo(), $sibling->getColNo() - 1); } } - if ($oRule->getLineNo() === 0 && $oRule->getColNo() === 0) { + if ($ruleToAdd->getLineNo() === 0 && $ruleToAdd->getColNo() === 0) { //this node is added manually, give it the next best line $rules = $this->getRules(); - $pos = count($rules); - if ($pos > 0) { - $last = $rules[$pos - 1]; - $oRule->setPosition($last->getLineNo() + 1, 0); + $rulesCount = \count($rules); + if ($rulesCount > 0) { + $last = $rules[$rulesCount - 1]; + $ruleToAdd->setPosition($last->getLineNo() + 1, 0); } } - array_splice($this->aRules[$sRule], $iPosition, 0, [$oRule]); + \array_splice($this->rules[$propertyName], $position, 0, [$ruleToAdd]); } /** * Returns all rules matching the given rule name * - * @example $oRuleSet->getRules('font') // returns array(0 => $oRule, …) or array(). + * @example $ruleSet->getRules('font') // returns array(0 => $rule, …) or array(). * - * @example $oRuleSet->getRules('font-') + * @example $ruleSet->getRules('font-') * //returns an array of all rules either beginning with font- or matching font. * - * @param Rule|string|null $mRule + * @param string|null $searchPattern * Pattern to search for. If null, returns all rules. * If the pattern ends with a dash, all rules starting with the pattern are returned * as well as one matching the pattern with the dash excluded. - * Passing a Rule behaves like calling `getRules($mRule->getRule())`. * - * @return array + * @return array, Rule> */ - public function getRules($mRule = null) + public function getRules(?string $searchPattern = null): array { - if ($mRule instanceof Rule) { - $mRule = $mRule->getRule(); - } - /** @var array $aResult */ - $aResult = []; - foreach ($this->aRules as $sName => $aRules) { + $result = []; + foreach ($this->rules as $propertyName => $rules) { // Either no search rule is given or the search rule matches the found rule exactly // or the search rule ends in “-” and the found rule starts with the search rule. if ( - !$mRule || $sName === $mRule + $searchPattern === null || $propertyName === $searchPattern || ( - strrpos($mRule, '-') === strlen($mRule) - strlen('-') - && (strpos($sName, $mRule) === 0 || $sName === substr($mRule, 0, -1)) + \strrpos($searchPattern, '-') === \strlen($searchPattern) - \strlen('-') + && (\strpos($propertyName, $searchPattern) === 0 + || $propertyName === \substr($searchPattern, 0, -1)) ) ) { - $aResult = array_merge($aResult, $aRules); + $result = \array_merge($result, $rules); } } - usort($aResult, function (Rule $first, Rule $second) { + \usort($result, static function (Rule $first, Rule $second): int { if ($first->getLineNo() === $second->getLineNo()) { return $first->getColNo() - $second->getColNo(); } return $first->getLineNo() - $second->getLineNo(); }); - return $aResult; + + return $result; } /** * Overrides all the rules of this set. * - * @param array $aRules The rules to override with. - * - * @return void + * @param array $rules The rules to override with. */ - public function setRules(array $aRules) + public function setRules(array $rules): void { - $this->aRules = []; - foreach ($aRules as $rule) { + $this->rules = []; + foreach ($rules as $rule) { $this->addRule($rule); } } @@ -199,134 +186,99 @@ public function setRules(array $aRules) * like `{ background-color: green; background-color; rgba(0, 127, 0, 0.7); }` will only yield an associative array * containing the rgba-valued rule while `getRules()` would yield an indexed array containing both. * - * @param Rule|string|null $mRule $mRule + * @param string|null $searchPattern * Pattern to search for. If null, returns all rules. If the pattern ends with a dash, * all rules starting with the pattern are returned as well as one matching the pattern with the dash - * excluded. Passing a Rule behaves like calling `getRules($mRule->getRule())`. + * excluded. * * @return array */ - public function getRulesAssoc($mRule = null) + public function getRulesAssoc(?string $searchPattern = null): array { - /** @var array $aResult */ - $aResult = []; - foreach ($this->getRules($mRule) as $oRule) { - $aResult[$oRule->getRule()] = $oRule; + /** @var array $result */ + $result = []; + foreach ($this->getRules($searchPattern) as $rule) { + $result[$rule->getRule()] = $rule; } - return $aResult; + + return $result; } /** - * Removes a rule from this RuleSet. This accepts all the possible values that `getRules()` accepts. - * - * If given a Rule, it will only remove this particular rule (by identity). - * If given a name, it will remove all rules by that name. - * - * Note: this is different from pre-v.2.0 behaviour of PHP-CSS-Parser, where passing a Rule instance would - * remove all rules with the same name. To get the old behaviour, use `removeRule($oRule->getRule())`. - * - * @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 + * Removes a `Rule` from this `RuleSet` by identity. */ - public function removeRule($mRule) + public function removeRule(Rule $ruleToRemove): void { - if ($mRule instanceof Rule) { - $sRule = $mRule->getRule(); - if (!isset($this->aRules[$sRule])) { - return; - } - foreach ($this->aRules[$sRule] as $iKey => $oRule) { - if ($oRule === $mRule) { - unset($this->aRules[$sRule][$iKey]); - } - } - } else { - foreach ($this->aRules as $sName => $aRules) { - // Either no search rule is given or the search rule matches the found rule exactly - // or the search rule ends in “-” and the found rule starts with the search rule or equals it - // (without the trailing dash). - if ( - !$mRule || $sName === $mRule - || (strrpos($mRule, '-') === strlen($mRule) - strlen('-') - && (strpos($sName, $mRule) === 0 || $sName === substr($mRule, 0, -1))) - ) { - unset($this->aRules[$sName]); - } + $nameOfPropertyToRemove = $ruleToRemove->getRule(); + if (!isset($this->rules[$nameOfPropertyToRemove])) { + return; + } + foreach ($this->rules[$nameOfPropertyToRemove] as $key => $rule) { + if ($rule === $ruleToRemove) { + unset($this->rules[$nameOfPropertyToRemove][$key]); } } } /** - * @return string - */ - public function __toString() - { - return $this->render(new OutputFormat()); - } - - /** - * @return string + * 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. */ - protected function renderRules(OutputFormat $oOutputFormat) + public function removeMatchingRules(string $searchPattern): void { - $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->rules as $propertyName => $rules) { + // Either the search rule matches the found rule exactly + // or the search rule ends in “-” and the found rule starts with the search rule or equals it + // (without the trailing dash). + if ( + $propertyName === $searchPattern + || (\strrpos($searchPattern, '-') === \strlen($searchPattern) - \strlen('-') + && (\strpos($propertyName, $searchPattern) === 0 + || $propertyName === \substr($searchPattern, 0, -1))) + ) { + unset($this->rules[$propertyName]); } } - - if (!$bIsFirst) { - // Had some output - $sResult .= $oOutputFormat->spaceAfterRules(); - } - - return $oOutputFormat->removeLastSemicolon($sResult); } - /** - * @param array $aComments - * - * @return void - */ - public function addComments(array $aComments) + public function removeAllRules(): void { - $this->aComments = array_merge($this->aComments, $aComments); + $this->rules = []; } - /** - * @return array - */ - public function getComments() + protected function renderRules(OutputFormat $outputFormat): string { - return $this->aComments; - } + $result = ''; + $isFirst = true; + $nextLevelFormat = $outputFormat->nextLevel(); + foreach ($this->getRules() as $rule) { + $nextLevelFormatter = $nextLevelFormat->getFormatter(); + $renderedRule = $nextLevelFormatter->safely(static function () use ($rule, $nextLevelFormat): string { + return $rule->render($nextLevelFormat); + }); + if ($renderedRule === null) { + continue; + } + if ($isFirst) { + $isFirst = false; + $result .= $nextLevelFormatter->spaceBeforeRules(); + } else { + $result .= $nextLevelFormatter->spaceBetweenRules(); + } + $result .= $renderedRule; + } - /** - * @param array $aComments - * - * @return void - */ - public function setComments(array $aComments) - { - $this->aComments = $aComments; + $formatter = $outputFormat->getFormatter(); + if (!$isFirst) { + // Had some output + $result .= $formatter->spaceAfterRules(); + } + + return $formatter->removeLastSemicolon($result); } } diff --git a/src/Settings.php b/src/Settings.php index 79d99803..a26d10e9 100644 --- a/src/Settings.php +++ b/src/Settings.php @@ -1,5 +1,7 @@ bMultibyteSupport = extension_loaded('mbstring'); + $this->multibyteSupport = \extension_loaded('mbstring'); } - /** - * @return self new instance - */ - public static function create() + public static function create(): self { return new Settings(); } @@ -52,49 +51,74 @@ public static function create() * If `true` (`mbstring` extension must be enabled), will use (slower) `mb_strlen`, `mb_convert_case`, `mb_substr` * and `mb_strpos` functions. Otherwise, the normal (ASCII-Only) functions will be used. * - * @param bool $bMultibyteSupport - * - * @return self fluent interface + * @return $this fluent interface */ - public function withMultibyteSupport($bMultibyteSupport = true) + public function withMultibyteSupport(bool $multibyteSupport = true): self { - $this->bMultibyteSupport = $bMultibyteSupport; + $this->multibyteSupport = $multibyteSupport; + return $this; } /** * Sets the charset to be used if the CSS does not contain an `@charset` declaration. * - * @param string $sDefaultCharset + * @param non-empty-string $defaultCharset * - * @return self fluent interface + * @return $this fluent interface */ - public function withDefaultCharset($sDefaultCharset) + public function withDefaultCharset(string $defaultCharset): self { - $this->sDefaultCharset = $sDefaultCharset; + $this->defaultCharset = $defaultCharset; + return $this; } /** * Configures whether the parser should silently ignore invalid rules. * - * @param bool $bLenientParsing - * - * @return self fluent interface + * @return $this fluent interface */ - public function withLenientParsing($bLenientParsing = true) + public function withLenientParsing(bool $usesLenientParsing = true): self { - $this->bLenientParsing = $bLenientParsing; + $this->lenientParsing = $usesLenientParsing; + return $this; } /** * Configures the parser to choke on invalid rules. * - * @return self fluent interface + * @return $this fluent interface */ - public function beStrict() + public function beStrict(): self { return $this->withLenientParsing(false); } + + /** + * @internal + */ + public function hasMultibyteSupport(): bool + { + return $this->multibyteSupport; + } + + /** + * @return non-empty-string + * + * @internal + */ + public function getDefaultCharset(): string + { + return $this->defaultCharset; + } + + /** + * @internal + */ + public function usesLenientParsing(): bool + { + return $this->lenientParsing; + } } diff --git a/src/Value/CSSFunction.php b/src/Value/CSSFunction.php index 300dc3ec..f78f7cb6 100644 --- a/src/Value/CSSFunction.php +++ b/src/Value/CSSFunction.php @@ -1,9 +1,14 @@ $aArguments - * @param string $sSeparator - * @param int $iLineNo + * @param non-empty-string $name + * @param RuleValueList|array $arguments + * @param non-empty-string $separator + * @param int<0, max> $lineNumber */ - public function __construct($sName, $aArguments, $sSeparator = ',', $iLineNo = 0) + public function __construct(string $name, $arguments, string $separator = ',', int $lineNumber = 0) { - if ($aArguments instanceof RuleValueList) { - $sSeparator = $aArguments->getListSeparator(); - $aArguments = $aArguments->getListComponents(); + if ($arguments instanceof RuleValueList) { + $separator = $arguments->getListSeparator(); + $arguments = $arguments->getListComponents(); } - $this->sName = $sName; - $this->iLineNo = $iLineNo; - parent::__construct($aArguments, $sSeparator, $iLineNo); + $this->name = $name; + $this->setPosition($lineNumber); // TODO: redundant? + parent::__construct($arguments, $separator, $lineNumber); } /** - * @param ParserState $oParserState - * @param bool $bIgnoreCase - * - * @return CSSFunction - * * @throws SourceException * @throws UnexpectedEOFException * @throws UnexpectedTokenException + * + * @internal since V8.8.0 */ - public static function parse(ParserState $oParserState, $bIgnoreCase = false) + public static function parse(ParserState $parserState, bool $ignoreCase = false): CSSFunction { - $mResult = $oParserState->parseIdentifier($bIgnoreCase); - $oParserState->consume('('); - $aArguments = Value::parseValue($oParserState, ['=', ' ', ',']); - $mResult = new CSSFunction($mResult, $aArguments, ',', $oParserState->currentLine()); - $oParserState->consume(')'); - return $mResult; + $name = self::parseName($parserState, $ignoreCase); + $parserState->consume('('); + $arguments = self::parseArguments($parserState); + + $result = new CSSFunction($name, $arguments, ',', $parserState->currentLine()); + $parserState->consume(')'); + + return $result; } /** - * @return string + * @throws SourceException + * @throws UnexpectedEOFException + * @throws UnexpectedTokenException */ - public function getName() + private static function parseName(ParserState $parserState, bool $ignoreCase = false): string { - return $this->sName; + return $parserState->parseIdentifier($ignoreCase); } /** - * @param string $sName + * @return Value|string * - * @return void + * @throws SourceException + * @throws UnexpectedEOFException + * @throws UnexpectedTokenException + */ + private static function parseArguments(ParserState $parserState) + { + return Value::parseValue($parserState, ['=', ' ', ',']); + } + + /** + * @return non-empty-string */ - public function setName($sName) + public function getName(): string { - $this->sName = $sName; + return $this->name; } /** - * @return array + * @param non-empty-string $name */ - public function getArguments() + public function setName(string $name): void { - return $this->aComponents; + $this->name = $name; } /** - * @return string + * @return array */ - public function __toString() + public function getArguments(): array { - return $this->render(new OutputFormat()); + return $this->components; } /** - * @return string + * @return non-empty-string */ - public function render(OutputFormat $oOutputFormat) + public function render(OutputFormat $outputFormat): string { - $aArguments = parent::render($oOutputFormat); - return "{$this->sName}({$aArguments})"; + $arguments = parent::render($outputFormat); + return "{$this->name}({$arguments})"; } } diff --git a/src/Value/CSSString.php b/src/Value/CSSString.php index da498d41..52b521e6 100644 --- a/src/Value/CSSString.php +++ b/src/Value/CSSString.php @@ -1,5 +1,7 @@ $lineNumber */ - public function __construct($sString, $iLineNo = 0) + public function __construct(string $string, int $lineNumber = 0) { - $this->sString = $sString; - parent::__construct($iLineNo); + $this->string = $string; + parent::__construct($lineNumber); } /** - * @return CSSString - * * @throws SourceException * @throws UnexpectedEOFException * @throws UnexpectedTokenException + * + * @internal since V8.8.0 */ - public static function parse(ParserState $oParserState) + public static function parse(ParserState $parserState): CSSString { - $sBegin = $oParserState->peek(); - $sQuote = null; - if ($sBegin === "'") { - $sQuote = "'"; - } elseif ($sBegin === '"') { - $sQuote = '"'; + $begin = $parserState->peek(); + $quote = null; + if ($begin === "'") { + $quote = "'"; + } elseif ($begin === '"') { + $quote = '"'; } - if ($sQuote !== null) { - $oParserState->consume($sQuote); + if ($quote !== null) { + $parserState->consume($quote); } - $sResult = ""; - $sContent = null; - if ($sQuote === null) { + $result = ''; + $content = null; + if ($quote === null) { // Unquoted strings end in whitespace or with braces, brackets, parentheses - while (!preg_match('/[\\s{}()<>\\[\\]]/isu', $oParserState->peek())) { - $sResult .= $oParserState->parseCharacter(false); + while (\preg_match('/[\\s{}()<>\\[\\]]/isu', $parserState->peek()) !== 1) { + $result .= $parserState->parseCharacter(false); } } else { - while (!$oParserState->comes($sQuote)) { - $sContent = $oParserState->parseCharacter(false); - if ($sContent === null) { + while (!$parserState->comes($quote)) { + $content = $parserState->parseCharacter(false); + if ($content === null) { throw new SourceException( - "Non-well-formed quoted string {$oParserState->peek(3)}", - $oParserState->currentLine() + "Non-well-formed quoted string {$parserState->peek(3)}", + $parserState->currentLine() ); } - $sResult .= $sContent; + $result .= $content; } - $oParserState->consume($sQuote); + $parserState->consume($quote); } - return new CSSString($sResult, $oParserState->currentLine()); + return new CSSString($result, $parserState->currentLine()); } - /** - * @param string $sString - * - * @return void - */ - public function setString($sString) + public function setString(string $string): void { - $this->sString = $sString; + $this->string = $string; } - /** - * @return string - */ - public function getString() - { - return $this->sString; - } - - /** - * @return string - */ - public function __toString() + public function getString(): string { - return $this->render(new OutputFormat()); + return $this->string; } /** - * @return string + * @return non-empty-string */ - public function render(OutputFormat $oOutputFormat) + public function render(OutputFormat $outputFormat): string { - $sString = addslashes($this->sString); - $sString = str_replace("\n", '\A', $sString); - return $oOutputFormat->getStringQuotingType() . $sString . $oOutputFormat->getStringQuotingType(); + $string = \addslashes($this->string); + $string = \str_replace("\n", '\\A', $string); + return $outputFormat->getStringQuotingType() . $string . $outputFormat->getStringQuotingType(); } } diff --git a/src/Value/CalcFunction.php b/src/Value/CalcFunction.php index 5ffd071f..12e2638c 100644 --- a/src/Value/CalcFunction.php +++ b/src/Value/CalcFunction.php @@ -1,5 +1,7 @@ parseIdentifier(); - if ($oParserState->peek() != '(') { + $operators = ['+', '-', '*', '/']; + $function = $parserState->parseIdentifier(); + if ($parserState->peek() != '(') { // Found ; or end of line before an opening bracket - throw new UnexpectedTokenException('(', $oParserState->peek(), 'literal', $oParserState->currentLine()); - } elseif (!in_array($sFunction, ['calc', '-moz-calc', '-webkit-calc'])) { + throw new UnexpectedTokenException('(', $parserState->peek(), 'literal', $parserState->currentLine()); + } elseif ($function !== 'calc') { // Found invalid calc definition. Example calc (... - throw new UnexpectedTokenException('calc', $sFunction, 'literal', $oParserState->currentLine()); + throw new UnexpectedTokenException('calc', $function, 'literal', $parserState->currentLine()); } - $oParserState->consume('('); - $oCalcList = new CalcRuleValueList($oParserState->currentLine()); - $oList = new RuleValueList(',', $oParserState->currentLine()); - $iNestingLevel = 0; - $iLastComponentType = null; - while (!$oParserState->comes(')') || $iNestingLevel > 0) { - if ($oParserState->isEnd() && $iNestingLevel === 0) { + $parserState->consume('('); + $calcRuleValueList = new CalcRuleValueList($parserState->currentLine()); + $list = new RuleValueList(',', $parserState->currentLine()); + $nestingLevel = 0; + $lastComponentType = null; + while (!$parserState->comes(')') || $nestingLevel > 0) { + if ($parserState->isEnd() && $nestingLevel === 0) { break; } - $oParserState->consumeWhiteSpace(); - if ($oParserState->comes('(')) { - $iNestingLevel++; - $oCalcList->addListComponent($oParserState->consume(1)); - $oParserState->consumeWhiteSpace(); + $parserState->consumeWhiteSpace(); + if ($parserState->comes('(')) { + $nestingLevel++; + $calcRuleValueList->addListComponent($parserState->consume(1)); + $parserState->consumeWhiteSpace(); continue; - } elseif ($oParserState->comes(')')) { - $iNestingLevel--; - $oCalcList->addListComponent($oParserState->consume(1)); - $oParserState->consumeWhiteSpace(); + } elseif ($parserState->comes(')')) { + $nestingLevel--; + $calcRuleValueList->addListComponent($parserState->consume(1)); + $parserState->consumeWhiteSpace(); continue; } - if ($iLastComponentType != CalcFunction::T_OPERAND) { - $oVal = Value::parsePrimitiveValue($oParserState); - $oCalcList->addListComponent($oVal); - $iLastComponentType = CalcFunction::T_OPERAND; + if ($lastComponentType != CalcFunction::T_OPERAND) { + $value = Value::parsePrimitiveValue($parserState); + $calcRuleValueList->addListComponent($value); + $lastComponentType = CalcFunction::T_OPERAND; } else { - if (in_array($oParserState->peek(), $aOperators)) { - if (($oParserState->comes('-') || $oParserState->comes('+'))) { + if (\in_array($parserState->peek(), $operators, true)) { + if (($parserState->comes('-') || $parserState->comes('+'))) { if ( - $oParserState->peek(1, -1) != ' ' - || !($oParserState->comes('- ') - || $oParserState->comes('+ ')) + $parserState->peek(1, -1) != ' ' + || !($parserState->comes('- ') + || $parserState->comes('+ ')) ) { throw new UnexpectedTokenException( - " {$oParserState->peek()} ", - $oParserState->peek(1, -1) . $oParserState->peek(2), + " {$parserState->peek()} ", + $parserState->peek(1, -1) . $parserState->peek(2), 'literal', - $oParserState->currentLine() + $parserState->currentLine() ); } } - $oCalcList->addListComponent($oParserState->consume(1)); - $iLastComponentType = CalcFunction::T_OPERATOR; + $calcRuleValueList->addListComponent($parserState->consume(1)); + $lastComponentType = CalcFunction::T_OPERATOR; } else { throw new UnexpectedTokenException( - sprintf( + \sprintf( 'Next token was expected to be an operand of type %s. Instead "%s" was found.', - implode(', ', $aOperators), - $oVal + \implode(', ', $operators), + $parserState->peek() ), '', 'custom', - $oParserState->currentLine() + $parserState->currentLine() ); } } - $oParserState->consumeWhiteSpace(); + $parserState->consumeWhiteSpace(); } - $oList->addListComponent($oCalcList); - if (!$oParserState->isEnd()) { - $oParserState->consume(')'); + $list->addListComponent($calcRuleValueList); + if (!$parserState->isEnd()) { + $parserState->consume(')'); } - return new CalcFunction($sFunction, $oList, ',', $oParserState->currentLine()); + return new CalcFunction($function, $list, ',', $parserState->currentLine()); } } diff --git a/src/Value/CalcRuleValueList.php b/src/Value/CalcRuleValueList.php index 7dbd26a1..3c0f24ce 100644 --- a/src/Value/CalcRuleValueList.php +++ b/src/Value/CalcRuleValueList.php @@ -1,5 +1,7 @@ $lineNumber */ - public function __construct($iLineNo = 0) + public function __construct(int $lineNumber = 0) { - parent::__construct(',', $iLineNo); + parent::__construct(',', $lineNumber); } - /** - * @return string - */ - public function render(OutputFormat $oOutputFormat) + public function render(OutputFormat $outputFormat): string { - return $oOutputFormat->implode(' ', $this->aComponents); + return $outputFormat->getFormatter()->implode(' ', $this->components); } } diff --git a/src/Value/Color.php b/src/Value/Color.php index 1cf00cce..028ce856 100644 --- a/src/Value/Color.php +++ b/src/Value/Color.php @@ -1,5 +1,7 @@ $aColor - * @param int $iLineNo + * @param array $colorValues + * @param int<0, max> $lineNumber */ - public function __construct(array $aColor, $iLineNo = 0) + public function __construct(array $colorValues, int $lineNumber = 0) { - parent::__construct(implode('', array_keys($aColor)), $aColor, ',', $iLineNo); + parent::__construct(\implode('', \array_keys($colorValues)), $colorValues, ',', $lineNumber); } /** - * @param ParserState $oParserState - * @param bool $bIgnoreCase - * - * @return Color|CSSFunction + * @throws UnexpectedEOFException + * @throws UnexpectedTokenException * + * @internal since V8.8.0 + */ + public static function parse(ParserState $parserState, bool $ignoreCase = false): CSSFunction + { + return $parserState->comes('#') + ? self::parseHexColor($parserState) + : self::parseColorFunction($parserState); + } + + /** * @throws UnexpectedEOFException * @throws UnexpectedTokenException */ - public static function parse(ParserState $oParserState, $bIgnoreCase = false) - { - $aColor = []; - if ($oParserState->comes('#')) { - $oParserState->consume('#'); - $sValue = $oParserState->parseIdentifier(false); - if ($oParserState->strlen($sValue) === 3) { - $sValue = $sValue[0] . $sValue[0] . $sValue[1] . $sValue[1] . $sValue[2] . $sValue[2]; - } elseif ($oParserState->strlen($sValue) === 4) { - $sValue = $sValue[0] . $sValue[0] . $sValue[1] . $sValue[1] . $sValue[2] . $sValue[2] . $sValue[3] - . $sValue[3]; - } + private static function parseHexColor(ParserState $parserState): Color + { + $parserState->consume('#'); + $hexValue = $parserState->parseIdentifier(false); + if ($parserState->strlen($hexValue) === 3) { + $hexValue = $hexValue[0] . $hexValue[0] . $hexValue[1] . $hexValue[1] . $hexValue[2] . $hexValue[2]; + } elseif ($parserState->strlen($hexValue) === 4) { + $hexValue = $hexValue[0] . $hexValue[0] . $hexValue[1] . $hexValue[1] . $hexValue[2] . $hexValue[2] + . $hexValue[3] . $hexValue[3]; + } + + if ($parserState->strlen($hexValue) === 8) { + $colorValues = [ + 'r' => new Size(\intval($hexValue[0] . $hexValue[1], 16), null, true, $parserState->currentLine()), + 'g' => new Size(\intval($hexValue[2] . $hexValue[3], 16), null, true, $parserState->currentLine()), + 'b' => new Size(\intval($hexValue[4] . $hexValue[5], 16), null, true, $parserState->currentLine()), + 'a' => new Size( + \round(self::mapRange(\intval($hexValue[6] . $hexValue[7], 16), 0, 255, 0, 1), 2), + null, + true, + $parserState->currentLine() + ), + ]; + } elseif ($parserState->strlen($hexValue) === 6) { + $colorValues = [ + 'r' => new Size(\intval($hexValue[0] . $hexValue[1], 16), null, true, $parserState->currentLine()), + 'g' => new Size(\intval($hexValue[2] . $hexValue[3], 16), null, true, $parserState->currentLine()), + 'b' => new Size(\intval($hexValue[4] . $hexValue[5], 16), null, true, $parserState->currentLine()), + ]; + } else { + throw new UnexpectedTokenException( + 'Invalid hex color value', + $hexValue, + 'custom', + $parserState->currentLine() + ); + } + + return new Color($colorValues, $parserState->currentLine()); + } + + /** + * @throws UnexpectedEOFException + * @throws UnexpectedTokenException + */ + private static function parseColorFunction(ParserState $parserState): CSSFunction + { + $colorValues = []; - if ($oParserState->strlen($sValue) === 8) { - $aColor = [ - 'r' => new Size(intval($sValue[0] . $sValue[1], 16), null, true, $oParserState->currentLine()), - 'g' => new Size(intval($sValue[2] . $sValue[3], 16), null, true, $oParserState->currentLine()), - 'b' => new Size(intval($sValue[4] . $sValue[5], 16), null, true, $oParserState->currentLine()), - 'a' => new Size( - round(self::mapRange(intval($sValue[6] . $sValue[7], 16), 0, 255, 0, 1), 2), - null, - true, - $oParserState->currentLine() - ), - ]; + $colorMode = $parserState->parseIdentifier(true); + $parserState->consumeWhiteSpace(); + $parserState->consume('('); + + // CSS Color Module Level 4 says that `rgb` and `rgba` are now aliases; likewise `hsl` and `hsla`. + // So, attempt to parse with the `a`, and allow for it not being there. + switch ($colorMode) { + case 'rgb': + $colorModeForParsing = 'rgba'; + $mayHaveOptionalAlpha = true; + break; + case 'hsl': + $colorModeForParsing = 'hsla'; + $mayHaveOptionalAlpha = true; + break; + case 'rgba': + // This is handled identically to the following case. + case 'hsla': + $colorModeForParsing = $colorMode; + $mayHaveOptionalAlpha = true; + break; + default: + $colorModeForParsing = $colorMode; + $mayHaveOptionalAlpha = false; + } + + $containsVar = false; + $containsNone = false; + $isLegacySyntax = false; + $expectedArgumentCount = $parserState->strlen($colorModeForParsing); + for ($argumentIndex = 0; $argumentIndex < $expectedArgumentCount; ++$argumentIndex) { + $parserState->consumeWhiteSpace(); + $valueKey = $colorModeForParsing[$argumentIndex]; + if ($parserState->comes('var')) { + $colorValues[$valueKey] = CSSFunction::parseIdentifierOrFunction($parserState); + $containsVar = true; + } elseif (!$isLegacySyntax && $parserState->comes('none')) { + $colorValues[$valueKey] = $parserState->parseIdentifier(); + $containsNone = true; } else { - $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()), - ]; + $colorValues[$valueKey] = Size::parse($parserState, true); } - } else { - $sColorMode = $oParserState->parseIdentifier(true); - $oParserState->consumeWhiteSpace(); - $oParserState->consume('('); - - $bContainsVar = false; - $iLength = $oParserState->strlen($sColorMode); - for ($i = 0; $i < $iLength; ++$i) { - $oParserState->consumeWhiteSpace(); - if ($oParserState->comes('var')) { - $aColor[$sColorMode[$i]] = CSSFunction::parseIdentifierOrFunction($oParserState); - $bContainsVar = true; - } else { - $aColor[$sColorMode[$i]] = Size::parse($oParserState, true); - } - if ($bContainsVar && $oParserState->comes(')')) { - // With a var argument the function can have fewer arguments - break; - } + // This must be done first, to consume comments as well, so that the `comes` test will work. + $parserState->consumeWhiteSpace(); - $oParserState->consumeWhiteSpace(); - if ($i < ($iLength - 1)) { - $oParserState->consume(','); + // With a `var` argument, the function can have fewer arguments. + // And as of CSS Color Module Level 4, the alpha argument is optional. + $canCloseNow = + $containsVar + || ($mayHaveOptionalAlpha && $argumentIndex >= $expectedArgumentCount - 2); + if ($canCloseNow && $parserState->comes(')')) { + break; + } + + // "Legacy" syntax is comma-delimited, and does not allow the `none` keyword. + // "Modern" syntax is space-delimited, with `/` as alpha delimiter. + // They cannot be mixed. + if ($argumentIndex === 0 && !$containsNone) { + // An immediate closing parenthesis is not valid. + if ($parserState->comes(')')) { + throw new UnexpectedTokenException( + 'Color function with no arguments', + '', + 'custom', + $parserState->currentLine() + ); } + $isLegacySyntax = $parserState->comes(','); + } + + if ($isLegacySyntax && $argumentIndex < ($expectedArgumentCount - 1)) { + $parserState->consume(','); } - $oParserState->consume(')'); - if ($bContainsVar) { - return new CSSFunction($sColorMode, array_values($aColor), ',', $oParserState->currentLine()); + // In the "modern" syntax, the alpha value must be delimited with `/`. + if (!$isLegacySyntax) { + if ($containsVar) { + // If the `var` substitution encompasses more than one argument, + // the alpha deliminator may come at any time. + if ($parserState->comes('/')) { + $parserState->consume('/'); + } + } elseif (($colorModeForParsing[$argumentIndex + 1] ?? '') === 'a') { + // Alpha value is the next expected argument. + // Since a closing parenthesis was not found, a `/` separator is now required. + $parserState->consume('/'); + } } } - return new Color($aColor, $oParserState->currentLine()); + $parserState->consume(')'); + + return $containsVar + ? new CSSFunction($colorMode, \array_values($colorValues), ',', $parserState->currentLine()) + : new Color($colorValues, $parserState->currentLine()); + } + + private static function mapRange(float $value, float $fromMin, float $fromMax, float $toMin, float $toMax): float + { + $fromRange = $fromMax - $fromMin; + $toRange = $toMax - $toMin; + $multiplier = $toRange / $fromRange; + $newValue = $value - $fromMin; + $newValue *= $multiplier; + + return $newValue + $toMin; } /** - * @param float $fVal - * @param float $fFromMin - * @param float $fFromMax - * @param float $fToMin - * @param float $fToMax - * - * @return float + * @return array + */ + public function getColor(): array + { + return $this->components; + } + + /** + * @param array $colorValues + */ + public function setColor(array $colorValues): void + { + $this->setName(\implode('', \array_keys($colorValues))); + $this->components = $colorValues; + } + + /** + * @return non-empty-string + */ + public function getColorDescription(): string + { + return $this->getName(); + } + + /** + * @return non-empty-string + */ + public function render(OutputFormat $outputFormat): string + { + if ($this->shouldRenderAsHex($outputFormat)) { + return $this->renderAsHex(); + } + + if ($this->shouldRenderInModernSyntax()) { + return $this->renderInModernSyntax($outputFormat); + } + + return parent::render($outputFormat); + } + + private function shouldRenderAsHex(OutputFormat $outputFormat): bool + { + return + $outputFormat->usesRgbHashNotation() + && $this->getRealName() === 'rgb' + && $this->allComponentsAreNumbers(); + } + + /** + * The function name is a concatenation of the array keys of the components, which is passed to the constructor. + * However, this can be changed by calling {@see CSSFunction::setName}, + * so is not reliable in situations where it's necessary to determine the function name based on the components. */ - private static function mapRange($fVal, $fFromMin, $fFromMax, $fToMin, $fToMax) + private function getRealName(): string { - $fFromRange = $fFromMax - $fFromMin; - $fToRange = $fToMax - $fToMin; - $fMultiplier = $fToRange / $fFromRange; - $fNewVal = $fVal - $fFromMin; - $fNewVal *= $fMultiplier; - return $fNewVal + $fToMin; + return \implode('', \array_keys($this->components)); } /** - * @return array + * Test whether all color components are absolute numbers (CSS type `number`), not percentages or anything else. + * If any component is not an instance of `Size`, the method will also return `false`. */ - public function getColor() + private function allComponentsAreNumbers(): bool { - return $this->aComponents; + foreach ($this->components as $component) { + if (!($component instanceof Size) || $component->getUnit() !== null) { + return false; + } + } + + return true; } /** - * @param array $aColor + * Note that this method assumes the following: + * - The `components` array has keys for `r`, `g` and `b`; + * - The values in the array are all instances of `Size`. * - * @return void + * Errors will be triggered or thrown if this is not the case. + * + * @return non-empty-string */ - public function setColor(array $aColor) + private function renderAsHex(): string { - $this->setName(implode('', array_keys($aColor))); - $this->aComponents = $aColor; + $result = \sprintf( + '%02x%02x%02x', + $this->components['r']->getSize(), + $this->components['g']->getSize(), + $this->components['b']->getSize() + ); + $canUseShortVariant = ($result[0] == $result[1]) && ($result[2] == $result[3]) && ($result[4] == $result[5]); + + return '#' . ($canUseShortVariant ? $result[0] . $result[2] . $result[4] : $result); } /** - * @return string + * The "legacy" syntax does not allow RGB colors to have a mixture of `percentage`s and `number`s, + * and does not allow `none` as any component value. + * + * The "legacy" and "modern" monikers are part of the formal W3C syntax. + * See the following for more information: + * - {@link + * https://developer.mozilla.org/en-US/docs/Web/CSS/color_value/rgb#formal_syntax + * Description of the formal syntax for `rgb()` on MDN + * }; + * - {@link + * https://www.w3.org/TR/css-color-4/#rgb-functions + * The same in the CSS Color Module Level 4 W3C Candidate Recommendation Draft + * } (as of 13 February 2024, at time of writing). */ - public function getColorDescription() + private function shouldRenderInModernSyntax(): bool { - return $this->getName(); + if ($this->hasNoneAsComponentValue()) { + return true; + } + + if (!$this->colorFunctionMayHaveMixedValueTypes($this->getRealName())) { + return false; + } + + $hasPercentage = false; + $hasNumber = false; + foreach ($this->components as $key => $value) { + if ($key === 'a') { + // Alpha can have units that don't match those of the RGB components in the "legacy" syntax. + // So it is not necessary to check it. It's also always last, hence `break` rather than `continue`. + break; + } + if (!($value instanceof Size)) { + // Unexpected, unknown, or modified via the API + return false; + } + $unit = $value->getUnit(); + // `switch` only does loose comparison + if ($unit === null) { + $hasNumber = true; + } elseif ($unit === '%') { + $hasPercentage = true; + } else { + // Invalid unit + return false; + } + } + + return $hasPercentage && $hasNumber; + } + + private function hasNoneAsComponentValue(): bool + { + return \in_array('none', $this->components, true); } /** - * @return string + * Some color functions, such as `rgb`, + * may have a mixture of `percentage`, `number`, or possibly other types in their arguments. + * + * Note that this excludes the alpha component, which is treated separately. */ - public function __toString() + private function colorFunctionMayHaveMixedValueTypes(string $function): bool { - return $this->render(new OutputFormat()); + $functionsThatMayHaveMixedValueTypes = ['rgb', 'rgba']; + + return \in_array($function, $functionsThatMayHaveMixedValueTypes, true); } /** - * @return string + * @return non-empty-string */ - public function render(OutputFormat $oOutputFormat) - { - // Shorthand RGB color values - if ($oOutputFormat->getRGBHashNotation() && implode('', array_keys($this->aComponents)) === 'rgb') { - $sResult = sprintf( - '%02x%02x%02x', - $this->aComponents['r']->getSize(), - $this->aComponents['g']->getSize(), - $this->aComponents['b']->getSize() - ); - return '#' . (($sResult[0] == $sResult[1]) && ($sResult[2] == $sResult[3]) && ($sResult[4] == $sResult[5]) - ? "$sResult[0]$sResult[2]$sResult[4]" : $sResult); + private function renderInModernSyntax(OutputFormat $outputFormat): string + { + // Maybe not yet without alpha, but will be... + $componentsWithoutAlpha = $this->components; + \end($componentsWithoutAlpha); + if (\key($componentsWithoutAlpha) === 'a') { + $alpha = $this->components['a']; + unset($componentsWithoutAlpha['a']); } - return parent::render($oOutputFormat); + + $formatter = $outputFormat->getFormatter(); + $arguments = $formatter->implode(' ', $componentsWithoutAlpha); + if (isset($alpha)) { + $separator = $formatter->spaceBeforeListArgumentSeparator('/') + . '/' . $formatter->spaceAfterListArgumentSeparator('/'); + $arguments = $formatter->implode($separator, [$arguments, $alpha]); + } + + return $this->getName() . '(' . $arguments . ')'; } } diff --git a/src/Value/LineName.php b/src/Value/LineName.php index e231ce38..791f0cc3 100644 --- a/src/Value/LineName.php +++ b/src/Value/LineName.php @@ -1,5 +1,7 @@ $aComponents - * @param int $iLineNo + * @param array $components + * @param int<0, max> $lineNumber */ - public function __construct(array $aComponents = [], $iLineNo = 0) + public function __construct(array $components = [], int $lineNumber = 0) { - parent::__construct($aComponents, ' ', $iLineNo); + parent::__construct($components, ' ', $lineNumber); } /** - * @return LineName - * * @throws UnexpectedTokenException * @throws UnexpectedEOFException + * + * @internal since V8.8.0 */ - public static function parse(ParserState $oParserState) + public static function parse(ParserState $parserState): LineName { - $oParserState->consume('['); - $oParserState->consumeWhiteSpace(); - $aNames = []; + $parserState->consume('['); + $parserState->consumeWhiteSpace(); + $names = []; do { - if ($oParserState->getSettings()->bLenientParsing) { + if ($parserState->getSettings()->usesLenientParsing()) { try { - $aNames[] = $oParserState->parseIdentifier(); + $names[] = $parserState->parseIdentifier(); } catch (UnexpectedTokenException $e) { - if (!$oParserState->comes(']')) { + if (!$parserState->comes(']')) { throw $e; } } } else { - $aNames[] = $oParserState->parseIdentifier(); + $names[] = $parserState->parseIdentifier(); } - $oParserState->consumeWhiteSpace(); - } while (!$oParserState->comes(']')); - $oParserState->consume(']'); - return new LineName($aNames, $oParserState->currentLine()); - } - - /** - * @return string - */ - public function __toString() - { - return $this->render(new OutputFormat()); + $parserState->consumeWhiteSpace(); + } while (!$parserState->comes(']')); + $parserState->consume(']'); + return new LineName($names, $parserState->currentLine()); } /** - * @return string + * @return non-empty-string */ - public function render(OutputFormat $oOutputFormat) + public function render(OutputFormat $outputFormat): string { return '[' . parent::render(OutputFormat::createCompact()) . ']'; } diff --git a/src/Value/PrimitiveValue.php b/src/Value/PrimitiveValue.php index 055a4397..f7f94092 100644 --- a/src/Value/PrimitiveValue.php +++ b/src/Value/PrimitiveValue.php @@ -1,14 +1,7 @@ $lineNumber */ - public function __construct($sSeparator = ',', $iLineNo = 0) + public function __construct(string $separator = ',', int $lineNumber = 0) { - parent::__construct([], $sSeparator, $iLineNo); + parent::__construct([], $separator, $lineNumber); } } diff --git a/src/Value/Size.php b/src/Value/Size.php index 36a32381..a5e15497 100644 --- a/src/Value/Size.php +++ b/src/Value/Size.php @@ -1,5 +1,7 @@ + * @var list */ - const ABSOLUTE_SIZE_UNITS = ['px', 'cm', 'mm', 'mozmm', 'in', 'pt', 'pc', 'vh', 'vw', 'vmin', 'vmax', 'rem']; + private const ABSOLUTE_SIZE_UNITS = [ + 'px', + 'pt', + 'pc', + 'cm', + 'mm', + 'mozmm', + 'in', + 'vh', + 'dvh', + 'svh', + 'lvh', + 'vw', + 'vmin', + 'vmax', + 'rem', + ]; /** - * @var array + * @var list */ - const RELATIVE_SIZE_UNITS = ['%', 'em', 'ex', 'ch', 'fr']; + private const RELATIVE_SIZE_UNITS = ['%', 'em', 'ex', 'ch', 'fr']; /** - * @var array + * @var list */ - const NON_SIZE_UNITS = ['deg', 'grad', 'rad', 's', 'ms', 'turn', 'Hz', 'kHz']; + private const NON_SIZE_UNITS = ['deg', 'grad', 'rad', 's', 'ms', 'turn', 'Hz', 'kHz']; /** - * @var array>|null + * @var array, array>|null */ private static $SIZE_UNITS = null; /** * @var float */ - private $fSize; + private $size; /** * @var string|null */ - private $sUnit; + private $unit; /** * @var bool */ - private $bIsColorComponent; + private $isColorComponent; /** - * @param float|int|string $fSize - * @param string|null $sUnit - * @param bool $bIsColorComponent - * @param int $iLineNo + * @param float|int|string $size + * @param int<0, max> $lineNumber */ - public function __construct($fSize, $sUnit = null, $bIsColorComponent = false, $iLineNo = 0) + public function __construct($size, ?string $unit = null, bool $isColorComponent = false, int $lineNumber = 0) { - parent::__construct($iLineNo); - $this->fSize = (float)$fSize; - $this->sUnit = $sUnit; - $this->bIsColorComponent = $bIsColorComponent; + parent::__construct($lineNumber); + $this->size = (float) $size; + $this->unit = $unit; + $this->isColorComponent = $isColorComponent; } /** - * @param bool $bIsColorComponent - * - * @return Size - * * @throws UnexpectedEOFException * @throws UnexpectedTokenException + * + * @internal since V8.8.0 */ - public static function parse(ParserState $oParserState, $bIsColorComponent = false) + public static function parse(ParserState $parserState, bool $isColorComponent = false): Size { - $sSize = ''; - if ($oParserState->comes('-')) { - $sSize .= $oParserState->consume('-'); + $size = ''; + if ($parserState->comes('-')) { + $size .= $parserState->consume('-'); } - while (is_numeric($oParserState->peek()) || $oParserState->comes('.') || $oParserState->comes('e', true)) { - if ($oParserState->comes('.')) { - $sSize .= $oParserState->consume('.'); - } elseif ($oParserState->comes('e', true)) { - $sLookahead = $oParserState->peek(1, 1); - if (is_numeric($sLookahead) || $sLookahead === '+' || $sLookahead === '-') { - $sSize .= $oParserState->consume(2); + while (\is_numeric($parserState->peek()) || $parserState->comes('.') || $parserState->comes('e', true)) { + if ($parserState->comes('.')) { + $size .= $parserState->consume('.'); + } elseif ($parserState->comes('e', true)) { + $lookahead = $parserState->peek(1, 1); + if (\is_numeric($lookahead) || $lookahead === '+' || $lookahead === '-') { + $size .= $parserState->consume(2); } else { break; // Reached the unit part of the number like "em" or "ex" } } else { - $sSize .= $oParserState->consume(1); + $size .= $parserState->consume(1); } } - $sUnit = null; - $aSizeUnits = self::getSizeUnits(); - foreach ($aSizeUnits as $iLength => &$aValues) { - $sKey = strtolower($oParserState->peek($iLength)); - if (array_key_exists($sKey, $aValues)) { - if (($sUnit = $aValues[$sKey]) !== null) { - $oParserState->consume($iLength); + $unit = null; + $sizeUnits = self::getSizeUnits(); + foreach ($sizeUnits as $length => &$values) { + $key = \strtolower($parserState->peek($length)); + if (\array_key_exists($key, $values)) { + if (($unit = $values[$key]) !== null) { + $parserState->consume($length); break; } } } - return new Size((float)$sSize, $sUnit, $bIsColorComponent, $oParserState->currentLine()); + return new Size((float) $size, $unit, $isColorComponent, $parserState->currentLine()); } /** - * @return array> + * @return array, array> */ - private static function getSizeUnits() + private static function getSizeUnits(): array { - if (!is_array(self::$SIZE_UNITS)) { + if (!\is_array(self::$SIZE_UNITS)) { self::$SIZE_UNITS = []; - foreach (array_merge(self::ABSOLUTE_SIZE_UNITS, self::RELATIVE_SIZE_UNITS, self::NON_SIZE_UNITS) as $val) { - $iSize = strlen($val); - if (!isset(self::$SIZE_UNITS[$iSize])) { - self::$SIZE_UNITS[$iSize] = []; + $sizeUnits = \array_merge(self::ABSOLUTE_SIZE_UNITS, self::RELATIVE_SIZE_UNITS, self::NON_SIZE_UNITS); + foreach ($sizeUnits as $sizeUnit) { + $tokenLength = \strlen($sizeUnit); + if (!isset(self::$SIZE_UNITS[$tokenLength])) { + self::$SIZE_UNITS[$tokenLength] = []; } - self::$SIZE_UNITS[$iSize][strtolower($val)] = $val; + self::$SIZE_UNITS[$tokenLength][\strtolower($sizeUnit)] = $sizeUnit; } - krsort(self::$SIZE_UNITS, SORT_NUMERIC); + \krsort(self::$SIZE_UNITS, SORT_NUMERIC); } return self::$SIZE_UNITS; } - /** - * @param string $sUnit - * - * @return void - */ - public function setUnit($sUnit) + public function setUnit(string $unit): void { - $this->sUnit = $sUnit; + $this->unit = $unit; } - /** - * @return string|null - */ - public function getUnit() + public function getUnit(): ?string { - return $this->sUnit; + return $this->unit; } /** - * @param float|int|string $fSize + * @param float|int|string $size */ - public function setSize($fSize) + public function setSize($size): void { - $this->fSize = (float)$fSize; + $this->size = (float) $size; } - /** - * @return float - */ - public function getSize() + public function getSize(): float { - return $this->fSize; + return $this->size; } - /** - * @return bool - */ - public function isColorComponent() + public function isColorComponent(): bool { - return $this->bIsColorComponent; + return $this->isColorComponent; } /** * Returns whether the number stored in this Size really represents a size (as in a length of something on screen). * - * @return false if the unit an angle, a duration, a frequency or the number is a component in a Color object. + * Returns `false` if the unit is an angle, a duration, a frequency, or the number is a component in a `Color` + * object. */ - public function isSize() + public function isSize(): bool { - if (in_array($this->sUnit, self::NON_SIZE_UNITS, true)) { + if (\in_array($this->unit, self::NON_SIZE_UNITS, true)) { return false; } return !$this->isColorComponent(); } - /** - * @return bool - */ - public function isRelative() + public function isRelative(): bool { - if (in_array($this->sUnit, self::RELATIVE_SIZE_UNITS, true)) { + if (\in_array($this->unit, self::RELATIVE_SIZE_UNITS, true)) { return true; } - if ($this->sUnit === null && $this->fSize != 0) { + if ($this->unit === null && $this->size != 0) { return true; } return false; } /** - * @return string + * @return non-empty-string */ - public function __toString() + public function render(OutputFormat $outputFormat): string { - return $this->render(new OutputFormat()); - } + $locale = \localeconv(); + $decimalPoint = \preg_quote($locale['decimal_point'], '/'); + $size = \preg_match('/[\\d\\.]+e[+-]?\\d+/i', (string) $this->size) + ? \preg_replace("/$decimalPoint?0+$/", '', \sprintf('%f', $this->size)) : (string) $this->size; - /** - * @return string - */ - public function render(OutputFormat $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; - return preg_replace(["/$sPoint/", "/^(-?)0\./"], ['.', '$1.'], $sSize) - . ($this->sUnit === null ? '' : $this->sUnit); + return \preg_replace(["/$decimalPoint/", '/^(-?)0\\./'], ['.', '$1.'], $size) . ($this->unit ?? ''); } } diff --git a/src/Value/URL.php b/src/Value/URL.php index cdb911c3..4b4fb4c8 100644 --- a/src/Value/URL.php +++ b/src/Value/URL.php @@ -1,5 +1,7 @@ $lineNumber */ - public function __construct(CSSString $oURL, $iLineNo = 0) + public function __construct(CSSString $url, int $lineNumber = 0) { - parent::__construct($iLineNo); - $this->oURL = $oURL; + parent::__construct($lineNumber); + $this->url = $url; } /** - * @return URL - * * @throws SourceException * @throws UnexpectedEOFException * @throws UnexpectedTokenException + * + * @internal since V8.8.0 */ - public static function parse(ParserState $oParserState) + public static function parse(ParserState $parserState): URL { - $oAnchor = $oParserState->anchor(); - $sIdentifier = ''; + $anchor = $parserState->anchor(); + $identifier = ''; for ($i = 0; $i < 3; $i++) { - $sChar = $oParserState->parseCharacter(true); - if ($sChar === null) { + $character = $parserState->parseCharacter(true); + if ($character === null) { break; } - $sIdentifier .= $sChar; + $identifier .= $character; } - $bUseUrl = $oParserState->streql($sIdentifier, 'url'); - if ($bUseUrl) { - $oParserState->consumeWhiteSpace(); - $oParserState->consume('('); + $useUrl = $parserState->streql($identifier, 'url'); + if ($useUrl) { + $parserState->consumeWhiteSpace(); + $parserState->consume('('); } else { - $oAnchor->backtrack(); + $anchor->backtrack(); } - $oParserState->consumeWhiteSpace(); - $oResult = new URL(CSSString::parse($oParserState), $oParserState->currentLine()); - if ($bUseUrl) { - $oParserState->consumeWhiteSpace(); - $oParserState->consume(')'); + $parserState->consumeWhiteSpace(); + $result = new URL(CSSString::parse($parserState), $parserState->currentLine()); + if ($useUrl) { + $parserState->consumeWhiteSpace(); + $parserState->consume(')'); } - return $oResult; - } - - /** - * @return void - */ - public function setURL(CSSString $oURL) - { - $this->oURL = $oURL; + return $result; } - /** - * @return CSSString - */ - public function getURL() + public function setURL(CSSString $url): void { - return $this->oURL; + $this->url = $url; } - /** - * @return string - */ - public function __toString() + public function getURL(): CSSString { - return $this->render(new OutputFormat()); + return $this->url; } /** - * @return string + * @return non-empty-string */ - public function render(OutputFormat $oOutputFormat) + public function render(OutputFormat $outputFormat): string { - return "url({$this->oURL->render($oOutputFormat)})"; + return "url({$this->url->render($outputFormat)})"; } } diff --git a/src/Value/Value.php b/src/Value/Value.php index a920396b..e33a2949 100644 --- a/src/Value/Value.php +++ b/src/Value/Value.php @@ -1,130 +1,135 @@ $lineNumber */ - public function __construct($iLineNo = 0) + public function __construct(int $lineNumber = 0) { - $this->iLineNo = $iLineNo; + $this->setPosition($lineNumber); } /** - * @param array $aListDelimiters + * @param array $listDelimiters * - * @return RuleValueList|CSSFunction|CSSString|LineName|Size|URL|string + * @return Value|string * * @throws UnexpectedTokenException * @throws UnexpectedEOFException + * + * @internal since V8.8.0 */ - public static function parseValue(ParserState $oParserState, array $aListDelimiters = []) + public static function parseValue(ParserState $parserState, array $listDelimiters = []) { - /** @var array $aStack */ - $aStack = []; - $oParserState->consumeWhiteSpace(); + /** @var list $stack */ + $stack = []; + $parserState->consumeWhiteSpace(); //Build a list of delimiters and parsed values while ( - !($oParserState->comes('}') || $oParserState->comes(';') || $oParserState->comes('!') - || $oParserState->comes(')') - || $oParserState->comes('\\') - || $oParserState->isEnd()) + !($parserState->comes('}') || $parserState->comes(';') || $parserState->comes('!') + || $parserState->comes(')') + || $parserState->isEnd()) ) { - if (count($aStack) > 0) { - $bFoundDelimiter = false; - foreach ($aListDelimiters as $sDelimiter) { - if ($oParserState->comes($sDelimiter)) { - array_push($aStack, $oParserState->consume($sDelimiter)); - $oParserState->consumeWhiteSpace(); - $bFoundDelimiter = true; + if (\count($stack) > 0) { + $foundDelimiter = false; + foreach ($listDelimiters as $delimiter) { + if ($parserState->comes($delimiter)) { + \array_push($stack, $parserState->consume($delimiter)); + $parserState->consumeWhiteSpace(); + $foundDelimiter = true; break; } } - if (!$bFoundDelimiter) { + if (!$foundDelimiter) { //Whitespace was the list delimiter - array_push($aStack, ' '); + \array_push($stack, ' '); } } - array_push($aStack, self::parsePrimitiveValue($oParserState)); - $oParserState->consumeWhiteSpace(); + \array_push($stack, self::parsePrimitiveValue($parserState)); + $parserState->consumeWhiteSpace(); } // Convert the list to list objects - foreach ($aListDelimiters as $sDelimiter) { - if (count($aStack) === 1) { - return $aStack[0]; + foreach ($listDelimiters as $delimiter) { + $stackSize = \count($stack); + if ($stackSize === 1) { + return $stack[0]; } - $iStartPosition = null; - while (($iStartPosition = array_search($sDelimiter, $aStack, true)) !== false) { - $iLength = 2; //Number of elements to be joined - for ($i = $iStartPosition + 2; $i < count($aStack); $i += 2, ++$iLength) { - if ($sDelimiter !== $aStack[$i]) { + $newStack = []; + for ($offset = 0; $offset < $stackSize; ++$offset) { + if ($offset === ($stackSize - 1) || $delimiter !== $stack[$offset + 1]) { + $newStack[] = $stack[$offset]; + continue; + } + $length = 2; //Number of elements to be joined + for ($i = $offset + 3; $i < $stackSize; $i += 2, ++$length) { + if ($delimiter !== $stack[$i]) { break; } } - $oList = new RuleValueList($sDelimiter, $oParserState->currentLine()); - for ($i = $iStartPosition - 1; $i - $iStartPosition + 1 < $iLength * 2; $i += 2) { - $oList->addListComponent($aStack[$i]); + $list = new RuleValueList($delimiter, $parserState->currentLine()); + for ($i = $offset; $i - $offset < $length * 2; $i += 2) { + $list->addListComponent($stack[$i]); } - array_splice($aStack, $iStartPosition - 1, $iLength * 2 - 1, [$oList]); + $newStack[] = $list; + $offset += $length * 2 - 2; } + $stack = $newStack; } - if (!isset($aStack[0])) { + if (!isset($stack[0])) { throw new UnexpectedTokenException( - " {$oParserState->peek()} ", - $oParserState->peek(1, -1) . $oParserState->peek(2), + " {$parserState->peek()} ", + $parserState->peek(1, -1) . $parserState->peek(2), 'literal', - $oParserState->currentLine() + $parserState->currentLine() ); } - return $aStack[0]; + return $stack[0]; } /** - * @param bool $bIgnoreCase - * * @return CSSFunction|string * * @throws UnexpectedEOFException * @throws UnexpectedTokenException + * + * @internal since V8.8.0 */ - public static function parseIdentifierOrFunction(ParserState $oParserState, $bIgnoreCase = false) + public static function parseIdentifierOrFunction(ParserState $parserState, bool $ignoreCase = false) { - $oAnchor = $oParserState->anchor(); - $mResult = $oParserState->parseIdentifier($bIgnoreCase); + $anchor = $parserState->anchor(); + $result = $parserState->parseIdentifier($ignoreCase); - if ($oParserState->comes('(')) { - $oAnchor->backtrack(); - if ($oParserState->streql('url', $mResult)) { - $mResult = URL::parse($oParserState); - } elseif ( - $oParserState->streql('calc', $mResult) - || $oParserState->streql('-webkit-calc', $mResult) - || $oParserState->streql('-moz-calc', $mResult) - ) { - $mResult = CalcFunction::parse($oParserState); + if ($parserState->comes('(')) { + $anchor->backtrack(); + if ($parserState->streql('url', $result)) { + $result = URL::parse($parserState); + } elseif ($parserState->streql('calc', $result)) { + $result = CalcFunction::parse($parserState); } else { - $mResult = CSSFunction::parse($oParserState, $bIgnoreCase); + $result = CSSFunction::parse($parserState, $ignoreCase); } } - return $mResult; + return $result; } /** @@ -133,73 +138,74 @@ public static function parseIdentifierOrFunction(ParserState $oParserState, $bIg * @throws UnexpectedEOFException * @throws UnexpectedTokenException * @throws SourceException + * + * @internal since V8.8.0 */ - public static function parsePrimitiveValue(ParserState $oParserState) + public static function parsePrimitiveValue(ParserState $parserState) { - $oValue = null; - $oParserState->consumeWhiteSpace(); + $value = null; + $parserState->consumeWhiteSpace(); if ( - is_numeric($oParserState->peek()) - || ($oParserState->comes('-.') - && is_numeric($oParserState->peek(1, 2))) - || (($oParserState->comes('-') || $oParserState->comes('.')) && is_numeric($oParserState->peek(1, 1))) + \is_numeric($parserState->peek()) + || ($parserState->comes('-.') + && \is_numeric($parserState->peek(1, 2))) + || (($parserState->comes('-') || $parserState->comes('.')) && \is_numeric($parserState->peek(1, 1))) ) { - $oValue = Size::parse($oParserState); - } elseif ($oParserState->comes('#') || $oParserState->comes('rgb', true) || $oParserState->comes('hsl', true)) { - $oValue = Color::parse($oParserState); - } elseif ($oParserState->comes("'") || $oParserState->comes('"')) { - $oValue = CSSString::parse($oParserState); - } elseif ($oParserState->comes("progid:") && $oParserState->getSettings()->bLenientParsing) { - $oValue = self::parseMicrosoftFilter($oParserState); - } elseif ($oParserState->comes("[")) { - $oValue = LineName::parse($oParserState); - } elseif ($oParserState->comes("U+")) { - $oValue = self::parseUnicodeRangeValue($oParserState); + $value = Size::parse($parserState); + } elseif ($parserState->comes('#') || $parserState->comes('rgb', true) || $parserState->comes('hsl', true)) { + $value = Color::parse($parserState); + } elseif ($parserState->comes("'") || $parserState->comes('"')) { + $value = CSSString::parse($parserState); + } elseif ($parserState->comes('progid:') && $parserState->getSettings()->usesLenientParsing()) { + $value = self::parseMicrosoftFilter($parserState); + } elseif ($parserState->comes('[')) { + $value = LineName::parse($parserState); + } elseif ($parserState->comes('U+')) { + $value = self::parseUnicodeRangeValue($parserState); } else { - $oValue = self::parseIdentifierOrFunction($oParserState); + $nextCharacter = $parserState->peek(1); + try { + $value = self::parseIdentifierOrFunction($parserState); + } catch (UnexpectedTokenException $e) { + if (\in_array($nextCharacter, ['+', '-', '*', '/'], true)) { + $value = $parserState->consume(1); + } else { + throw $e; + } + } } - $oParserState->consumeWhiteSpace(); - return $oValue; + $parserState->consumeWhiteSpace(); + + return $value; } /** - * @return CSSFunction - * * @throws UnexpectedEOFException * @throws UnexpectedTokenException */ - private static function parseMicrosoftFilter(ParserState $oParserState) + private static function parseMicrosoftFilter(ParserState $parserState): CSSFunction { - $sFunction = $oParserState->consumeUntil('(', false, true); - $aArguments = Value::parseValue($oParserState, [',', '=']); - return new CSSFunction($sFunction, $aArguments, ',', $oParserState->currentLine()); + $function = $parserState->consumeUntil('(', false, true); + $arguments = Value::parseValue($parserState, [',', '=']); + return new CSSFunction($function, $arguments, ',', $parserState->currentLine()); } /** - * @return string - * * @throws UnexpectedEOFException * @throws UnexpectedTokenException */ - private static function parseUnicodeRangeValue(ParserState $oParserState) + private static function parseUnicodeRangeValue(ParserState $parserState): string { - $iCodepointMaxLength = 6; // Code points outside BMP can use up to six digits - $sRange = ""; - $oParserState->consume("U+"); + $codepointMaxLength = 6; // Code points outside BMP can use up to six digits + $range = ''; + $parserState->consume('U+'); do { - if ($oParserState->comes('-')) { - $iCodepointMaxLength = 13; // Max length is 2 six digit code points + the dash(-) between them + if ($parserState->comes('-')) { + $codepointMaxLength = 13; // Max length is 2 six-digit code points + the dash(-) between them } - $sRange .= $oParserState->consume(1); - } while (strlen($sRange) < $iCodepointMaxLength && preg_match("/[A-Fa-f0-9\?-]/", $oParserState->peek())); - return "U+{$sRange}"; - } + $range .= $parserState->consume(1); + } while (\strlen($range) < $codepointMaxLength && \preg_match('/[A-Fa-f0-9\\?-]/', $parserState->peek())); - /** - * @return int - */ - public function getLineNo() - { - return $this->iLineNo; + return "U+{$range}"; } } diff --git a/src/Value/ValueList.php b/src/Value/ValueList.php index a93acc7b..1d26b74d 100644 --- a/src/Value/ValueList.php +++ b/src/Value/ValueList.php @@ -1,5 +1,7 @@ + * @var array + * + * @internal since 8.8.0 */ - protected $aComponents; + protected $components; /** - * @var string + * @var non-empty-string + * + * @internal since 8.8.0 */ - protected $sSeparator; + protected $separator; /** - * phpcs:ignore Generic.Files.LineLength - * @param array|RuleValueList|CSSFunction|CSSString|LineName|Size|URL|string $aComponents - * @param string $sSeparator - * @param int $iLineNo + * @param array|Value|string $components + * @param non-empty-string $separator + * @param int<0, max> $lineNumber */ - public function __construct($aComponents = [], $sSeparator = ',', $iLineNo = 0) + public function __construct($components = [], $separator = ',', int $lineNumber = 0) { - parent::__construct($iLineNo); - if (!is_array($aComponents)) { - $aComponents = [$aComponents]; + parent::__construct($lineNumber); + if (!\is_array($components)) { + $components = [$components]; } - $this->aComponents = $aComponents; - $this->sSeparator = $sSeparator; + $this->components = $components; + $this->separator = $separator; } /** - * @param RuleValueList|CSSFunction|CSSString|LineName|Size|URL|string $mComponent - * - * @return void + * @param Value|string $component */ - public function addListComponent($mComponent) + public function addListComponent($component): void { - $this->aComponents[] = $mComponent; + $this->components[] = $component; } /** - * @return array + * @return array */ - public function getListComponents() + public function getListComponents(): array { - return $this->aComponents; + return $this->components; } /** - * @param array $aComponents - * - * @return void + * @param array $components */ - public function setListComponents(array $aComponents) + public function setListComponents(array $components): void { - $this->aComponents = $aComponents; + $this->components = $components; } /** - * @return string + * @return non-empty-string */ - public function getListSeparator() + public function getListSeparator(): string { - return $this->sSeparator; + return $this->separator; } /** - * @param string $sSeparator - * - * @return void + * @param non-empty-string $separator */ - public function setListSeparator($sSeparator) + public function setListSeparator(string $separator): void { - $this->sSeparator = $sSeparator; + $this->separator = $separator; } - /** - * @return string - */ - public function __toString() + public function render(OutputFormat $outputFormat): string { - return $this->render(new OutputFormat()); - } + $formatter = $outputFormat->getFormatter(); - /** - * @return string - */ - public function render(OutputFormat $oOutputFormat) - { - return $oOutputFormat->implode( - $oOutputFormat->spaceBeforeListArgumentSeparator($this->sSeparator) . $this->sSeparator - . $oOutputFormat->spaceAfterListArgumentSeparator($this->sSeparator), - $this->aComponents + return $formatter->implode( + $formatter->spaceBeforeListArgumentSeparator($this->separator) . $this->separator + . $formatter->spaceAfterListArgumentSeparator($this->separator), + $this->components ); } } diff --git a/tests/CSSList/AtRuleBlockListTest.php b/tests/CSSList/AtRuleBlockListTest.php index 48e6e578..9a725b21 100644 --- a/tests/CSSList/AtRuleBlockListTest.php +++ b/tests/CSSList/AtRuleBlockListTest.php @@ -1,86 +1,94 @@ */ - public function implementsAtRule() + public static function provideMinWidthMediaRule(): array { - $subject = new AtRuleBlockList(''); - - self::assertInstanceOf(AtRuleBlockList::class, $subject); + return [ + 'without spaces around arguments' => ['@media(min-width: 768px){.class{color:red}}'], + 'with spaces around arguments' => ['@media (min-width: 768px) {.class{color:red}}'], + ]; } /** - * @test + * @return array */ - public function implementsRenderable() + public static function provideSyntacticallyCorrectAtRule(): array { - $subject = new AtRuleBlockList(''); - - self::assertInstanceOf(Renderable::class, $subject); + 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 + * + * @dataProvider provideMinWidthMediaRule */ - public function implementsCommentable() + public function parsesRuleNameOfMediaQueries(string $css): void { - $subject = new AtRuleBlockList(''); - - self::assertInstanceOf(Commentable::class, $subject); - } + $contents = (new Parser($css))->parse()->getContents(); + $atRuleBlockList = $contents[0]; - /** - * @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}}'], - ]; + self::assertInstanceOf(AtRuleBlockList::class, $atRuleBlockList); + self::assertSame('media', $atRuleBlockList->atRuleName()); } /** * @test * - * @param string $css - * - * @dataProvider mediaRuleDataProvider + * @dataProvider provideMinWidthMediaRule */ - public function parsesRuleNameOfMediaQueries($css) + public function parsesArgumentsOfMediaQueries(string $css): void { $contents = (new Parser($css))->parse()->getContents(); $atRuleBlockList = $contents[0]; - self::assertSame('media', $atRuleBlockList->atRuleName()); + self::assertInstanceOf(AtRuleBlockList::class, $atRuleBlockList); + self::assertSame('(min-width: 768px)', $atRuleBlockList->atRuleArgs()); } /** * @test * - * @param string $css - * - * @dataProvider mediaRuleDataProvider + * @dataProvider provideMinWidthMediaRule + * @dataProvider provideSyntacticallyCorrectAtRule */ - public function parsesArgumentsOfMediaQueries($css) + public function parsesSyntacticallyCorrectAtRuleInStrictMode(string $css): void { - $contents = (new Parser($css))->parse()->getContents(); - $atRuleBlockList = $contents[0]; + $contents = (new Parser($css, Settings::create()->beStrict()))->parse()->getContents(); - self::assertSame('(min-width: 768px)', $atRuleBlockList->atRuleArgs()); + self::assertNotEmpty($contents, 'Failing CSS: `' . $css . '`'); } } diff --git a/tests/CSSList/DocumentTest.php b/tests/CSSList/DocumentTest.php deleted file mode 100644 index a727400b..00000000 --- a/tests/CSSList/DocumentTest.php +++ /dev/null @@ -1,88 +0,0 @@ -subject = new Document(); - } - - /** - * @test - */ - public function implementsRenderable() - { - self::assertInstanceOf(Renderable::class, $this->subject); - } - - /** - * @test - */ - public function implementsCommentable() - { - self::assertInstanceOf(Commentable::class, $this->subject); - } - - /** - * @test - */ - public function getContentsInitiallyReturnsEmptyArray() - { - self::assertSame([], $this->subject->getContents()); - } - - /** - * @return array>> - */ - public function contentsDataProvider() - { - return [ - 'empty array' => [[]], - '1 item' => [[new DeclarationBlock()]], - '2 items' => [[new DeclarationBlock(), new DeclarationBlock()]], - ]; - } - - /** - * @test - * - * @param array $contents - * - * @dataProvider contentsDataProvider - */ - public function setContentsSetsContents(array $contents) - { - $this->subject->setContents($contents); - - self::assertSame($contents, $this->subject->getContents()); - } - - /** - * @test - */ - public function setContentsReplacesContentsSetInPreviousCall() - { - $contents2 = [new DeclarationBlock()]; - - $this->subject->setContents([new DeclarationBlock()]); - $this->subject->setContents($contents2); - - self::assertSame($contents2, $this->subject->getContents()); - } -} diff --git a/tests/CSSList/KeyFrameTest.php b/tests/CSSList/KeyFrameTest.php deleted file mode 100644 index 080d5f94..00000000 --- a/tests/CSSList/KeyFrameTest.php +++ /dev/null @@ -1,49 +0,0 @@ -subject = new KeyFrame(); - } - - /** - * @test - */ - public function implementsAtRule() - { - self::assertInstanceOf(AtRule::class, $this->subject); - } - - /** - * @test - */ - public function implementsRenderable() - { - self::assertInstanceOf(Renderable::class, $this->subject); - } - - /** - * @test - */ - public function implementsCommentable() - { - self::assertInstanceOf(Commentable::class, $this->subject); - } -} diff --git a/tests/Comment/CommentTest.php b/tests/Comment/CommentTest.php index 29385f01..15d5983e 100644 --- a/tests/Comment/CommentTest.php +++ b/tests/Comment/CommentTest.php @@ -1,118 +1,24 @@ getComment()); - } - - /** - * @test - */ - public function getCommentInitiallyReturnsCommentPassedToConstructor() - { - $comment = 'There is no spoon.'; - $subject = new Comment($comment); - - self::assertSame($comment, $subject->getComment()); - } - - /** - * @test - */ - public function setCommentSetsComments() - { - $comment = 'There is no spoon.'; - $subject = new Comment(); - - $subject->setComment($comment); - - self::assertSame($comment, $subject->getComment()); - } - - /** - * @test - */ - public function getLineNoOnEmptyInstanceReturnsReturnsZero() - { - $subject = new Comment(); - - self::assertSame(0, $subject->getLineNo()); - } - - /** - * @test - */ - public function getLineNoInitiallyReturnsLineNumberPassedToConstructor() - { - $lineNumber = 42; - $subject = new Comment('', $lineNumber); - - self::assertSame($lineNumber, $subject->getLineNo()); - } - - /** - * @test - */ - public function toStringRendersCommentEnclosedInCommentDelimiters() - { - $comment = 'There is no spoon.'; - $subject = new Comment(); - - $subject->setComment($comment); - - self::assertSame('/*' . $comment . '*/', (string)$subject); - } - - /** - * @test - */ - public function renderRendersCommentEnclosedInCommentDelimiters() - { - $comment = 'There is no spoon.'; - $subject = new Comment(); - - $subject->setComment($comment); - - self::assertSame('/*' . $comment . '*/', $subject->render(new OutputFormat())); - } - - /** - * @test - */ - public function keepCommentsInOutput() + public function keepCommentsInOutput(): void { - $oCss = TestsParserTest::parsedStructureForFile('comments'); + $cssDocument = TestsParserTest::parsedStructureForFile('comments'); self::assertSame('/** Number 11 **/ /** @@ -137,24 +43,24 @@ public function keepCommentsInOutput() position: absolute; } } -', $oCss->render(OutputFormat::createPretty())); +', $cssDocument->render(OutputFormat::createPretty())); self::assertSame( '/** Number 11 **//**' . "\n" - . ' * Comments' . "\n" - . ' *//* Hell */@import url("some/url.css") screen;' - . '/* Number 4 *//* Number 5 */.foo,#bar{' - . '/* Number 6 */background-color:#000;}@media screen{' - . '/** Number 10 **/#foo.bar{/** Number 10b **/position:absolute;}}', - $oCss->render(OutputFormat::createCompact()->setRenderComments(true)) + . ' * Comments' . "\n" + . ' *//* Hell */@import url("some/url.css") screen;' + . '/* Number 4 *//* Number 5 */.foo,#bar{' + . '/* Number 6 */background-color:#000;}@media screen{' + . '/** Number 10 **/#foo.bar{/** Number 10b **/position:absolute;}}', + $cssDocument->render(OutputFormat::createCompact()->setRenderComments(true)) ); } /** * @test */ - public function stripCommentsFromOutput() + public function stripCommentsFromOutput(): void { - $oCss = TestsParserTest::parsedStructureForFile('comments'); + $css = TestsParserTest::parsedStructureForFile('comments'); self::assertSame(' @import url("some/url.css") screen; @@ -167,12 +73,12 @@ public function stripCommentsFromOutput() position: absolute; } } -', $oCss->render(OutputFormat::createPretty()->setRenderComments(false))); +', $css->render(OutputFormat::createPretty()->setRenderComments(false))); self::assertSame( '@import url("some/url.css") screen;' - . '.foo,#bar{background-color:#000;}' - . '@media screen{#foo.bar{position:absolute;}}', - $oCss->render(OutputFormat::createCompact()) + . '.foo,#bar{background-color:#000;}' + . '@media screen{#foo.bar{position:absolute;}}', + $css->render(OutputFormat::createCompact()) ); } } diff --git a/tests/Functional/CSSList/DocumentTest.php b/tests/Functional/CSSList/DocumentTest.php new file mode 100644 index 00000000..71334f7f --- /dev/null +++ b/tests/Functional/CSSList/DocumentTest.php @@ -0,0 +1,137 @@ +render()); + } + + /** + * @test + */ + public function renderWithVirginOutputFormatCanRenderEmptyDocument(): void + { + $subject = new Document(); + + self::assertSame('', $subject->render(new OutputFormat())); + } + + /** + * @test + */ + public function renderWithDefaultOutputFormatCanRenderEmptyDocument(): void + { + $subject = new Document(); + + self::assertSame('', $subject->render(OutputFormat::create())); + } + + /** + * @test + */ + public function renderWithCompactOutputFormatCanRenderEmptyDocument(): void + { + $subject = new Document(); + + self::assertSame('', $subject->render(OutputFormat::createCompact())); + } + + /** + * @test + */ + public function renderWithPrettyOutputFormatCanRenderEmptyDocument(): void + { + $subject = new Document(); + + self::assertSame('', $subject->render(OutputFormat::createPretty())); + } + + /** + * Builds a subject with one `@charset` rule and one `@media` rule. + */ + private function buildSubjectWithAtRules(): Document + { + $subject = new Document(); + $charset = new Charset(new CSSString('UTF-8')); + $subject->append($charset); + $mediaQuery = new AtRuleBlockList('media', 'screen'); + $subject->append($mediaQuery); + + return $subject; + } + + /** + * @test + */ + public function renderWithoutOutputFormatCanRenderAtRules(): void + { + $subject = $this->buildSubjectWithAtRules(); + + $expected = '@charset "UTF-8";' . "\n" . '@media screen {}'; + self::assertSame($expected, $subject->render()); + } + + /** + * @test + */ + public function renderWithVirginOutputFormatCanRenderAtRules(): void + { + $subject = $this->buildSubjectWithAtRules(); + + $expected = '@charset "UTF-8";' . "\n" . '@media screen {}'; + self::assertSame($expected, $subject->render(new OutputFormat())); + } + + /** + * @test + */ + public function renderWithDefaultOutputFormatCanRenderAtRules(): void + { + $subject = $this->buildSubjectWithAtRules(); + + $expected = '@charset "UTF-8";' . "\n" . '@media screen {}'; + self::assertSame($expected, $subject->render(OutputFormat::create())); + } + + /** + * @test + */ + public function renderWithCompactOutputFormatCanRenderAtRules(): void + { + $subject = $this->buildSubjectWithAtRules(); + + $expected = '@charset "UTF-8";@media screen{}'; + self::assertSame($expected, $subject->render(OutputFormat::createCompact())); + } + + /** + * @test + */ + public function renderWithPrettyOutputFormatCanRenderAtRules(): void + { + $subject = $this->buildSubjectWithAtRules(); + + $expected = "\n" . '@charset "UTF-8";' . "\n\n" . '@media screen {}' . "\n"; + self::assertSame($expected, $subject->render(OutputFormat::createPretty())); + } +} diff --git a/tests/Functional/Comment/CommentTest.php b/tests/Functional/Comment/CommentTest.php new file mode 100644 index 00000000..bbfa06a7 --- /dev/null +++ b/tests/Functional/Comment/CommentTest.php @@ -0,0 +1,67 @@ +setComment($comment); + + self::assertSame('/*' . $comment . '*/', $subject->render(new OutputFormat())); + } + + /** + * @test + */ + public function renderWithDefaultOutputFormatRendersCommentEnclosedInCommentDelimiters(): void + { + $comment = 'There is no spoon.'; + $subject = new Comment(); + + $subject->setComment($comment); + + self::assertSame('/*' . $comment . '*/', $subject->render(OutputFormat::create())); + } + + /** + * @test + */ + public function renderWithCompactOutputFormatRendersCommentEnclosedInCommentDelimiters(): void + { + $comment = 'There is no spoon.'; + $subject = new Comment(); + + $subject->setComment($comment); + + self::assertSame('/*' . $comment . '*/', $subject->render(OutputFormat::createCompact())); + } + + /** + * @test + */ + public function renderWithPrettyOutputFormatRendersCommentEnclosedInCommentDelimiters(): void + { + $comment = 'There is no spoon.'; + $subject = new Comment(); + + $subject->setComment($comment); + + self::assertSame('/*' . $comment . '*/', $subject->render(OutputFormat::createPretty())); + } +} diff --git a/tests/Functional/ParserTest.php b/tests/Functional/ParserTest.php new file mode 100644 index 00000000..982bb3a2 --- /dev/null +++ b/tests/Functional/ParserTest.php @@ -0,0 +1,39 @@ +parse(); + + self::assertInstanceOf(Document::class, $result); + } + + /** + * @test + */ + public function parseWithOneRuleSetReturnsDocument(): void + { + $parser = new Parser('.thing { }'); + + $result = $parser->parse(); + + self::assertInstanceOf(Document::class, $result); + } +} diff --git a/tests/Functional/Property/SelectorTest.php b/tests/Functional/Property/SelectorTest.php new file mode 100644 index 00000000..397dbc72 --- /dev/null +++ b/tests/Functional/Property/SelectorTest.php @@ -0,0 +1,59 @@ +render(new OutputFormat())); + } + + /** + * @test + */ + public function renderWithDefaultOutputFormatRendersSelectorPassedToConstructor(): void + { + $pattern = 'a'; + $subject = new Selector($pattern); + + self::assertSame($pattern, $subject->render(OutputFormat::create())); + } + + /** + * @test + */ + public function renderWithCompactOutputFormatRendersSelectorPassedToConstructor(): void + { + $pattern = 'a'; + $subject = new Selector($pattern); + + self::assertSame($pattern, $subject->render(OutputFormat::createCompact())); + } + + /** + * @test + */ + public function renderWithPrettyOutputFormatRendersSelectorPassedToConstructor(): void + { + $pattern = 'a'; + $subject = new Selector($pattern); + + self::assertSame($pattern, $subject->render(OutputFormat::createPretty())); + } +} diff --git a/tests/Functional/RuleSet/DeclarationBlockTest.php b/tests/Functional/RuleSet/DeclarationBlockTest.php new file mode 100644 index 00000000..fe6b9e51 --- /dev/null +++ b/tests/Functional/RuleSet/DeclarationBlockTest.php @@ -0,0 +1,69 @@ + + */ + public static function provideInvalidDeclarationBlock(): array + { + return [ + 'no selector' => ['{ color: red; }'], + 'invalid selector' => ['/ { color: red; }'], + 'no opening brace' => ['body color: red; }'], + ]; + } + + /** + * @test + * + * @dataProvider provideInvalidDeclarationBlock + */ + public function parseReturnsNullForInvalidDeclarationBlock(string $invalidDeclarationBlock): void + { + $parserState = new ParserState($invalidDeclarationBlock, Settings::create()); + + $result = DeclarationBlock::parse($parserState); + + self::assertNull($result); + } + + /** + * @test + */ + public function rendersRulesInOrderProvided(): void + { + $declarationBlock = new DeclarationBlock(); + $declarationBlock->setSelectors([new Selector('.test')]); + + $rule1 = new Rule('background-color'); + $rule1->setValue('transparent'); + $declarationBlock->addRule($rule1); + + $rule2 = new Rule('background'); + $rule2->setValue('#222'); + $declarationBlock->addRule($rule2); + + $rule3 = new Rule('background-color'); + $rule3->setValue('#fff'); + $declarationBlock->addRule($rule3); + + $expectedRendering = 'background-color: transparent;background: #222;background-color: #fff'; + self::assertStringContainsString($expectedRendering, $declarationBlock->render(new OutputFormat())); + } +} diff --git a/tests/Functional/Value/ValueTest.php b/tests/Functional/Value/ValueTest.php new file mode 100644 index 00000000..4d65ee2a --- /dev/null +++ b/tests/Functional/Value/ValueTest.php @@ -0,0 +1,45 @@ + + */ + private const DEFAULT_DELIMITERS = [',', ' ', '/']; + + /** + * @test + */ + public function parsesFirstArgumentInMaxFunction(): void + { + $parsedValue = Value::parseValue( + new ParserState('max(300px, 400px);', Settings::create()), + self::DEFAULT_DELIMITERS + ); + + self::assertInstanceOf(CSSFunction::class, $parsedValue); + $size = $parsedValue->getArguments()[0]; + self::assertInstanceOf(Size::class, $size); + self::assertSame(300.0, $size->getSize()); + self::assertSame('px', $size->getUnit()); + self::assertFalse($size->isColorComponent()); + } +} diff --git a/tests/FunctionalDeprecated/.gitkeep b/tests/FunctionalDeprecated/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/tests/OutputFormatTest.php b/tests/OutputFormatTest.php index 0de39123..9852ae58 100644 --- a/tests/OutputFormatTest.php +++ b/tests/OutputFormatTest.php @@ -1,5 +1,7 @@ oParser = new Parser(self::TEST_CSS); - $this->oDocument = $this->oParser->parse(); + $this->parser = new Parser(self::TEST_CSS); + $this->document = $this->parser->parse(); } /** * @test */ - public function plain() + public function plain(): void { self::assertSame( '.main, .test {font: italic normal bold 16px/1.2 "Helvetica",Verdana,sans-serif;background: white;} @media screen {.main {background-size: 100% 100%;font-size: 1.3em;background-color: #fff;}}', - $this->oDocument->render() + $this->document->render() ); } /** * @test */ - public function compact() + public function compact(): void { self::assertSame( '.main,.test{font:italic normal bold 16px/1.2 "Helvetica",Verdana,sans-serif;background:white;}' . '@media screen{.main{background-size:100% 100%;font-size:1.3em;background-color:#fff;}}', - $this->oDocument->render(OutputFormat::createCompact()) + $this->document->render(OutputFormat::createCompact()) ); } /** * @test */ - public function pretty() + public function pretty(): void { - self::assertSame(self::TEST_CSS, $this->oDocument->render(OutputFormat::createPretty())); + self::assertSame(self::TEST_CSS, $this->document->render(OutputFormat::createPretty())); } /** * @test */ - public function spaceAfterListArgumentSeparator() + public function spaceAfterListArgumentSeparator(): void { self::assertSame( '.main, .test {font: italic normal bold 16px/ 1.2 ' . '"Helvetica", Verdana, sans-serif;background: white;}' . "\n@media screen {.main {background-size: 100% 100%;font-size: 1.3em;background-color: #fff;}}", - $this->oDocument->render(OutputFormat::create()->setSpaceAfterListArgumentSeparator(" ")) + $this->document->render(OutputFormat::create()->setSpaceAfterListArgumentSeparator(' ')) ); } /** * @test */ - public function spaceAfterListArgumentSeparatorComplex() + public function spaceAfterListArgumentSeparatorComplex(): void { self::assertSame( '.main, .test {font: italic normal bold 16px/1.2 "Helvetica", Verdana, sans-serif;background: white;}' . "\n@media screen {.main {background-size: 100% 100%;font-size: 1.3em;background-color: #fff;}}", - $this->oDocument->render(OutputFormat::create()->setSpaceAfterListArgumentSeparator([ - 'default' => ' ', - ',' => "\t", - '/' => '', - ' ' => '', - ])) + $this->document->render( + OutputFormat::create() + ->setSpaceAfterListArgumentSeparator(' ') + ->setSpaceAfterListArgumentSeparators([ + ',' => "\t", + '/' => '', + ' ' => '', + ]) + ) ); } /** * @test */ - public function spaceAfterSelectorSeparator() + public function spaceAfterSelectorSeparator(): void { self::assertSame( '.main, .test {font: italic normal bold 16px/1.2 "Helvetica",Verdana,sans-serif;background: white;} @media screen {.main {background-size: 100% 100%;font-size: 1.3em;background-color: #fff;}}', - $this->oDocument->render(OutputFormat::create()->setSpaceAfterSelectorSeparator("\n")) + $this->document->render(OutputFormat::create()->setSpaceAfterSelectorSeparator("\n")) ); } /** * @test */ - public function stringQuotingType() + public function stringQuotingType(): void { self::assertSame( '.main, .test {font: italic normal bold 16px/1.2 \'Helvetica\',Verdana,sans-serif;background: white;} @media screen {.main {background-size: 100% 100%;font-size: 1.3em;background-color: #fff;}}', - $this->oDocument->render(OutputFormat::create()->setStringQuotingType("'")) + $this->document->render(OutputFormat::create()->setStringQuotingType("'")) ); } /** * @test */ - public function rGBHashNotation() + public function rGBHashNotation(): void { self::assertSame( '.main, .test {font: italic normal bold 16px/1.2 "Helvetica",Verdana,sans-serif;background: white;} @media screen {.main {background-size: 100% 100%;font-size: 1.3em;background-color: rgb(255,255,255);}}', - $this->oDocument->render(OutputFormat::create()->setRGBHashNotation(false)) + $this->document->render(OutputFormat::create()->setRGBHashNotation(false)) ); } /** * @test */ - public function semicolonAfterLastRule() + public function semicolonAfterLastRule(): void { self::assertSame( '.main, .test {font: italic normal bold 16px/1.2 "Helvetica",Verdana,sans-serif;background: white} @media screen {.main {background-size: 100% 100%;font-size: 1.3em;background-color: #fff}}', - $this->oDocument->render(OutputFormat::create()->setSemicolonAfterLastRule(false)) + $this->document->render(OutputFormat::create()->setSemicolonAfterLastRule(false)) ); } /** * @test */ - public function spaceAfterRuleName() + public function spaceAfterRuleName(): void { self::assertSame( '.main, .test {font: italic normal bold 16px/1.2 "Helvetica",Verdana,sans-serif;background: white;} @media screen {.main {background-size: 100% 100%;font-size: 1.3em;background-color: #fff;}}', - $this->oDocument->render(OutputFormat::create()->setSpaceAfterRuleName("\t")) + $this->document->render(OutputFormat::create()->setSpaceAfterRuleName("\t")) ); } /** * @test */ - public function spaceRules() + public function spaceRules(): void { + $outputFormat = OutputFormat::create() + ->setSpaceBeforeRules("\n") + ->setSpaceBetweenRules("\n") + ->setSpaceAfterRules("\n"); + self::assertSame('.main, .test { font: italic normal bold 16px/1.2 "Helvetica",Verdana,sans-serif; background: white; @@ -185,27 +195,40 @@ public function spaceRules() background-size: 100% 100%; font-size: 1.3em; background-color: #fff; - }}', $this->oDocument->render(OutputFormat::create()->set('Space*Rules', "\n"))); + }}', $this->document->render($outputFormat)); } /** * @test */ - public function spaceBlocks() + public function spaceBlocks(): void { + $outputFormat = OutputFormat::create() + ->setSpaceBeforeBlocks("\n") + ->setSpaceBetweenBlocks("\n") + ->setSpaceAfterBlocks("\n"); + self::assertSame(' .main, .test {font: italic normal bold 16px/1.2 "Helvetica",Verdana,sans-serif;background: white;} @media screen { .main {background-size: 100% 100%;font-size: 1.3em;background-color: #fff;} } -', $this->oDocument->render(OutputFormat::create()->set('Space*Blocks', "\n"))); +', $this->document->render($outputFormat)); } /** * @test */ - public function spaceBoth() + public function spaceBoth(): void { + $outputFormat = OutputFormat::create() + ->setSpaceBeforeRules("\n") + ->setSpaceBetweenRules("\n") + ->setSpaceAfterRules("\n") + ->setSpaceBeforeBlocks("\n") + ->setSpaceBetweenBlocks("\n") + ->setSpaceAfterBlocks("\n"); + self::assertSame(' .main, .test { font: italic normal bold 16px/1.2 "Helvetica",Verdana,sans-serif; @@ -218,26 +241,38 @@ public function spaceBoth() background-color: #fff; } } -', $this->oDocument->render(OutputFormat::create()->set('Space*Rules', "\n")->set('Space*Blocks', "\n"))); +', $this->document->render($outputFormat)); } /** * @test */ - public function spaceBetweenBlocks() + public function spaceBetweenBlocks(): void { + $outputFormat = OutputFormat::create() + ->setSpaceBetweenBlocks(''); + self::assertSame( '.main, .test {font: italic normal bold 16px/1.2 "Helvetica",Verdana,sans-serif;background: white;}' . '@media screen {.main {background-size: 100% 100%;font-size: 1.3em;background-color: #fff;}}', - $this->oDocument->render(OutputFormat::create()->setSpaceBetweenBlocks('')) + $this->document->render($outputFormat) ); } /** * @test */ - public function indentation() + public function indentation(): void { + $outputFormat = OutputFormat::create() + ->setSpaceBeforeRules("\n") + ->setSpaceBetweenRules("\n") + ->setSpaceAfterRules("\n") + ->setSpaceBeforeBlocks("\n") + ->setSpaceBetweenBlocks("\n") + ->setSpaceAfterBlocks("\n") + ->setIndentation(''); + self::assertSame(' .main, .test { font: italic normal bold 16px/1.2 "Helvetica",Verdana,sans-serif; @@ -250,55 +285,59 @@ public function indentation() background-color: #fff; } } -', $this->oDocument->render(OutputFormat::create() - ->set('Space*Rules', "\n") - ->set('Space*Blocks', "\n") - ->setIndentation(''))); +', $this->document->render($outputFormat)); } /** * @test */ - public function spaceBeforeBraces() + public function spaceBeforeBraces(): void { + $outputFormat = OutputFormat::create() + ->setSpaceBeforeOpeningBrace(''); + self::assertSame( '.main, .test{font: italic normal bold 16px/1.2 "Helvetica",Verdana,sans-serif;background: white;} @media screen{.main{background-size: 100% 100%;font-size: 1.3em;background-color: #fff;}}', - $this->oDocument->render(OutputFormat::create()->setSpaceBeforeOpeningBrace('')) + $this->document->render($outputFormat) ); } /** * @test */ - public function ignoreExceptionsOff() + public function ignoreExceptionsOff(): void { $this->expectException(OutputException::class); - $aBlocks = $this->oDocument->getAllDeclarationBlocks(); - $oFirstBlock = $aBlocks[0]; - $oFirstBlock->removeSelector('.main'); + $outputFormat = OutputFormat::create()->setIgnoreExceptions(false); + + $declarationBlocks = $this->document->getAllDeclarationBlocks(); + $firstDeclarationBlock = $declarationBlocks[0]; + $firstDeclarationBlock->removeSelector('.main'); self::assertSame( '.test {font: italic normal bold 16px/1.2 "Helvetica",Verdana,sans-serif;background: white;} @media screen {.main {background-size: 100% 100%;font-size: 1.3em;background-color: #fff;}}', - $this->oDocument->render(OutputFormat::create()->setIgnoreExceptions(false)) + $this->document->render($outputFormat) ); - $oFirstBlock->removeSelector('.test'); - $this->oDocument->render(OutputFormat::create()->setIgnoreExceptions(false)); + $firstDeclarationBlock->removeSelector('.test'); + $this->document->render($outputFormat); } /** * @test */ - public function ignoreExceptionsOn() + public function ignoreExceptionsOn(): void { - $aBlocks = $this->oDocument->getAllDeclarationBlocks(); - $oFirstBlock = $aBlocks[0]; - $oFirstBlock->removeSelector('.main'); - $oFirstBlock->removeSelector('.test'); + $outputFormat = OutputFormat::create()->setIgnoreExceptions(true); + + $declarationBlocks = $this->document->getAllDeclarationBlocks(); + $firstDeclarationBlock = $declarationBlocks[0]; + $firstDeclarationBlock->removeSelector('.main'); + $firstDeclarationBlock->removeSelector('.test'); self::assertSame( '@media screen {.main {background-size: 100% 100%;font-size: 1.3em;background-color: #fff;}}', - $this->oDocument->render(OutputFormat::create()->setIgnoreExceptions(true)) + $this->document->render($outputFormat) ); } } diff --git a/tests/ParserTest.php b/tests/ParserTest.php index 74449ee2..656d943b 100644 --- a/tests/ParserTest.php +++ b/tests/ParserTest.php @@ -1,8 +1,12 @@ parse()->render()); + self::assertNotEquals('', $parser->parse()->render()); } catch (\Exception $e) { self::fail($e); } } - closedir($rHandle); + \closedir($directoryHandle); } } @@ -90,66 +88,74 @@ public function files() * * @test */ - public function colorParsing() + public function colorParsing(): void { - $oDoc = self::parsedStructureForFile('colortest'); - foreach ($oDoc->getAllRuleSets() as $oRuleSet) { - if (!$oRuleSet instanceof DeclarationBlock) { + $document = self::parsedStructureForFile('colortest'); + foreach ($document->getAllRuleSets() as $ruleSet) { + if (!($ruleSet instanceof DeclarationBlock)) { continue; } - $sSelector = $oRuleSet->getSelectors(); - $sSelector = $sSelector[0]->getSelector(); - if ($sSelector === '#mine') { - $aColorRule = $oRuleSet->getRules('color'); - $oColor = $aColorRule[0]->getValue(); - self::assertSame('red', $oColor); - $aColorRule = $oRuleSet->getRules('background-'); - $oColor = $aColorRule[0]->getValue(); + $selectors = $ruleSet->getSelectors(); + $selector = $selectors[0]->getSelector(); + if ($selector === '#mine') { + $colorRules = $ruleSet->getRules('color'); + $colorRuleValue = $colorRules[0]->getValue(); + self::assertSame('red', $colorRuleValue); + $colorRules = $ruleSet->getRules('background-'); + $colorRuleValue = $colorRules[0]->getValue(); + self::assertInstanceOf(Color::class, $colorRuleValue); self::assertEquals([ - 'r' => new Size(35.0, null, true, $oColor->getLineNo()), - 'g' => new Size(35.0, null, true, $oColor->getLineNo()), - 'b' => new Size(35.0, null, true, $oColor->getLineNo()), - ], $oColor->getColor()); - $aColorRule = $oRuleSet->getRules('border-color'); - $oColor = $aColorRule[0]->getValue(); + 'r' => new Size(35.0, null, true, $colorRuleValue->getLineNo()), + 'g' => new Size(35.0, null, true, $colorRuleValue->getLineNo()), + 'b' => new Size(35.0, null, true, $colorRuleValue->getLineNo()), + ], $colorRuleValue->getColor()); + $colorRules = $ruleSet->getRules('border-color'); + $colorRuleValue = $colorRules[0]->getValue(); + self::assertInstanceOf(Color::class, $colorRuleValue); self::assertEquals([ - 'r' => new Size(10.0, null, true, $oColor->getLineNo()), - 'g' => new Size(100.0, null, true, $oColor->getLineNo()), - 'b' => new Size(230.0, null, true, $oColor->getLineNo()), - ], $oColor->getColor()); - $oColor = $aColorRule[1]->getValue(); + 'r' => new Size(10.0, null, true, $colorRuleValue->getLineNo()), + 'g' => new Size(100.0, null, true, $colorRuleValue->getLineNo()), + 'b' => new Size(230.0, null, true, $colorRuleValue->getLineNo()), + ], $colorRuleValue->getColor()); + $colorRuleValue = $colorRules[1]->getValue(); + self::assertInstanceOf(Color::class, $colorRuleValue); self::assertEquals([ - 'r' => new Size(10.0, null, true, $oColor->getLineNo()), - 'g' => new Size(100.0, null, true, $oColor->getLineNo()), - 'b' => new Size(231.0, null, true, $oColor->getLineNo()), - 'a' => new Size("0000.3", null, true, $oColor->getLineNo()), - ], $oColor->getColor()); - $aColorRule = $oRuleSet->getRules('outline-color'); - $oColor = $aColorRule[0]->getValue(); + 'r' => new Size(10.0, null, true, $colorRuleValue->getLineNo()), + 'g' => new Size(100.0, null, true, $colorRuleValue->getLineNo()), + 'b' => new Size(231.0, null, true, $colorRuleValue->getLineNo()), + 'a' => new Size('0000.3', null, true, $colorRuleValue->getLineNo()), + ], $colorRuleValue->getColor()); + $colorRules = $ruleSet->getRules('outline-color'); + $colorRuleValue = $colorRules[0]->getValue(); + self::assertInstanceOf(Color::class, $colorRuleValue); self::assertEquals([ - 'r' => new Size(34.0, null, true, $oColor->getLineNo()), - 'g' => new Size(34.0, null, true, $oColor->getLineNo()), - 'b' => new Size(34.0, null, true, $oColor->getLineNo()), - ], $oColor->getColor()); - } elseif ($sSelector === '#yours') { - $aColorRule = $oRuleSet->getRules('background-color'); - $oColor = $aColorRule[0]->getValue(); + 'r' => new Size(34.0, null, true, $colorRuleValue->getLineNo()), + 'g' => new Size(34.0, null, true, $colorRuleValue->getLineNo()), + 'b' => new Size(34.0, null, true, $colorRuleValue->getLineNo()), + ], $colorRuleValue->getColor()); + } elseif ($selector === '#yours') { + $colorRules = $ruleSet->getRules('background-color'); + $colorRuleValue = $colorRules[0]->getValue(); + self::assertInstanceOf(Color::class, $colorRuleValue); self::assertEquals([ - 'h' => new Size(220.0, null, true, $oColor->getLineNo()), - 's' => new Size(10.0, '%', true, $oColor->getLineNo()), - 'l' => new Size(220.0, '%', true, $oColor->getLineNo()), - ], $oColor->getColor()); - $oColor = $aColorRule[1]->getValue(); + 'h' => new Size(220.0, null, true, $colorRuleValue->getLineNo()), + 's' => new Size(10.0, '%', true, $colorRuleValue->getLineNo()), + 'l' => new Size(220.0, '%', true, $colorRuleValue->getLineNo()), + ], $colorRuleValue->getColor()); + $colorRuleValue = $colorRules[1]->getValue(); + self::assertInstanceOf(Color::class, $colorRuleValue); self::assertEquals([ - 'h' => new Size(220.0, null, true, $oColor->getLineNo()), - 's' => new Size(10.0, '%', true, $oColor->getLineNo()), - 'l' => new Size(220.0, '%', true, $oColor->getLineNo()), - 'a' => new Size(0000.3, null, true, $oColor->getLineNo()), - ], $oColor->getColor()); + 'h' => new Size(220.0, null, true, $colorRuleValue->getLineNo()), + 's' => new Size(10.0, '%', true, $colorRuleValue->getLineNo()), + 'l' => new Size(220.0, '%', true, $colorRuleValue->getLineNo()), + 'a' => new Size(0000.3, null, true, $colorRuleValue->getLineNo()), + ], $colorRuleValue->getColor()); + $colorRules = $ruleSet->getRules('outline-color'); + self::assertEmpty($colorRules); } } - foreach ($oDoc->getAllValues('color') as $sColor) { - self::assertSame('red', $sColor); + foreach ($document->getAllValues(null, 'color') as $colorValue) { + self::assertSame('red', $colorValue); } self::assertSame( '#mine {color: red;border-color: #0a64e6;border-color: rgba(10,100,231,.3);outline-color: #222;' @@ -163,57 +169,56 @@ public function colorParsing() . "\n" . '#variables-alpha {background-color: rgba(var(--some-rgb),.1);' . 'background-color: rgba(var(--some-rg),255,.1);background-color: hsla(var(--some-hsl),.1);}', - $oDoc->render() + $document->render() ); } /** * @test */ - public function unicodeParsing() + public function unicodeParsing(): void { - $oDoc = self::parsedStructureForFile('unicode'); - foreach ($oDoc->getAllDeclarationBlocks() as $oRuleSet) { - $sSelector = $oRuleSet->getSelectors(); - $sSelector = $sSelector[0]->getSelector(); - if (substr($sSelector, 0, strlen('.test-')) !== '.test-') { + $document = self::parsedStructureForFile('unicode'); + foreach ($document->getAllDeclarationBlocks() as $ruleSet) { + $selectors = $ruleSet->getSelectors(); + $selector = $selectors[0]->getSelector(); + if (\substr($selector, 0, \strlen('.test-')) !== '.test-') { continue; } - $aContentRules = $oRuleSet->getRules('content'); - $aContents = $aContentRules[0]->getValues(); - $sString = $aContents[0][0]->__toString(); - if ($sSelector == '.test-1') { - self::assertSame('" "', $sString); + $contentRules = $ruleSet->getRules('content'); + $firstContentRuleAsString = $contentRules[0]->getValue()->render(OutputFormat::create()); + if ($selector === '.test-1') { + self::assertSame('" "', $firstContentRuleAsString); } - if ($sSelector == '.test-2') { - self::assertSame('"é"', $sString); + if ($selector === '.test-2') { + self::assertSame('"é"', $firstContentRuleAsString); } - if ($sSelector == '.test-3') { - self::assertSame('" "', $sString); + if ($selector === '.test-3') { + self::assertSame('" "', $firstContentRuleAsString); } - if ($sSelector == '.test-4') { - self::assertSame('"𝄞"', $sString); + if ($selector === '.test-4') { + self::assertSame('"𝄞"', $firstContentRuleAsString); } - if ($sSelector == '.test-5') { - self::assertSame('"水"', $sString); + if ($selector === '.test-5') { + self::assertSame('"水"', $firstContentRuleAsString); } - if ($sSelector == '.test-6') { - self::assertSame('"¥"', $sString); + if ($selector === '.test-6') { + self::assertSame('"¥"', $firstContentRuleAsString); } - if ($sSelector == '.test-7') { - self::assertSame('"\A"', $sString); + if ($selector === '.test-7') { + self::assertSame('"\\A"', $firstContentRuleAsString); } - if ($sSelector == '.test-8') { - self::assertSame('"\"\""', $sString); + if ($selector === '.test-8') { + self::assertSame('"\\"\\""', $firstContentRuleAsString); } - if ($sSelector == '.test-9') { - self::assertSame('"\"\\\'"', $sString); + if ($selector === '.test-9') { + self::assertSame('"\\"\\\'"', $firstContentRuleAsString); } - if ($sSelector == '.test-10') { - self::assertSame('"\\\'\\\\"', $sString); + if ($selector === '.test-10') { + self::assertSame('"\\\'\\\\"', $firstContentRuleAsString); } - if ($sSelector == '.test-11') { - self::assertSame('"test"', $sString); + if ($selector === '.test-11') { + self::assertSame('"test"', $firstContentRuleAsString); } } } @@ -221,71 +226,47 @@ public function unicodeParsing() /** * @test */ - public function unicodeRangeParsing() + public function unicodeRangeParsing(): void { - $oDoc = self::parsedStructureForFile('unicode-range'); - $sExpected = "@font-face {unicode-range: U+0100-024F,U+0259,U+1E??-2EFF,U+202F;}"; - self::assertSame($sExpected, $oDoc->render()); + $document = self::parsedStructureForFile('unicode-range'); + $expected = '@font-face {unicode-range: U+0100-024F,U+0259,U+1E??-2EFF,U+202F;}'; + self::assertSame($expected, $document->render()); } /** * @test */ - public function specificity() + public function specificity(): void { - $oDoc = self::parsedStructureForFile('specificity'); - $oDeclarationBlock = $oDoc->getAllDeclarationBlocks(); - $oDeclarationBlock = $oDeclarationBlock[0]; - $aSelectors = $oDeclarationBlock->getSelectors(); - foreach ($aSelectors as $oSelector) { - switch ($oSelector->getSelector()) { - case "#test .help": - self::assertSame(110, $oSelector->getSpecificity()); - break; - case "#file": - self::assertSame(100, $oSelector->getSpecificity()); - break; - case ".help:hover": - self::assertSame(20, $oSelector->getSpecificity()); - break; - case "ol li::before": - self::assertSame(3, $oSelector->getSpecificity()); - break; - case "li.green": - self::assertSame(11, $oSelector->getSpecificity()); - break; - default: - self::fail("specificity: untested selector " . $oSelector->getSelector()); - } - } - self::assertEquals([new Selector('#test .help', true)], $oDoc->getSelectorsBySpecificity('> 100')); + $document = self::parsedStructureForFile('specificity'); + self::assertEquals([new Selector('#test .help')], $document->getSelectorsBySpecificity('> 100')); self::assertEquals( - [new Selector('#test .help', true), new Selector('#file', true)], - $oDoc->getSelectorsBySpecificity('>= 100') + [new Selector('#test .help'), new Selector('#file')], + $document->getSelectorsBySpecificity('>= 100') ); - self::assertEquals([new Selector('#file', true)], $oDoc->getSelectorsBySpecificity('=== 100')); - self::assertEquals([new Selector('#file', true)], $oDoc->getSelectorsBySpecificity('== 100')); + self::assertEquals([new Selector('#file')], $document->getSelectorsBySpecificity('=== 100')); + self::assertEquals([new Selector('#file')], $document->getSelectorsBySpecificity('== 100')); self::assertEquals([ - new Selector('#file', true), - new Selector('.help:hover', true), - new Selector('li.green', true), - new Selector('ol li::before', true), - ], $oDoc->getSelectorsBySpecificity('<= 100')); + new Selector('#file'), + new Selector('.help:hover'), + new Selector('li.green'), + new Selector('ol li::before'), + ], $document->getSelectorsBySpecificity('<= 100')); self::assertEquals([ - new Selector('.help:hover', true), - new Selector('li.green', true), - new Selector('ol li::before', true), - ], $oDoc->getSelectorsBySpecificity('< 100')); - self::assertEquals([new Selector('li.green', true)], $oDoc->getSelectorsBySpecificity('11')); - self::assertEquals([new Selector('ol li::before', true)], $oDoc->getSelectorsBySpecificity(3)); + new Selector('.help:hover'), + new Selector('li.green'), + new Selector('ol li::before'), + ], $document->getSelectorsBySpecificity('< 100')); + self::assertEquals([new Selector('li.green')], $document->getSelectorsBySpecificity('11')); + self::assertEquals([new Selector('ol li::before')], $document->getSelectorsBySpecificity('3')); } /** * @test */ - public function manipulation() + public function manipulation(): void { - $oDoc = self::parsedStructureForFile('atrules'); + $document = self::parsedStructureForFile('atrules'); self::assertSame( '@charset "utf-8";' . "\n" @@ -317,12 +298,12 @@ public function manipulation() . '@media screen and (orientation: landscape) {@-ms-viewport {width: 1024px;height: 768px;}}' . "\n" . '@region-style #intro {p {color: blue;}}', - $oDoc->render() + $document->render() ); - foreach ($oDoc->getAllDeclarationBlocks() as $oBlock) { - foreach ($oBlock->getSelectors() as $oSelector) { + foreach ($document->getAllDeclarationBlocks() as $declarationBlock) { + foreach ($declarationBlock->getSelectors() as $selector) { //Loop over all selector parts (the comma-separated strings in a selector) and prepend the id - $oSelector->setSelector('#my_id ' . $oSelector->getSelector()); + $selector->setSelector('#my_id ' . $selector->getSelector()); } } self::assertSame( @@ -356,107 +337,111 @@ public function manipulation() . '@media screen and (orientation: landscape) {@-ms-viewport {width: 1024px;height: 768px;}}' . "\n" . '@region-style #intro {#my_id p {color: blue;}}', - $oDoc->render(OutputFormat::create()->setRenderComments(false)) + $document->render(OutputFormat::create()->setRenderComments(false)) ); - $oDoc = self::parsedStructureForFile('values'); + $document = self::parsedStructureForFile('values'); self::assertSame( '#header {margin: 10px 2em 1cm 2%;font-family: Verdana,Helvetica,"Gill Sans",sans-serif;' . 'font-size: 10px;color: red !important;background-color: green;' . 'background-color: rgba(0,128,0,.7);frequency: 30Hz;transform: rotate(1turn);} body {color: green;font: 75% "Lucida Grande","Trebuchet MS",Verdana,sans-serif;}', - $oDoc->render() + $document->render() ); - foreach ($oDoc->getAllRuleSets() as $oRuleSet) { - $oRuleSet->removeRule('font-'); + foreach ($document->getAllRuleSets() as $ruleSet) { + $ruleSet->removeMatchingRules('font-'); } self::assertSame( '#header {margin: 10px 2em 1cm 2%;color: red !important;background-color: green;' . 'background-color: rgba(0,128,0,.7);frequency: 30Hz;transform: rotate(1turn);} body {color: green;}', - $oDoc->render() + $document->render() ); - foreach ($oDoc->getAllRuleSets() as $oRuleSet) { - $oRuleSet->removeRule('background-'); + foreach ($document->getAllRuleSets() as $ruleSet) { + $ruleSet->removeMatchingRules('background-'); } self::assertSame( '#header {margin: 10px 2em 1cm 2%;color: red !important;frequency: 30Hz;transform: rotate(1turn);} body {color: green;}', - $oDoc->render() + $document->render() ); } /** * @test */ - public function ruleGetters() + public function ruleGetters(): void { - $oDoc = self::parsedStructureForFile('values'); - $aBlocks = $oDoc->getAllDeclarationBlocks(); - $oHeaderBlock = $aBlocks[0]; - $oBodyBlock = $aBlocks[1]; - $aHeaderRules = $oHeaderBlock->getRules('background-'); - self::assertCount(2, $aHeaderRules); - self::assertSame('background-color', $aHeaderRules[0]->getRule()); - self::assertSame('background-color', $aHeaderRules[1]->getRule()); - $aHeaderRules = $oHeaderBlock->getRulesAssoc('background-'); - self::assertCount(1, $aHeaderRules); - self::assertTrue($aHeaderRules['background-color']->getValue() instanceof Color); - self::assertSame('rgba', $aHeaderRules['background-color']->getValue()->getColorDescription()); - $oHeaderBlock->removeRule($aHeaderRules['background-color']); - $aHeaderRules = $oHeaderBlock->getRules('background-'); - self::assertCount(1, $aHeaderRules); - self::assertSame('green', $aHeaderRules[0]->getValue()); + $document = self::parsedStructureForFile('values'); + $declarationBlocks = $document->getAllDeclarationBlocks(); + $headerBlock = $declarationBlocks[0]; + $bodyBlock = $declarationBlocks[1]; + $backgroundHeaderRules = $headerBlock->getRules('background-'); + self::assertCount(2, $backgroundHeaderRules); + self::assertSame('background-color', $backgroundHeaderRules[0]->getRule()); + self::assertSame('background-color', $backgroundHeaderRules[1]->getRule()); + $backgroundHeaderRules = $headerBlock->getRulesAssoc('background-'); + self::assertCount(1, $backgroundHeaderRules); + self::assertInstanceOf(Color::class, $backgroundHeaderRules['background-color']->getValue()); + self::assertSame('rgba', $backgroundHeaderRules['background-color']->getValue()->getColorDescription()); + $headerBlock->removeRule($backgroundHeaderRules['background-color']); + $backgroundHeaderRules = $headerBlock->getRules('background-'); + self::assertCount(1, $backgroundHeaderRules); + self::assertSame('green', $backgroundHeaderRules[0]->getValue()); } /** * @test */ - public function slashedValues() + public function slashedValues(): void { - $oDoc = self::parsedStructureForFile('slashed'); + $document = self::parsedStructureForFile('slashed'); self::assertSame( '.test {font: 12px/1.5 Verdana,Arial,sans-serif;border-radius: 5px 10px 5px 10px/10px 5px 10px 5px;}', - $oDoc->render() + $document->render() ); - foreach ($oDoc->getAllValues(null) as $mValue) { - if ($mValue instanceof Size && $mValue->isSize() && !$mValue->isRelative()) { - $mValue->setSize($mValue->getSize() * 3); + foreach ($document->getAllValues(null) as $value) { + if ($value instanceof Size && $value->isSize() && !$value->isRelative()) { + $value->setSize($value->getSize() * 3); } } - foreach ($oDoc->getAllDeclarationBlocks() as $oBlock) { - $oRule = $oBlock->getRules('font'); - $oRule = $oRule[0]; - $oSpaceList = $oRule->getValue(); - self::assertSame(' ', $oSpaceList->getListSeparator()); - $oSlashList = $oSpaceList->getListComponents(); - $oCommaList = $oSlashList[1]; - $oSlashList = $oSlashList[0]; - self::assertSame(',', $oCommaList->getListSeparator()); - self::assertSame('/', $oSlashList->getListSeparator()); - $oRule = $oBlock->getRules('border-radius'); - $oRule = $oRule[0]; - $oSlashList = $oRule->getValue(); - self::assertSame('/', $oSlashList->getListSeparator()); - $oSpaceList1 = $oSlashList->getListComponents(); - $oSpaceList2 = $oSpaceList1[1]; - $oSpaceList1 = $oSpaceList1[0]; - self::assertSame(' ', $oSpaceList1->getListSeparator()); - self::assertSame(' ', $oSpaceList2->getListSeparator()); + foreach ($document->getAllDeclarationBlocks() as $declarationBlock) { + $fontRules = $declarationBlock->getRules('font'); + $fontRule = $fontRules[0]; + $fontRuleValue = $fontRule->getValue(); + self::assertSame(' ', $fontRuleValue->getListSeparator()); + $fontRuleValueComponents = $fontRuleValue->getListComponents(); + $commaList = $fontRuleValueComponents[1]; + self::assertInstanceOf(ValueList::class, $commaList); + $slashList = $fontRuleValueComponents[0]; + self::assertInstanceOf(ValueList::class, $slashList); + self::assertSame(',', $commaList->getListSeparator()); + self::assertSame('/', $slashList->getListSeparator()); + $borderRadiusRules = $declarationBlock->getRules('border-radius'); + $borderRadiusRule = $borderRadiusRules[0]; + $slashList = $borderRadiusRule->getValue(); + self::assertSame('/', $slashList->getListSeparator()); + $slashListComponents = $slashList->getListComponents(); + $secondSlashListComponent = $slashListComponents[1]; + self::assertInstanceOf(ValueList::class, $secondSlashListComponent); + $firstSlashListComponent = $slashListComponents[0]; + self::assertInstanceOf(ValueList::class, $firstSlashListComponent); + self::assertSame(' ', $firstSlashListComponent->getListSeparator()); + self::assertSame(' ', $secondSlashListComponent->getListSeparator()); } self::assertSame( '.test {font: 36px/1.5 Verdana,Arial,sans-serif;border-radius: 15px 30px 15px 30px/30px 15px 30px 15px;}', - $oDoc->render() + $document->render() ); } /** * @test */ - public function functionSyntax() + public function functionSyntax(): void { - $oDoc = self::parsedStructureForFile('functions'); - $sExpected = 'div.main {background-image: linear-gradient(#000,#fff);}' + $document = self::parsedStructureForFile('functions'); + $expected = 'div.main {background-image: linear-gradient(#000,#fff);}' . "\n" . '.collapser::before, .collapser::-moz-before, .collapser::-webkit-before {content: "»";font-size: 1.2em;' . 'margin-right: .2em;-moz-transition-property: -moz-transform;-moz-transition-duration: .2s;' @@ -469,391 +454,353 @@ public function functionSyntax() . '-moz-transition-duration: .3s;}' . "\n" . '.collapser.expanded + * {height: auto;}'; - self::assertSame($sExpected, $oDoc->render()); + self::assertSame($expected, $document->render()); - foreach ($oDoc->getAllValues(null, true) as $mValue) { - if ($mValue instanceof Size && $mValue->isSize()) { - $mValue->setSize($mValue->getSize() * 3); + foreach ($document->getAllValues(null, null, true) as $value) { + if ($value instanceof Size && $value->isSize()) { + $value->setSize($value->getSize() * 3); } } - $sExpected = str_replace(['1.2em', '.2em', '60%'], ['3.6em', '.6em', '180%'], $sExpected); - self::assertSame($sExpected, $oDoc->render()); + $expected = \str_replace(['1.2em', '.2em', '60%'], ['3.6em', '.6em', '180%'], $expected); + self::assertSame($expected, $document->render()); - foreach ($oDoc->getAllValues(null, true) as $mValue) { - if ($mValue instanceof Size && !$mValue->isRelative() && !$mValue->isColorComponent()) { - $mValue->setSize($mValue->getSize() * 2); + foreach ($document->getAllValues(null, null, true) as $value) { + if ($value instanceof Size && !$value->isRelative() && !$value->isColorComponent()) { + $value->setSize($value->getSize() * 2); } } - $sExpected = str_replace(['.2s', '.3s', '90deg'], ['.4s', '.6s', '180deg'], $sExpected); - self::assertSame($sExpected, $oDoc->render()); - } - - /** - * @test - */ - public function expandShorthands() - { - $oDoc = self::parsedStructureForFile('expand-shorthands'); - $sExpected = 'body {font: italic 500 14px/1.618 "Trebuchet MS",Georgia,serif;border: 2px solid #f0f;' - . 'background: #ccc url("/images/foo.png") no-repeat left top;margin: 1em !important;' - . 'padding: 2px 6px 3px;}'; - self::assertSame($sExpected, $oDoc->render()); - $oDoc->expandShorthands(); - $sExpected = 'body {margin-top: 1em !important;margin-right: 1em !important;margin-bottom: 1em !important;' - . 'margin-left: 1em !important;padding-top: 2px;padding-right: 6px;padding-bottom: 3px;' - . 'padding-left: 6px;border-top-color: #f0f;border-right-color: #f0f;border-bottom-color: #f0f;' - . 'border-left-color: #f0f;border-top-style: solid;border-right-style: solid;' - . 'border-bottom-style: solid;border-left-style: solid;border-top-width: 2px;' - . 'border-right-width: 2px;border-bottom-width: 2px;border-left-width: 2px;font-style: italic;' - . 'font-variant: normal;font-weight: 500;font-size: 14px;line-height: 1.618;' - . 'font-family: "Trebuchet MS",Georgia,serif;background-color: #ccc;' - . 'background-image: url("/images/foo.png");background-repeat: no-repeat;background-attachment: scroll;' - . 'background-position: left top;}'; - self::assertSame($sExpected, $oDoc->render()); + $expected = \str_replace(['.2s', '.3s', '90deg'], ['.4s', '.6s', '180deg'], $expected); + self::assertSame($expected, $document->render()); } /** * @test */ - public function createShorthands() + public function namespaces(): void { - $oDoc = self::parsedStructureForFile('create-shorthands'); - $sExpected = 'body {font-size: 2em;font-family: Helvetica,Arial,sans-serif;font-weight: bold;' - . 'border-width: 2px;border-color: #999;border-style: dotted;background-color: #fff;' - . 'background-image: url("foobar.png");background-repeat: repeat-y;margin-top: 2px;margin-right: 3px;' - . 'margin-bottom: 4px;margin-left: 5px;}'; - self::assertSame($sExpected, $oDoc->render()); - $oDoc->createShorthands(); - $sExpected = 'body {background: #fff url("foobar.png") repeat-y;margin: 2px 5px 4px 3px;' - . 'border: 2px dotted #999;font: bold 2em Helvetica,Arial,sans-serif;}'; - self::assertSame($sExpected, $oDoc->render()); - } - - /** - * @test - */ - public function namespaces() - { - $oDoc = self::parsedStructureForFile('namespaces'); - $sExpected = '@namespace toto "http://toto.example.org"; + $document = self::parsedStructureForFile('namespaces'); + $expected = '@namespace toto "http://toto.example.org"; @namespace "http://example.com/foo"; @namespace foo url("http://www.example.com/"); @namespace foo url("http://www.example.com/"); foo|test {gaga: 1;} |test {gaga: 2;}'; - self::assertSame($sExpected, $oDoc->render()); + self::assertSame($expected, $document->render()); } /** * @test */ - public function innerColors() + public function innerColors(): void { - $oDoc = self::parsedStructureForFile('inner-color'); - $sExpected = 'test {background: -webkit-gradient(linear,0 0,0 bottom,from(#006cad),to(hsl(202,100%,49%)));}'; - self::assertSame($sExpected, $oDoc->render()); + $document = self::parsedStructureForFile('inner-color'); + $expected = 'test {background: -webkit-gradient(linear,0 0,0 bottom,from(#006cad),to(hsl(202,100%,49%)));}'; + self::assertSame($expected, $document->render()); } /** * @test */ - public function prefixedGradient() + public function prefixedGradient(): void { - $oDoc = self::parsedStructureForFile('webkit'); - $sExpected = '.test {background: -webkit-linear-gradient(top right,white,black);}'; - self::assertSame($sExpected, $oDoc->render()); + $document = self::parsedStructureForFile('webkit'); + $expected = '.test {background: -webkit-linear-gradient(top right,white,black);}'; + self::assertSame($expected, $document->render()); } /** * @test */ - public function listValueRemoval() + public function listValueRemoval(): void { - $oDoc = self::parsedStructureForFile('atrules'); - foreach ($oDoc->getContents() as $oItem) { - if ($oItem instanceof AtRule) { - $oDoc->remove($oItem); + $document = self::parsedStructureForFile('atrules'); + foreach ($document->getContents() as $contentItem) { + if ($contentItem instanceof AtRule) { + $document->remove($contentItem); continue; } } - self::assertSame('html, body {font-size: -.6em;}', $oDoc->render()); + self::assertSame('html, body {font-size: -.6em;}', $document->render()); - $oDoc = self::parsedStructureForFile('nested'); - foreach ($oDoc->getAllDeclarationBlocks() as $oBlock) { - $oDoc->removeDeclarationBlockBySelector($oBlock, false); + $document = self::parsedStructureForFile('nested'); + foreach ($document->getAllDeclarationBlocks() as $declarationBlock) { + $document->removeDeclarationBlockBySelector($declarationBlock, false); break; } self::assertSame( 'html {some-other: -test(val1);} @media screen {html {some: -test(val2);}} #unrelated {other: yes;}', - $oDoc->render() + $document->render() ); - $oDoc = self::parsedStructureForFile('nested'); - foreach ($oDoc->getAllDeclarationBlocks() as $oBlock) { - $oDoc->removeDeclarationBlockBySelector($oBlock, true); + $document = self::parsedStructureForFile('nested'); + foreach ($document->getAllDeclarationBlocks() as $declarationBlock) { + $document->removeDeclarationBlockBySelector($declarationBlock, true); break; } self::assertSame( '@media screen {html {some: -test(val2);}} #unrelated {other: yes;}', - $oDoc->render() + $document->render() ); } /** * @test */ - public function selectorRemoval() + public function selectorRemoval(): void { $this->expectException(OutputException::class); - $oDoc = self::parsedStructureForFile('1readme'); - $aBlocks = $oDoc->getAllDeclarationBlocks(); - $oBlock1 = $aBlocks[0]; - self::assertTrue($oBlock1->removeSelector('html')); - $sExpected = '@charset "utf-8"; + $document = self::parsedStructureForFile('1readme'); + $declarationsBlocks = $document->getAllDeclarationBlocks(); + $declarationBlock = $declarationsBlocks[0]; + self::assertTrue($declarationBlock->removeSelector('html')); + $expected = '@charset "utf-8"; @font-face {font-family: "CrassRoots";src: url("../media/cr.ttf");} body {font-size: 1.6em;}'; - self::assertSame($sExpected, $oDoc->render()); - self::assertFalse($oBlock1->removeSelector('html')); - self::assertTrue($oBlock1->removeSelector('body')); + self::assertSame($expected, $document->render()); + self::assertFalse($declarationBlock->removeSelector('html')); + self::assertTrue($declarationBlock->removeSelector('body')); // This tries to output a declaration block without a selector and throws. - $oDoc->render(); + $document->render(); } /** * @test */ - public function comments() + public function comments(): void { - $oDoc = self::parsedStructureForFile('comments'); - $sExpected = <<render()); + self::assertSame($expected, $document->render()); } /** * @test */ - public function urlInFile() + public function urlInFile(): void { - $oDoc = self::parsedStructureForFile('url', Settings::create()->withMultibyteSupport(true)); - $sExpected = 'body {background: #fff url("https://somesite.com/images/someimage.gif") repeat top center;} + $document = self::parsedStructureForFile('url', Settings::create()->withMultibyteSupport(true)); + $expected = 'body {background: #fff url("https://somesite.com/images/someimage.gif") repeat top center;} body {background-url: url("https://somesite.com/images/someimage.gif");}'; - self::assertSame($sExpected, $oDoc->render()); + self::assertSame($expected, $document->render()); } /** * @test */ - public function hexAlphaInFile() + public function hexAlphaInFile(): void { - $oDoc = self::parsedStructureForFile('hex-alpha', Settings::create()->withMultibyteSupport(true)); - $sExpected = 'div {background: rgba(17,34,51,.27);} + $document = self::parsedStructureForFile('hex-alpha', Settings::create()->withMultibyteSupport(true)); + $expected = 'div {background: rgba(17,34,51,.27);} div {background: rgba(17,34,51,.27);}'; - self::assertSame($sExpected, $oDoc->render()); + self::assertSame($expected, $document->render()); } /** * @test */ - public function calcInFile() + public function calcInFile(): void { - $oDoc = self::parsedStructureForFile('calc', Settings::create()->withMultibyteSupport(true)); - $sExpected = 'div {width: calc(100% / 4);} + $document = self::parsedStructureForFile('calc', Settings::create()->withMultibyteSupport(true)); + $expected = 'div {width: calc(100% / 4);} div {margin-top: calc(-120% - 4px);} -div {height: -webkit-calc(9 / 16 * 100%) !important;width: -moz-calc(( 50px - 50% ) * 2);} +div {height: calc(9 / 16 * 100%) !important;width: calc(( 50px - 50% ) * 2);} div {width: calc(50% - ( ( 4% ) * .5 ));}'; - self::assertSame($sExpected, $oDoc->render()); + self::assertSame($expected, $document->render()); } /** * @test */ - public function calcNestedInFile() + public function calcNestedInFile(): void { - $oDoc = self::parsedStructureForFile('calc-nested', Settings::create()->withMultibyteSupport(true)); - $sExpected = '.test {font-size: calc(( 3 * 4px ) + -2px);top: calc(200px - calc(20 * 3px));}'; - self::assertSame($sExpected, $oDoc->render()); + $document = self::parsedStructureForFile('calc-nested', Settings::create()->withMultibyteSupport(true)); + $expected = '.test {font-size: calc(( 3 * 4px ) + -2px);top: calc(200px - calc(20 * 3px));}'; + self::assertSame($expected, $document->render()); } /** * @test */ - public function invalidCalcInFile() + public function invalidCalcInFile(): void { - $oDoc = self::parsedStructureForFile('calc-invalid', Settings::create()->withMultibyteSupport(true)); - $sExpected = 'div {} + $document = self::parsedStructureForFile('calc-invalid', Settings::create()->withMultibyteSupport(true)); + $expected = 'div {} div {} div {} div {height: -moz-calc;} div {height: calc;}'; - self::assertSame($sExpected, $oDoc->render()); + self::assertSame($expected, $document->render()); } /** * @test */ - public function invalidCalc() + public function invalidCalc(): void { $parser = new Parser('div { height: calc(100px'); - $oDoc = $parser->parse(); - self::assertSame('div {height: calc(100px);}', $oDoc->render()); + $document = $parser->parse(); + self::assertSame('div {height: calc(100px);}', $document->render()); $parser = new Parser('div { height: calc(100px)'); - $oDoc = $parser->parse(); - self::assertSame('div {height: calc(100px);}', $oDoc->render()); + $document = $parser->parse(); + self::assertSame('div {height: calc(100px);}', $document->render()); $parser = new Parser('div { height: calc(100px);'); - $oDoc = $parser->parse(); - self::assertSame('div {height: calc(100px);}', $oDoc->render()); + $document = $parser->parse(); + self::assertSame('div {height: calc(100px);}', $document->render()); $parser = new Parser('div { height: calc(100px}'); - $oDoc = $parser->parse(); - self::assertSame('div {}', $oDoc->render()); + $document = $parser->parse(); + self::assertSame('div {}', $document->render()); $parser = new Parser('div { height: calc(100px;'); - $oDoc = $parser->parse(); - self::assertSame('div {}', $oDoc->render()); + $document = $parser->parse(); + self::assertSame('div {}', $document->render()); $parser = new Parser('div { height: calc(100px;}'); - $oDoc = $parser->parse(); - self::assertSame('div {}', $oDoc->render()); + $document = $parser->parse(); + self::assertSame('div {}', $document->render()); } /** * @test */ - public function gridLineNameInFile() + public function gridLineNameInFile(): void { - $oDoc = self::parsedStructureForFile('grid-linename', Settings::create()->withMultibyteSupport(true)); - $sExpected = "div {grid-template-columns: [linename] 100px;}\n" - . "span {grid-template-columns: [linename1 linename2] 100px;}"; - self::assertSame($sExpected, $oDoc->render()); + $document = self::parsedStructureForFile('grid-linename', Settings::create()->withMultibyteSupport(true)); + $expected = "div {grid-template-columns: [linename] 100px;}\n" + . 'span {grid-template-columns: [linename1 linename2] 100px;}'; + self::assertSame($expected, $document->render()); } /** * @test */ - public function emptyGridLineNameLenientInFile() + public function emptyGridLineNameLenientInFile(): void { - $oDoc = self::parsedStructureForFile('empty-grid-linename'); - $sExpected = '.test {grid-template-columns: [] 100px;}'; - self::assertSame($sExpected, $oDoc->render()); + $document = self::parsedStructureForFile('empty-grid-linename'); + $expected = '.test {grid-template-columns: [] 100px;}'; + self::assertSame($expected, $document->render()); } /** * @test */ - public function invalidGridLineNameInFile() + public function invalidGridLineNameInFile(): void { - $oDoc = self::parsedStructureForFile('invalid-grid-linename', Settings::create()->withMultibyteSupport(true)); - $sExpected = "div {}"; - self::assertSame($sExpected, $oDoc->render()); + $document = self::parsedStructureForFile( + 'invalid-grid-linename', + Settings::create()->withMultibyteSupport(true) + ); + $expected = 'div {}'; + self::assertSame($expected, $document->render()); } /** * @test */ - public function unmatchedBracesInFile() + public function unmatchedBracesInFile(): void { - $oDoc = self::parsedStructureForFile('unmatched_braces', Settings::create()->withMultibyteSupport(true)); - $sExpected = 'button, input, checkbox, textarea {outline: 0;margin: 0;}'; - self::assertSame($sExpected, $oDoc->render()); + $document = self::parsedStructureForFile('unmatched_braces', Settings::create()->withMultibyteSupport(true)); + $expected = 'button, input, checkbox, textarea {outline: 0;margin: 0;}'; + self::assertSame($expected, $document->render()); } /** * @test */ - public function invalidSelectorsInFile() + public function invalidSelectorsInFile(): void { - $oDoc = self::parsedStructureForFile('invalid-selectors', Settings::create()->withMultibyteSupport(true)); - $sExpected = '@keyframes mymove {from {top: 0px;}} + $document = self::parsedStructureForFile('invalid-selectors', Settings::create()->withMultibyteSupport(true)); + $expected = '@keyframes mymove {from {top: 0px;}} #test {color: white;background: green;} #test {display: block;background: white;color: black;}'; - self::assertSame($sExpected, $oDoc->render()); + self::assertSame($expected, $document->render()); - $oDoc = self::parsedStructureForFile('invalid-selectors-2', Settings::create()->withMultibyteSupport(true)); - $sExpected = '@media only screen and (max-width: 1215px) {.breadcrumb {padding-left: 10px;} + $document = self::parsedStructureForFile('invalid-selectors-2', Settings::create()->withMultibyteSupport(true)); + $expected = '@media only screen and (max-width: 1215px) {.breadcrumb {padding-left: 10px;} .super-menu > li:first-of-type {border-left-width: 0;} .super-menu > li:last-of-type {border-right-width: 0;} html[dir="rtl"] .super-menu > li:first-of-type {border-left-width: 1px;border-right-width: 0;} html[dir="rtl"] .super-menu > li:last-of-type {border-left-width: 0;}} body {background-color: red;}'; - self::assertSame($sExpected, $oDoc->render()); + self::assertSame($expected, $document->render()); } /** * @test */ - public function selectorEscapesInFile() + public function selectorEscapesInFile(): void { - $oDoc = self::parsedStructureForFile('selector-escapes', Settings::create()->withMultibyteSupport(true)); - $sExpected = '#\# {color: red;} -.col-sm-1\/5 {width: 20%;}'; - self::assertSame($sExpected, $oDoc->render()); + $document = self::parsedStructureForFile('selector-escapes', Settings::create()->withMultibyteSupport(true)); + $expected = '#\\# {color: red;} +.col-sm-1\\/5 {width: 20%;}'; + self::assertSame($expected, $document->render()); - $oDoc = self::parsedStructureForFile('invalid-selectors-2', Settings::create()->withMultibyteSupport(true)); - $sExpected = '@media only screen and (max-width: 1215px) {.breadcrumb {padding-left: 10px;} + $document = self::parsedStructureForFile('invalid-selectors-2', Settings::create()->withMultibyteSupport(true)); + $expected = '@media only screen and (max-width: 1215px) {.breadcrumb {padding-left: 10px;} .super-menu > li:first-of-type {border-left-width: 0;} .super-menu > li:last-of-type {border-right-width: 0;} html[dir="rtl"] .super-menu > li:first-of-type {border-left-width: 1px;border-right-width: 0;} html[dir="rtl"] .super-menu > li:last-of-type {border-left-width: 0;}} body {background-color: red;}'; - self::assertSame($sExpected, $oDoc->render()); + self::assertSame($expected, $document->render()); } /** * @test */ - public function identifierEscapesInFile() + public function identifierEscapesInFile(): void { - $oDoc = self::parsedStructureForFile('identifier-escapes', Settings::create()->withMultibyteSupport(true)); - $sExpected = 'div {font: 14px Font Awesome\ 5 Pro;font: 14px Font Awesome\} 5 Pro;' - . 'font: 14px Font Awesome\; 5 Pro;f\;ont: 14px Font Awesome\; 5 Pro;}'; - self::assertSame($sExpected, $oDoc->render()); + $document = self::parsedStructureForFile('identifier-escapes', Settings::create()->withMultibyteSupport(true)); + $expected = 'div {font: 14px Font Awesome\\ 5 Pro;font: 14px Font Awesome\\} 5 Pro;' + . 'font: 14px Font Awesome\\; 5 Pro;f\\;ont: 14px Font Awesome\\; 5 Pro;}'; + self::assertSame($expected, $document->render()); } /** * @test */ - public function selectorIgnoresInFile() + public function selectorIgnoresInFile(): void { - $oDoc = self::parsedStructureForFile('selector-ignores', Settings::create()->withMultibyteSupport(true)); - $sExpected = '.some[selectors-may=\'contain-a-{\'] {}' + $document = self::parsedStructureForFile('selector-ignores', Settings::create()->withMultibyteSupport(true)); + $expected = '.some[selectors-may=\'contain-a-{\'] {}' . "\n" . '.this-selector .valid {width: 100px;}' . "\n" . '@media only screen and (min-width: 200px) {.test {prop: val;}}'; - self::assertSame($sExpected, $oDoc->render()); + self::assertSame($expected, $document->render()); } /** * @test */ - public function keyframeSelectors() + public function keyframeSelectors(): void { - $oDoc = self::parsedStructureForFile( + $document = self::parsedStructureForFile( 'keyframe-selector-validation', Settings::create()->withMultibyteSupport(true) ); - $sExpected = '@-webkit-keyframes zoom {0% {-webkit-transform: scale(1,1);}' + $expected = '@-webkit-keyframes zoom {0% {-webkit-transform: scale(1,1);}' . "\n\t" . '50% {-webkit-transform: scale(1.2,1.2);}' . "\n\t" . '100% {-webkit-transform: scale(1,1);}}'; - self::assertSame($sExpected, $oDoc->render()); + self::assertSame($expected, $document->render()); } /** * @test */ - public function lineNameFailure() + public function lineNameFailure(): void { $this->expectException(UnexpectedTokenException::class); @@ -863,7 +810,7 @@ public function lineNameFailure() /** * @test */ - public function calcFailure() + public function calcFailure(): void { $this->expectException(UnexpectedTokenException::class); @@ -873,69 +820,69 @@ public function calcFailure() /** * @test */ - public function urlInFileMbOff() + public function urlInFileMbOff(): void { - $oDoc = self::parsedStructureForFile('url', Settings::create()->withMultibyteSupport(false)); - $sExpected = 'body {background: #fff url("https://somesite.com/images/someimage.gif") repeat top center;}' + $document = self::parsedStructureForFile('url', Settings::create()->withMultibyteSupport(false)); + $expected = 'body {background: #fff url("https://somesite.com/images/someimage.gif") repeat top center;}' . "\n" . 'body {background-url: url("https://somesite.com/images/someimage.gif");}'; - self::assertSame($sExpected, $oDoc->render()); + self::assertSame($expected, $document->render()); } /** * @test */ - public function emptyFile() + public function emptyFile(): void { - $oDoc = self::parsedStructureForFile('-empty', Settings::create()->withMultibyteSupport(true)); - $sExpected = ''; - self::assertSame($sExpected, $oDoc->render()); + $document = self::parsedStructureForFile('-empty', Settings::create()->withMultibyteSupport(true)); + $expected = ''; + self::assertSame($expected, $document->render()); } /** * @test */ - public function emptyFileMbOff() + public function emptyFileMbOff(): void { - $oDoc = self::parsedStructureForFile('-empty', Settings::create()->withMultibyteSupport(false)); - $sExpected = ''; - self::assertSame($sExpected, $oDoc->render()); + $document = self::parsedStructureForFile('-empty', Settings::create()->withMultibyteSupport(false)); + $expected = ''; + self::assertSame($expected, $document->render()); } /** * @test */ - public function charsetLenient1() + public function charsetLenient1(): void { - $oDoc = self::parsedStructureForFile('-charset-after-rule', Settings::create()->withLenientParsing(true)); - $sExpected = '#id {prop: var(--val);}'; - self::assertSame($sExpected, $oDoc->render()); + $document = self::parsedStructureForFile('-charset-after-rule', Settings::create()->withLenientParsing(true)); + $expected = '#id {prop: var(--val);}'; + self::assertSame($expected, $document->render()); } /** * @test */ - public function charsetLenient2() + public function charsetLenient2(): void { - $oDoc = self::parsedStructureForFile('-charset-in-block', Settings::create()->withLenientParsing(true)); - $sExpected = '@media print {}'; - self::assertSame($sExpected, $oDoc->render()); + $document = self::parsedStructureForFile('-charset-in-block', Settings::create()->withLenientParsing(true)); + $expected = '@media print {}'; + self::assertSame($expected, $document->render()); } /** * @test */ - public function trailingWhitespace() + public function trailingWhitespace(): void { - $oDoc = self::parsedStructureForFile('trailing-whitespace', Settings::create()->withLenientParsing(false)); - $sExpected = 'div {width: 200px;}'; - self::assertSame($sExpected, $oDoc->render()); + $document = self::parsedStructureForFile('trailing-whitespace', Settings::create()->withLenientParsing(false)); + $expected = 'div {width: 200px;}'; + self::assertSame($expected, $document->render()); } /** * @test */ - public function charsetFailure1() + public function charsetFailure1(): void { $this->expectException(UnexpectedTokenException::class); @@ -945,7 +892,7 @@ public function charsetFailure1() /** * @test */ - public function charsetFailure2() + public function charsetFailure2(): void { $this->expectException(UnexpectedTokenException::class); @@ -955,7 +902,7 @@ public function charsetFailure2() /** * @test */ - public function unopenedClosingBracketFailure() + public function unopenedClosingBracketFailure(): void { $this->expectException(SourceException::class); @@ -965,11 +912,9 @@ public function unopenedClosingBracketFailure() /** * Ensure that a missing property value raises an exception. * - * @covers \Sabberworm\CSS\Value\Value::parseValue() - * * @test */ - public function missingPropertyValueStrict() + public function missingPropertyValueStrict(): void { $this->expectException(UnexpectedTokenException::class); @@ -979,11 +924,9 @@ public function missingPropertyValueStrict() /** * Ensure that a missing property value is ignored when in lenient parsing mode. * - * @covers \Sabberworm\CSS\Value\Value::parseValue() - * * @test */ - public function missingPropertyValueLenient() + public function missingPropertyValueLenient(): void { $parsed = self::parsedStructureForFile('missing-property-value', Settings::create()->withLenientParsing(true)); $rulesets = $parsed->getAllRuleSets(); @@ -1001,16 +944,14 @@ public function missingPropertyValueLenient() /** * Parses structure for file. * - * @param string $sFileName - * @param Settings|null $oSettings - * - * @return Document parsed document + * @param string $filename + * @param Settings|null $settings */ - public static function parsedStructureForFile($sFileName, $oSettings = null) + public static function parsedStructureForFile($filename, $settings = null): Document { - $sFile = __DIR__ . "/fixtures/$sFileName.css"; - $oParser = new Parser(file_get_contents($sFile), $oSettings); - return $oParser->parse(); + $filename = __DIR__ . "/fixtures/$filename.css"; + $parser = new Parser(\file_get_contents($filename), $settings); + return $parser->parse(); } /** @@ -1018,11 +959,11 @@ public static function parsedStructureForFile($sFileName, $oSettings = null) * * @test */ - public function lineNumbersParsing() + public function lineNumbersParsing(): void { - $oDoc = self::parsedStructureForFile('line-numbers'); + $document = self::parsedStructureForFile('line-numbers'); // array key is the expected line number - $aExpected = [ + $expected = [ 1 => [Charset::class], 3 => [CSSNamespace::class], 5 => [AtRuleSet::class], @@ -1033,100 +974,83 @@ public function lineNumbersParsing() 25 => [DeclarationBlock::class], ]; - $aActual = []; - foreach ($oDoc->getContents() as $oContent) { - $aActual[$oContent->getLineNo()] = [get_class($oContent)]; - if ($oContent instanceof KeyFrame) { - foreach ($oContent->getContents() as $block) { - $aActual[$oContent->getLineNo()][] = $block->getLineNo(); + $actual = []; + foreach ($document->getContents() as $contentItem) { + self::assertInstanceOf(Positionable::class, $contentItem); + $actual[$contentItem->getLineNumber()] = [\get_class($contentItem)]; + if ($contentItem instanceof KeyFrame) { + foreach ($contentItem->getContents() as $block) { + self::assertInstanceOf(Positionable::class, $block); + $actual[$contentItem->getLineNumber()][] = $block->getLineNumber(); } } } - $aUrlExpected = [7, 26]; // expected line numbers - $aUrlActual = []; - foreach ($oDoc->getAllValues() as $oValue) { - if ($oValue instanceof URL) { - $aUrlActual[] = $oValue->getLineNo(); + $expectedLineNumbers = [7, 26]; + $actualLineNumbers = []; + foreach ($document->getAllValues() as $value) { + if ($value instanceof URL) { + $actualLineNumbers[] = $value->getLineNo(); } } // Checking for the multiline color rule lines 27-31 - $aExpectedColorLines = [28, 29, 30]; - $aDeclBlocks = $oDoc->getAllDeclarationBlocks(); + $expectedColorLineNumbers = [28, 29, 30]; + $declarationBlocks = $document->getAllDeclarationBlocks(); // Choose the 2nd one - $oDeclBlock = $aDeclBlocks[1]; - $aRules = $oDeclBlock->getRules(); + $secondDeclarationBlock = $declarationBlocks[1]; + $rules = $secondDeclarationBlock->getRules(); // Choose the 2nd one - $oColor = $aRules[1]->getValue(); - self::assertSame(27, $aRules[1]->getLineNo()); + $valueOfSecondRule = $rules[1]->getValue(); + self::assertInstanceOf(Color::class, $valueOfSecondRule); + self::assertSame(27, $rules[1]->getLineNo()); - $aActualColorLines = []; - foreach ($oColor->getColor() as $oSize) { - $aActualColorLines[] = $oSize->getLineNo(); + $actualColorLineNumbers = []; + foreach ($valueOfSecondRule->getColor() as $size) { + $actualColorLineNumbers[] = $size->getLineNo(); } - self::assertSame($aExpectedColorLines, $aActualColorLines); - self::assertSame($aUrlExpected, $aUrlActual); - self::assertSame($aExpected, $aActual); + self::assertSame($expectedColorLineNumbers, $actualColorLineNumbers); + self::assertSame($expectedLineNumbers, $actualLineNumbers); + self::assertSame($expected, $actual); } /** * @test */ - public function unexpectedTokenExceptionLineNo() + public function unexpectedTokenExceptionLineNo(): void { $this->expectException(UnexpectedTokenException::class); - $oParser = new Parser("\ntest: 1;", Settings::create()->beStrict()); + $parser = new Parser("\ntest: 1;", Settings::create()->beStrict()); try { - $oParser->parse(); + $parser->parse(); } catch (UnexpectedTokenException $e) { self::assertSame(2, $e->getLineNo()); throw $e; } } - /** - * @test - */ - public function ieHacksStrictParsing() - { - $this->expectException(UnexpectedTokenException::class); - - // We can't strictly parse IE hacks. - self::parsedStructureForFile('ie-hacks', Settings::create()->beStrict()); - } - - /** - * @test - */ - public function ieHacksParsing() - { - $oDoc = self::parsedStructureForFile('ie-hacks', Settings::create()->withLenientParsing(true)); - $sExpected = 'p {padding-right: .75rem \9;background-image: none \9;color: red \9\0;' - . 'background-color: red \9\0;background-color: red \9\0 !important;content: "red \0";content: "red઼";}'; - self::assertSame($sExpected, $oDoc->render()); - } - /** * @depends files * * @test */ - public function commentExtracting() + public function commentExtracting(): void { - $oDoc = self::parsedStructureForFile('comments'); - $aNodes = $oDoc->getContents(); + $document = self::parsedStructureForFile('comments'); + $nodes = $document->getContents(); // Import property. - $importComments = $aNodes[0]->getComments(); + self::assertInstanceOf(Commentable::class, $nodes[0]); + $importComments = $nodes[0]->getComments(); self::assertCount(2, $importComments); self::assertSame("*\n * Comments\n ", $importComments[0]->getComment()); - self::assertSame(" Hell ", $importComments[1]->getComment()); + self::assertSame(' Hell ', $importComments[1]->getComment()); // Declaration block. - $fooBarBlock = $aNodes[1]; + $fooBarBlock = $nodes[1]; + self::assertInstanceOf(Commentable::class, $fooBarBlock); $fooBarBlockComments = $fooBarBlock->getComments(); // TODO Support comments in selectors. // $this->assertCount(2, $fooBarBlockComments); @@ -1134,120 +1058,186 @@ public function commentExtracting() // $this->assertSame("* Number 5 *", $fooBarBlockComments[1]->getComment()); // Declaration rules. + self::assertInstanceOf(RuleSet::class, $fooBarBlock); $fooBarRules = $fooBarBlock->getRules(); $fooBarRule = $fooBarRules[0]; $fooBarRuleComments = $fooBarRule->getComments(); self::assertCount(1, $fooBarRuleComments); - self::assertSame(" Number 6 ", $fooBarRuleComments[0]->getComment()); + self::assertSame(' Number 6 ', $fooBarRuleComments[0]->getComment()); // Media property. - $mediaComments = $aNodes[2]->getComments(); + self::assertInstanceOf(Commentable::class, $nodes[2]); + $mediaComments = $nodes[2]->getComments(); self::assertCount(0, $mediaComments); // Media children. - $mediaRules = $aNodes[2]->getContents(); + self::assertInstanceOf(CSSList::class, $nodes[2]); + $mediaRules = $nodes[2]->getContents(); + self::assertInstanceOf(Commentable::class, $mediaRules[0]); $fooBarComments = $mediaRules[0]->getComments(); self::assertCount(1, $fooBarComments); - self::assertSame("* Number 10 *", $fooBarComments[0]->getComment()); + self::assertSame('* Number 10 *', $fooBarComments[0]->getComment()); // Media -> declaration -> rule. + self::assertInstanceOf(RuleSet::class, $mediaRules[0]); $fooBarRules = $mediaRules[0]->getRules(); $fooBarChildComments = $fooBarRules[0]->getComments(); self::assertCount(1, $fooBarChildComments); - self::assertSame("* Number 10b *", $fooBarChildComments[0]->getComment()); + self::assertSame('* Number 10b *', $fooBarChildComments[0]->getComment()); } /** * @test */ - public function flatCommentExtracting() + public function flatCommentExtractingOneComment(): void { $parser = new Parser('div {/*Find Me!*/left:10px; text-align:left;}'); - $doc = $parser->parse(); - $contents = $doc->getContents(); + $document = $parser->parse(); + + $contents = $document->getContents(); + self::assertInstanceOf(RuleSet::class, $contents[0]); $divRules = $contents[0]->getRules(); $comments = $divRules[0]->getComments(); + self::assertCount(1, $comments); - self::assertSame("Find Me!", $comments[0]->getComment()); + self::assertSame('Find Me!', $comments[0]->getComment()); + } + + /** + * @test + */ + public function flatCommentExtractingTwoConjoinedCommentsForOneRule(): void + { + $parser = new Parser('div {/*Find Me!*//*Find Me Too!*/left:10px; text-align:left;}'); + $document = $parser->parse(); + + $contents = $document->getContents(); + self::assertInstanceOf(RuleSet::class, $contents[0]); + $divRules = $contents[0]->getRules(); + $comments = $divRules[0]->getComments(); + + self::assertCount(2, $comments); + self::assertSame('Find Me!', $comments[0]->getComment()); + self::assertSame('Find Me Too!', $comments[1]->getComment()); + } + + /** + * @test + */ + public function flatCommentExtractingTwoSpaceSeparatedCommentsForOneRule(): void + { + $parser = new Parser('div { /*Find Me!*/ /*Find Me Too!*/ left:10px; text-align:left;}'); + $document = $parser->parse(); + + $contents = $document->getContents(); + self::assertInstanceOf(RuleSet::class, $contents[0]); + $divRules = $contents[0]->getRules(); + $comments = $divRules[0]->getComments(); + + self::assertCount(2, $comments); + self::assertSame('Find Me!', $comments[0]->getComment()); + self::assertSame('Find Me Too!', $comments[1]->getComment()); } /** * @test */ - public function topLevelCommentExtracting() + public function flatCommentExtractingCommentsForTwoRules(): void + { + $parser = new Parser('div {/*Find Me!*/left:10px; /*Find Me Too!*/text-align:left;}'); + $document = $parser->parse(); + + $contents = $document->getContents(); + self::assertInstanceOf(RuleSet::class, $contents[0]); + $divRules = $contents[0]->getRules(); + $rule1Comments = $divRules[0]->getComments(); + $rule2Comments = $divRules[1]->getComments(); + + self::assertCount(1, $rule1Comments); + self::assertCount(1, $rule2Comments); + self::assertSame('Find Me!', $rule1Comments[0]->getComment()); + self::assertSame('Find Me Too!', $rule2Comments[0]->getComment()); + } + + /** + * @test + */ + public function topLevelCommentExtracting(): void { $parser = new Parser('/*Find Me!*/div {left:10px; text-align:left;}'); - $doc = $parser->parse(); - $contents = $doc->getContents(); + $document = $parser->parse(); + $contents = $document->getContents(); + self::assertInstanceOf(Commentable::class, $contents[0]); $comments = $contents[0]->getComments(); self::assertCount(1, $comments); - self::assertSame("Find Me!", $comments[0]->getComment()); + self::assertSame('Find Me!', $comments[0]->getComment()); } /** * @test */ - public function microsoftFilterStrictParsing() + public function microsoftFilterStrictParsing(): void { $this->expectException(UnexpectedTokenException::class); - $oDoc = self::parsedStructureForFile('ms-filter', Settings::create()->beStrict()); + $document = self::parsedStructureForFile('ms-filter', Settings::create()->beStrict()); } /** * @test */ - public function microsoftFilterParsing() + public function microsoftFilterParsing(): void { - $oDoc = self::parsedStructureForFile('ms-filter'); - $sExpected = '.test {filter: progid:DXImageTransform.Microsoft.gradient(startColorstr="#80000000",' + $document = self::parsedStructureForFile('ms-filter'); + $expected = '.test {filter: progid:DXImageTransform.Microsoft.gradient(startColorstr="#80000000",' . 'endColorstr="#00000000",GradientType=1);}'; - self::assertSame($sExpected, $oDoc->render()); + self::assertSame($expected, $document->render()); } /** * @test */ - public function largeSizeValuesInFile() + public function largeSizeValuesInFile(): void { - $oDoc = self::parsedStructureForFile('large-z-index', Settings::create()->withMultibyteSupport(false)); - $sExpected = '.overlay {z-index: 10000000000000000000000;}'; - self::assertSame($sExpected, $oDoc->render()); + $document = self::parsedStructureForFile('large-z-index', Settings::create()->withMultibyteSupport(false)); + $expected = '.overlay {z-index: 10000000000000000000000;}'; + self::assertSame($expected, $document->render()); } /** * @test */ - public function scientificNotationSizeValuesInFile() + public function scientificNotationSizeValuesInFile(): void { - $oDoc = $this->parsedStructureForFile( + $document = self::parsedStructureForFile( 'scientific-notation-numbers', Settings::create()->withMultibyteSupport(false) ); - $sExpected = '' + $expected = '' . 'body {background-color: rgba(62,174,151,3041820656523200167936);' . 'z-index: .030418206565232;font-size: 1em;top: 192.3478px;}'; - self::assertSame($sExpected, $oDoc->render()); + self::assertSame($expected, $document->render()); } /** * @test */ - public function lonelyImport() + public function lonelyImport(): void { - $oDoc = self::parsedStructureForFile('lonely-import'); - $sExpected = "@import url(\"example.css\") only screen and (max-width: 600px);"; - self::assertSame($sExpected, $oDoc->render()); + $document = self::parsedStructureForFile('lonely-import'); + $expected = '@import url("example.css") only screen and (max-width: 600px);'; + self::assertSame($expected, $document->render()); } - public function escapedSpecialCaseTokens() + public function escapedSpecialCaseTokens(): void { - $oDoc = $this->parsedStructureForFile('escaped-tokens'); - $contents = $oDoc->getContents(); + $document = self::parsedStructureForFile('escaped-tokens'); + $contents = $document->getContents(); + self::assertInstanceOf(RuleSet::class, $contents[0]); $rules = $contents[0]->getRules(); $urlRule = $rules[0]; $calcRule = $rules[1]; - self::assertTrue(is_a($urlRule->getValue(), '\Sabberworm\CSS\Value\URL')); - self::assertTrue(is_a($calcRule->getValue(), '\Sabberworm\CSS\Value\CalcFunction')); + self::assertInstanceOf(URL::class, $urlRule->getValue()); + self::assertInstanceOf(CalcFunction::class, $calcRule->getValue()); } } diff --git a/tests/RuleSet/DeclarationBlockTest.php b/tests/RuleSet/DeclarationBlockTest.php index 49526952..5aaf0662 100644 --- a/tests/RuleSet/DeclarationBlockTest.php +++ b/tests/RuleSet/DeclarationBlockTest.php @@ -1,453 +1,136 @@ parse(); - foreach ($oDoc->getAllDeclarationBlocks() as $oDeclaration) { - $oDeclaration->expandBorderShorthand(); - } - self::assertSame(trim((string)$oDoc), $sExpected); - } + $css = '.wrapper { left: 10px; text-align: left; }'; + $parser = new Parser($css); + $document = $parser->parse(); + $rule = new Rule('right'); + $rule->setValue('-10px'); + $contents = $document->getContents(); + $wrapper = $contents[0]; - /** - * @return array> - */ - public function expandBorderShorthandProvider() - { - return [ - ['body{ border: 2px solid #000 }', 'body {border-width: 2px;border-style: solid;border-color: #000;}'], - ['body{ border: none }', 'body {border-style: none;}'], - ['body{ border: 2px }', 'body {border-width: 2px;}'], - ['body{ border: #f00 }', 'body {border-color: #f00;}'], - ['body{ border: 1em solid }', 'body {border-width: 1em;border-style: solid;}'], - ['body{ margin: 1em; }', 'body {margin: 1em;}'], - ]; - } + self::assertInstanceOf(RuleSet::class, $wrapper); + self::assertCount(2, $wrapper->getRules()); + $wrapper->setRules([$rule]); - /** - * @param string $sCss - * @param string $sExpected - * - * @dataProvider expandFontShorthandProvider - * - * @test - */ - public function expandFontShorthand($sCss, $sExpected) - { - $oParser = new Parser($sCss); - $oDoc = $oParser->parse(); - foreach ($oDoc->getAllDeclarationBlocks() as $oDeclaration) { - $oDeclaration->expandFontShorthand(); - } - self::assertSame(trim((string)$oDoc), $sExpected); + $rules = $wrapper->getRules(); + self::assertCount(1, $rules); + self::assertSame('right', $rules[0]->getRule()); + self::assertSame('-10px', $rules[0]->getValue()); } /** - * @return array> - */ - public function expandFontShorthandProvider() - { - return [ - [ - 'body{ margin: 1em; }', - 'body {margin: 1em;}', - ], - [ - 'body {font: 12px serif;}', - 'body {font-style: normal;font-variant: normal;font-weight: normal;font-size: 12px;' - . 'line-height: normal;font-family: serif;}', - ], - [ - 'body {font: italic 12px serif;}', - 'body {font-style: italic;font-variant: normal;font-weight: normal;font-size: 12px;' - . 'line-height: normal;font-family: serif;}', - ], - [ - 'body {font: italic bold 12px serif;}', - 'body {font-style: italic;font-variant: normal;font-weight: bold;font-size: 12px;' - . 'line-height: normal;font-family: serif;}', - ], - [ - 'body {font: italic bold 12px/1.6 serif;}', - 'body {font-style: italic;font-variant: normal;font-weight: bold;font-size: 12px;' - . 'line-height: 1.6;font-family: serif;}', - ], - [ - 'body {font: italic small-caps bold 12px/1.6 serif;}', - 'body {font-style: italic;font-variant: small-caps;font-weight: bold;font-size: 12px;' - . 'line-height: 1.6;font-family: serif;}', - ], - ]; - } - - /** - * @param string $sCss - * @param string $sExpected - * - * @dataProvider expandBackgroundShorthandProvider - * * @test */ - public function expandBackgroundShorthand($sCss, $sExpected) + public function ruleInsertion(): void { - $oParser = new Parser($sCss); - $oDoc = $oParser->parse(); - foreach ($oDoc->getAllDeclarationBlocks() as $oDeclaration) { - $oDeclaration->expandBackgroundShorthand(); - } - self::assertSame(trim((string)$oDoc), $sExpected); - } + $css = '.wrapper { left: 10px; text-align: left; }'; + $parser = new Parser($css); + $document = $parser->parse(); + $contents = $document->getContents(); + $wrapper = $contents[0]; - /** - * @return array> - */ - public function expandBackgroundShorthandProvider() - { - return [ - ['body {border: 1px;}', 'body {border: 1px;}'], - [ - 'body {background: #f00;}', - 'body {background-color: #f00;background-image: none;background-repeat: repeat;' - . 'background-attachment: scroll;background-position: 0% 0%;}', - ], - [ - 'body {background: #f00 url("foobar.png");}', - 'body {background-color: #f00;background-image: url("foobar.png");background-repeat: repeat;' - . 'background-attachment: scroll;background-position: 0% 0%;}', - ], - [ - 'body {background: #f00 url("foobar.png") no-repeat;}', - 'body {background-color: #f00;background-image: url("foobar.png");background-repeat: no-repeat;' - . 'background-attachment: scroll;background-position: 0% 0%;}', - ], - [ - 'body {background: #f00 url("foobar.png") no-repeat center;}', - 'body {background-color: #f00;background-image: url("foobar.png");background-repeat: no-repeat;' - . 'background-attachment: scroll;background-position: center center;}', - ], - [ - 'body {background: #f00 url("foobar.png") no-repeat top left;}', - 'body {background-color: #f00;background-image: url("foobar.png");background-repeat: no-repeat;' - . 'background-attachment: scroll;background-position: top left;}', - ], - ]; - } + self::assertInstanceOf(RuleSet::class, $wrapper); - /** - * @param string $sCss - * @param string $sExpected - * - * @dataProvider expandDimensionsShorthandProvider - * - * @test - */ - public function expandDimensionsShorthand($sCss, $sExpected) - { - $oParser = new Parser($sCss); - $oDoc = $oParser->parse(); - foreach ($oDoc->getAllDeclarationBlocks() as $oDeclaration) { - $oDeclaration->expandDimensionsShorthand(); - } - self::assertSame(trim((string)$oDoc), $sExpected); - } + $leftRules = $wrapper->getRules('left'); + self::assertCount(1, $leftRules); + $firstLeftRule = $leftRules[0]; - /** - * @return array> - */ - public function expandDimensionsShorthandProvider() - { - return [ - ['body {border: 1px;}', 'body {border: 1px;}'], - ['body {margin-top: 1px;}', 'body {margin-top: 1px;}'], - ['body {margin: 1em;}', 'body {margin-top: 1em;margin-right: 1em;margin-bottom: 1em;margin-left: 1em;}'], - [ - 'body {margin: 1em 2em;}', - 'body {margin-top: 1em;margin-right: 2em;margin-bottom: 1em;margin-left: 2em;}', - ], - [ - 'body {margin: 1em 2em 3em;}', - 'body {margin-top: 1em;margin-right: 2em;margin-bottom: 3em;margin-left: 2em;}', - ], - ]; - } + $textRules = $wrapper->getRules('text-'); + self::assertCount(1, $textRules); + $firstTextRule = $textRules[0]; - /** - * @param string $sCss - * @param string $sExpected - * - * @dataProvider createBorderShorthandProvider - * - * @test - */ - public function createBorderShorthand($sCss, $sExpected) - { - $oParser = new Parser($sCss); - $oDoc = $oParser->parse(); - foreach ($oDoc->getAllDeclarationBlocks() as $oDeclaration) { - $oDeclaration->createBorderShorthand(); - } - self::assertSame(trim((string)$oDoc), $sExpected); - } + $leftPrefixRule = new Rule('left'); + $leftPrefixRule->setValue(new Size(16, 'em')); - /** - * @return array> - */ - public function createBorderShorthandProvider() - { - return [ - ['body {border-width: 2px;border-style: solid;border-color: #000;}', 'body {border: 2px solid #000;}'], - ['body {border-style: none;}', 'body {border: none;}'], - ['body {border-width: 1em;border-style: solid;}', 'body {border: 1em solid;}'], - ['body {margin: 1em;}', 'body {margin: 1em;}'], - ]; - } + $textAlignRule = new Rule('text-align'); + $textAlignRule->setValue(new Size(1)); - /** - * @param string $sCss - * @param string $sExpected - * - * @dataProvider createFontShorthandProvider - * - * @test - */ - public function createFontShorthand($sCss, $sExpected) - { - $oParser = new Parser($sCss); - $oDoc = $oParser->parse(); - foreach ($oDoc->getAllDeclarationBlocks() as $oDeclaration) { - $oDeclaration->createFontShorthand(); - } - self::assertSame(trim((string)$oDoc), $sExpected); - } + $borderBottomRule = new Rule('border-bottom-width'); + $borderBottomRule->setValue(new Size(1, 'px')); - /** - * @return array> - */ - public function createFontShorthandProvider() - { - return [ - ['body {font-size: 12px; font-family: serif}', 'body {font: 12px serif;}'], - ['body {font-size: 12px; font-family: serif; font-style: italic;}', 'body {font: italic 12px serif;}'], - [ - 'body {font-size: 12px; font-family: serif; font-style: italic; font-weight: bold;}', - 'body {font: italic bold 12px serif;}', - ], - [ - 'body {font-size: 12px; font-family: serif; font-style: italic; font-weight: bold; line-height: 1.6;}', - 'body {font: italic bold 12px/1.6 serif;}', - ], - [ - 'body {font-size: 12px; font-family: serif; font-style: italic; font-weight: bold; ' - . 'line-height: 1.6; font-variant: small-caps;}', - 'body {font: italic small-caps bold 12px/1.6 serif;}', - ], - ['body {margin: 1em;}', 'body {margin: 1em;}'], - ]; - } + $wrapper->addRule($borderBottomRule); + $wrapper->addRule($leftPrefixRule, $firstLeftRule); + $wrapper->addRule($textAlignRule, $firstTextRule); - /** - * @param string $sCss - * @param string $sExpected - * - * @dataProvider createDimensionsShorthandProvider - * - * @test - */ - public function createDimensionsShorthand($sCss, $sExpected) - { - $oParser = new Parser($sCss); - $oDoc = $oParser->parse(); - foreach ($oDoc->getAllDeclarationBlocks() as $oDeclaration) { - $oDeclaration->createDimensionsShorthand(); - } - self::assertSame(trim((string)$oDoc), $sExpected); - } + $rules = $wrapper->getRules(); - /** - * @return array> - */ - public function createDimensionsShorthandProvider() - { - return [ - ['body {border: 1px;}', 'body {border: 1px;}'], - ['body {margin-top: 1px;}', 'body {margin-top: 1px;}'], - ['body {margin-top: 1em; margin-right: 1em; margin-bottom: 1em; margin-left: 1em;}', 'body {margin: 1em;}'], - [ - 'body {margin-top: 1em; margin-right: 2em; margin-bottom: 1em; margin-left: 2em;}', - 'body {margin: 1em 2em;}', - ], - [ - 'body {margin-top: 1em; margin-right: 2em; margin-bottom: 3em; margin-left: 2em;}', - 'body {margin: 1em 2em 3em;}', - ], - ]; - } + self::assertSame($leftPrefixRule, $rules[0]); + self::assertSame($firstLeftRule, $rules[1]); + self::assertSame($textAlignRule, $rules[2]); + self::assertSame($firstTextRule, $rules[3]); + self::assertSame($borderBottomRule, $rules[4]); - /** - * @param string $sCss - * @param string $sExpected - * - * @dataProvider createBackgroundShorthandProvider - * - * @test - */ - public function createBackgroundShorthand($sCss, $sExpected) - { - $oParser = new Parser($sCss); - $oDoc = $oParser->parse(); - foreach ($oDoc->getAllDeclarationBlocks() as $oDeclaration) { - $oDeclaration->createBackgroundShorthand(); - } - self::assertSame(trim((string)$oDoc), $sExpected); + self::assertSame( + '.wrapper {left: 16em;left: 10px;text-align: 1;text-align: left;border-bottom-width: 1px;}', + $document->render() + ); } /** - * @return array> + * @return array */ - public function createBackgroundShorthandProvider() + public static function declarationBlocksWithCommentsProvider(): array { return [ - ['body {border: 1px;}', 'body {border: 1px;}'], - ['body {background-color: #f00;}', 'body {background: #f00;}'], - [ - 'body {background-color: #f00;background-image: url(foobar.png);}', - 'body {background: #f00 url("foobar.png");}', - ], - [ - 'body {background-color: #f00;background-image: url(foobar.png);background-repeat: no-repeat;}', - 'body {background: #f00 url("foobar.png") no-repeat;}', - ], - [ - 'body {background-color: #f00;background-image: url(foobar.png);background-repeat: no-repeat;}', - 'body {background: #f00 url("foobar.png") no-repeat;}', - ], - [ - 'body {background-color: #f00;background-image: url(foobar.png);background-repeat: no-repeat;' - . 'background-position: center;}', - 'body {background: #f00 url("foobar.png") no-repeat center;}', - ], - [ - 'body {background-color: #f00;background-image: url(foobar.png);background-repeat: no-repeat;' - . 'background-position: top left;}', - 'body {background: #f00 url("foobar.png") no-repeat top left;}', - ], + 'CSS comments with one asterisk' => ['p {color: #000;/* black */}', 'p {color: #000;}'], + 'CSS comments with two asterisks' => ['p {color: #000;/** black */}', 'p {color: #000;}'], ]; } /** * @test + * @dataProvider declarationBlocksWithCommentsProvider */ - public function overrideRules() - { - $sCss = '.wrapper { left: 10px; text-align: left; }'; - $oParser = new Parser($sCss); - $oDoc = $oParser->parse(); - $oRule = new Rule('right'); - $oRule->setValue('-10px'); - $aContents = $oDoc->getContents(); - $oWrapper = $aContents[0]; - - self::assertCount(2, $oWrapper->getRules()); - $aContents[0]->setRules([$oRule]); - - $aRules = $oWrapper->getRules(); - self::assertCount(1, $aRules); - self::assertSame('right', $aRules[0]->getRule()); - self::assertSame('-10px', $aRules[0]->getValue()); - } - - /** - * @test - */ - public function ruleInsertion() - { - $sCss = '.wrapper { left: 10px; text-align: left; }'; - $oParser = new Parser($sCss); - $oDoc = $oParser->parse(); - $aContents = $oDoc->getContents(); - $oWrapper = $aContents[0]; - - $oFirst = $oWrapper->getRules('left'); - self::assertCount(1, $oFirst); - $oFirst = $oFirst[0]; + public function canRemoveCommentsFromRulesUsingLenientParsing( + string $cssWithComments, + string $cssWithoutComments + ): void { + $parserSettings = ParserSettings::create()->withLenientParsing(true); + $document = (new Parser($cssWithComments, $parserSettings))->parse(); - $oSecond = $oWrapper->getRules('text-'); - self::assertCount(1, $oSecond); - $oSecond = $oSecond[0]; + $outputFormat = (new OutputFormat())->setRenderComments(false); + $renderedDocument = $document->render($outputFormat); - $oBefore = new Rule('left'); - $oBefore->setValue(new Size(16, 'em')); - - $oMiddle = new Rule('text-align'); - $oMiddle->setValue(new Size(1)); - - $oAfter = new Rule('border-bottom-width'); - $oAfter->setValue(new Size(1, 'px')); - - $oWrapper->addRule($oAfter); - $oWrapper->addRule($oBefore, $oFirst); - $oWrapper->addRule($oMiddle, $oSecond); - - $aRules = $oWrapper->getRules(); - - self::assertSame($oBefore, $aRules[0]); - self::assertSame($oFirst, $aRules[1]); - self::assertSame($oMiddle, $aRules[2]); - self::assertSame($oSecond, $aRules[3]); - self::assertSame($oAfter, $aRules[4]); - - self::assertSame( - '.wrapper {left: 16em;left: 10px;text-align: 1;text-align: left;border-bottom-width: 1px;}', - $oDoc->render() - ); + self::assertSame($cssWithoutComments, $renderedDocument); } /** * @test - * - * TODO: The order is different on PHP 5.6 than on PHP >= 7.0. + * @dataProvider declarationBlocksWithCommentsProvider */ - public function orderOfElementsMatchingOriginalOrderAfterExpandingShorthands() - { - $sCss = '.rule{padding:5px;padding-top: 20px}'; - $oParser = new Parser($sCss); - $oDoc = $oParser->parse(); - $aDocs = $oDoc->getAllDeclarationBlocks(); - - self::assertCount(1, $aDocs); + public function canRemoveCommentsFromRulesUsingStrictParsing( + string $cssWithComments, + string $cssWithoutComments + ): void { + $parserSettings = ParserSettings::create()->withLenientParsing(false); + $document = (new Parser($cssWithComments, $parserSettings))->parse(); - $oDeclaration = array_pop($aDocs); - $oDeclaration->expandShorthands(); + $outputFormat = (new OutputFormat())->setRenderComments(false); + $renderedDocument = $document->render($outputFormat); - self::assertEquals( - [ - 'padding-top' => 'padding-top: 20px;', - 'padding-right' => 'padding-right: 5px;', - 'padding-bottom' => 'padding-bottom: 5px;', - 'padding-left' => 'padding-left: 5px;', - ], - array_map('strval', $oDeclaration->getRulesAssoc()) - ); + self::assertSame($cssWithoutComments, $renderedDocument); } } diff --git a/tests/RuleSet/LenientParsingTest.php b/tests/RuleSet/LenientParsingTest.php index 5f5f224a..c014f021 100644 --- a/tests/RuleSet/LenientParsingTest.php +++ b/tests/RuleSet/LenientParsingTest.php @@ -1,126 +1,120 @@ expectException(UnexpectedTokenException::class); - $sFile = __DIR__ . '/../fixtures/-fault-tolerance.css'; - $oParser = new Parser(file_get_contents($sFile), Settings::create()->beStrict()); - $oParser->parse(); + $pathToFile = __DIR__ . '/../fixtures/-fault-tolerance.css'; + $parser = new Parser(\file_get_contents($pathToFile), Settings::create()->beStrict()); + $parser->parse(); } /** * @test */ - public function faultToleranceOn() + public function faultToleranceOn(): void { - $sFile = __DIR__ . '/../fixtures/-fault-tolerance.css'; - $oParser = new Parser(file_get_contents($sFile), Settings::create()->withLenientParsing(true)); - $oResult = $oParser->parse(); + $pathToFile = __DIR__ . '/../fixtures/-fault-tolerance.css'; + $parser = new Parser(\file_get_contents($pathToFile), Settings::create()->withLenientParsing(true)); + $result = $parser->parse(); self::assertSame( '.test1 {}' . "\n" . '.test2 {hello: 2.2;hello: 2000000000000.2;}' . "\n" . '#test {}' . "\n" . '#test2 {help: none;}', - $oResult->render() + $result->render() ); } /** * @test */ - public function endToken() + public function endToken(): void { $this->expectException(UnexpectedTokenException::class); - $sFile = __DIR__ . '/../fixtures/-end-token.css'; - $oParser = new Parser(file_get_contents($sFile), Settings::create()->beStrict()); - $oParser->parse(); + $pathToFile = __DIR__ . '/../fixtures/-end-token.css'; + $parser = new Parser(\file_get_contents($pathToFile), Settings::create()->beStrict()); + $parser->parse(); } /** * @test */ - public function endToken2() + public function endToken2(): void { $this->expectException(UnexpectedTokenException::class); - $sFile = __DIR__ . '/../fixtures/-end-token-2.css'; - $oParser = new Parser(file_get_contents($sFile), Settings::create()->beStrict()); - $oParser->parse(); + $pathToFile = __DIR__ . '/../fixtures/-end-token-2.css'; + $parser = new Parser(\file_get_contents($pathToFile), Settings::create()->beStrict()); + $parser->parse(); } /** * @test */ - public function endTokenPositive() + public function endTokenPositive(): void { - $sFile = __DIR__ . '/../fixtures/-end-token.css'; - $oParser = new Parser(file_get_contents($sFile), Settings::create()->withLenientParsing(true)); - $oResult = $oParser->parse(); - self::assertSame("", $oResult->render()); + $pathToFile = __DIR__ . '/../fixtures/-end-token.css'; + $parser = new Parser(\file_get_contents($pathToFile), Settings::create()->withLenientParsing(true)); + $result = $parser->parse(); + self::assertSame('', $result->render()); } /** * @test */ - public function endToken2Positive() + public function endToken2Positive(): void { - $sFile = __DIR__ . '/../fixtures/-end-token-2.css'; - $oParser = new Parser(file_get_contents($sFile), Settings::create()->withLenientParsing(true)); - $oResult = $oParser->parse(); + $pathToFile = __DIR__ . '/../fixtures/-end-token-2.css'; + $parser = new Parser(\file_get_contents($pathToFile), Settings::create()->withLenientParsing(true)); + $result = $parser->parse(); self::assertSame( '#home .bg-layout {background-image: url("/bundles/main/img/bg1.png?5");}', - $oResult->render() + $result->render() ); } /** * @test */ - public function localeTrap() + public function localeTrap(): void { - setlocale(LC_ALL, "pt_PT", "no"); - $sFile = __DIR__ . '/../fixtures/-fault-tolerance.css'; - $oParser = new Parser(file_get_contents($sFile), Settings::create()->withLenientParsing(true)); - $oResult = $oParser->parse(); + \setlocale(LC_ALL, 'pt_PT', 'no'); + $pathToFile = __DIR__ . '/../fixtures/-fault-tolerance.css'; + $parser = new Parser(\file_get_contents($pathToFile), Settings::create()->withLenientParsing(true)); + $result = $parser->parse(); self::assertSame( '.test1 {}' . "\n" . '.test2 {hello: 2.2;hello: 2000000000000.2;}' . "\n" . '#test {}' . "\n" . '#test2 {help: none;}', - $oResult->render() + $result->render() ); } /** * @test */ - public function caseInsensitivity() + public function caseInsensitivity(): void { - $sFile = __DIR__ . '/../fixtures/case-insensitivity.css'; - $oParser = new Parser(file_get_contents($sFile)); - $oResult = $oParser->parse(); + $pathToFile = __DIR__ . '/../fixtures/case-insensitivity.css'; + $parser = new Parser(\file_get_contents($pathToFile)); + $result = $parser->parse(); self::assertSame( '@charset "utf-8";' . "\n" @@ -128,7 +122,31 @@ public function caseInsensitivity() . "\n@media screen {}" . "\n#myid {case: insensitive !important;frequency: 30Hz;font-size: 1em;color: #ff0;" . 'color: hsl(40,40%,30%);font-family: Arial;}', - $oResult->render() + $result->render() ); } + + /** + * @test + */ + public function cssWithInvalidColorStillGetsParsedAsDocument(): void + { + $pathToFile = __DIR__ . '/../fixtures/invalid-color.css'; + $parser = new Parser(\file_get_contents($pathToFile), Settings::create()->withLenientParsing(true)); + $result = $parser->parse(); + + self::assertInstanceOf(Document::class, $result); + } + + /** + * @test + */ + public function invalidColorStrict(): void + { + $this->expectException(UnexpectedTokenException::class); + + $pathToFile = __DIR__ . '/../fixtures/invalid-color.css'; + $parser = new Parser(\file_get_contents($pathToFile), Settings::create()->beStrict()); + $parser->parse(); + } } diff --git a/tests/Unit/CSSList/AtRuleBlockListTest.php b/tests/Unit/CSSList/AtRuleBlockListTest.php new file mode 100644 index 00000000..0252f7d3 --- /dev/null +++ b/tests/Unit/CSSList/AtRuleBlockListTest.php @@ -0,0 +1,138 @@ +atRuleName()); + } + + /** + * @test + */ + public function atRuleArgsByDefaultReturnsEmptyString(): void + { + $subject = new AtRuleBlockList('supports'); + + self::assertSame('', $subject->atRuleArgs()); + } + + /** + * @test + */ + public function atRuleArgsReturnsArgumentsProvidedToConstructor(): void + { + $arguments = 'bar'; + + $subject = new AtRuleBlockList('', $arguments); + + self::assertSame($arguments, $subject->atRuleArgs()); + } + + /** + * @test + */ + public function getLineNoReturnsLineNumberProvidedToConstructor(): void + { + $lineNumber = 42; + + $subject = new AtRuleBlockList('', '', $lineNumber); + + self::assertSame($lineNumber, $subject->getLineNo()); + } + + /** + * @test + */ + public function isRootListAlwaysReturnsFalse(): void + { + $subject = new AtRuleBlockList('supports'); + + self::assertFalse($subject->isRootList()); + } +} diff --git a/tests/Unit/CSSList/CSSBlockListTest.php b/tests/Unit/CSSList/CSSBlockListTest.php new file mode 100644 index 00000000..41f2b41f --- /dev/null +++ b/tests/Unit/CSSList/CSSBlockListTest.php @@ -0,0 +1,474 @@ +getAllDeclarationBlocks()); + } + + /** + * @test + */ + public function getAllDeclarationBlocksReturnsOneDeclarationBlockDirectlySetAsContent(): void + { + $subject = new ConcreteCSSBlockList(); + + $declarationBlock = new DeclarationBlock(); + $subject->setContents([$declarationBlock]); + + $result = $subject->getAllDeclarationBlocks(); + + self::assertSame([$declarationBlock], $result); + } + + /** + * @test + */ + public function getAllDeclarationBlocksReturnsMultipleDeclarationBlocksDirectlySetAsContents(): void + { + $subject = new ConcreteCSSBlockList(); + + $declarationBlock1 = new DeclarationBlock(); + $declarationBlock2 = new DeclarationBlock(); + $subject->setContents([$declarationBlock1, $declarationBlock2]); + + $result = $subject->getAllDeclarationBlocks(); + + self::assertSame([$declarationBlock1, $declarationBlock2], $result); + } + + /** + * @test + */ + public function getAllDeclarationBlocksReturnsDeclarationBlocksWithinAtRuleBlockList(): void + { + $subject = new ConcreteCSSBlockList(); + + $declarationBlock = new DeclarationBlock(); + $atRuleBlockList = new AtRuleBlockList('media'); + $atRuleBlockList->setContents([$declarationBlock]); + $subject->setContents([$atRuleBlockList]); + + $result = $subject->getAllDeclarationBlocks(); + + self::assertSame([$declarationBlock], $result); + } + + /** + * @test + */ + public function getAllDeclarationBlocksIgnoresImport(): void + { + $subject = new ConcreteCSSBlockList(); + + $import = new Import(new URL(new CSSString('https://www.example.com/')), ''); + $subject->setContents([$import]); + + $result = $subject->getAllDeclarationBlocks(); + + self::assertSame([], $result); + } + + /** + * @test + */ + public function getAllDeclarationBlocksIgnoresCharset(): void + { + $subject = new ConcreteCSSBlockList(); + + $charset = new Charset(new CSSString('UTF-8')); + $subject->setContents([$charset]); + + $result = $subject->getAllDeclarationBlocks(); + + self::assertSame([], $result); + } + + /** + * @test + */ + public function getAllRuleSetsWhenNoContentSetReturnsEmptyArray(): void + { + $subject = new ConcreteCSSBlockList(); + + self::assertSame([], $subject->getAllRuleSets()); + } + + /** + * @test + */ + public function getAllRuleSetsReturnsOneDeclarationBlockDirectlySetAsContent(): void + { + $subject = new ConcreteCSSBlockList(); + + $declarationBlock = new DeclarationBlock(); + $subject->setContents([$declarationBlock]); + + $result = $subject->getAllRuleSets(); + + self::assertSame([$declarationBlock], $result); + } + + /** + * @test + */ + public function getAllRuleSetsReturnsOneAtRuleSetDirectlySetAsContent(): void + { + $subject = new ConcreteCSSBlockList(); + + $atRuleSet = new AtRuleSet('media'); + $subject->setContents([$atRuleSet]); + + $result = $subject->getAllRuleSets(); + + self::assertSame([$atRuleSet], $result); + } + + /** + * @test + */ + public function getAllRuleSetsReturnsMultipleDeclarationBlocksDirectlySetAsContents(): void + { + $subject = new ConcreteCSSBlockList(); + + $declarationBlock1 = new DeclarationBlock(); + $declarationBlock2 = new DeclarationBlock(); + $subject->setContents([$declarationBlock1, $declarationBlock2]); + + $result = $subject->getAllRuleSets(); + + self::assertSame([$declarationBlock1, $declarationBlock2], $result); + } + + /** + * @test + */ + public function getAllRuleSetsReturnsMultipleAtRuleSetsDirectlySetAsContents(): void + { + $subject = new ConcreteCSSBlockList(); + + $atRuleSet1 = new AtRuleSet('media'); + $atRuleSet2 = new AtRuleSet('media'); + $subject->setContents([$atRuleSet1, $atRuleSet2]); + + $result = $subject->getAllRuleSets(); + + self::assertSame([$atRuleSet1, $atRuleSet2], $result); + } + + /** + * @test + */ + public function getAllRuleSetsReturnsDeclarationBlocksWithinAtRuleBlockList(): void + { + $subject = new ConcreteCSSBlockList(); + + $declarationBlock = new DeclarationBlock(); + $atRuleBlockList = new AtRuleBlockList('media'); + $atRuleBlockList->setContents([$declarationBlock]); + $subject->setContents([$atRuleBlockList]); + + $result = $subject->getAllRuleSets(); + + self::assertSame([$declarationBlock], $result); + } + + /** + * @test + */ + public function getAllRuleSetsReturnsAtRuleSetsWithinAtRuleBlockList(): void + { + $subject = new ConcreteCSSBlockList(); + + $atRule = new AtRuleSet('media'); + $atRuleBlockList = new AtRuleBlockList('media'); + $atRuleBlockList->setContents([$atRule]); + $subject->setContents([$atRuleBlockList]); + + $result = $subject->getAllRuleSets(); + + self::assertSame([$atRule], $result); + } + + /** + * @test + */ + public function getAllRuleSetsIgnoresImport(): void + { + $subject = new ConcreteCSSBlockList(); + + $import = new Import(new URL(new CSSString('https://www.example.com/')), ''); + $subject->setContents([$import]); + + $result = $subject->getAllRuleSets(); + + self::assertSame([], $result); + } + + /** + * @test + */ + public function getAllRuleSetsIgnoresCharset(): void + { + $subject = new ConcreteCSSBlockList(); + + $charset = new Charset(new CSSString('UTF-8')); + $subject->setContents([$charset]); + + $result = $subject->getAllRuleSets(); + + self::assertSame([], $result); + } + + /** + * @test + */ + public function getAllValuesWhenNoContentSetReturnsEmptyArray(): void + { + $subject = new ConcreteCSSBlockList(); + + self::assertSame([], $subject->getAllValues()); + } + + /** + * @test + */ + public function getAllValuesReturnsOneValueDirectlySetAsContent(): void + { + $subject = new ConcreteCSSBlockList(); + + $value = new CSSString('Superfont'); + + $declarationBlock = new DeclarationBlock(); + $rule = new Rule('font-family'); + $rule->setValue($value); + $declarationBlock->addRule($rule); + $subject->setContents([$declarationBlock]); + + $result = $subject->getAllValues(); + + self::assertSame([$value], $result); + } + + /** + * @test + */ + public function getAllValuesReturnsMultipleValuesDirectlySetAsContentInOneDeclarationBlock(): void + { + $subject = new ConcreteCSSBlockList(); + + $value1 = new CSSString('Superfont'); + $value2 = new CSSString('aquamarine'); + + $declarationBlock = new DeclarationBlock(); + $rule1 = new Rule('font-family'); + $rule1->setValue($value1); + $declarationBlock->addRule($rule1); + $rule2 = new Rule('color'); + $rule2->setValue($value2); + $declarationBlock->addRule($rule2); + $subject->setContents([$declarationBlock]); + + $result = $subject->getAllValues(); + + self::assertSame([$value1, $value2], $result); + } + + /** + * @test + */ + public function getAllValuesReturnsMultipleValuesDirectlySetAsContentInMultipleDeclarationBlocks(): void + { + $subject = new ConcreteCSSBlockList(); + + $value1 = new CSSString('Superfont'); + $value2 = new CSSString('aquamarine'); + + $declarationBlock1 = new DeclarationBlock(); + $rule1 = new Rule('font-family'); + $rule1->setValue($value1); + $declarationBlock1->addRule($rule1); + $declarationBlock2 = new DeclarationBlock(); + $rule2 = new Rule('color'); + $rule2->setValue($value2); + $declarationBlock2->addRule($rule2); + $subject->setContents([$declarationBlock1, $declarationBlock2]); + + $result = $subject->getAllValues(); + + self::assertSame([$value1, $value2], $result); + } + + /** + * @test + */ + public function getAllValuesReturnsValuesWithinAtRuleBlockList(): void + { + $subject = new ConcreteCSSBlockList(); + + $value = new CSSString('Superfont'); + + $declarationBlock = new DeclarationBlock(); + $rule = new Rule('font-family'); + $rule->setValue($value); + $declarationBlock->addRule($rule); + $atRuleBlockList = new AtRuleBlockList('media'); + $atRuleBlockList->setContents([$declarationBlock]); + $subject->setContents([$atRuleBlockList]); + + $result = $subject->getAllValues(); + + self::assertSame([$value], $result); + } + + /** + * @test + */ + public function getAllValuesWithElementProvidedReturnsOnlyValuesWithinThatElement(): void + { + $subject = new ConcreteCSSBlockList(); + + $value1 = new CSSString('Superfont'); + $value2 = new CSSString('aquamarine'); + + $declarationBlock1 = new DeclarationBlock(); + $rule1 = new Rule('font-family'); + $rule1->setValue($value1); + $declarationBlock1->addRule($rule1); + $declarationBlock2 = new DeclarationBlock(); + $rule2 = new Rule('color'); + $rule2->setValue($value2); + $declarationBlock2->addRule($rule2); + $subject->setContents([$declarationBlock1, $declarationBlock2]); + + $result = $subject->getAllValues($declarationBlock1); + + self::assertSame([$value1], $result); + } + + /** + * @test + */ + public function getAllValuesWithSearchStringProvidedReturnsOnlyValuesFromMatchingRules(): void + { + $subject = new ConcreteCSSBlockList(); + + $value1 = new CSSString('Superfont'); + $value2 = new CSSString('aquamarine'); + + $declarationBlock = new DeclarationBlock(); + $rule1 = new Rule('font-family'); + $rule1->setValue($value1); + $declarationBlock->addRule($rule1); + $rule2 = new Rule('color'); + $rule2->setValue($value2); + $declarationBlock->addRule($rule2); + $subject->setContents([$declarationBlock]); + + $result = $subject->getAllValues(null, 'font-'); + + self::assertSame([$value1], $result); + } + + /** + * @test + */ + public function getAllValuesByDefaultDoesNotReturnValuesInFunctionArguments(): void + { + $subject = new ConcreteCSSBlockList(); + + $value1 = new Size(10, 'px'); + $value2 = new Size(2, '%'); + + $declarationBlock = new DeclarationBlock(); + $rule = new Rule('margin'); + $rule->setValue(new CSSFunction('max', [$value1, $value2])); + $declarationBlock->addRule($rule); + $subject->setContents([$declarationBlock]); + + $result = $subject->getAllValues(); + + self::assertSame([], $result); + } + + /** + * @test + */ + public function getAllValuesWithSearchInFunctionArgumentsReturnsValuesInFunctionArguments(): void + { + $subject = new ConcreteCSSBlockList(); + + $value1 = new Size(10, 'px'); + $value2 = new Size(2, '%'); + + $declarationBlock = new DeclarationBlock(); + $rule = new Rule('margin'); + $rule->setValue(new CSSFunction('max', [$value1, $value2])); + $declarationBlock->addRule($rule); + $subject->setContents([$declarationBlock]); + + $result = $subject->getAllValues(null, null, true); + + self::assertSame([$value1, $value2], $result); + } +} diff --git a/tests/Unit/CSSList/CSSListTest.php b/tests/Unit/CSSList/CSSListTest.php new file mode 100644 index 00000000..03539533 --- /dev/null +++ b/tests/Unit/CSSList/CSSListTest.php @@ -0,0 +1,193 @@ +getLineNo()); + } + + /** + * @test + */ + public function getLineNoReturnsLineNumberProvidedToConstructor(): void + { + $lineNumber = 42; + + $subject = new ConcreteCSSList($lineNumber); + + self::assertSame($lineNumber, $subject->getLineNo()); + } + + /** + * @test + */ + public function getContentsInitiallyReturnsEmptyArray(): void + { + $subject = new ConcreteCSSList(); + + self::assertSame([], $subject->getContents()); + } + + /** + * @return array}> + */ + public static function contentsDataProvider(): array + { + return [ + 'empty array' => [[]], + '1 item' => [[new DeclarationBlock()]], + '2 items' => [[new DeclarationBlock(), new DeclarationBlock()]], + ]; + } + + /** + * @test + * + * @param list $contents + * + * @dataProvider contentsDataProvider + */ + public function setContentsSetsContents(array $contents): void + { + $subject = new ConcreteCSSList(); + + $subject->setContents($contents); + + self::assertSame($contents, $subject->getContents()); + } + + /** + * @test + */ + public function setContentsReplacesContentsSetInPreviousCall(): void + { + $subject = new ConcreteCSSList(); + + $contents2 = [new DeclarationBlock()]; + + $subject->setContents([new DeclarationBlock()]); + $subject->setContents($contents2); + + self::assertSame($contents2, $subject->getContents()); + } + + /** + * @test + */ + public function insertBeforeInsertsContentBeforeSibling(): void + { + $subject = new ConcreteCSSList(); + + $bogusOne = new DeclarationBlock(); + $bogusOne->setSelectors('.bogus-one'); + $bogusTwo = new DeclarationBlock(); + $bogusTwo->setSelectors('.bogus-two'); + + $item = new DeclarationBlock(); + $item->setSelectors('.item'); + + $sibling = new DeclarationBlock(); + $sibling->setSelectors('.sibling'); + + $subject->setContents([$bogusOne, $sibling, $bogusTwo]); + + self::assertCount(3, $subject->getContents()); + + $subject->insertBefore($item, $sibling); + + self::assertCount(4, $subject->getContents()); + self::assertSame([$bogusOne, $item, $sibling, $bogusTwo], $subject->getContents()); + } + + /** + * @test + */ + public function insertBeforeAppendsIfSiblingNotFound(): void + { + $subject = new ConcreteCSSList(); + + $bogusOne = new DeclarationBlock(); + $bogusOne->setSelectors('.bogus-one'); + $bogusTwo = new DeclarationBlock(); + $bogusTwo->setSelectors('.bogus-two'); + + $item = new DeclarationBlock(); + $item->setSelectors('.item'); + + $sibling = new DeclarationBlock(); + $sibling->setSelectors('.sibling'); + + $orphan = new DeclarationBlock(); + $orphan->setSelectors('.forever-alone'); + + $subject->setContents([$bogusOne, $sibling, $bogusTwo]); + + self::assertCount(3, $subject->getContents()); + + $subject->insertBefore($item, $orphan); + + self::assertCount(4, $subject->getContents()); + self::assertSame([$bogusOne, $sibling, $bogusTwo, $item], $subject->getContents()); + } +} diff --git a/tests/Unit/CSSList/DocumentTest.php b/tests/Unit/CSSList/DocumentTest.php new file mode 100644 index 00000000..77e8aa81 --- /dev/null +++ b/tests/Unit/CSSList/DocumentTest.php @@ -0,0 +1,66 @@ +isRootList()); + } +} diff --git a/tests/Unit/CSSList/Fixtures/ConcreteCSSBlockList.php b/tests/Unit/CSSList/Fixtures/ConcreteCSSBlockList.php new file mode 100644 index 00000000..956c7036 --- /dev/null +++ b/tests/Unit/CSSList/Fixtures/ConcreteCSSBlockList.php @@ -0,0 +1,21 @@ +getLineNo()); + } + + /** + * @test + */ + public function getAnimationNameByDefaultReturnsNone(): void + { + $subject = new KeyFrame(); + + self::assertSame('none', $subject->getAnimationName()); + } + + /** + * @test + */ + public function getVendorKeyFrameByDefaultReturnsKeyframes(): void + { + $subject = new KeyFrame(); + + self::assertSame('keyframes', $subject->getVendorKeyFrame()); + } +} diff --git a/tests/Unit/Comment/CommentContainerTest.php b/tests/Unit/Comment/CommentContainerTest.php new file mode 100644 index 00000000..d0e844a4 --- /dev/null +++ b/tests/Unit/Comment/CommentContainerTest.php @@ -0,0 +1,236 @@ +subject = new ConcreteCommentContainer(); + } + + /** + * @test + */ + public function getCommentsInitiallyReturnsEmptyArray(): void + { + self::assertSame([], $this->subject->getComments()); + } + + /** + * @return array}> + */ + public function provideCommentArray(): array + { + return [ + 'no comment' => [[]], + 'one comment' => [[new Comment('Is this really a spoon?')]], + 'two comments' => [ + [ + new Comment('I’m a teapot.'), + new Comment('I’m a cafetière.'), + ], + ], + ]; + } + + /** + * @test + * + * @param list $comments + * + * @dataProvider provideCommentArray + */ + public function addCommentsOnVirginContainerAddsCommentsProvided(array $comments): void + { + $this->subject->addComments($comments); + + self::assertSame($comments, $this->subject->getComments()); + } + + /** + * @test + * + * @param list $comments + * + * @dataProvider provideCommentArray + */ + public function addCommentsWithEmptyArrayKeepsOriginalCommentsUnchanged(array $comments): void + { + $this->subject->setComments($comments); + + $this->subject->addComments([]); + + self::assertSame($comments, $this->subject->getComments()); + } + + /** + * @return array}> + */ + public function provideAlternativeCommentArray(): array + { + return [ + 'no comment' => [[]], + 'one comment' => [[new Comment('Can I eat it with my hands?')]], + 'two comments' => [ + [ + new Comment('I’m a beer barrel.'), + new Comment('I’m a vineyard.'), + ], + ], + ]; + } + + /** + * @return array}> + */ + public function provideAlternativeNonemptyCommentArray(): array + { + $data = $this->provideAlternativeCommentArray(); + + unset($data['no comment']); + + return $data; + } + + /** + * This provider crosses two comment arrays (0, 1 or 2 comments) with different comments, + * so that all combinations can be tested. + * + * @return DataProvider, 1: list}> + */ + public function provideTwoDistinctCommentArrays(): DataProvider + { + return DataProvider::cross($this->provideCommentArray(), $this->provideAlternativeCommentArray()); + } + + /** + * @return DataProvider, 1: non-empty-list}> + */ + public function provideTwoDistinctCommentArraysWithSecondNonempty(): DataProvider + { + return DataProvider::cross($this->provideCommentArray(), $this->provideAlternativeNonemptyCommentArray()); + } + + private static function createContainsConstraint(Comment $comment): TraversableContains + { + return new TraversableContains($comment); + } + + /** + * @param non-empty-list $comments + * + * @return non-empty-list + */ + private static function createContainsConstraints(array $comments): array + { + return \array_map([self::class, 'createContainsConstraint'], $comments); + } + + /** + * @test + * + * @param list $commentsToAdd + * @param non-empty-list $originalComments + * + * @dataProvider provideTwoDistinctCommentArraysWithSecondNonempty + */ + public function addCommentsKeepsOriginalComments(array $commentsToAdd, array $originalComments): void + { + $this->subject->setComments($originalComments); + + $this->subject->addComments($commentsToAdd); + + self::assertThat( + $this->subject->getComments(), + LogicalAnd::fromConstraints(...self::createContainsConstraints($originalComments)) + ); + } + + /** + * @test + * + * @param list $originalComments + * @param non-empty-list $commentsToAdd + * + * @dataProvider provideTwoDistinctCommentArraysWithSecondNonempty + */ + public function addCommentsAfterCommentsSetAddsCommentsProvided(array $originalComments, array $commentsToAdd): void + { + $this->subject->setComments($originalComments); + + $this->subject->addComments($commentsToAdd); + + self::assertThat( + $this->subject->getComments(), + LogicalAnd::fromConstraints(...self::createContainsConstraints($commentsToAdd)) + ); + } + + /** + * @test + * + * @param non-empty-list $comments + * + * @dataProvider provideAlternativeNonemptyCommentArray + */ + public function addCommentsAppends(array $comments): void + { + $firstComment = new Comment('I must be first!'); + $this->subject->setComments([$firstComment]); + + $this->subject->addComments($comments); + + $result = $this->subject->getComments(); + self::assertNotEmpty($result); + self::assertSame($firstComment, $result[0]); + } + + /** + * @test + * + * @param list $comments + * + * @dataProvider provideCommentArray + */ + public function setCommentsOnVirginContainerSetsCommentsProvided(array $comments): void + { + $this->subject->setComments($comments); + + self::assertSame($comments, $this->subject->getComments()); + } + + /** + * @test + * + * @param list $originalComments + * @param list $commentsToSet + * + * @dataProvider provideTwoDistinctCommentArrays + */ + public function setCommentsReplacesWithCommentsProvided(array $originalComments, array $commentsToSet): void + { + $this->subject->setComments($originalComments); + + $this->subject->setComments($commentsToSet); + + self::assertSame($commentsToSet, $this->subject->getComments()); + } +} diff --git a/tests/Unit/Comment/CommentTest.php b/tests/Unit/Comment/CommentTest.php new file mode 100644 index 00000000..2bfe670c --- /dev/null +++ b/tests/Unit/Comment/CommentTest.php @@ -0,0 +1,80 @@ +getComment()); + } + + /** + * @test + */ + public function getCommentInitiallyReturnsCommentPassedToConstructor(): void + { + $comment = 'There is no spoon.'; + $subject = new Comment($comment); + + self::assertSame($comment, $subject->getComment()); + } + + /** + * @test + */ + public function setCommentSetsComments(): void + { + $comment = 'There is no spoon.'; + $subject = new Comment(); + + $subject->setComment($comment); + + self::assertSame($comment, $subject->getComment()); + } + + /** + * @test + */ + public function getLineNoOnEmptyInstanceReturnsZero(): void + { + $subject = new Comment(); + + self::assertSame(0, $subject->getLineNo()); + } + + /** + * @test + */ + public function getLineNoInitiallyReturnsLineNumberPassedToConstructor(): void + { + $lineNumber = 42; + $subject = new Comment('', $lineNumber); + + self::assertSame($lineNumber, $subject->getLineNo()); + } +} diff --git a/tests/Unit/Comment/Fixtures/ConcreteCommentContainer.php b/tests/Unit/Comment/Fixtures/ConcreteCommentContainer.php new file mode 100644 index 00000000..39f6ec37 --- /dev/null +++ b/tests/Unit/Comment/Fixtures/ConcreteCommentContainer.php @@ -0,0 +1,13 @@ +subject = new OutputFormat(); + } + + /** + * @test + */ + public function getStringQuotingTypeInitiallyReturnsDoubleQuote(): void + { + self::assertSame('"', $this->subject->getStringQuotingType()); + } + + /** + * @test + */ + public function setStringQuotingTypeSetsStringQuotingType(): void + { + $value = "'"; + $this->subject->setStringQuotingType($value); + + self::assertSame($value, $this->subject->getStringQuotingType()); + } + + /** + * @test + */ + public function setStringQuotingTypeProvidesFluentInterface(): void + { + self::assertSame($this->subject, $this->subject->setStringQuotingType('"')); + } + + /** + * @test + */ + public function usesRgbHashNotationInitiallyReturnsTrue(): void + { + self::assertTrue($this->subject->usesRgbHashNotation()); + } + + /** + * @return array + */ + public static function provideBooleans(): array + { + return [ + 'true' => [true], + 'false' => [false], + ]; + } + + /** + * @test + * + * @dataProvider provideBooleans + */ + public function setRGBHashNotationSetsRGBHashNotation(bool $value): void + { + $this->subject->setRGBHashNotation($value); + + self::assertSame($value, $this->subject->usesRgbHashNotation()); + } + + /** + * @test + */ + public function setRGBHashNotationProvidesFluentInterface(): void + { + self::assertSame($this->subject, $this->subject->setRGBHashNotation(true)); + } + + /** + * @test + */ + public function shouldRenderSemicolonAfterLastRuleInitiallyReturnsTrue(): void + { + self::assertTrue($this->subject->shouldRenderSemicolonAfterLastRule()); + } + + /** + * @test + * + * @dataProvider provideBooleans + */ + public function setSemicolonAfterLastRuleSetsSemicolonAfterLastRule(bool $value): void + { + $this->subject->setSemicolonAfterLastRule($value); + + self::assertSame($value, $this->subject->shouldRenderSemicolonAfterLastRule()); + } + + /** + * @test + */ + public function setSemicolonAfterLastRuleProvidesFluentInterface(): void + { + self::assertSame($this->subject, $this->subject->setSemicolonAfterLastRule(true)); + } + + /** + * @test + */ + public function getSpaceAfterRuleNameInitiallyReturnsSingleSpace(): void + { + self::assertSame(' ', $this->subject->getSpaceAfterRuleName()); + } + + /** + * @test + */ + public function setSpaceAfterRuleNameSetsSpaceAfterRuleName(): void + { + $value = "\n"; + $this->subject->setSpaceAfterRuleName($value); + + self::assertSame($value, $this->subject->getSpaceAfterRuleName()); + } + + /** + * @test + */ + public function setSpaceAfterRuleNameProvidesFluentInterface(): void + { + self::assertSame($this->subject, $this->subject->setSpaceAfterRuleName("\n")); + } + + /** + * @test + */ + public function getSpaceBeforeRulesInitiallyReturnsEmptyString(): void + { + self::assertSame('', $this->subject->getSpaceBeforeRules()); + } + + /** + * @test + */ + public function setSpaceBeforeRulesSetsSpaceBeforeRules(): void + { + $value = ' '; + $this->subject->setSpaceBeforeRules($value); + + self::assertSame($value, $this->subject->getSpaceBeforeRules()); + } + + /** + * @test + */ + public function setSpaceBeforeRulesProvidesFluentInterface(): void + { + self::assertSame($this->subject, $this->subject->setSpaceBeforeRules(' ')); + } + + /** + * @test + */ + public function getSpaceAfterRulesInitiallyReturnsEmptyString(): void + { + self::assertSame('', $this->subject->getSpaceAfterRules()); + } + + /** + * @test + */ + public function setSpaceAfterRulesSetsSpaceAfterRules(): void + { + $value = ' '; + $this->subject->setSpaceAfterRules($value); + + self::assertSame($value, $this->subject->getSpaceAfterRules()); + } + + /** + * @test + */ + public function setSpaceAfterRulesProvidesFluentInterface(): void + { + self::assertSame($this->subject, $this->subject->setSpaceAfterRules(' ')); + } + + /** + * @test + */ + public function getSpaceBetweenRulesInitiallyReturnsEmptyString(): void + { + self::assertSame('', $this->subject->getSpaceBetweenRules()); + } + + /** + * @test + */ + public function setSpaceBetweenRulesSetsSpaceBetweenRules(): void + { + $value = ' '; + $this->subject->setSpaceBetweenRules($value); + + self::assertSame($value, $this->subject->getSpaceBetweenRules()); + } + + /** + * @test + */ + public function setSpaceBetweenRulesProvidesFluentInterface(): void + { + self::assertSame($this->subject, $this->subject->setSpaceBetweenRules(' ')); + } + + /** + * @test + */ + public function getSpaceBeforeBlocksInitiallyReturnsEmptyString(): void + { + self::assertSame('', $this->subject->getSpaceBeforeBlocks()); + } + + /** + * @test + */ + public function setSpaceBeforeBlocksSetsSpaceBeforeBlocks(): void + { + $value = ' '; + $this->subject->setSpaceBeforeBlocks($value); + + self::assertSame($value, $this->subject->getSpaceBeforeBlocks()); + } + + /** + * @test + */ + public function setSpaceBeforeBlocksProvidesFluentInterface(): void + { + self::assertSame($this->subject, $this->subject->setSpaceBeforeBlocks(' ')); + } + + /** + * @test + */ + public function getSpaceAfterBlocksInitiallyReturnsEmptyString(): void + { + self::assertSame('', $this->subject->getSpaceAfterBlocks()); + } + + /** + * @test + */ + public function setSpaceAfterBlocksSetsSpaceAfterBlocks(): void + { + $value = ' '; + $this->subject->setSpaceAfterBlocks($value); + + self::assertSame($value, $this->subject->getSpaceAfterBlocks()); + } + + /** + * @test + */ + public function setSpaceAfterBlocksProvidesFluentInterface(): void + { + self::assertSame($this->subject, $this->subject->setSpaceAfterBlocks(' ')); + } + + /** + * @test + */ + public function getSpaceBetweenBlocksInitiallyReturnsNewline(): void + { + self::assertSame("\n", $this->subject->getSpaceBetweenBlocks()); + } + + /** + * @test + */ + public function setSpaceBetweenBlocksSetsSpaceBetweenBlocks(): void + { + $value = ' '; + $this->subject->setSpaceBetweenBlocks($value); + + self::assertSame($value, $this->subject->getSpaceBetweenBlocks()); + } + + /** + * @test + */ + public function setSpaceBetweenBlocksProvidesFluentInterface(): void + { + self::assertSame($this->subject, $this->subject->setSpaceBetweenBlocks(' ')); + } + + /** + * @test + */ + public function getContentBeforeAtRuleBlockInitiallyReturnsEmptyString(): void + { + self::assertSame('', $this->subject->getContentBeforeAtRuleBlock()); + } + + /** + * @test + */ + public function setBeforeAtRuleBlockSetsBeforeAtRuleBlock(): void + { + $value = ' '; + $this->subject->setBeforeAtRuleBlock($value); + + self::assertSame($value, $this->subject->getContentBeforeAtRuleBlock()); + } + + /** + * @test + */ + public function setBeforeAtRuleBlockProvidesFluentInterface(): void + { + self::assertSame($this->subject, $this->subject->setBeforeAtRuleBlock(' ')); + } + + /** + * @test + */ + public function getContentAfterAtRuleBlockInitiallyReturnsEmptyString(): void + { + self::assertSame('', $this->subject->getContentAfterAtRuleBlock()); + } + + /** + * @test + */ + public function setAfterAtRuleBlockSetsAfterAtRuleBlock(): void + { + $value = ' '; + $this->subject->setAfterAtRuleBlock($value); + + self::assertSame($value, $this->subject->getContentAfterAtRuleBlock()); + } + + /** + * @test + */ + public function setAfterAtRuleBlockProvidesFluentInterface(): void + { + self::assertSame($this->subject, $this->subject->setAfterAtRuleBlock(' ')); + } + + /** + * @test + */ + public function getSpaceBeforeSelectorSeparatorInitiallyReturnsEmptyString(): void + { + self::assertSame('', $this->subject->getSpaceBeforeSelectorSeparator()); + } + + /** + * @test + */ + public function setSpaceBeforeSelectorSeparatorSetsSpaceBeforeSelectorSeparator(): void + { + $value = ' '; + $this->subject->setSpaceBeforeSelectorSeparator($value); + + self::assertSame($value, $this->subject->getSpaceBeforeSelectorSeparator()); + } + + /** + * @test + */ + public function setSpaceBeforeSelectorSeparatorProvidesFluentInterface(): void + { + self::assertSame($this->subject, $this->subject->setSpaceBeforeSelectorSeparator(' ')); + } + + /** + * @test + */ + public function getSpaceAfterSelectorSeparatorInitiallyReturnsSpace(): void + { + self::assertSame(' ', $this->subject->getSpaceAfterSelectorSeparator()); + } + + /** + * @test + */ + public function setSpaceAfterSelectorSeparatorSetsSpaceAfterSelectorSeparator(): void + { + $value = ' '; + $this->subject->setSpaceAfterSelectorSeparator($value); + + self::assertSame($value, $this->subject->getSpaceAfterSelectorSeparator()); + } + + /** + * @test + */ + public function setSpaceAfterSelectorSeparatorProvidesFluentInterface(): void + { + self::assertSame($this->subject, $this->subject->setSpaceAfterSelectorSeparator(' ')); + } + + /** + * @test + */ + public function getSpaceBeforeListArgumentSeparatorInitiallyReturnsEmptyString(): void + { + self::assertSame('', $this->subject->getSpaceBeforeListArgumentSeparator()); + } + + /** + * @test + */ + public function setSpaceBeforeListArgumentSeparatorSetsSpaceBeforeListArgumentSeparator(): void + { + $value = ' '; + $this->subject->setSpaceBeforeListArgumentSeparator($value); + + self::assertSame($value, $this->subject->getSpaceBeforeListArgumentSeparator()); + } + + /** + * @test + */ + public function setSpaceBeforeListArgumentSeparatorProvidesFluentInterface(): void + { + self::assertSame($this->subject, $this->subject->setSpaceBeforeListArgumentSeparator(' ')); + } + + /** + * @test + */ + public function getSpaceBeforeListArgumentSeparatorsInitiallyReturnsEmptyArray(): void + { + self::assertSame([], $this->subject->getSpaceBeforeListArgumentSeparators()); + } + + /** + * @test + */ + public function setSpaceBeforeListArgumentSeparatorsSetsSpaceBeforeListArgumentSeparators(): void + { + $value = ['/' => ' ']; + $this->subject->setSpaceBeforeListArgumentSeparators($value); + + self::assertSame($value, $this->subject->getSpaceBeforeListArgumentSeparators()); + } + + /** + * @test + */ + public function setSpaceBeforeListArgumentSeparatorsProvidesFluentInterface(): void + { + self::assertSame($this->subject, $this->subject->setSpaceBeforeListArgumentSeparators([])); + } + + /** + * @test + */ + public function getSpaceAfterListArgumentSeparatorInitiallyReturnsEmptyString(): void + { + self::assertSame('', $this->subject->getSpaceAfterListArgumentSeparator()); + } + + /** + * @test + */ + public function setSpaceAfterListArgumentSeparatorSetsSpaceAfterListArgumentSeparator(): void + { + $value = ' '; + $this->subject->setSpaceAfterListArgumentSeparator($value); + + self::assertSame($value, $this->subject->getSpaceAfterListArgumentSeparator()); + } + + /** + * @test + */ + public function setSpaceAfterListArgumentSeparatorProvidesFluentInterface(): void + { + self::assertSame($this->subject, $this->subject->setSpaceAfterListArgumentSeparator(' ')); + } + + /** + * @test + */ + public function getSpaceAfterListArgumentSeparatorsInitiallyReturnsEmptyArray(): void + { + self::assertSame([], $this->subject->getSpaceAfterListArgumentSeparators()); + } + + /** + * @test + */ + public function setSpaceAfterListArgumentSeparatorsSetsSpaceAfterListArgumentSeparators(): void + { + $value = [',' => ' ']; + $this->subject->setSpaceAfterListArgumentSeparators($value); + + self::assertSame($value, $this->subject->getSpaceAfterListArgumentSeparators()); + } + + /** + * @test + */ + public function setSpaceAfterListArgumentSeparatorsProvidesFluentInterface(): void + { + self::assertSame($this->subject, $this->subject->setSpaceAfterListArgumentSeparators([])); + } + + /** + * @test + */ + public function getSpaceBeforeOpeningBraceInitiallyReturnsSpace(): void + { + self::assertSame(' ', $this->subject->getSpaceBeforeOpeningBrace()); + } + + /** + * @test + */ + public function setSpaceBeforeOpeningBraceSetsSpaceBeforeOpeningBrace(): void + { + $value = "\t"; + $this->subject->setSpaceBeforeOpeningBrace($value); + + self::assertSame($value, $this->subject->getSpaceBeforeOpeningBrace()); + } + + /** + * @test + */ + public function setSpaceBeforeOpeningBraceProvidesFluentInterface(): void + { + self::assertSame($this->subject, $this->subject->setSpaceBeforeOpeningBrace(' ')); + } + + /** + * @test + */ + public function getContentBeforeDeclarationBlockInitiallyReturnsEmptyString(): void + { + self::assertSame('', $this->subject->getContentBeforeDeclarationBlock()); + } + + /** + * @test + */ + public function setBeforeDeclarationBlockSetsBeforeDeclarationBlock(): void + { + $value = ' '; + $this->subject->setBeforeDeclarationBlock($value); + + self::assertSame($value, $this->subject->getContentBeforeDeclarationBlock()); + } + + /** + * @test + */ + public function setBeforeDeclarationBlockProvidesFluentInterface(): void + { + self::assertSame($this->subject, $this->subject->setBeforeDeclarationBlock(' ')); + } + + /** + * @test + */ + public function getContentAfterDeclarationBlockSelectorsInitiallyReturnsEmptyString(): void + { + self::assertSame('', $this->subject->getContentAfterDeclarationBlockSelectors()); + } + + /** + * @test + */ + public function setAfterDeclarationBlockSelectorsSetsAfterDeclarationBlockSelectors(): void + { + $value = ' '; + $this->subject->setAfterDeclarationBlockSelectors($value); + + self::assertSame($value, $this->subject->getContentAfterDeclarationBlockSelectors()); + } + + /** + * @test + */ + public function setAfterDeclarationBlockSelectorsProvidesFluentInterface(): void + { + self::assertSame($this->subject, $this->subject->setAfterDeclarationBlockSelectors(' ')); + } + + /** + * @test + */ + public function getContentAfterDeclarationBlockInitiallyReturnsEmptyString(): void + { + self::assertSame('', $this->subject->getContentAfterDeclarationBlock()); + } + + /** + * @test + */ + public function setAfterDeclarationBlockSetsAfterDeclarationBlock(): void + { + $value = ' '; + $this->subject->setAfterDeclarationBlock($value); + + self::assertSame($value, $this->subject->getContentAfterDeclarationBlock()); + } + + /** + * @test + */ + public function setAfterDeclarationBlockProvidesFluentInterface(): void + { + self::assertSame($this->subject, $this->subject->setAfterDeclarationBlock(' ')); + } + + /** + * @test + */ + public function getIndentationInitiallyReturnsTab(): void + { + self::assertSame("\t", $this->subject->getIndentation()); + } + + /** + * @test + */ + public function setIndentationSetsIndentation(): void + { + $value = ' '; + $this->subject->setIndentation($value); + + self::assertSame($value, $this->subject->getIndentation()); + } + + /** + * @test + */ + public function setIndentationProvidesFluentInterface(): void + { + self::assertSame($this->subject, $this->subject->setIndentation(' ')); + } + + /** + * @test + */ + public function shouldIgnoreExceptionsInitiallyReturnsFalse(): void + { + self::assertFalse($this->subject->shouldIgnoreExceptions()); + } + + /** + * @test + * + * @dataProvider provideBooleans + */ + public function setIgnoreExceptionsSetsIgnoreExceptions(bool $value): void + { + $this->subject->setIgnoreExceptions($value); + + self::assertSame($value, $this->subject->shouldIgnoreExceptions()); + } + + /** + * @test + */ + public function setIgnoreExceptionsProvidesFluentInterface(): void + { + self::assertSame($this->subject, $this->subject->setIgnoreExceptions(true)); + } + + /** + * @test + */ + public function shouldRenderCommentsInitiallyReturnsFalse(): void + { + self::assertFalse($this->subject->shouldRenderComments()); + } + + /** + * @test + * + * @dataProvider provideBooleans + */ + public function setRenderCommentsSetsRenderComments(bool $value): void + { + $this->subject->setRenderComments($value); + + self::assertSame($value, $this->subject->shouldRenderComments()); + } + + /** + * @test + */ + public function setRenderCommentsProvidesFluentInterface(): void + { + self::assertSame($this->subject, $this->subject->setRenderComments(true)); + } + + /** + * @test + */ + public function getIndentationLevelInitiallyReturnsZero(): void + { + self::assertSame(0, $this->subject->getIndentationLevel()); + } + + /** + * @test + */ + public function indentWithTabsByDefaultSetsIndentationToOneTab(): void + { + $this->subject->indentWithTabs(); + + self::assertSame("\t", $this->subject->getIndentation()); + } + + /** + * @return array, 1: string}> + */ + public static function provideTabIndentation(): array + { + return [ + 'zero tabs' => [0, ''], + 'one tab' => [1, "\t"], + 'two tabs' => [2, "\t\t"], + 'three tabs' => [3, "\t\t\t"], + ]; + } + + /** + * @test + * @dataProvider provideTabIndentation + */ + public function indentWithTabsSetsIndentationToTheProvidedNumberOfTabs( + int $numberOfTabs, + string $expectedIndentation + ): void { + $this->subject->indentWithTabs($numberOfTabs); + + self::assertSame($expectedIndentation, $this->subject->getIndentation()); + } + + /** + * @test + */ + public function indentWithTabsProvidesFluentInterface(): void + { + self::assertSame($this->subject, $this->subject->indentWithTabs()); + } + + /** + * @test + */ + public function indentWithSpacesByDefaultSetsIndentationToTwoSpaces(): void + { + $this->subject->indentWithSpaces(); + + self::assertSame(' ', $this->subject->getIndentation()); + } + + /** + * @return array, 1: string}> + */ + public static function provideSpaceIndentation(): array + { + return [ + 'zero spaces' => [0, ''], + 'one space' => [1, ' '], + 'two spaces' => [2, ' '], + 'three spaces' => [3, ' '], + 'four spaces' => [4, ' '], + ]; + } + + /** + * @test + * @dataProvider provideSpaceIndentation + */ + public function indentWithSpacesSetsIndentationToTheProvidedNumberOfSpaces( + int $numberOfSpaces, + string $expectedIndentation + ): void { + $this->subject->indentWithSpaces($numberOfSpaces); + + self::assertSame($expectedIndentation, $this->subject->getIndentation()); + } + + /** + * @test + */ + public function indentWithSpacesProvidesFluentInterface(): void + { + self::assertSame($this->subject, $this->subject->indentWithSpaces()); + } + + /** + * @test + */ + public function nextLevelReturnsOutputFormatInstance(): void + { + self::assertInstanceOf(OutputFormat::class, $this->subject->nextLevel()); + } + + /** + * @test + */ + public function nextLevelReturnsDifferentInstance(): void + { + self::assertNotSame($this->subject, $this->subject->nextLevel()); + } + + /** + * @test + */ + public function nextLevelReturnsCloneWithSameProperties(): void + { + $space = ' '; + $this->subject->setSpaceAfterRuleName($space); + + self::assertSame($space, $this->subject->nextLevel()->getSpaceAfterRuleName()); + } + + /** + * @test + */ + public function nextLevelReturnsInstanceWithIndentationLevelIncreasedByOne(): void + { + $originalIndentationLevel = $this->subject->getIndentationLevel(); + + self::assertSame($originalIndentationLevel + 1, $this->subject->nextLevel()->getIndentationLevel()); + } + + /** + * @test + */ + public function nextLevelReturnsInstanceWithDifferentFormatterInstance(): void + { + $formatter = $this->subject->getFormatter(); + + self::assertNotSame($formatter, $this->subject->nextLevel()->getFormatter()); + } + + /** + * @test + */ + public function beLenientSetsIgnoreExceptionsToTrue(): void + { + $this->subject->setIgnoreExceptions(false); + + $this->subject->beLenient(); + + self::assertTrue($this->subject->shouldIgnoreExceptions()); + } + + /** + * @test + */ + public function getFormatterReturnsOutputFormatterInstance(): void + { + self::assertInstanceOf(OutputFormatter::class, $this->subject->getFormatter()); + } + + /** + * @test + */ + public function getFormatterCalledTwoTimesReturnsSameInstance(): void + { + $firstCallResult = $this->subject->getFormatter(); + $secondCallResult = $this->subject->getFormatter(); + + self::assertSame($firstCallResult, $secondCallResult); + } + + /** + * @test + */ + public function createReturnsOutputFormatInstance(): void + { + self::assertInstanceOf(OutputFormat::class, OutputFormat::create()); + } + + /** + * @test + */ + public function createCreatesInstanceWithDefaultSettings(): void + { + self::assertEquals(new OutputFormat(), OutputFormat::create()); + } + + /** + * @test + */ + public function createCalledTwoTimesReturnsDifferentInstances(): void + { + $firstCallResult = OutputFormat::create(); + $secondCallResult = OutputFormat::create(); + + self::assertNotSame($firstCallResult, $secondCallResult); + } + + /** + * @test + */ + public function createCompactReturnsOutputFormatInstance(): void + { + self::assertInstanceOf(OutputFormat::class, OutputFormat::createCompact()); + } + + /** + * @test + */ + public function createCompactCalledTwoTimesReturnsDifferentInstances(): void + { + $firstCallResult = OutputFormat::createCompact(); + $secondCallResult = OutputFormat::createCompact(); + + self::assertNotSame($firstCallResult, $secondCallResult); + } + + /** + * @test + */ + public function createCompactReturnsInstanceWithSpaceBeforeRulesSetToEmptyString(): void + { + $newInstance = OutputFormat::createCompact(); + + self::assertSame('', $newInstance->getSpaceBeforeRules()); + } + + /** + * @test + */ + public function createCompactReturnsInstanceWithSpaceBetweenRulesSetToEmptyString(): void + { + $newInstance = OutputFormat::createCompact(); + + self::assertSame('', $newInstance->getSpaceBetweenRules()); + } + + /** + * @test + */ + public function createCompactReturnsInstanceWithSpaceAfterRulesSetToEmptyString(): void + { + $newInstance = OutputFormat::createCompact(); + + self::assertSame('', $newInstance->getSpaceAfterRules()); + } + + /** + * @test + */ + public function createCompactReturnsInstanceWithSpaceBeforeBlocksSetToEmptyString(): void + { + $newInstance = OutputFormat::createCompact(); + + self::assertSame('', $newInstance->getSpaceBeforeBlocks()); + } + + /** + * @test + */ + public function createCompactReturnsInstanceWithSpaceBetweenBlocksSetToEmptyString(): void + { + $newInstance = OutputFormat::createCompact(); + + self::assertSame('', $newInstance->getSpaceBetweenBlocks()); + } + + /** + * @test + */ + public function createCompactReturnsInstanceWithSpaceAfterBlocksSetToEmptyString(): void + { + $newInstance = OutputFormat::createCompact(); + + self::assertSame('', $newInstance->getSpaceAfterBlocks()); + } + + /** + * @test + */ + public function createCompactReturnsInstanceWithSpaceAfterRuleNameSetToEmptyString(): void + { + $newInstance = OutputFormat::createCompact(); + + self::assertSame('', $newInstance->getSpaceAfterRuleName()); + } + + /** + * @test + */ + public function createCompactReturnsInstanceWithSpaceBeforeOpeningBraceSetToEmptyString(): void + { + $newInstance = OutputFormat::createCompact(); + + self::assertSame('', $newInstance->getSpaceBeforeOpeningBrace()); + } + + /** + * @test + */ + public function createCompactReturnsInstanceWithSpaceAfterSelectorSeparatorSetToEmptyString(): void + { + $newInstance = OutputFormat::createCompact(); + + self::assertSame('', $newInstance->getSpaceAfterSelectorSeparator()); + } + + /** + * @test + */ + public function createCompactReturnsInstanceWithSpaceAfterListArgumentSeparatorsSetToEmptyArray(): void + { + $newInstance = OutputFormat::createCompact(); + + self::assertSame([], $newInstance->getSpaceAfterListArgumentSeparators()); + } + + /** + * @test + */ + public function createCompactReturnsInstanceWithRenderCommentsDisabled(): void + { + $newInstance = OutputFormat::createCompact(); + + self::assertFalse($newInstance->shouldRenderComments()); + } + + /** + * @test + */ + public function createPrettyReturnsOutputFormatInstance(): void + { + self::assertInstanceOf(OutputFormat::class, OutputFormat::createPretty()); + } + + /** + * @test + */ + public function createPrettyCalledTwoTimesReturnsDifferentInstances(): void + { + $firstCallResult = OutputFormat::createPretty(); + $secondCallResult = OutputFormat::createPretty(); + + self::assertNotSame($firstCallResult, $secondCallResult); + } + + /** + * @test + */ + public function createPrettyReturnsInstanceWithSpaceBeforeRulesSetToNewline(): void + { + $newInstance = OutputFormat::createPretty(); + + self::assertSame("\n", $newInstance->getSpaceBeforeRules()); + } + + /** + * @test + */ + public function createPrettyReturnsInstanceWithSpaceBetweenRulesSetToNewline(): void + { + $newInstance = OutputFormat::createPretty(); + + self::assertSame("\n", $newInstance->getSpaceBetweenRules()); + } + + /** + * @test + */ + public function createPrettyReturnsInstanceWithSpaceAfterRulesSetToNewline(): void + { + $newInstance = OutputFormat::createPretty(); + + self::assertSame("\n", $newInstance->getSpaceAfterRules()); + } + + /** + * @test + */ + public function createPrettyReturnsInstanceWithSpaceBeforeBlocksSetToNewline(): void + { + $newInstance = OutputFormat::createPretty(); + + self::assertSame("\n", $newInstance->getSpaceBeforeBlocks()); + } + + /** + * @test + */ + public function createPrettyReturnsInstanceWithSpaceBetweenBlocksSetToTwoNewlines(): void + { + $newInstance = OutputFormat::createPretty(); + + self::assertSame("\n\n", $newInstance->getSpaceBetweenBlocks()); + } + + /** + * @test + */ + public function createPrettyReturnsInstanceWithSpaceAfterBlocksSetToNewline(): void + { + $newInstance = OutputFormat::createPretty(); + + self::assertSame("\n", $newInstance->getSpaceAfterBlocks()); + } + + /** + * @test + */ + public function createPrettyReturnsInstanceWithSpaceAfterRuleNameSetToSpace(): void + { + $newInstance = OutputFormat::createPretty(); + + self::assertSame(' ', $newInstance->getSpaceAfterRuleName()); + } + + /** + * @test + */ + public function createPrettyReturnsInstanceWithSpaceBeforeOpeningBraceSetToSpace(): void + { + $newInstance = OutputFormat::createPretty(); + + self::assertSame(' ', $newInstance->getSpaceBeforeOpeningBrace()); + } + + /** + * @test + */ + public function createPrettyReturnsInstanceWithSpaceAfterSelectorSeparatorSetToSpace(): void + { + $newInstance = OutputFormat::createPretty(); + + self::assertSame(' ', $newInstance->getSpaceAfterSelectorSeparator()); + } + + /** + * @test + */ + public function createPrettyReturnsInstanceWithSpaceAfterListArgumentSeparatorsSetToSpaceForCommaOnly(): void + { + $newInstance = OutputFormat::createPretty(); + + self::assertSame([',' => ' '], $newInstance->getSpaceAfterListArgumentSeparators()); + } + + /** + * @test + */ + public function createPrettyReturnsInstanceWithRenderCommentsEnabled(): void + { + $newInstance = OutputFormat::createPretty(); + + self::assertTrue($newInstance->shouldRenderComments()); + } +} diff --git a/tests/Unit/OutputFormatterTest.php b/tests/Unit/OutputFormatterTest.php new file mode 100644 index 00000000..2caf30e4 --- /dev/null +++ b/tests/Unit/OutputFormatterTest.php @@ -0,0 +1,622 @@ +outputFormat = new OutputFormat(); + $this->subject = new OutputFormatter($this->outputFormat); + } + + /** + * @test + */ + public function spaceAfterRuleNameReturnsSpaceAfterRuleNameFromOutputFormat(): void + { + $space = ' '; + $this->outputFormat->setSpaceAfterRuleName($space); + + self::assertSame($space, $this->subject->spaceAfterRuleName()); + } + + /** + * @test + */ + public function spaceBeforeRulesReturnsSpaceBeforeRulesFromOutputFormat(): void + { + $space = ' '; + $this->outputFormat->setSpaceBeforeRules($space); + + self::assertSame($space, $this->subject->spaceBeforeRules()); + } + + /** + * @test + */ + public function spaceAfterRulesReturnsSpaceAfterRulesFromOutputFormat(): void + { + $space = ' '; + $this->outputFormat->setSpaceAfterRules($space); + + self::assertSame($space, $this->subject->spaceAfterRules()); + } + + /** + * @test + */ + public function spaceBetweenRulesReturnsSpaceBetweenRulesFromOutputFormat(): void + { + $space = ' '; + $this->outputFormat->setSpaceBetweenRules($space); + + self::assertSame($space, $this->subject->spaceBetweenRules()); + } + + /** + * @test + */ + public function spaceBeforeBlocksReturnsSpaceBeforeBlocksFromOutputFormat(): void + { + $space = ' '; + $this->outputFormat->setSpaceBeforeBlocks($space); + + self::assertSame($space, $this->subject->spaceBeforeBlocks()); + } + + /** + * @test + */ + public function spaceAfterBlocksReturnsSpaceAfterBlocksFromOutputFormat(): void + { + $space = ' '; + $this->outputFormat->setSpaceAfterBlocks($space); + + self::assertSame($space, $this->subject->spaceAfterBlocks()); + } + + /** + * @test + */ + public function spaceBetweenBlocksReturnsSpaceBetweenBlocksFromOutputFormat(): void + { + $space = ' '; + $this->outputFormat->setSpaceBetweenBlocks($space); + + self::assertSame($space, $this->subject->spaceBetweenBlocks()); + } + + /** + * @test + */ + public function spaceBeforeSelectorSeparatorReturnsSpaceBeforeSelectorSeparatorFromOutputFormat(): void + { + $space = ' '; + $this->outputFormat->setSpaceBeforeSelectorSeparator($space); + + self::assertSame($space, $this->subject->spaceBeforeSelectorSeparator()); + } + + /** + * @test + */ + public function spaceAfterSelectorSeparatorReturnsSpaceAfterSelectorSeparatorFromOutputFormat(): void + { + $space = ' '; + $this->outputFormat->setSpaceAfterSelectorSeparator($space); + + self::assertSame($space, $this->subject->spaceAfterSelectorSeparator()); + } + + /** + * @test + */ + public function spaceBeforeListArgumentSeparatorReturnsSpaceSetForSpecificSeparator(): void + { + $separator = ','; + $space = ' '; + $this->outputFormat->setSpaceBeforeListArgumentSeparators([$separator => $space]); + $defaultSpace = "\t\t\t\t"; + $this->outputFormat->setSpaceBeforeListArgumentSeparator($defaultSpace); + + self::assertSame($space, $this->subject->spaceBeforeListArgumentSeparator($separator)); + } + + /** + * @test + */ + public function spaceBeforeListArgumentSeparatorWithoutSpecificSettingReturnsDefaultSpace(): void + { + $space = ' '; + $this->outputFormat->setSpaceBeforeListArgumentSeparators([',' => $space]); + $defaultSpace = "\t\t\t\t"; + $this->outputFormat->setSpaceBeforeListArgumentSeparator($defaultSpace); + + self::assertSame($defaultSpace, $this->subject->spaceBeforeListArgumentSeparator(';')); + } + + /** + * @test + */ + public function spaceAfterListArgumentSeparatorReturnsSpaceSetForSpecificSeparator(): void + { + $separator = ','; + $space = ' '; + $this->outputFormat->setSpaceAfterListArgumentSeparators([$separator => $space]); + $defaultSpace = "\t\t\t\t"; + $this->outputFormat->setSpaceAfterListArgumentSeparator($defaultSpace); + + self::assertSame($space, $this->subject->spaceAfterListArgumentSeparator($separator)); + } + + /** + * @test + */ + public function spaceAfterListArgumentSeparatorWithoutSpecificSettingReturnsDefaultSpace(): void + { + $space = ' '; + $this->outputFormat->setSpaceAfterListArgumentSeparators([',' => $space]); + $defaultSpace = "\t\t\t\t"; + $this->outputFormat->setSpaceAfterListArgumentSeparator($defaultSpace); + + self::assertSame($defaultSpace, $this->subject->spaceAfterListArgumentSeparator(';')); + } + + /** + * @test + */ + public function spaceBeforeOpeningBraceReturnsSpaceBeforeOpeningBraceFromOutputFormat(): void + { + $space = ' '; + $this->outputFormat->setSpaceBeforeOpeningBrace($space); + + self::assertSame($space, $this->subject->spaceBeforeOpeningBrace()); + } + + /** + * @test + */ + public function implodeForEmptyValuesReturnsEmptyString(): void + { + $values = []; + + $result = $this->subject->implode(', ', $values); + + self::assertSame('', $result); + } + + /** + * @test + */ + public function implodeWithOneStringValueReturnsStringValue(): void + { + $value = 'tea'; + $values = [$value]; + + $result = $this->subject->implode(', ', $values); + + self::assertSame($value, $result); + } + + /** + * @test + */ + public function implodeWithMultipleStringValuesReturnsValuesSeparatedBySeparator(): void + { + $value1 = 'tea'; + $value2 = 'coffee'; + $values = [$value1, $value2]; + $separator = ', '; + + $result = $this->subject->implode($separator, $values); + + self::assertSame($value1 . $separator . $value2, $result); + } + + /** + * @test + */ + public function implodeWithOneRenderableReturnsRenderedRenderable(): void + { + $renderable = $this->createMock(Renderable::class); + $renderedRenderable = 'tea'; + $renderable->method('render')->with($this->outputFormat)->willReturn($renderedRenderable); + $values = [$renderable]; + + $result = $this->subject->implode(', ', $values); + + self::assertSame($renderedRenderable, $result); + } + + /** + * @test + */ + public function implodeWithMultipleRenderablesReturnsRenderedRenderablesSeparatedBySeparator(): void + { + $renderable1 = $this->createMock(Renderable::class); + $renderedRenderable1 = 'tea'; + $renderable1->method('render')->with($this->outputFormat)->willReturn($renderedRenderable1); + $renderable2 = $this->createMock(Renderable::class); + $renderedRenderable2 = 'coffee'; + $renderable2->method('render')->with($this->outputFormat)->willReturn($renderedRenderable2); + $values = [$renderable1, $renderable2]; + $separator = ', '; + + $result = $this->subject->implode($separator, $values); + + self::assertSame($renderedRenderable1 . $separator . $renderedRenderable2, $result); + } + + /** + * @test + */ + public function implodeWithIncreaseLevelFalseUsesDefaultIndentationLevelForRendering(): void + { + $renderable = $this->createMock(Renderable::class); + $renderedRenderable = 'tea'; + $renderable->method('render')->with($this->outputFormat)->willReturn($renderedRenderable); + $values = [$renderable]; + + $result = $this->subject->implode(', ', $values, false); + + self::assertSame($renderedRenderable, $result); + } + + /** + * @test + */ + public function implodeWithIncreaseLevelTrueIncreasesIndentationLevelForRendering(): void + { + $renderable = $this->createMock(Renderable::class); + $renderedRenderable = 'tea'; + $renderable->method('render')->with($this->outputFormat->nextLevel())->willReturn($renderedRenderable); + $values = [$renderable]; + + $result = $this->subject->implode(', ', $values, true); + + self::assertSame($renderedRenderable, $result); + } + + /** + * @return array + */ + public function provideUnchangedStringForRemoveLastSemicolon(): array + { + return [ + 'empty string' => [''], + 'string without semicolon' => ['earl-grey: hot'], + 'string with trailing semicolon' => ['Earl Grey: hot;'], + 'string with semicolon in the middle' => ['Earl Grey: hot; Coffee: Americano'], + 'string with semicolons in the middle and trailing' => ['Earl Grey: hot; Coffee: Americano;'], + ]; + } + + /** + * @test + * @dataProvider provideUnchangedStringForRemoveLastSemicolon + */ + public function removeLastSemicolonWithSemicolonAfterLastRuleEnabledReturnsUnchangedArgument(string $string): void + { + $this->outputFormat->setSemicolonAfterLastRule(true); + + $result = $this->subject->removeLastSemicolon($string); + + self::assertSame($string, $result); + } + + /** + * @return array + */ + public function provideChangedStringForRemoveLastSemicolon(): array + { + return [ + 'empty string' => ['', ''], + 'non-empty string without semicolon' => ['Earl Grey: hot', 'Earl Grey: hot'], + 'just 1 semicolon' => [';', ''], + 'just 2 semicolons' => [';;', ';'], + 'string with trailing semicolon' => ['Earl Grey: hot;', 'Earl Grey: hot'], + 'string with semicolon in the middle' => [ + 'Earl Grey: hot; Coffee: Americano', + 'Earl Grey: hot Coffee: Americano', + ], + 'string with semicolon in the middle and trailing' => [ + 'Earl Grey: hot; Coffee: Americano;', + 'Earl Grey: hot; Coffee: Americano', + ], + 'string with 2 semicolons in the middle' => ['tea; coffee; Club-Mate', 'tea; coffee Club-Mate'], + 'string with 2 semicolons in the middle surrounded by spaces' => [ + 'Earl Grey: hot ; Coffee: Americano ; Club-Mate: cold', + 'Earl Grey: hot ; Coffee: Americano Club-Mate: cold', + ], + 'string with 2 adjacent semicolons in the middle' => [ + 'Earl Grey: hot;; Coffee: Americano', + 'Earl Grey: hot; Coffee: Americano', + ], + 'string with 3 adjacent semicolons in the middle' => [ + 'Earl Grey: hot;;; Coffee: Americano', + 'Earl Grey: hot;; Coffee: Americano', + ], + ]; + } + + /** + * @test + * @dataProvider provideChangedStringForRemoveLastSemicolon + */ + public function removeLastSemicolonWithSemicolonAfterLastRuleDisabledRemovesLastSemicolon( + string $input, + string $expected + ): void { + $this->outputFormat->setSemicolonAfterLastRule(false); + + $result = $this->subject->removeLastSemicolon($input); + + self::assertSame($expected, $result); + } + + /** + * @test + */ + public function commentsWithEmptyCommentableAndRenderCommentsDisabledDoesNotReturnSpaceBetweenBlocks(): void + { + $this->outputFormat->setRenderComments(false); + $spaceBetweenBlocks = ' between-space '; + $this->outputFormat->setSpaceBetweenBlocks($spaceBetweenBlocks); + + $commentable = $this->createMock(Commentable::class); + $commentable->method('getComments')->willReturn([]); + + $result = $this->subject->comments($commentable); + + self::assertStringNotContainsString($spaceBetweenBlocks, $result); + } + + /** + * @test + */ + public function commentsWithEmptyCommentableAndRenderCommentsDisabledDoesNotReturnSpaceAfterBlocks(): void + { + $this->outputFormat->setRenderComments(false); + $spaceAfterBlocks = ' after-space '; + $this->outputFormat->setSpaceAfterBlocks($spaceAfterBlocks); + + $commentable = $this->createMock(Commentable::class); + $commentable->method('getComments')->willReturn([]); + + $result = $this->subject->comments($commentable); + + self::assertStringNotContainsString($spaceAfterBlocks, $result); + } + + /** + * @test + */ + public function commentsWithEmptyCommentableAndRenderCommentsDisabledReturnsEmptyString(): void + { + $this->outputFormat->setRenderComments(false); + + $commentable = $this->createMock(Commentable::class); + $commentable->method('getComments')->willReturn([]); + + $result = $this->subject->comments($commentable); + + self::assertSame('', $result); + } + + /** + * @test + */ + public function commentsWithEmptyCommentableAndRenderCommentsEnabledDoesNotReturnSpaceBetweenBlocks(): void + { + $this->outputFormat->setRenderComments(true); + $spaceBetweenBlocks = ' between-space '; + $this->outputFormat->setSpaceBetweenBlocks($spaceBetweenBlocks); + + $commentable = $this->createMock(Commentable::class); + $commentable->method('getComments')->willReturn([]); + + $result = $this->subject->comments($commentable); + + self::assertStringNotContainsString($spaceBetweenBlocks, $result); + } + + /** + * @test + */ + public function commentsWithEmptyCommentableAndRenderCommentsEnabledDoesNotReturnSpaceAfterBlocks(): void + { + $this->outputFormat->setRenderComments(true); + $spaceAfterBlocks = ' after-space '; + $this->outputFormat->setSpaceAfterBlocks($spaceAfterBlocks); + + $commentable = $this->createMock(Commentable::class); + $commentable->method('getComments')->willReturn([]); + + $result = $this->subject->comments($commentable); + + self::assertStringNotContainsString($spaceAfterBlocks, $result); + } + + /** + * @test + */ + public function commentsWithEmptyCommentableAndRenderCommentsEnabledReturnsEmptyString(): void + { + $this->outputFormat->setRenderComments(true); + + $commentable = $this->createMock(Commentable::class); + $commentable->method('getComments')->willReturn([]); + + $result = $this->subject->comments($commentable); + + self::assertSame('', $result); + } + + /** + * @test + */ + public function commentsWithCommentableWithOneCommentAndRenderCommentsDisabledReturnsEmptyString(): void + { + $this->outputFormat->setRenderComments(false); + + $commentText = 'I am a teapot.'; + $comment = new Comment($commentText); + $commentable = $this->createMock(Commentable::class); + $commentable->method('getComments')->willReturn([$comment]); + + $result = $this->subject->comments($commentable); + + self::assertSame('', $result); + } + + /** + * @test + */ + public function commentsWithCommentableWithOneCommentRendersComment(): void + { + $this->outputFormat->setRenderComments(true); + + $commentText = 'I am a teapot.'; + $comment = new Comment($commentText); + $commentable = $this->createMock(Commentable::class); + $commentable->method('getComments')->willReturn([$comment]); + + $result = $this->subject->comments($commentable); + + self::assertStringContainsString('/*' . $commentText . '*/', $result); + } + + /** + * @test + */ + public function commentsWithCommentableWithOneCommentPutsSpaceAfterBlocksAfterRenderedComment(): void + { + $this->outputFormat->setRenderComments(true); + $afterSpace = ' after-space '; + $this->outputFormat->setSpaceAfterBlocks($afterSpace); + + $commentText = 'I am a teapot.'; + $comment = new Comment($commentText); + $commentable = $this->createMock(Commentable::class); + $commentable->method('getComments')->willReturn([$comment]); + + $result = $this->subject->comments($commentable); + + self::assertSame('/*' . $commentText . '*/' . $afterSpace, $result); + } + + /** + * @test + */ + public function commentsWithCommentableWithTwoCommentsPutsSpaceAfterBlocksAfterLastRenderedComment(): void + { + $this->outputFormat->setRenderComments(true); + $afterSpace = ' after-space '; + $this->outputFormat->setSpaceAfterBlocks($afterSpace); + + $commentText1 = 'I am a teapot.'; + $comment1 = new Comment($commentText1); + $commentText2 = 'But I am not.'; + $comment2 = new Comment($commentText2); + $commentable = $this->createMock(Commentable::class); + $commentable->method('getComments')->willReturn([$comment1, $comment2]); + + $result = $this->subject->comments($commentable); + + self::assertStringContainsString('/*' . $commentText2 . '*/' . $afterSpace, $result); + } + + /** + * @test + */ + public function commentsWithCommentableWithTwoCommentsSeparatesCommentsBySpaceBetweenBlocks(): void + { + $this->outputFormat->setRenderComments(true); + $betweenSpace = ' between-space '; + $this->outputFormat->setSpaceBetweenBlocks($betweenSpace); + + $commentText1 = 'I am a teapot.'; + $comment1 = new Comment($commentText1); + $commentText2 = 'But I am not.'; + $comment2 = new Comment($commentText2); + $commentable = $this->createMock(Commentable::class); + $commentable->method('getComments')->willReturn([$comment1, $comment2]); + + $result = $this->subject->comments($commentable); + + $expected = '/*' . $commentText1 . '*/' . $betweenSpace . '/*' . $commentText2 . '*/'; + self::assertStringContainsString($expected, $result); + } + + /** + * @test + */ + public function commentsWithCommentableWithMoreThanTwoCommentsPutsSpaceAfterBlocksAfterLastRenderedComment(): void + { + $this->outputFormat->setRenderComments(true); + $afterSpace = ' after-space '; + $this->outputFormat->setSpaceAfterBlocks($afterSpace); + + $commentText1 = 'I am a teapot.'; + $comment1 = new Comment($commentText1); + $commentText2 = 'But I am not.'; + $comment2 = new Comment($commentText2); + $commentText3 = 'So what am I then?'; + $comment3 = new Comment($commentText3); + $commentable = $this->createMock(Commentable::class); + $commentable->method('getComments')->willReturn([$comment1, $comment2, $comment3]); + + $result = $this->subject->comments($commentable); + + self::assertStringContainsString('/*' . $commentText3 . '*/' . $afterSpace, $result); + } + + /** + * @test + */ + public function commentsWithCommentableWithMoreThanTwoCommentsSeparatesCommentsBySpaceBetweenBlocks(): void + { + $this->outputFormat->setRenderComments(true); + $betweenSpace = ' between-space '; + $this->outputFormat->setSpaceBetweenBlocks($betweenSpace); + + $commentText1 = 'I am a teapot.'; + $comment1 = new Comment($commentText1); + $commentText2 = 'But I am not.'; + $comment2 = new Comment($commentText2); + $commentText3 = 'So what am I then?'; + $comment3 = new Comment($commentText3); + $commentable = $this->createMock(Commentable::class); + $commentable->method('getComments')->willReturn([$comment1, $comment2, $comment3]); + + $result = $this->subject->comments($commentable); + + $expected = '/*' . $commentText1 . '*/' + . $betweenSpace . '/*' . $commentText2 . '*/' + . $betweenSpace . '/*' . $commentText3 . '*/'; + self::assertStringContainsString($expected, $result); + } +} diff --git a/tests/Unit/Parsing/OutputExceptionTest.php b/tests/Unit/Parsing/OutputExceptionTest.php new file mode 100644 index 00000000..d3409aa4 --- /dev/null +++ b/tests/Unit/Parsing/OutputExceptionTest.php @@ -0,0 +1,76 @@ +getMessage()); + } + + /** + * @test + */ + public function getLineNoByDefaultReturnsZero(): void + { + $exception = new OutputException('foo'); + + self::assertSame(0, $exception->getLineNo()); + } + + /** + * @test + */ + public function getLineNoReturnsLineNumberProvidedToConstructor(): void + { + $lineNumber = 17; + $exception = new OutputException('foo', $lineNumber); + + self::assertSame($lineNumber, $exception->getLineNo()); + } + + /** + * @test + */ + public function getMessageWithLineNumberProvidedIncludesLineNumber(): void + { + $lineNumber = 17; + $exception = new OutputException('foo', $lineNumber); + + self::assertStringContainsString(' [line no: ' . $lineNumber . ']', $exception->getMessage()); + } + + /** + * @test + */ + public function canBeThrown(): void + { + $this->expectException(OutputException::class); + + throw new OutputException('foo'); + } +} diff --git a/tests/Unit/Parsing/SourceExceptionTest.php b/tests/Unit/Parsing/SourceExceptionTest.php new file mode 100644 index 00000000..b497ff52 --- /dev/null +++ b/tests/Unit/Parsing/SourceExceptionTest.php @@ -0,0 +1,67 @@ +getMessage()); + } + + /** + * @test + */ + public function getLineNoByDefaultReturnsZero(): void + { + $exception = new SourceException('foo'); + + self::assertSame(0, $exception->getLineNo()); + } + + /** + * @test + */ + public function getLineNoReturnsLineNumberProvidedToConstructor(): void + { + $lineNumber = 17; + $exception = new SourceException('foo', $lineNumber); + + self::assertSame($lineNumber, $exception->getLineNo()); + } + + /** + * @test + */ + public function getMessageWithLineNumberProvidedIncludesLineNumber(): void + { + $lineNumber = 17; + $exception = new SourceException('foo', $lineNumber); + + self::assertStringContainsString(' [line no: ' . $lineNumber . ']', $exception->getMessage()); + } + + /** + * @test + */ + public function canBeThrown(): void + { + $this->expectException(SourceException::class); + + throw new SourceException('foo'); + } +} diff --git a/tests/Unit/Parsing/UnexpectedEOFExceptionTest.php b/tests/Unit/Parsing/UnexpectedEOFExceptionTest.php new file mode 100644 index 00000000..929609ef --- /dev/null +++ b/tests/Unit/Parsing/UnexpectedEOFExceptionTest.php @@ -0,0 +1,177 @@ +getLineNo()); + } + + /** + * @test + */ + public function getLineNoReturnsLineNumberProvidedToConstructor(): void + { + $lineNumber = 17; + $exception = new UnexpectedEOFException('expected', 'found', 'literal', $lineNumber); + + self::assertSame($lineNumber, $exception->getLineNo()); + } + + /** + * @test + */ + public function getMessageWithLineNumberProvidedIncludesLineNumber(): void + { + $lineNumber = 17; + $exception = new UnexpectedEOFException('expected', 'found', 'literal', $lineNumber); + + self::assertStringContainsString(' [line no: ' . $lineNumber . ']', $exception->getMessage()); + } + + /** + * @test + */ + public function canBeThrown(): void + { + $this->expectException(UnexpectedEOFException::class); + + throw new UnexpectedEOFException('expected', 'found'); + } + + /** + * @test + */ + public function messageByDefaultRefersToTokenNotFound(): void + { + $expected = 'tea'; + $found = 'coffee'; + + $exception = new UnexpectedEOFException($expected, $found); + + $expectedMessage = 'Token “' . $expected . '” (literal) not found. Got “' . $found . '”.'; + self::assertStringContainsString($expectedMessage, $exception->getMessage()); + } + + /** + * @test + */ + public function messageForInvalidMatchTypeRefersToTokenNotFound(): void + { + $expected = 'tea'; + $found = 'coffee'; + + $exception = new UnexpectedEOFException($expected, $found, 'coding'); + + $expectedMessage = 'Token “' . $expected . '” (coding) not found. Got “' . $found . '”.'; + self::assertStringContainsString($expectedMessage, $exception->getMessage()); + } + + /** + * @test + */ + public function messageForLiteralMatchTypeRefersToTokenNotFound(): void + { + $expected = 'tea'; + $found = 'coffee'; + + $exception = new UnexpectedEOFException($expected, $found, 'literal'); + + $expectedMessage = 'Token “' . $expected . '” (literal) not found. Got “' . $found . '”.'; + self::assertStringContainsString($expectedMessage, $exception->getMessage()); + } + + /** + * @test + */ + public function messageForSearchMatchTypeRefersToNoResults(): void + { + $expected = 'tea'; + $found = 'coffee'; + + $exception = new UnexpectedEOFException($expected, $found, 'search'); + + $expectedMessage = 'Search for “' . $expected . '” returned no results. Context: “' . $found . '”.'; + self::assertStringContainsString($expectedMessage, $exception->getMessage()); + } + + /** + * @test + */ + public function messageForCountMatchTypeRefersToNumberOfCharacters(): void + { + $expected = 'tea'; + $found = 'coffee'; + + $exception = new UnexpectedEOFException($expected, $found, 'count'); + + $expectedMessage = 'Next token was expected to have ' . $expected . ' chars. Context: “' . $found . '”.'; + self::assertStringContainsString($expectedMessage, $exception->getMessage()); + } + + /** + * @test + */ + public function messageForIdentifierMatchTypeRefersToIdentifier(): void + { + $expected = 'tea'; + $found = 'coffee'; + + $exception = new UnexpectedEOFException($expected, $found, 'identifier'); + + $expectedMessage = 'Identifier expected. Got “' . $found . '”'; + self::assertStringContainsString($expectedMessage, $exception->getMessage()); + } + + /** + * @test + */ + public function messageForCustomMatchTypeMentionsExpectedAndFound(): void + { + $expected = 'tea'; + $found = 'coffee'; + + $exception = new UnexpectedEOFException($expected, $found, 'custom'); + + $expectedMessage = $expected . ' ' . $found; + self::assertStringContainsString($expectedMessage, $exception->getMessage()); + } + + /** + * @test + */ + public function messageForCustomMatchTypeTrimsMessage(): void + { + $expected = 'tea'; + $found = 'coffee'; + + $exception = new UnexpectedEOFException(' ' . $expected, $found . ' ', 'custom'); + + $expectedMessage = $expected . ' ' . $found; + self::assertStringContainsString($expectedMessage, $exception->getMessage()); + } +} diff --git a/tests/Unit/Parsing/UnexpectedTokenExceptionTest.php b/tests/Unit/Parsing/UnexpectedTokenExceptionTest.php new file mode 100644 index 00000000..e5c7a64d --- /dev/null +++ b/tests/Unit/Parsing/UnexpectedTokenExceptionTest.php @@ -0,0 +1,177 @@ +getLineNo()); + } + + /** + * @test + */ + public function getLineNoReturnsLineNumberProvidedToConstructor(): void + { + $lineNumber = 17; + $exception = new UnexpectedTokenException('expected', 'found', 'literal', $lineNumber); + + self::assertSame($lineNumber, $exception->getLineNo()); + } + + /** + * @test + */ + public function getMessageWithLineNumberProvidedIncludesLineNumber(): void + { + $lineNumber = 17; + $exception = new UnexpectedTokenException('expected', 'found', 'literal', $lineNumber); + + self::assertStringContainsString(' [line no: ' . $lineNumber . ']', $exception->getMessage()); + } + + /** + * @test + */ + public function canBeThrown(): void + { + $this->expectException(UnexpectedTokenException::class); + + throw new UnexpectedTokenException('expected', 'found'); + } + + /** + * @test + */ + public function messageByDefaultRefersToTokenNotFound(): void + { + $expected = 'tea'; + $found = 'coffee'; + + $exception = new UnexpectedTokenException($expected, $found); + + $expectedMessage = 'Token “' . $expected . '” (literal) not found. Got “' . $found . '”.'; + self::assertStringContainsString($expectedMessage, $exception->getMessage()); + } + + /** + * @test + */ + public function messageForInvalidMatchTypeRefersToTokenNotFound(): void + { + $expected = 'tea'; + $found = 'coffee'; + + $exception = new UnexpectedTokenException($expected, $found, 'coding'); + + $expectedMessage = 'Token “' . $expected . '” (coding) not found. Got “' . $found . '”.'; + self::assertStringContainsString($expectedMessage, $exception->getMessage()); + } + + /** + * @test + */ + public function messageForLiteralMatchTypeRefersToTokenNotFound(): void + { + $expected = 'tea'; + $found = 'coffee'; + + $exception = new UnexpectedTokenException($expected, $found, 'literal'); + + $expectedMessage = 'Token “' . $expected . '” (literal) not found. Got “' . $found . '”.'; + self::assertStringContainsString($expectedMessage, $exception->getMessage()); + } + + /** + * @test + */ + public function messageForSearchMatchTypeRefersToNoResults(): void + { + $expected = 'tea'; + $found = 'coffee'; + + $exception = new UnexpectedTokenException($expected, $found, 'search'); + + $expectedMessage = 'Search for “' . $expected . '” returned no results. Context: “' . $found . '”.'; + self::assertStringContainsString($expectedMessage, $exception->getMessage()); + } + + /** + * @test + */ + public function messageForCountMatchTypeRefersToNumberOfCharacters(): void + { + $expected = 'tea'; + $found = 'coffee'; + + $exception = new UnexpectedTokenException($expected, $found, 'count'); + + $expectedMessage = 'Next token was expected to have ' . $expected . ' chars. Context: “' . $found . '”.'; + self::assertStringContainsString($expectedMessage, $exception->getMessage()); + } + + /** + * @test + */ + public function messageForIdentifierMatchTypeRefersToIdentifier(): void + { + $expected = 'tea'; + $found = 'coffee'; + + $exception = new UnexpectedTokenException($expected, $found, 'identifier'); + + $expectedMessage = 'Identifier expected. Got “' . $found . '”'; + self::assertStringContainsString($expectedMessage, $exception->getMessage()); + } + + /** + * @test + */ + public function messageForCustomMatchTypeMentionsExpectedAndFound(): void + { + $expected = 'tea'; + $found = 'coffee'; + + $exception = new UnexpectedTokenException($expected, $found, 'custom'); + + $expectedMessage = $expected . ' ' . $found; + self::assertStringContainsString($expectedMessage, $exception->getMessage()); + } + + /** + * @test + */ + public function messageForCustomMatchTypeTrimsMessage(): void + { + $expected = 'tea'; + $found = 'coffee'; + + $exception = new UnexpectedTokenException(' ' . $expected, $found . ' ', 'custom'); + + $expectedMessage = $expected . ' ' . $found; + self::assertStringContainsString($expectedMessage, $exception->getMessage()); + } +} diff --git a/tests/Unit/Position/Fixtures/ConcretePosition.php b/tests/Unit/Position/Fixtures/ConcretePosition.php new file mode 100644 index 00000000..0db38706 --- /dev/null +++ b/tests/Unit/Position/Fixtures/ConcretePosition.php @@ -0,0 +1,13 @@ +subject = new ConcretePosition(); + } + + /** + * @test + */ + public function getLineNumberInitiallyReturnsNull(): void + { + self::assertNull($this->subject->getLineNumber()); + } + + /** + * @test + */ + public function getColumnNumberInitiallyReturnsNull(): void + { + self::assertNull($this->subject->getColumnNumber()); + } + + /** + * @return array}> + */ + public function provideLineNumber(): array + { + return [ + 'line 1' => [1], + 'line 42' => [42], + ]; + } + + /** + * @test + * + * @param int<1, max> $lineNumber + * + * @dataProvider provideLineNumber + */ + public function setPositionOnVirginSetsLineNumber(int $lineNumber): void + { + $this->subject->setPosition($lineNumber); + + self::assertSame($lineNumber, $this->subject->getLineNumber()); + } + + /** + * @test + * + * @param int<1, max> $lineNumber + * + * @dataProvider provideLineNumber + */ + public function setPositionSetsNewLineNumber(int $lineNumber): void + { + $this->subject->setPosition(99); + + $this->subject->setPosition($lineNumber); + + self::assertSame($lineNumber, $this->subject->getLineNumber()); + } + + /** + * @test + */ + public function setPositionWithNullClearsLineNumber(): void + { + $this->subject->setPosition(99); + + $this->subject->setPosition(null); + + self::assertNull($this->subject->getLineNumber()); + } + + /** + * @return array}> + */ + public function provideColumnNumber(): array + { + return [ + 'column 0' => [0], + 'column 14' => [14], + 'column 39' => [39], + ]; + } + + /** + * @test + * + * @param int<0, max> $columnNumber + * + * @dataProvider provideColumnNumber + */ + public function setPositionOnVirginSetsColumnNumber(int $columnNumber): void + { + $this->subject->setPosition(1, $columnNumber); + + self::assertSame($columnNumber, $this->subject->getColumnNumber()); + } + + /** + * @test + * + * @dataProvider provideColumnNumber + */ + public function setPositionSetsNewColumnNumber(int $columnNumber): void + { + $this->subject->setPosition(1, 99); + + $this->subject->setPosition(2, $columnNumber); + + self::assertSame($columnNumber, $this->subject->getColumnNumber()); + } + + /** + * @test + */ + public function setPositionWithoutColumnNumberClearsColumnNumber(): void + { + $this->subject->setPosition(1, 99); + + $this->subject->setPosition(2); + + self::assertNull($this->subject->getColumnNumber()); + } + + /** + * @test + */ + public function setPositionWithNullForColumnNumberClearsColumnNumber(): void + { + $this->subject->setPosition(1, 99); + + $this->subject->setPosition(2, null); + + self::assertNull($this->subject->getColumnNumber()); + } + + /** + * @return DataProvider, 1: int<0, max>}> + */ + public function provideLineAndColumnNumber(): DataProvider + { + return DataProvider::cross($this->provideLineNumber(), $this->provideColumnNumber()); + } + + /** + * @test + * + * @dataProvider provideLineAndColumnNumber + */ + public function setPositionOnVirginSetsLineAndColumnNumber(int $lineNumber, int $columnNumber): void + { + $this->subject->setPosition($lineNumber, $columnNumber); + + self::assertSame($lineNumber, $this->subject->getLineNumber()); + self::assertSame($columnNumber, $this->subject->getColumnNumber()); + } + + /** + * @test + * + * @dataProvider provideLineAndColumnNumber + */ + public function setPositionSetsNewLineAndColumnNumber(int $lineNumber, int $columnNumber): void + { + $this->subject->setPosition(98, 99); + + $this->subject->setPosition($lineNumber, $columnNumber); + + self::assertSame($lineNumber, $this->subject->getLineNumber()); + self::assertSame($columnNumber, $this->subject->getColumnNumber()); + } +} diff --git a/tests/Unit/Property/CSSNamespaceTest.php b/tests/Unit/Property/CSSNamespaceTest.php new file mode 100644 index 00000000..2e4d9922 --- /dev/null +++ b/tests/Unit/Property/CSSNamespaceTest.php @@ -0,0 +1,34 @@ +subject = new CSSNamespace(new CSSString('http://www.w3.org/2000/svg')); + } + + /** + * @test + */ + public function implementsCSSListItem(): void + { + self::assertInstanceOf(CSSListItem::class, $this->subject); + } +} diff --git a/tests/Unit/Property/CharsetTest.php b/tests/Unit/Property/CharsetTest.php new file mode 100644 index 00000000..e0645f5e --- /dev/null +++ b/tests/Unit/Property/CharsetTest.php @@ -0,0 +1,34 @@ +subject = new Charset(new CSSString('UTF-8')); + } + + /** + * @test + */ + public function implementsCSSListItem(): void + { + self::assertInstanceOf(CSSListItem::class, $this->subject); + } +} diff --git a/tests/Unit/Property/ImportTest.php b/tests/Unit/Property/ImportTest.php new file mode 100644 index 00000000..4ec028e3 --- /dev/null +++ b/tests/Unit/Property/ImportTest.php @@ -0,0 +1,35 @@ +subject = new Import(new URL(new CSSString('https://example.org/')), null); + } + + /** + * @test + */ + public function implementsCSSListItem(): void + { + self::assertInstanceOf(CSSListItem::class, $this->subject); + } +} diff --git a/tests/Unit/Property/Selector/SpecificityCalculatorTest.php b/tests/Unit/Property/Selector/SpecificityCalculatorTest.php new file mode 100644 index 00000000..088bd517 --- /dev/null +++ b/tests/Unit/Property/Selector/SpecificityCalculatorTest.php @@ -0,0 +1,94 @@ +}> + */ + public static function provideSelectorsAndSpecificities(): array + { + return [ + 'element' => ['a', 1], + 'element and descendant with pseudo-selector' => ['ol li::before', 3], + 'class' => ['.highlighted', 10], + 'element with class' => ['li.green', 11], + 'class with pseudo-selector' => ['.help:hover', 20], + 'ID' => ['#file', 100], + 'ID and descendant class' => ['#test .help', 110], + ]; + } + + /** + * @test + * + * @param non-empty-string $selector + * @param int<0, max> $expectedSpecificity + * + * @dataProvider provideSelectorsAndSpecificities + */ + public function calculateReturnsSpecificityForProvidedSelector( + string $selector, + int $expectedSpecificity + ): void { + self::assertSame($expectedSpecificity, SpecificityCalculator::calculate($selector)); + } + + /** + * @test + * + * @param non-empty-string $selector + * @param int<0, max> $expectedSpecificity + * + * @dataProvider provideSelectorsAndSpecificities + */ + public function calculateAfterClearingCacheReturnsSpecificityForProvidedSelector( + string $selector, + int $expectedSpecificity + ): void { + SpecificityCalculator::clearCache(); + + self::assertSame($expectedSpecificity, SpecificityCalculator::calculate($selector)); + } + + /** + * @test + */ + public function calculateCalledTwoTimesReturnsSameSpecificityForProvidedSelector(): void + { + $selector = '#test .help'; + + $firstResult = SpecificityCalculator::calculate($selector); + $secondResult = SpecificityCalculator::calculate($selector); + + self::assertSame($firstResult, $secondResult); + } + + /** + * @test + */ + public function calculateCalledReturnsSameSpecificityForProvidedSelectorBeforeAndAfterClearingCache(): void + { + $selector = '#test .help'; + + $firstResult = SpecificityCalculator::calculate($selector); + SpecificityCalculator::clearCache(); + $secondResult = SpecificityCalculator::calculate($selector); + + self::assertSame($firstResult, $secondResult); + } +} diff --git a/tests/Unit/Property/SelectorTest.php b/tests/Unit/Property/SelectorTest.php new file mode 100644 index 00000000..c2d59b60 --- /dev/null +++ b/tests/Unit/Property/SelectorTest.php @@ -0,0 +1,141 @@ +getSelector()); + } + + /** + * @test + */ + public function setSelectorOverwritesSelectorProvidedToConstructor(): void + { + $subject = new Selector('a'); + + $selector = 'input'; + $subject->setSelector($selector); + + self::assertSame($selector, $subject->getSelector()); + } + + /** + * @return array}> + */ + public static function provideSelectorsAndSpecificities(): array + { + return [ + 'element' => ['a', 1], + 'element and descendant with pseudo-selector' => ['ol li::before', 3], + 'class' => ['.highlighted', 10], + 'element with class' => ['li.green', 11], + 'class with pseudo-selector' => ['.help:hover', 20], + 'ID' => ['#file', 100], + 'ID and descendant class' => ['#test .help', 110], + ]; + } + + /** + * @test + * + * @param non-empty-string $selector + * @param int<0, max> $expectedSpecificity + * + * @dataProvider provideSelectorsAndSpecificities + */ + public function getSpecificityByDefaultReturnsSpecificityOfSelectorProvidedToConstructor( + string $selector, + int $expectedSpecificity + ): void { + $subject = new Selector($selector); + + self::assertSame($expectedSpecificity, $subject->getSpecificity()); + } + + /** + * @test + * + * @param non-empty-string $selector + * @param int<0, max> $expectedSpecificity + * + * @dataProvider provideSelectorsAndSpecificities + */ + public function getSpecificityReturnsSpecificityOfSelectorLastProvidedViaSetSelector( + string $selector, + int $expectedSpecificity + ): void { + $subject = new Selector('p'); + + $subject->setSelector($selector); + + self::assertSame($expectedSpecificity, $subject->getSpecificity()); + } + + /** + * @test + * + * @dataProvider provideSelectorsAndSpecificities + */ + public function isValidForValidSelectorReturnsTrue(string $selector): void + { + self::assertTrue(Selector::isValid($selector)); + } + + /** + * @return array + */ + public static function provideInvalidSelectors(): array + { + return [ + // This is currently broken. + // 'empty string' => [''], + 'percent sign' => ['%'], + // This is currently broken. + // 'hash only' => ['#'], + // This is currently broken. + // 'dot only' => ['.'], + 'slash' => ['/'], + 'less-than sign' => ['<'], + // This is currently broken. + // 'whitespace only' => [" \t\n\r"], + ]; + } + + /** + * @test + * + * @dataProvider provideInvalidSelectors + */ + public function isValidForInvalidSelectorReturnsFalse(string $selector): void + { + self::assertFalse(Selector::isValid($selector)); + } +} diff --git a/tests/Unit/Rule/RuleTest.php b/tests/Unit/Rule/RuleTest.php new file mode 100644 index 00000000..008bcfc1 --- /dev/null +++ b/tests/Unit/Rule/RuleTest.php @@ -0,0 +1,73 @@ +}> + */ + public static function provideRulesAndExpectedParsedValueListTypes(): array + { + return [ + 'src (e.g. in @font-face)' => [ + " + src: url('../fonts/open-sans-italic-300.woff2') format('woff2'), + url('../fonts/open-sans-italic-300.ttf') format('truetype'); + ", + [RuleValueList::class, RuleValueList::class], + ], + ]; + } + + /** + * @test + * + * @param list $expectedTypeClassnames + * + * @dataProvider provideRulesAndExpectedParsedValueListTypes + */ + public function parsesValuesIntoExpectedTypeList(string $rule, array $expectedTypeClassnames): void + { + $subject = Rule::parse(new ParserState($rule, Settings::create())); + + $value = $subject->getValue(); + self::assertInstanceOf(ValueList::class, $value); + + $actualClassnames = \array_map( + /** + * @param Value|string $component + */ + static function ($component): string { + return \is_string($component) ? 'string' : \get_class($component); + }, + $value->getListComponents() + ); + + self::assertSame($expectedTypeClassnames, $actualClassnames); + } +} diff --git a/tests/Unit/RuleSet/AtRuleSetTest.php b/tests/Unit/RuleSet/AtRuleSetTest.php new file mode 100644 index 00000000..0e3e0c97 --- /dev/null +++ b/tests/Unit/RuleSet/AtRuleSetTest.php @@ -0,0 +1,33 @@ +subject = new AtRuleSet('supports'); + } + + /** + * @test + */ + public function implementsCSSListItem(): void + { + self::assertInstanceOf(CSSListItem::class, $this->subject); + } +} diff --git a/tests/Unit/RuleSet/DeclarationBlockTest.php b/tests/Unit/RuleSet/DeclarationBlockTest.php new file mode 100644 index 00000000..b473da35 --- /dev/null +++ b/tests/Unit/RuleSet/DeclarationBlockTest.php @@ -0,0 +1,33 @@ +subject = new DeclarationBlock(); + } + + /** + * @test + */ + public function implementsCSSListItem(): void + { + self::assertInstanceOf(CSSListItem::class, $this->subject); + } +} diff --git a/tests/Unit/RuleSet/Fixtures/ConcreteRuleSet.php b/tests/Unit/RuleSet/Fixtures/ConcreteRuleSet.php new file mode 100644 index 00000000..9c79c09d --- /dev/null +++ b/tests/Unit/RuleSet/Fixtures/ConcreteRuleSet.php @@ -0,0 +1,19 @@ +subject = new ConcreteRuleSet(); + } + + /** + * @test + */ + public function implementsCSSElement(): void + { + self::assertInstanceOf(CSSElement::class, $this->subject); + } + + /** + * @test + */ + public function implementsCSSListItem(): void + { + self::assertInstanceOf(CSSListItem::class, $this->subject); + } + + /** + * @test + */ + public function implementsRuleContainer(): void + { + self::assertInstanceOf(RuleContainer::class, $this->subject); + } + + /** + * @return array}> + */ + public static function providePropertyNamesToBeSetInitially(): array + { + return [ + 'no properties' => [[]], + 'one property' => [['color']], + 'two different properties' => [['color', 'display']], + 'two of the same property' => [['color', 'color']], + ]; + } + + /** + * @return array + */ + public static function providePropertyNameToAdd(): array + { + return [ + 'property name `color` maybe matching that of existing declaration' => ['color'], + 'property name `display` maybe matching that of existing declaration' => ['display'], + 'property name `width` not matching that of existing declaration' => ['width'], + ]; + } + + /** + * @return DataProvider, 1: string}> + */ + public static function provideInitialPropertyNamesAndPropertyNameToAdd(): DataProvider + { + return DataProvider::cross(self::providePropertyNamesToBeSetInitially(), self::providePropertyNameToAdd()); + } + + /** + * @test + * + * @param list $initialPropertyNames + * + * @dataProvider provideInitialPropertyNamesAndPropertyNameToAdd + */ + public function addRuleWithoutSiblingAddsRuleAfterInitialRulesAndSetsValidLineAndColumnNumbers( + array $initialPropertyNames, + string $propertyNameToAdd + ): void { + if ($initialPropertyNames === []) { + self::markTestSkipped('currently broken - first rule added does not have valid line number set'); + } + + $ruleToAdd = new Rule($propertyNameToAdd); + $this->setRulesFromPropertyNames($initialPropertyNames); + + $this->subject->addRule($ruleToAdd); + + $rules = $this->subject->getRules(); + self::assertSame($ruleToAdd, \end($rules)); + self::assertIsInt($ruleToAdd->getLineNumber(), 'line number not set'); + self::assertGreaterThanOrEqual(1, $ruleToAdd->getLineNumber(), 'line number not valid'); + self::assertIsInt($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 + ): void { + self::markTestSkipped('currently broken - does not set column number'); + + $ruleToAdd = new Rule($propertyNameToAdd); + $ruleToAdd->setPosition(42); + $this->setRulesFromPropertyNames($initialPropertyNames); + + $this->subject->addRule($ruleToAdd); + + self::assertContains($ruleToAdd, $this->subject->getRules()); + self::assertIsInt($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 addRuleWithOnlyColumnNumberAddsRuleAndSetsLineNumberPreservingColumnNumber( + array $initialPropertyNames, + string $propertyNameToAdd + ): void { + self::markTestSkipped('currently broken - does not preserve column number'); + + $ruleToAdd = new Rule($propertyNameToAdd); + $ruleToAdd->setPosition(null, 42); + $this->setRulesFromPropertyNames($initialPropertyNames); + + $this->subject->addRule($ruleToAdd); + + self::assertContains($ruleToAdd, $this->subject->getRules()); + self::assertIsInt($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 + ): void { + $ruleToAdd = new Rule($propertyNameToAdd); + $ruleToAdd->setPosition(42, 64); + $this->setRulesFromPropertyNames($initialPropertyNames); + + $this->subject->addRule($ruleToAdd); + + self::assertContains($ruleToAdd, $this->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(): array + { + return [ + 'removing single rule' => [ + ['color'], + 'color', + [], + ], + 'removing first rule' => [ + ['color', 'display'], + 'color', + ['display'], + ], + 'removing last rule' => [ + ['color', 'display'], + 'display', + ['color'], + ], + 'removing middle rule' => [ + ['color', 'display', 'width'], + 'display', + ['color', 'width'], + ], + 'removing multiple rules' => [ + ['color', 'color'], + 'color', + [], + ], + 'removing multiple rules with another kept' => [ + ['color', 'color', 'display'], + 'color', + ['display'], + ], + 'removing nonexistent rule from empty list' => [ + [], + 'color', + [], + ], + 'removing nonexistent rule from nonempty list' => [ + ['color', 'display'], + 'width', + ['color', 'display'], + ], + ]; + } + + /** + * @test + * + * @param list $initialPropertyNames + * @param list $expectedRemainingPropertyNames + * + * @dataProvider providePropertyNamesAndPropertyNameToRemoveAndExpectedRemainingPropertyNames + */ + public function removeMatchingRulesRemovesRulesByPropertyNameAndKeepsOthers( + array $initialPropertyNames, + string $propertyNameToRemove, + array $expectedRemainingPropertyNames + ): void { + $this->setRulesFromPropertyNames($initialPropertyNames); + + $this->subject->removeMatchingRules($propertyNameToRemove); + + $remainingRules = $this->subject->getRulesAssoc(); + self::assertArrayNotHasKey($propertyNameToRemove, $remainingRules); + foreach ($expectedRemainingPropertyNames as $expectedPropertyName) { + self::assertArrayHasKey($expectedPropertyName, $remainingRules); + } + } + + /** + * @return array, 1: string, 2: list}> + */ + public static function providePropertyNamesAndPropertyNamePrefixToRemoveAndExpectedRemainingPropertyNames(): array + { + return [ + 'removing shorthand rule' => [ + ['font'], + 'font', + [], + ], + 'removing longhand rule' => [ + ['font-size'], + 'font', + [], + ], + 'removing shorthand and longhand rule' => [ + ['font', 'font-size'], + 'font', + [], + ], + 'removing shorthand rule with another kept' => [ + ['font', 'color'], + 'font', + ['color'], + ], + 'removing longhand rule with another kept' => [ + ['font-size', 'color'], + 'font', + ['color'], + ], + 'keeping other rules whose property names begin with the same characters' => [ + ['contain', 'container', 'container-type'], + 'contain', + ['container', 'container-type'], + ], + ]; + } + + /** + * @test + * + * @param list $initialPropertyNames + * @param list $expectedRemainingPropertyNames + * + * @dataProvider providePropertyNamesAndPropertyNamePrefixToRemoveAndExpectedRemainingPropertyNames + */ + public function removeMatchingRulesRemovesRulesByPropertyNamePrefixAndKeepsOthers( + array $initialPropertyNames, + string $propertyNamePrefix, + array $expectedRemainingPropertyNames + ): void { + $propertyNamePrefixWithHyphen = $propertyNamePrefix . '-'; + $this->setRulesFromPropertyNames($initialPropertyNames); + + $this->subject->removeMatchingRules($propertyNamePrefixWithHyphen); + + $remainingRules = $this->subject->getRulesAssoc(); + self::assertArrayNotHasKey($propertyNamePrefix, $remainingRules); + foreach (\array_keys($remainingRules) as $remainingPropertyName) { + self::assertStringStartsNotWith($propertyNamePrefixWithHyphen, $remainingPropertyName); + } + foreach ($expectedRemainingPropertyNames as $expectedPropertyName) { + self::assertArrayHasKey($expectedPropertyName, $remainingRules); + } + } + + /** + * @test + * + * @param list $propertyNamesToRemove + * + * @dataProvider providePropertyNamesToBeSetInitially + */ + public function removeAllRulesRemovesAllRules(array $propertyNamesToRemove): void + { + $this->setRulesFromPropertyNames($propertyNamesToRemove); + + $this->subject->removeAllRules(); + + self::assertSame([], $this->subject->getRules()); + } + + /** + * @param list $propertyNames + */ + private function setRulesFromPropertyNames(array $propertyNames): void + { + $this->subject->setRules(\array_map( + static function (string $propertyName): Rule { + return new Rule($propertyName); + }, + $propertyNames + )); + } +} diff --git a/tests/Unit/SettingsTest.php b/tests/Unit/SettingsTest.php new file mode 100644 index 00000000..63f94521 --- /dev/null +++ b/tests/Unit/SettingsTest.php @@ -0,0 +1,155 @@ +subject = Settings::create(); + } + + /** + * @test + */ + public function createReturnsInstance(): void + { + $settings = Settings::create(); + + self::assertInstanceOf(Settings::class, $settings); + } + + /** + * @test + */ + public function createReturnsANewInstanceForEachCall(): void + { + $settings1 = Settings::create(); + $settings2 = Settings::create(); + + self::assertNotSame($settings1, $settings2); + } + + /** + * @test + */ + public function multibyteSupportByDefaultStateOfMbStringExtension(): void + { + self::assertSame(\extension_loaded('mbstring'), $this->subject->hasMultibyteSupport()); + } + + /** + * @test + */ + public function withMultibyteSupportProvidesFluentInterface(): void + { + self::assertSame($this->subject, $this->subject->withMultibyteSupport()); + } + + /** + * @return array + */ + public static function booleanDataProvider(): array + { + return [ + 'true' => [true], + 'false' => [false], + ]; + } + + /** + * @test + * @dataProvider booleanDataProvider + */ + public function withMultibyteSupportSetsMultibyteSupport(bool $value): void + { + $this->subject->withMultibyteSupport($value); + + self::assertSame($value, $this->subject->hasMultibyteSupport()); + } + + /** + * @test + */ + public function defaultCharsetByDefaultIsUtf8(): void + { + self::assertSame('utf-8', $this->subject->getDefaultCharset()); + } + + /** + * @test + */ + public function withDefaultCharsetProvidesFluentInterface(): void + { + self::assertSame($this->subject, $this->subject->withDefaultCharset('UTF-8')); + } + + /** + * @test + */ + public function withDefaultCharsetSetsDefaultCharset(): void + { + $charset = 'ISO-8859-1'; + $this->subject->withDefaultCharset($charset); + + self::assertSame($charset, $this->subject->getDefaultCharset()); + } + + /** + * @test + */ + public function lenientParsingByDefaultIsTrue(): void + { + self::assertTrue($this->subject->usesLenientParsing()); + } + + /** + * @test + */ + public function withLenientParsingProvidesFluentInterface(): void + { + self::assertSame($this->subject, $this->subject->withLenientParsing()); + } + + /** + * @test + * @dataProvider booleanDataProvider + */ + public function withLenientParsingSetsLenientParsing(bool $value): void + { + $this->subject->withLenientParsing($value); + + self::assertSame($value, $this->subject->usesLenientParsing()); + } + + /** + * @test + */ + public function beStrictProvidesFluentInterface(): void + { + self::assertSame($this->subject, $this->subject->beStrict()); + } + + /** + * @test + */ + public function beStrictSetsLenientParsingToFalse(): void + { + $this->subject->beStrict(); + + self::assertFalse($this->subject->usesLenientParsing()); + } +} diff --git a/tests/Unit/Value/CSSStringTest.php b/tests/Unit/Value/CSSStringTest.php new file mode 100644 index 00000000..e88f3554 --- /dev/null +++ b/tests/Unit/Value/CSSStringTest.php @@ -0,0 +1,84 @@ +getString()); + } + + /** + * @test + */ + public function setStringSetsString(): void + { + $subject = new CSSString(''); + $string = 'coffee'; + + $subject->setString($string); + + self::assertSame($string, $subject->getString()); + } + + /** + * @test + */ + public function getLineNoByDefaultReturnsZero(): void + { + $subject = new CSSString(''); + + self::assertSame(0, $subject->getLineNo()); + } + + /** + * @test + */ + public function getLineNoReturnsLineNumberProvidedToConstructor(): void + { + $lineNumber = 42; + + $subject = new CSSString('', $lineNumber); + + self::assertSame($lineNumber, $subject->getLineNo()); + } +} diff --git a/tests/Value/CalcRuleValueListTest.php b/tests/Unit/Value/CalcRuleValueListTest.php similarity index 67% rename from tests/Value/CalcRuleValueListTest.php rename to tests/Unit/Value/CalcRuleValueListTest.php index 0a2c5304..5d73d9e9 100644 --- a/tests/Value/CalcRuleValueListTest.php +++ b/tests/Unit/Value/CalcRuleValueListTest.php @@ -1,6 +1,8 @@ + */ + public static function provideValidColorAndExpectedRendering(): array + { + return [ + '3-digit hex color' => [ + '#070', + '#070', + ], + '6-digit hex color that can be represented as 3-digit' => [ + '#007700', + '#070', + ], + '6-digit hex color that cannot be represented as 3-digit' => [ + '#007600', + '#007600', + ], + '4-digit hex color (with alpha)' => [ + '#0707', + 'rgba(0,119,0,.47)', + ], + '8-digit hex color (with alpha)' => [ + '#0077007F', + 'rgba(0,119,0,.5)', + ], + 'legacy rgb that can be represented as 3-digit hex' => [ + 'rgb(0, 119, 0)', + '#070', + ], + 'legacy rgb that cannot be represented as 3-digit hex' => [ + 'rgb(0, 118, 0)', + '#007600', + ], + 'legacy rgb with percentage components' => [ + 'rgb(0%, 60%, 0%)', + 'rgb(0%,60%,0%)', + ], + 'legacy rgba with fractional alpha' => [ + 'rgba(0, 119, 0, 0.5)', + 'rgba(0,119,0,.5)', + ], + 'legacy rgba with percentage alpha' => [ + 'rgba(0, 119, 0, 50%)', + 'rgba(0,119,0,50%)', + ], + 'legacy rgba with percentage components and fractional alpha' => [ + 'rgba(0%, 60%, 0%, 0.5)', + 'rgba(0%,60%,0%,.5)', + ], + 'legacy rgba with percentage components and percentage alpha' => [ + 'rgba(0%, 60%, 0%, 50%)', + 'rgba(0%,60%,0%,50%)', + ], + 'legacy rgb as rgba' => [ + 'rgba(0, 119, 0)', + '#070', + ], + 'legacy rgba as rgb' => [ + 'rgb(0, 119, 0, 0.5)', + 'rgba(0,119,0,.5)', + ], + 'modern rgb' => [ + 'rgb(0 119 0)', + '#070', + ], + 'modern rgb with percentage R' => [ + 'rgb(0% 119 0)', + 'rgb(0% 119 0)', + ], + 'modern rgb with percentage G' => [ + 'rgb(0 60% 0)', + 'rgb(0 60% 0)', + ], + 'modern rgb with percentage B' => [ + 'rgb(0 119 0%)', + 'rgb(0 119 0%)', + ], + 'modern rgb with percentage R&G' => [ + 'rgb(0% 60% 0)', + 'rgb(0% 60% 0)', + ], + 'modern rgb with percentage R&B' => [ + 'rgb(0% 119 0%)', + 'rgb(0% 119 0%)', + ], + 'modern rgb with percentage G&B' => [ + 'rgb(0 60% 0%)', + 'rgb(0 60% 0%)', + ], + 'modern rgb with percentage components' => [ + 'rgb(0% 60% 0%)', + 'rgb(0%,60%,0%)', + ], + 'modern rgb with none as red' => [ + 'rgb(none 119 0)', + 'rgb(none 119 0)', + ], + 'modern rgb with none as green' => [ + 'rgb(0 none 0)', + 'rgb(0 none 0)', + ], + 'modern rgb with none as blue' => [ + 'rgb(0 119 none)', + 'rgb(0 119 none)', + ], + 'modern rgba with fractional alpha' => [ + 'rgb(0 119 0 / 0.5)', + 'rgba(0,119,0,.5)', + ], + 'modern rgba with percentage alpha' => [ + 'rgb(0 119 0 / 50%)', + 'rgba(0,119,0,50%)', + ], + 'modern rgba with percentage R' => [ + 'rgb(0% 119 0 / 0.5)', + 'rgba(0% 119 0/.5)', + ], + 'modern rgba with percentage G' => [ + 'rgb(0 60% 0 / 0.5)', + 'rgba(0 60% 0/.5)', + ], + 'modern rgba with percentage B' => [ + 'rgb(0 119 0% / 0.5)', + 'rgba(0 119 0%/.5)', + ], + 'modern rgba with percentage RGB' => [ + 'rgb(0% 60% 0% / 0.5)', + 'rgba(0%,60%,0%,.5)', + ], + 'modern rgba with percentage components' => [ + 'rgb(0% 60% 0% / 50%)', + 'rgba(0%,60%,0%,50%)', + ], + 'modern rgba with none as alpha' => [ + 'rgb(0 119 0 / none)', + 'rgba(0 119 0/none)', + ], + 'legacy rgb with var for R' => [ + 'rgb(var(--r), 119, 0)', + 'rgb(var(--r),119,0)', + ], + 'legacy rgb with var for G' => [ + 'rgb(0, var(--g), 0)', + 'rgb(0,var(--g),0)', + ], + 'legacy rgb with var for B' => [ + 'rgb(0, 119, var(--b))', + 'rgb(0,119,var(--b))', + ], + 'legacy rgb with var for RG' => [ + 'rgb(var(--rg), 0)', + 'rgb(var(--rg),0)', + ], + 'legacy rgb with var for GB' => [ + 'rgb(0, var(--gb))', + 'rgb(0,var(--gb))', + ], + 'legacy rgba with var for R' => [ + 'rgba(var(--r), 119, 0, 0.5)', + 'rgba(var(--r),119,0,.5)', + ], + 'legacy rgba with var for G' => [ + 'rgba(0, var(--g), 0, 0.5)', + 'rgba(0,var(--g),0,.5)', + ], + 'legacy rgba with var for B' => [ + 'rgb(0, 119, var(--b), 0.5)', + 'rgb(0,119,var(--b),.5)', + ], + 'legacy rgba with var for A' => [ + 'rgba(0, 119, 0, var(--a))', + 'rgba(0,119,0,var(--a))', + ], + 'legacy rgba with var for RG' => [ + 'rgba(var(--rg), 0, 0.5)', + 'rgba(var(--rg),0,.5)', + ], + 'legacy rgba with var for GB' => [ + 'rgba(0, var(--gb), 0.5)', + 'rgba(0,var(--gb),.5)', + ], + 'legacy rgba with var for BA' => [ + 'rgba(0, 119, var(--ba))', + 'rgba(0,119,var(--ba))', + ], + 'legacy rgba with var for RGB' => [ + 'rgba(var(--rgb), 0.5)', + 'rgba(var(--rgb),.5)', + ], + 'legacy rgba with var for GBA' => [ + 'rgba(0, var(--gba))', + 'rgba(0,var(--gba))', + ], + 'modern rgb with var for R' => [ + 'rgb(var(--r) 119 0)', + 'rgb(var(--r),119,0)', + ], + 'modern rgb with var for G' => [ + 'rgb(0 var(--g) 0)', + 'rgb(0,var(--g),0)', + ], + 'modern rgb with var for B' => [ + 'rgb(0 119 var(--b))', + 'rgb(0,119,var(--b))', + ], + 'modern rgb with var for RG' => [ + 'rgb(var(--rg) 0)', + 'rgb(var(--rg),0)', + ], + 'modern rgb with var for GB' => [ + 'rgb(0 var(--gb))', + 'rgb(0,var(--gb))', + ], + 'modern rgba with var for R' => [ + 'rgba(var(--r) 119 0 / 0.5)', + 'rgba(var(--r),119,0,.5)', + ], + 'modern rgba with var for G' => [ + 'rgba(0 var(--g) 0 / 0.5)', + 'rgba(0,var(--g),0,.5)', + ], + 'modern rgba with var for B' => [ + 'rgba(0 119 var(--b) / 0.5)', + 'rgba(0,119,var(--b),.5)', + ], + 'modern rgba with var for A' => [ + 'rgba(0 119 0 / var(--a))', + 'rgba(0,119,0,var(--a))', + ], + 'modern rgba with var for RG' => [ + 'rgba(var(--rg) 0 / 0.5)', + 'rgba(var(--rg),0,.5)', + ], + 'modern rgba with var for GB' => [ + 'rgba(0 var(--gb) / 0.5)', + 'rgba(0,var(--gb),.5)', + ], + 'modern rgba with var for BA' => [ + 'rgba(0 119 var(--ba))', + 'rgba(0,119,var(--ba))', + ], + 'modern rgba with var for RGB' => [ + 'rgba(var(--rgb) / 0.5)', + 'rgba(var(--rgb),.5)', + ], + 'modern rgba with var for GBA' => [ + 'rgba(0 var(--gba))', + 'rgba(0,var(--gba))', + ], + 'rgba with var for RGBA' => [ + 'rgba(var(--rgba))', + 'rgba(var(--rgba))', + ], + 'legacy hsl' => [ + 'hsl(120, 100%, 25%)', + 'hsl(120,100%,25%)', + ], + 'legacy hsl with deg' => [ + 'hsl(120deg, 100%, 25%)', + 'hsl(120deg,100%,25%)', + ], + 'legacy hsl with grad' => [ + 'hsl(133grad, 100%, 25%)', + 'hsl(133grad,100%,25%)', + ], + 'legacy hsl with rad' => [ + 'hsl(2.094rad, 100%, 25%)', + 'hsl(2.094rad,100%,25%)', + ], + 'legacy hsl with turn' => [ + 'hsl(0.333turn, 100%, 25%)', + 'hsl(.333turn,100%,25%)', + ], + 'legacy hsla with fractional alpha' => [ + 'hsla(120, 100%, 25%, 0.5)', + 'hsla(120,100%,25%,.5)', + ], + 'legacy hsla with percentage alpha' => [ + 'hsla(120, 100%, 25%, 50%)', + 'hsla(120,100%,25%,50%)', + ], + 'legacy hsl as hsla' => [ + 'hsla(120, 100%, 25%)', + 'hsl(120,100%,25%)', + ], + 'legacy hsla as hsl' => [ + 'hsl(120, 100%, 25%, 0.5)', + 'hsla(120,100%,25%,.5)', + ], + 'modern hsl' => [ + 'hsl(120 100% 25%)', + 'hsl(120,100%,25%)', + ], + 'modern hsl with none as hue' => [ + 'hsl(none 100% 25%)', + 'hsl(none 100% 25%)', + ], + 'modern hsl with none as saturation' => [ + 'hsl(120 none 25%)', + 'hsl(120 none 25%)', + ], + 'modern hsl with none as lightness' => [ + 'hsl(120 100% none)', + 'hsl(120 100% none)', + ], + 'modern hsla' => [ + 'hsl(120 100% 25% / 0.5)', + 'hsla(120,100%,25%,.5)', + ], + 'modern hsla with none as alpha' => [ + 'hsl(120 100% 25% / none)', + 'hsla(120 100% 25%/none)', + ], + ]; + } + + /** + * @test + * + * @dataProvider provideValidColorAndExpectedRendering + */ + public function parsesAndRendersValidColor(string $color, string $expectedRendering): void + { + $subject = Color::parse(new ParserState($color, Settings::create())); + + $renderedResult = $subject->render(OutputFormat::create()); + + self::assertSame($expectedRendering, $renderedResult); + } + + /** + * Browsers reject all these, thus so should the parser. + * + * @return array + */ + public static function provideInvalidColor(): array + { + return [ + 'hex color with 0 digits' => [ + '#', + ], + 'hex color with 1 digit' => [ + '#f', + ], + 'hex color with 2 digits' => [ + '#f0', + ], + 'hex color with 5 digits' => [ + '#ff000', + ], + 'hex color with 7 digits' => [ + '#ff00000', + ], + 'hex color with 9 digits' => [ + '#ff0000000', + ], + 'rgb color with 0 arguments' => [ + 'rgb()', + ], + 'rgb color with 1 argument' => [ + 'rgb(255)', + ], + 'legacy rgb color with 2 arguments' => [ + 'rgb(255, 0)', + ], + 'legacy rgb color with 5 arguments' => [ + 'rgb(255, 0, 0, 0.5, 0)', + ], + /* + 'legacy rgb color with invalid unit' => [ + 'rgb(255, 0px, 0)', + ], + //*/ + 'legacy rgb color with none as red' => [ + 'rgb(none, 0, 0)', + ], + 'legacy rgb color with none as green' => [ + 'rgb(255, none, 0)', + ], + 'legacy rgb color with none as blue' => [ + 'rgb(255, 0, none)', + ], + 'legacy rgba color with none as alpha' => [ + 'rgba(255, 0, 0, none)', + ], + 'modern rgb color without slash separator for alpha' => [ + 'rgb(255 0 0 0.5)', + ], + 'rgb color with mixed separators, comma first' => [ + 'rgb(255, 0 0)', + ], + 'rgb color with mixed separators, space first' => [ + 'rgb(255 0, 0)', + ], + 'hsl color with 0 arguments' => [ + 'hsl()', + ], + 'hsl color with 1 argument' => [ + 'hsl(0)', + ], + 'legacy hsl color with 2 arguments' => [ + 'hsl(0, 100%)', + ], + 'legacy hsl color with 5 arguments' => [ + 'hsl(0, 100%, 50%, 0.5, 0)', + ], + 'legacy hsl color with none as hue' => [ + 'hsl(none, 100%, 50%)', + ], + 'legacy hsl color with none as saturation' => [ + 'hsl(0, none, 50%)', + ], + 'legacy hsl color with none as lightness' => [ + 'hsl(0, 100%, none)', + ], + 'legacy hsla color with none as alpha' => [ + 'hsl(0, 100%, 50%, none)', + ], + /* + 'legacy hsl color without % for S/L units' => [ + 'hsl(0, 1, 0.5)' + ], + 'legacy hsl color with invalid unit for H' => [ + 'hsl(0px, 100%, 50%)' + ], + //*/ + 'modern hsl color without slash separator for alpha' => [ + 'rgb(0 100% 50% 0.5)', + ], + 'hsl color with mixed separators, comma first' => [ + 'hsl(0, 100% 50%)', + ], + 'hsl color with mixed separators, space first' => [ + 'hsl(0 100%, 50%)', + ], + ]; + } + + /** + * @test + * + * @dataProvider provideInvalidColor + */ + public function throwsExceptionWithInvalidColor(string $color): void + { + $this->expectException(SourceException::class); + + Color::parse(new ParserState($color, Settings::create())); + } +} diff --git a/tests/Unit/Value/Fixtures/ConcreteValue.php b/tests/Unit/Value/Fixtures/ConcreteValue.php new file mode 100644 index 00000000..b6e92480 --- /dev/null +++ b/tests/Unit/Value/Fixtures/ConcreteValue.php @@ -0,0 +1,19 @@ + + */ + public static function provideUnit(): array + { + $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( + static function (string $unit): array { + return [$unit]; + }, + $units + ) + ); + } + + /** + * @test + * + * @param non-empty-string $unit + * + * @dataProvider provideUnit + */ + public function parsesUnit(string $unit): void + { + $parsedSize = Size::parse(new ParserState('1' . $unit, Settings::create())); + + self::assertSame($unit, $parsedSize->getUnit()); + } +} diff --git a/tests/Unit/Value/URLTest.php b/tests/Unit/Value/URLTest.php new file mode 100644 index 00000000..42d96e29 --- /dev/null +++ b/tests/Unit/Value/URLTest.php @@ -0,0 +1,84 @@ +getURL()); + } + + /** + * @test + */ + public function setUrlReplacesUrl(): void + { + $subject = new URL(new CSSString('http://example.com')); + + $newUrl = new CSSString('http://example.org'); + $subject->setURL($newUrl); + + self::assertSame($newUrl, $subject->getURL()); + } + + /** + * @test + */ + public function getLineNoByDefaultReturnsZero(): void + { + $subject = new URL(new CSSString('http://example.com')); + + self::assertSame(0, $subject->getLineNo()); + } + + /** + * @test + */ + public function getLineNoReturnsLineNumberProvidedToConstructor(): void + { + $lineNumber = 17; + + $subject = new URL(new CSSString('http://example.com'), $lineNumber); + + self::assertSame($lineNumber, $subject->getLineNo()); + } +} diff --git a/tests/Unit/Value/ValueTest.php b/tests/Unit/Value/ValueTest.php new file mode 100644 index 00000000..9a664a77 --- /dev/null +++ b/tests/Unit/Value/ValueTest.php @@ -0,0 +1,146 @@ + + */ + private const DEFAULT_DELIMITERS = [',', ' ', '/']; + + /** + * @test + */ + public function implementsCSSElement(): void + { + $subject = new ConcreteValue(); + + self::assertInstanceOf(CSSElement::class, $subject); + } + + /** + * @return array + */ + public static function provideArithmeticOperator(): array + { + return [ + '+' => ['+'], + '-' => ['-'], + '*' => ['*'], + '/' => ['/'], + ]; + } + + /** + * @test + * + * @dataProvider provideArithmeticOperator + */ + public function parsesArithmeticInFunctions(string $operator): void + { + $subject = Value::parseValue( + new ParserState('max(300px, 50vh ' . $operator . ' 10px);', Settings::create()), + self::DEFAULT_DELIMITERS + ); + + self::assertInstanceOf(CSSFunction::class, $subject); + self::assertSame('max(300px,50vh ' . $operator . ' 10px)', $subject->render(OutputFormat::createCompact())); + } + + /** + * @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(): array + { + return [ + 'calc' => [ + 'to be parsed' => 'calc(%s);', + 'expected' => 'calc(%s)', + ], + 'max' => [ + 'to be parsed' => 'max(300px, %s);', + 'expected' => 'max(300px,%s)', + ], + 'clamp' => [ + 'to be parsed' => 'clamp(2.19rem, %s, 2.5rem);', + 'expected' => 'clamp(2.19rem,%s,2.5rem)', + ], + ]; + } + + /** + * @test + * + * @dataProvider provideCssFunctionTemplates + */ + public function parsesArithmeticWithMultipleOperatorsInFunctions( + string $parserTemplate, + string $expectedResultTemplate + ): void { + static $expression = '300px + 10% + 10vw'; + + $subject = Value::parseValue( + new ParserState(\sprintf($parserTemplate, $expression), Settings::create()), + self::DEFAULT_DELIMITERS + ); + + self::assertInstanceOf(CSSFunction::class, $subject); + self::assertSame( + \sprintf($expectedResultTemplate, $expression), + $subject->render(OutputFormat::createCompact()) + ); + } + + /** + * @return array + */ + public static function provideMalformedLengthOperands(): array + { + 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(string $leftOperand, string $rightOperand): void + { + $subject = Value::parseValue( + new ParserState('max(300px, ' . $leftOperand . ' + ' . $rightOperand . ');', Settings::create()), + self::DEFAULT_DELIMITERS + ); + + self::assertInstanceOf(CSSFunction::class, $subject); + self::assertSame( + 'max(300px,' . $leftOperand . ' + ' . $rightOperand . ')', + $subject->render(OutputFormat::createCompact()) + ); + } +} diff --git a/tests/UnitDeprecated/Position/PositionTest.php b/tests/UnitDeprecated/Position/PositionTest.php new file mode 100644 index 00000000..2e06cc6d --- /dev/null +++ b/tests/UnitDeprecated/Position/PositionTest.php @@ -0,0 +1,135 @@ +subject = new ConcretePosition(); + } + + /** + * @return array}> + */ + public function provideLineNumber(): array + { + return [ + 'line 1' => [1], + 'line 42' => [42], + ]; + } + + /** + * @return array}> + */ + public function provideColumnNumber(): array + { + return [ + 'column 0' => [0], + 'column 14' => [14], + 'column 39' => [39], + ]; + } + + /** + * @test + */ + public function getLineNoInitiallyReturnsZero(): void + { + self::assertSame(0, $this->subject->getLineNo()); + } + + /** + * @test + * + * @dataProvider provideLineNumber + */ + public function getLineNoReturnsLineNumberSet(int $lineNumber): void + { + $this->subject->setPosition($lineNumber); + + self::assertSame($lineNumber, $this->subject->getLineNo()); + } + + /** + * @test + */ + public function getLineNoReturnsZeroAfterLineNumberCleared(): void + { + $this->subject->setPosition(99); + + $this->subject->setPosition(null); + + self::assertSame(0, $this->subject->getLineNo()); + } + + /** + * @test + */ + public function getColNoInitiallyReturnsZero(): void + { + self::assertSame(0, $this->subject->getColNo()); + } + + /** + * @test + * + * @dataProvider provideColumnNumber + */ + public function getColNoReturnsColumnNumberSet(int $columnNumber): void + { + $this->subject->setPosition(1, $columnNumber); + + self::assertSame($columnNumber, $this->subject->getColNo()); + } + + /** + * @test + */ + public function getColNoReturnsZeroAfterColumnNumberCleared(): void + { + $this->subject->setPosition(1, 99); + + $this->subject->setPosition(2); + + self::assertSame(0, $this->subject->getColNo()); + } + + /** + * @test + */ + public function setPositionWithZeroClearsLineNumber(): void + { + $this->subject->setPosition(99); + + $this->subject->setPosition(0); + + self::assertNull($this->subject->getLineNumber()); + } + + /** + * @test + */ + public function getLineNoAfterSetPositionWithZeroReturnsZero(): void + { + $this->subject->setPosition(99); + + $this->subject->setPosition(0); + + self::assertSame(0, $this->subject->getLineNo()); + } +} diff --git a/tests/fixtures/-fault-tolerance.css b/tests/fixtures/-fault-tolerance.css index 7a922157..7d4e6105 100644 --- a/tests/fixtures/-fault-tolerance.css +++ b/tests/fixtures/-fault-tolerance.css @@ -1,15 +1,15 @@ .test1 { - //gaga: hello; + //gaga: hello; } .test2 { - *hello: 1; - hello: 2.2; - hello: 2000000000000.2; + *hello: 1; + hello: 2.2; + hello: 2000000000000.2; } #test { - #hello: 1} + #hello: 1} #test2 { - help: none; \ No newline at end of file + help: none; diff --git a/tests/fixtures/1readme.css b/tests/fixtures/1readme.css index f782fad9..adfa9f99 100644 --- a/tests/fixtures/1readme.css +++ b/tests/fixtures/1readme.css @@ -4,7 +4,7 @@ font-family: "CrassRoots"; src: url("../media/cr.ttf") } - + html, body { - font-size: 1.6em + font-size: 1.6em } diff --git a/tests/fixtures/2readme.css b/tests/fixtures/2readme.css index 9b8dbcca..8deae980 100644 --- a/tests/fixtures/2readme.css +++ b/tests/fixtures/2readme.css @@ -1,5 +1,5 @@ #header { - margin: 10px 2em 1cm 2%; - font-family: Verdana, Helvetica, "Gill Sans", sans-serif; - color: red !important; + margin: 10px 2em 1cm 2%; + font-family: Verdana, Helvetica, "Gill Sans", sans-serif; + color: red !important; } diff --git a/tests/fixtures/atrules.css b/tests/fixtures/atrules.css index 58b27c1f..d4b4d21a 100644 --- a/tests/fixtures/atrules.css +++ b/tests/fixtures/atrules.css @@ -1,28 +1,28 @@ @charset "utf-8"; @font-face { - font-family: "CrassRoots"; - src: url("../media/cr.ttf") + font-family: "CrassRoots"; + src: url("../media/cr.ttf") } html, body { - font-size: -0.6em + font-size: -0.6em } @keyframes mymove { - from { top: 0px; } - to { top: 200px; } + from { top: 0px; } + to { top: 200px; } } @-moz-keyframes some-move { - from { top: 0px; } - to { top: 200px; } + from { top: 0px; } + to { top: 200px; } } @supports ( (perspective: 10px) or (-moz-perspective: 10px) or (-webkit-perspective: 10px) or (-ms-perspective: 10px) or (-o-perspective: 10px) ) { - body { - font-family: 'Helvetica'; - } + body { + font-family: 'Helvetica'; + } } @page :pseudo-class { @@ -54,4 +54,4 @@ html, body { @region-style #intro { p { color: blue; } -} \ No newline at end of file +} diff --git a/tests/fixtures/calc.css b/tests/fixtures/calc.css index 9fd0d973..e794ade6 100644 --- a/tests/fixtures/calc.css +++ b/tests/fixtures/calc.css @@ -1,7 +1,7 @@ div { width: calc(100% / 4); } div { margin-top: calc(-120% - 4px); } div { - height: -webkit-calc(9/16 * 100%)!important; - width: -moz-calc((50px - 50%)*2); + height: calc(9/16 * 100%)!important; + width: calc((50px - 50%)*2); } div { width: calc(50% - ( ( 4% ) * 0.5 ) ); } diff --git a/tests/fixtures/case-insensitivity.css b/tests/fixtures/case-insensitivity.css index 43716029..bfc20ffe 100644 --- a/tests/fixtures/case-insensitivity.css +++ b/tests/fixtures/case-insensitivity.css @@ -6,10 +6,10 @@ } #myid { - CaSe: insensitive !imPORTANT; - frequency: 30hz; - font-size: 1EM; - color: RGB(255, 255, 0); - color: hSL(40, 40%, 30%); - font-Family: Arial; /* The value needs to remain capitalized */ -} \ No newline at end of file + CaSe: insensitive !imPORTANT; + frequency: 30hz; + font-size: 1EM; + color: RGB(255, 255, 0); + color: hSL(40, 40%, 30%); + font-Family: Arial; /* The value needs to remain capitalized */ +} diff --git a/tests/fixtures/colortest.css b/tests/fixtures/colortest.css index 1c89cf41..f344137b 100644 --- a/tests/fixtures/colortest.css +++ b/tests/fixtures/colortest.css @@ -1,28 +1,29 @@ #mine { - color: red; - border-color: rgb(10, 100, 230); - border-color: rgba(10, 100, 231, 0.3); - outline-color: #222; - background-color: #232323; + color: red; + border-color: rgb(10, 100, 230); + border-color: rgba(10, 100, 231, 0.3); + outline-color: #222; + background-color: #232323; } #yours { - background-color: hsl(220, 10%, 220%); - background-color: hsla(220, 10%, 220%, 0.3); + background-color: hsl(220, 10%, 220%); + background-color: hsla(220, 10%, 220%, 0.3); + outline-color: #22; } #variables { - background-color: rgb(var(--some-rgb)); - background-color: rgb(var(--r), var(--g), var(--b)); - background-color: rgb(255, var(--g), var(--b)); - background-color: rgb(255, 255, var(--b)); - background-color: rgb(255, var(--rg)); + background-color: rgb(var(--some-rgb)); + background-color: rgb(var(--r), var(--g), var(--b)); + background-color: rgb(255, var(--g), var(--b)); + background-color: rgb(255, 255, var(--b)); + background-color: rgb(255, var(--rg)); - background-color: hsl(var(--some-hsl)); + background-color: hsl(var(--some-hsl)); } #variables-alpha { - background-color: rgba(var(--some-rgb), 0.1); - background-color: rgba(var(--some-rg), 255, 0.1); - background-color: hsla(var(--some-hsl), 0.1); + background-color: rgba(var(--some-rgb), 0.1); + background-color: rgba(var(--some-rg), 255, 0.1); + background-color: hsla(var(--some-hsl), 0.1); } diff --git a/tests/fixtures/create-shorthands.css b/tests/fixtures/create-shorthands.css deleted file mode 100644 index 72198043..00000000 --- a/tests/fixtures/create-shorthands.css +++ /dev/null @@ -1,6 +0,0 @@ -body { - font-size: 2em; font-family: Helvetica,Arial,sans-serif; font-weight: bold; - border-width: 2px; border-color: #999; border-style: dotted; - background-color: #fff; background-image: url('foobar.png'); background-repeat: repeat-y; - margin-top: 2px; margin-right: 3px; margin-bottom: 4px; margin-left: 5px; -} diff --git a/tests/fixtures/escaped-tokens.css b/tests/fixtures/escaped-tokens.css index 333c6569..97e2dfeb 100644 --- a/tests/fixtures/escaped-tokens.css +++ b/tests/fixtures/escaped-tokens.css @@ -2,6 +2,6 @@ * Special case function-like tokens, with an escape backslash followed by a non-newline and non-hex digit character, should be parsed as the appropriate \Sabberworm\CSS\Value\ type */ body { - background: u\rl("//example.org/picture.jpg"); - height: ca\lc(100% - 1px); -} + background: u\rl("//example.org/picture.jpg"); + height: ca\lc(100% - 1px); +} diff --git a/tests/fixtures/expand-shorthands.css b/tests/fixtures/expand-shorthands.css deleted file mode 100644 index 89aab1e2..00000000 --- a/tests/fixtures/expand-shorthands.css +++ /dev/null @@ -1,7 +0,0 @@ -body { - font: italic 500 14px/1.618 "Trebuchet MS", Georgia, serif; - border: 2px solid #f0f; - background: #ccc url("/images/foo.png") no-repeat left top; - margin: 1em !important; - padding: 2px 6px 3px; -} diff --git a/tests/fixtures/ie-hacks.css b/tests/fixtures/ie-hacks.css deleted file mode 100644 index 3f5f215e..00000000 --- a/tests/fixtures/ie-hacks.css +++ /dev/null @@ -1,9 +0,0 @@ -p { - padding-right: .75rem \9; - background-image: none \9; - color:red\9\0; - background-color:red \9 \0; - background-color:red \9 \0 !important; - content: "red \9\0"; - content: "red\0abc"; -} diff --git a/tests/fixtures/ie.css b/tests/fixtures/ie.css index 6c0fb381..9f070e24 100644 --- a/tests/fixtures/ie.css +++ b/tests/fixtures/ie.css @@ -1,6 +1,6 @@ .nav-thumb-wrapper:hover img, a.activeSlide img { - filter: alpha(opacity=100); - -moz-opacity: 1; - -khtml-opacity: 1; - opacity: 1; -} + filter: alpha(opacity=100); + -moz-opacity: 1; + -khtml-opacity: 1; + opacity: 1; +} diff --git a/tests/fixtures/inner-color.css b/tests/fixtures/inner-color.css index 7fb28b6d..541a65f3 100644 --- a/tests/fixtures/inner-color.css +++ b/tests/fixtures/inner-color.css @@ -1,3 +1,3 @@ test { - background: -webkit-gradient(linear, 0 0, 0 bottom, from(#006cad), to(hsl(202, 100%, 49%))); -} \ No newline at end of file + background: -webkit-gradient(linear, 0 0, 0 bottom, from(#006cad), to(hsl(202, 100%, 49%))); +} diff --git a/tests/fixtures/invalid-color.css b/tests/fixtures/invalid-color.css new file mode 100644 index 00000000..31602f37 --- /dev/null +++ b/tests/fixtures/invalid-color.css @@ -0,0 +1,11 @@ +#test { + color: #a; + background: #ab; +} + +body + color: #abcd; + background: #abcde; +} + +a { color: #fffff;} diff --git a/tests/fixtures/missing-property-value.css b/tests/fixtures/missing-property-value.css index 33eb473d..22d87c8f 100644 --- a/tests/fixtures/missing-property-value.css +++ b/tests/fixtures/missing-property-value.css @@ -1,4 +1,4 @@ div { - display: inline-block; - display: + display: inline-block; + display: } diff --git a/tests/fixtures/namespaces.css b/tests/fixtures/namespaces.css index c396c974..3577c026 100644 --- a/tests/fixtures/namespaces.css +++ b/tests/fixtures/namespaces.css @@ -10,9 +10,9 @@ foo|test { - gaga: 1; + gaga: 1; } |test { - gaga: 2; -} \ No newline at end of file + gaga: 2; +} diff --git a/tests/fixtures/nested.css b/tests/fixtures/nested.css index b59dc80e..e1f41fe6 100644 --- a/tests/fixtures/nested.css +++ b/tests/fixtures/nested.css @@ -1,17 +1,17 @@ html { - some: -test(val1); + some: -test(val1); } html { - some-other: -test(val1); + some-other: -test(val1); } @media screen { - html { - some: -test(val2); - } + html { + some: -test(val2); + } } #unrelated { - other: yes; + other: yes; } diff --git a/tests/fixtures/slashed.css b/tests/fixtures/slashed.css index 5b629be5..86f4ee82 100644 --- a/tests/fixtures/slashed.css +++ b/tests/fixtures/slashed.css @@ -1,4 +1,4 @@ .test { - font: 12px/1.5 Verdana, Arial, sans-serif; - border-radius: 5px 10px 5px 10px / 10px 5px 10px 5px; + font: 12px/1.5 Verdana, Arial, sans-serif; + border-radius: 5px 10px 5px 10px / 10px 5px 10px 5px; } diff --git a/tests/fixtures/specificity.css b/tests/fixtures/specificity.css index 82a2939a..1a7a0121 100644 --- a/tests/fixtures/specificity.css +++ b/tests/fixtures/specificity.css @@ -3,5 +3,5 @@ .help:hover, li.green, ol li::before { - font-family: Helvetica; + font-family: Helvetica; } diff --git a/tests/fixtures/url.css b/tests/fixtures/url.css index 93aae97f..feb91bc3 100644 --- a/tests/fixtures/url.css +++ b/tests/fixtures/url.css @@ -1,4 +1,4 @@ body { background: #FFFFFF url("https://somesite.com/images/someimage.gif") repeat top center; } body { - background-url: url("https://somesite.com/images/someimage.gif"); -} \ No newline at end of file + background-url: url("https://somesite.com/images/someimage.gif"); +} diff --git a/tests/fixtures/values.css b/tests/fixtures/values.css index 35dbd729..f00c0768 100644 --- a/tests/fixtures/values.css +++ b/tests/fixtures/values.css @@ -1,15 +1,15 @@ #header { - margin: 10px 2em 1cm 2%; - font-family: Verdana, Helvetica, "Gill Sans", sans-serif; - font-size: 10px; - color: red !important; - background-color: green; - background-color: rgba(0,128,0,0.7); - frequency: 30Hz; + margin: 10px 2em 1cm 2%; + font-family: Verdana, Helvetica, "Gill Sans", sans-serif; + font-size: 10px; + color: red !important; + background-color: green; + background-color: rgba(0,128,0,0.7); + frequency: 30Hz; transform: rotate(1turn); } body { - color: green; - font: 75% "Lucida Grande", "Trebuchet MS", Verdana, sans-serif; + color: green; + font: 75% "Lucida Grande", "Trebuchet MS", Verdana, sans-serif; } diff --git a/tests/fixtures/whitespace.css b/tests/fixtures/whitespace.css index 6b21c24f..de127ece 100644 --- a/tests/fixtures/whitespace.css +++ b/tests/fixtures/whitespace.css @@ -1,3 +1,3 @@ .test { - background-image : url ( 4px ) ; + background-image : url ( 4px ) ; }