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
-[](https://github.com/sabberworm/PHP-CSS-Parser/actions/)
+[](https://github.com/MyIntervals/PHP-CSS-Parser/actions/)
+[](https://coveralls.io/github/MyIntervals/PHP-CSS-Parser?branch=main)
A Parser for CSS Files written in PHP. Allows extraction of CSS files into a data structure, manipulation of said structure and output as (optimized) CSS.
@@ -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 ) ;
}