diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..833336b3 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,17 @@ +# top-most EditorConfig file +root = true + +# Unix-style newlines with a newline ending every file, and with sane defaults +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true +indent_style = space +indent_size = 4 +max_line_length = 120 + +[*.md] +max_line_length = 80 +# GitHub-flavored markdown uses two spaces and the end of a line to indicate a linebreak. +trim_trailing_whitespace = false diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..b6b5061f --- /dev/null +++ b/.gitattributes @@ -0,0 +1,9 @@ +/.editorconfig export-ignore +/.gitattributes export-ignore +/.github/ export-ignore +/.gitignore export-ignore +/.phive/ export-ignore +/bin/ export-ignore +/config/ export-ignore +/phpunit.xml export-ignore +/tests/ export-ignore diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..77c5338b --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,129 @@ +# https://help.github.com/en/categories/automating-your-workflow-with-github-actions + +on: + pull_request: + push: + schedule: + - cron: '3 3 * * 1' + +name: CI + +jobs: + php-lint: + name: PHP Lint + runs-on: ubuntu-20.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@v2 + + - 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-20.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@v2 + + - name: Install PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php-version }} + tools: composer:v2 + coverage: "${{ matrix.coverage }}" + + - name: Cache dependencies installed with composer + uses: actions/cache@v1 + 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-20.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@v2 + + - name: Install PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php-version }} + tools: "composer:v2, phive" + coverage: none + + - name: Cache dependencies installed with composer + uses: actions/cache@v1 + 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 }} diff --git a/.gitignore b/.gitignore index 48b8bf90..c1747f26 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,7 @@ -vendor/ +/.phive/* +/.php-cs-fixer.cache +/.php_cs.cache +/composer.lock +/phpstan.neon +/vendor/ +!/.phive/phars.xml diff --git a/.phive/phars.xml b/.phive/phars.xml new file mode 100644 index 00000000..2433736a --- /dev/null +++ b/.phive/phars.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 622f22b7..00000000 --- a/.travis.yml +++ /dev/null @@ -1,22 +0,0 @@ -language: php -php: - - "5.4" - - "5.5" - - "5.6" - - "7.0" - - "7.1" - - "7.2" - - "7.3" - - hhvm -sudo: false -dist: trusty -matrix: - include: - - - php: "5.3" - dist: precise - sudo: false -before_script: rm composer.lock && composer install -script: ./vendor/bin/phpunit --coverage-clover build/coverage/xml -after_script: ./vendor/bin/codacycoverage clover build/coverage/xml - diff --git a/CHANGELOG.md b/CHANGELOG.md index 524fcd6b..a23fd0e6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,26 +1,47 @@ # Revision History -## 8.0 +## 8.4.0 -### 8.0.0 (2016-06-30) +### Features -* Store source CSS line numbers in tokens and parsing exceptions. +* Support for PHP 8.x +* PHPDoc annotations +* Allow usage of CSS variables inside color functions (by parsing them as regular functions) +* Use PSR-12 code style * *No deprecations* -#### Backwards-incompatible changes +### Bugfixes -* Unrecoverable parser errors throw an exception of type `Sabberworm\CSS\Parsing\SourceException` instead of `\Exception`. +* Improved handling of whitespace in `calc()` +* Fix parsing units whose prefix is also a valid unit, like `vmin` +* Allow passing an object to `CSSList#replace` +* Fix PHP 7.3 warnings +* Correctly parse keyframes with `%` +* Don’t convert large numbers to scientific notation +* 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. +* Prevent an infinite loop when parsing invalid grid line names +* Remove invalid unit `vm` +* Retain rule order after expanding shorthands -### 8.1.0 (2016-07-19) +### Backwards-incompatible changes -* 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. +* PHP ≥ 5.6 is now required +* HHVM compatibility target dropped + +## 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. +* Swallow more errors in lenient mode, thanks to @raxbg. +* Allow specifying arbitrary strings to output before and after declaration blocks, thanks to @westonruter. * *No backwards-incompatible changes* * *No deprecations* -### 8.2.0 (2018-07-13) +## 8.2.0 (2018-07-13) * Support parsing `calc()`, thanks to @raxbg. * Support parsing grid-lines, again thanks to @raxbg. @@ -29,188 +50,184 @@ * *No backwards-incompatible changes* * *No deprecations* -### 8.3.0 (2019-02-22) +## 8.1.0 (2016-07-19) -* 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. +* 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* -## 7.0 - -### 7.0.0 (2015-08-24) +## 8.0.0 (2016-06-30) -* Compatibility with PHP 7. Well timed, eh? +* Store source CSS line numbers in tokens and parsing exceptions. * *No deprecations* -#### Backwards-incompatible changes +### Backwards-incompatible changes -* The `Sabberworm\CSS\Value\String` class has been renamed to `Sabberworm\CSS\Value\CSSString`. +* Unrecoverable parser errors throw an exception of type `Sabberworm\CSS\Parsing\SourceException` instead of `\Exception`. -### 7.0.1 (2015-12-25) +## 7.0.3 (2016-04-27) -* No more suppressed `E_NOTICE` +* Fixed parsing empty CSS when multibyte is off * *No backwards-incompatible changes* * *No deprecations* -### 7.0.2 (2016-02-11) +## 7.0.2 (2016-02-11) * 150 time performance boost thanks to @[ossinkine](https://github.com/ossinkine) * *No backwards-incompatible changes* * *No deprecations* -### 7.0.3 (2016-04-27) +## 7.0.1 (2015-12-25) -* Fixed parsing empty CSS when multibyte is off +* No more suppressed `E_NOTICE` * *No backwards-incompatible changes* * *No deprecations* -## 6.0 +## 7.0.0 (2015-08-24) -### 6.0.0 (2014-07-03) - -* Format output using Sabberworm\CSS\OutputFormat -* *No backwards-incompatible changes* +* Compatibility with PHP 7. Well timed, eh? +* *No deprecations* -#### Deprecations +### Backwards-incompatible changes -* The parse() method replaces __toString with an optional argument (instance of the OutputFormat class) +* The `Sabberworm\CSS\Value\String` class has been renamed to `Sabberworm\CSS\Value\CSSString`. -### 6.0.1 (2015-08-24) +## 6.0.1 (2015-08-24) * Remove some declarations in interfaces incompatible with PHP 5.3 (< 5.3.9) * *No deprecations* -## 5.0 +## 6.0.0 (2014-07-03) + +* Format output using Sabberworm\CSS\OutputFormat +* *No backwards-incompatible changes* + +### Deprecations -### 5.0.0 (2013-03-20) +* 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 -* Correctly parse all known CSS 3 units (including Hz and kHz). -* Output RGB colors in short (#aaa or #ababab) notation -* Be case-insensitive when parsing identifiers. * *No deprecations* #### Backwards-incompatible changes -* `Sabberworm\CSS\Value\Color`’s `__toString` method overrides `CSSList`’s to maybe return something other than `type(value, …)` (see above). +* Outputting a declaration block that has no selectors throws an OuputException instead of outputting an invalid ` {…}` into the CSS document. -### 5.0.1 (2013-03-20) +## 5.1.2 (2013-10-30) -* Internal cleanup +* 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* * *No deprecations* -### 5.0.2 (2013-03-21) +## 5.1.1 (2013-10-28) -* CHANGELOG.md file added to distribution +* Updated CHANGELOG.md to reflect changes since 5.0.4 * *No backwards-incompatible changes* * *No deprecations* -### 5.0.3 (2013-03-21) +## 5.1.0 (2013-10-24) -* More size units recognized +* Performance enhancements by Michael M Slusarz +* More rescue entry points for lenient parsing (unexpected tokens between declaration blocks and unclosed comments) * *No backwards-incompatible changes* * *No deprecations* -### 5.0.4 (2013-03-21) +## 5.0.8 (2013-08-15) -* Don’t output floats with locale-aware separator chars +* Make default settings’ multibyte parsing option dependent on whether or not the mbstring extension is actually installed. * *No backwards-incompatible changes* * *No deprecations* -### 5.0.5 (2013-04-17) +## 5.0.7 (2013-08-04) -* Initial support for lenient parsing (setting this parser option will catch some exceptions internally and recover the parser’s state as neatly as possible). +* Fix broken decimal point output optimization * *No backwards-incompatible changes* * *No deprecations* -### 5.0.6 (2013-05-31) +## 5.0.6 (2013-05-31) * Fix broken unit test * *No backwards-incompatible changes* * *No deprecations* -### 5.0.7 (2013-08-04) +## 5.0.5 (2013-04-17) -* Fix broken decimal point output optimization +* 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* -### 5.0.8 (2013-08-15) +## 5.0.4 (2013-03-21) -* Make default settings’ multibyte parsing option dependent on whether or not the mbstring extension is actually installed. +* Don’t output floats with locale-aware separator chars * *No backwards-incompatible changes* * *No deprecations* -### 5.1.0 (2013-10-24) +## 5.0.3 (2013-03-21) -* Performance enhancements by Michael M Slusarz -* More rescue entry points for lenient parsing (unexpected tokens between declaration blocks and unclosed comments) +* More size units recognized * *No backwards-incompatible changes* * *No deprecations* -### 5.1.1 (2013-10-28) +## 5.0.2 (2013-03-21) -* Updated CHANGELOG.md to reflect changes since 5.0.4 +* CHANGELOG.md file added to distribution * *No backwards-incompatible changes* * *No deprecations* -### 5.1.2 (2013-10-30) +## 5.0.1 (2013-03-20) -* 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 +* Internal cleanup * *No backwards-incompatible changes* * *No deprecations* -### 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 +## 5.0.0 (2013-03-20) +* Correctly parse all known CSS 3 units (including Hz and kHz). +* Output RGB colors in short (#aaa or #ababab) notation +* Be case-insensitive when parsing identifiers. * *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. +### Backwards-incompatible changes -## 4.0 +* `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) +## 4.0.0 (2013-03-19) * Support for more @-rules * Generic interface `Sabberworm\CSS\Property\AtRule`, implemented by all @-rule classes * *No deprecations* -#### Backwards-incompatible changes +### 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`). -## 3.0 - -### 3.0.0 (2013-03-06) +## 3.0.0 (2013-03-06) * Support for lenient parsing (on by default) * *No deprecations* -#### Backwards-incompatible changes +### 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. -## 2.0 - -### 2.0.0 (2013-01-29) +## 2.0.0 (2013-01-29) * Allow multiple rules of the same type per rule set -#### Backwards-incompatible changes +### 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()`; diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..686a4e31 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2011 Raphael Schweikert, https://www.sabberworm.com/ + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 23ab0809..66fb1c65 100644 --- a/README.md +++ b/README.md @@ -1,22 +1,15 @@ -PHP CSS Parser --------------- +# PHP CSS Parser -[![build status](https://api.travis-ci.org/sabberworm/PHP-CSS-Parser.svg)](https://travis-ci.org/sabberworm/PHP-CSS-Parser) [![HHVM Status](http://hhvm.h4cc.de/badge/sabberworm/php-css-parser.svg)](http://hhvm.h4cc.de/package/sabberworm/php-css-parser) +[![Build Status](https://github.com/sabberworm/PHP-CSS-Parser/workflows/CI/badge.svg?branch=master)](https://github.com/sabberworm/PHP-CSS-Parser/actions/) 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. ## Usage -### Installation using composer +### Installation using Composer -Add php-css-parser to your composer.json - -```json -{ - "require": { - "sabberworm/php-css-parser": "*" - } -} +```bash +composer require sabberworm/php-css-parser ``` ### Extraction @@ -24,14 +17,14 @@ Add php-css-parser to your composer.json To use the CSS Parser, create a new instance. The constructor takes the following form: ```php -new Sabberworm\CSS\Parser($sText); +new \Sabberworm\CSS\Parser($css); ``` To read a file, for example, you’d do the following: ```php -$oCssParser = new Sabberworm\CSS\Parser(file_get_contents('somefile.css')); -$oCssDocument = $oCssParser->parse(); +$parser = new \Sabberworm\CSS\Parser(file_get_contents('somefile.css')); +$cssDocument = $parser->parse(); ``` The resulting CSS document structure can be manipulated prior to being output. @@ -40,42 +33,46 @@ The resulting CSS document structure can be manipulated prior to being output. #### Charset -The charset option is used only if no @charset declaration is found in the CSS file. UTF-8 is the default, so you won’t have to create a settings object at all if you don’t intend to change that. +The charset option is used only if no `@charset` declaration is found in the CSS file. UTF-8 is the default, so you won’t have to create a settings object at all if you don’t intend to change that. ```php -$oSettings = Sabberworm\CSS\Settings::create()->withDefaultCharset('windows-1252'); -new Sabberworm\CSS\Parser($sText, $oSettings); +$settings = \Sabberworm\CSS\Settings::create() + ->withDefaultCharset('windows-1252'); +$parser = new \Sabberworm\CSS\Parser($css, $settings); ``` #### Strict parsing -To have the parser choke on invalid rules, supply a thusly configured Sabberworm\CSS\Settings object: +To have the parser choke on invalid rules, supply a thusly configured `\Sabberworm\CSS\Settings` object: ```php -$oCssParser = new Sabberworm\CSS\Parser(file_get_contents('somefile.css'), Sabberworm\CSS\Settings::create()->beStrict()); +$parser = new \Sabberworm\CSS\Parser( + file_get_contents('somefile.css'), + \Sabberworm\CSS\Settings::create()->beStrict() +); ``` #### Disable multibyte functions -To achieve faster parsing, you can choose to have PHP-CSS-Parser use regular string functions instead of `mb_*` functions. This should work fine in most cases, even for UTF-8 files, as all the multibyte characters are in string literals. Still it’s not recommended to use this with input you have no control over as it’s not thoroughly covered by test cases. +To achieve faster parsing, you can choose to have PHP-CSS-Parser use regular string functions instead of `mb_*` functions. This should work fine in most cases, even for UTF-8 files, as all the multibyte characters are in string literals. Still it’s not recommended using this with input you have no control over as it’s not thoroughly covered by test cases. ```php -$oSettings = Sabberworm\CSS\Settings::create()->withMultibyteSupport(false); -new Sabberworm\CSS\Parser($sText, $oSettings); +$settings = \Sabberworm\CSS\Settings::create()->withMultibyteSupport(false); +$parser = new \Sabberworm\CSS\Parser($css, $settings); ``` ### Manipulation -The resulting data structure consists mainly of five basic types: `CSSList`, `RuleSet`, `Rule`, `Selector` and `Value`. There are two additional types used: `Import` and `Charset` which you won’t use often. +The resulting data structure consists mainly of five basic types: `CSSList`, `RuleSet`, `Rule`, `Selector` and `Value`. There are two additional types used: `Import` and `Charset`, which you won’t use often. #### CSSList -`CSSList` represents a generic CSS container, most likely containing declaration blocks (rule sets with a selector) but it may also contain at-rules, charset declarations, etc. `CSSList` has the following concrete subtypes: +`CSSList` represents a generic CSS container, most likely containing declaration blocks (rule sets with a selector), but it may also contain at-rules, charset declarations, etc. `CSSList` has the following concrete subtypes: * `Document` – representing the root of a CSS file. -* `MediaQuery` – represents a subsection of a CSSList that only applies to a output device matching the contained media query. +* `MediaQuery` – represents a subsection of a `CSSList` that only applies to an output device matching the contained media query. -To access the items stored in a `CSSList` – like the document you got back when calling `$oCssParser->parse()` –, use `getContents()`, then iterate over that collection and use instanceof to check whether you’re dealing with another `CSSList`, a `RuleSet`, a `Import` or a `Charset`. +To access the items stored in a `CSSList` – like the document you got back when calling `$parser->parse()` –, use `getContents()`, then iterate over that collection and use instanceof to check whether you’re dealing with another `CSSList`, a `RuleSet`, a `Import` or a `Charset`. To append a new item (selector, media query, etc.) to an existing `CSSList`, construct it using the constructor for this class and use the `append($oItem)` method. @@ -83,12 +80,12 @@ To append a new item (selector, media query, etc.) to an existing `CSSList`, con `RuleSet` is a container for individual rules. The most common form of a rule set is one constrained by a selector. The following concrete subtypes exist: -* `AtRuleSet` – for generic at-rules which do not match the ones specifically mentioned like @import, @charset or @media. A common example for this is @font-face. -* `DeclarationBlock` – a RuleSet constrained by a `Selector`; contains an array of selector objects (comma-separated in the CSS) as well as the rules to be applied to the matching elements. +* `AtRuleSet` – for generic at-rules which do not match the ones specifically mentioned like `@import`, `@charset` or `@media`. A common example for this is `@font-face`. +* `DeclarationBlock` – a `RuleSet` constrained by a `Selector`; contains an array of selector objects (comma-separated in the CSS) as well as the rules to be applied to the matching elements. -Note: A `CSSList` can contain other `CSSList`s (and `Import`s as well as a `Charset`) while a `RuleSet` can only contain `Rule`s. +Note: A `CSSList` can contain other `CSSList`s (and `Import`s as well as a `Charset`), while a `RuleSet` can only contain `Rule`s. -If you want to manipulate a `RuleSet`, use the methods `addRule(Rule $oRule)`, `getRules()` and `removeRule($mRule)` (which accepts either a Rule instance or a rule name; optionally suffixed by a dash to remove all related rules). +If you want to manipulate a `RuleSet`, use the methods `addRule(Rule $rule)`, `getRules()` and `removeRule($rule)` (which accepts either a `Rule` instance or a rule name; optionally suffixed by a dash to remove all related rules). #### Rule @@ -118,46 +115,50 @@ There are a few convenience methods on Document to ease finding, manipulating an ## To-Do -* More convenience methods [like `selectorsWithElement($sId/Class/TagName)`, `attributesOfType($sType)`, `removeAttributesOfType($sType)`] -* Real multibyte support. Currently only multibyte charsets whose first 255 code points take up only one byte and are identical with ASCII are supported (yes, UTF-8 fits this description). +* More convenience methods (like `selectorsWithElement($sId/Class/TagName)`, `attributesOfType($type)`, `removeAttributesOfType($type)`) +* Real multibyte support. Currently, only multibyte charsets whose first 255 code points take up only one byte and are identical with ASCII are supported (yes, UTF-8 fits this description). * Named color support (using `Color` instead of an anonymous string literal) ## Use cases -### Use `Parser` to prepend an id to all selectors +### Use `Parser` to prepend an ID to all selectors ```php -$sMyId = "#my_id"; -$oParser = new Sabberworm\CSS\Parser($sText); -$oCss = $oParser->parse(); -foreach($oCss->getAllDeclarationBlocks() as $oBlock) { - foreach($oBlock->getSelectors() as $oSelector) { - //Loop over all selector parts (the comma-separated strings in a selector) and prepend the id - $oSelector->setSelector($sMyId.' '.$oSelector->getSelector()); - } +$myId = "#my_id"; +$parser = new \Sabberworm\CSS\Parser($css); +$cssDocument = $parser->parse(); +foreach ($cssDocument->getAllDeclarationBlocks() as $block) { + foreach ($block->getSelectors() as $selector) { + // Loop over all selector parts (the comma-separated strings in a + // selector) and prepend the ID. + $selector->setSelector($myId.' '.$selector->getSelector()); + } } ``` ### Shrink all absolute sizes to half ```php -$oParser = new Sabberworm\CSS\Parser($sText); -$oCss = $oParser->parse(); -foreach($oCss->getAllValues() as $mValue) { - if($mValue instanceof CSSSize && !$mValue->isRelative()) { - $mValue->setSize($mValue->getSize()/2); - } +$parser = new \Sabberworm\CSS\Parser($css); +$cssDocument = $parser->parse(); +foreach ($cssDocument->getAllValues() as $value) { + if ($value instanceof CSSSize && !$value->isRelative()) { + $value->setSize($value->getSize() / 2); + } } ``` ### Remove unwanted rules ```php -$oParser = new Sabberworm\CSS\Parser($sText); -$oCss = $oParser->parse(); -foreach($oCss->getAllRuleSets() as $oRuleSet) { - $oRuleSet->removeRule('font-'); //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('cursor'); +$parser = new \Sabberworm\CSS\Parser($css); +$cssDocument = $parser->parse(); +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('cursor'); } ``` @@ -166,26 +167,27 @@ foreach($oCss->getAllRuleSets() as $oRuleSet) { To output the entire CSS document into a variable, just use `->render()`: ```php -$oCssParser = new Sabberworm\CSS\Parser(file_get_contents('somefile.css')); -$oCssDocument = $oCssParser->parse(); -print $oCssDocument->render(); +$parser = new \Sabberworm\CSS\Parser(file_get_contents('somefile.css')); +$cssDocument = $parser->parse(); +print $cssDocument->render(); ``` -If you want to format the output, pass an instance of type `Sabberworm\CSS\OutputFormat`: +If you want to format the output, pass an instance of type `\Sabberworm\CSS\OutputFormat`: ```php -$oFormat = Sabberworm\CSS\OutputFormat::create()->indentWithSpaces(4)->setSpaceBetweenRules("\n"); -print $oCssDocument->render($oFormat); +$format = \Sabberworm\CSS\OutputFormat::create() + ->indentWithSpaces(4)->setSpaceBetweenRules("\n"); +print $cssDocument->render($format); ``` Or use one of the predefined formats: ```php -print $oCssDocument->render(Sabberworm\CSS\OutputFormat::createPretty()); -print $oCssDocument->render(Sabberworm\CSS\OutputFormat::createCompact()); +print $cssDocument->render(Sabberworm\CSS\OutputFormat::createPretty()); +print $cssDocument->render(Sabberworm\CSS\OutputFormat::createCompact()); ``` -To see what you can do with output formatting, look at the tests in `tests/Sabberworm/CSS/OutputFormatTest.php`. +To see what you can do with output formatting, look at the tests in `tests/OutputFormatTest.php`. ## Examples @@ -198,16 +200,16 @@ To see what you can do with output formatting, look at the tests in `tests/Sabbe @font-face { font-family: "CrassRoots"; - src: url("../media/cr.ttf") + src: url("../media/cr.ttf"); } html, body { - font-size: 1.6em + font-size: 1.6em; } @keyframes mymove { - from { top: 0px; } - to { top: 200px; } + from { top: 0px; } + to { top: 200px; } } ``` @@ -440,8 +442,7 @@ class Sabberworm\CSS\CSSList\Document#4 (2) { @charset "utf-8"; @font-face {font-family: "CrassRoots";src: url("../media/cr.ttf");} html, body {font-size: 1.6em;} -@keyframes mymove {from {top: 0px;} - to {top: 200px;}} +@keyframes mymove {from {top: 0px;} to {top: 200px;}} ``` ### Example 2 (Values) @@ -450,9 +451,9 @@ html, body {font-size: 1.6em;} ```css #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; } ``` @@ -611,13 +612,14 @@ class Sabberworm\CSS\CSSList\Document#4 (2) { ## Contributors/Thanks to +* [oliverklee](https://github.com/oliverklee) for lots of refactorings, code modernizations and CI integrations * [raxbg](https://github.com/raxbg) for contributions to parse `calc`, grid lines, and various bugfixes. * [westonruter](https://github.com/westonruter) for bugfixes and improvements. * [FMCorz](https://github.com/FMCorz) for many patches and suggestions, for being able to parse comments and IE hacks (in lenient mode). * [Lullabot](https://github.com/Lullabot) for a patch that allows to know the line number for each parsed token. * [ju1ius](https://github.com/ju1ius) for the specificity parsing code and the ability to expand/compact shorthand properties. * [ossinkine](https://github.com/ossinkine) for a 150 time performance boost. -* [GaryJones](https://github.com/GaryJones) for lots of input and [http://css-specificity.info/](http://css-specificity.info/). +* [GaryJones](https://github.com/GaryJones) for lots of input and [https://css-specificity.info/](https://css-specificity.info/). * [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. @@ -627,16 +629,4 @@ class Sabberworm\CSS\CSSList\Document#4 (2) { ## 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/phpunit/phpunit/phpunit`. - -## License - -PHP-CSS-Parser is freely distributable under the terms of an MIT-style license. - -Copyright (c) 2011 Raphael Schweikert, http://sabberworm.com/ - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +* Running Tests: To run all unit tests for this project, run `composer install` to install phpunit and use `./vendor/bin/phpunit`. diff --git a/bin/quickdump.php b/bin/quickdump.php new file mode 100755 index 00000000..f371ab1b --- /dev/null +++ b/bin/quickdump.php @@ -0,0 +1,23 @@ +#!/usr/bin/env php +parse(); +echo "\n" . '#### Input' . "\n\n```css\n"; +print $sSource; + +echo "\n```\n\n" . '#### Structure (`var_dump()`)' . "\n\n```php\n"; +var_dump($oDoc); + +echo "\n```\n\n" . '#### Output (`render()`)' . "\n\n```css\n"; +print $oDoc->render(); + +echo "\n```\n"; diff --git a/composer.json b/composer.json index 5e73c8aa..e192dd56 100644 --- a/composer.json +++ b/composer.json @@ -2,20 +2,68 @@ "name": "sabberworm/php-css-parser", "type": "library", "description": "Parser for CSS Files written in PHP", - "keywords": ["parser", "css", "stylesheet"], - "homepage": "http://www.sabberworm.com/blog/2010/6/10/php-css-parser", + "keywords": [ + "parser", + "css", + "stylesheet" + ], + "homepage": "https://www.sabberworm.com/blog/2010/6/10/php-css-parser", "license": "MIT", "authors": [ - {"name": "Raphael Schweikert"} + { + "name": "Raphael Schweikert" + } ], "require": { - "php": ">=5.3.2" + "php": ">=5.6.20", + "ext-iconv": "*" }, "require-dev": { - "phpunit/phpunit": "~4.8", + "phpunit/phpunit": "^4.8.36", "codacy/coverage": "^1.4" }, + "suggest": { + "ext-mbstring": "for parsing UTF-8 CSS" + }, "autoload": { - "psr-0": { "Sabberworm\\CSS": "lib/" } + "psr-4": { + "Sabberworm\\CSS\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "Sabberworm\\CSS\\Tests\\": "tests/" + } + }, + "scripts": { + "ci": [ + "@ci:static" + ], + "ci:php:fixer": "@php ./.phive/php-cs-fixer.phar --config=config/php-cs-fixer.php fix --dry-run -v --show-progress=dots bin src tests", + "ci:php:sniffer": "@php ./.phive/phpcs.phar --standard=config/phpcs.xml bin src tests", + "ci:php:stan": "@php ./.phive/phpstan.phar --configuration=config/phpstan.neon", + "ci:static": [ + "@ci:php:fixer", + "@ci:php:sniffer", + "@ci:php:stan" + ], + "fix:php": [ + "@fix:php:fixer", + "@fix:php:sniffer" + ], + "fix:php:fixer": "@php ./.phive/php-cs-fixer.phar --config=config/php-cs-fixer.php fix bin src tests", + "fix:php:sniffer": "@php ./.phive/phpcbf.phar --standard=config/phpcs.xml bin src tests", + "phpstan:baseline": "@php ./.phive/phpstan.phar --configuration=config/phpstan.neon --generate-baseline=config/phpstan-baseline.neon" + }, + "scripts-descriptions": { + "ci": "Runs all dynamic and static code checks (i.e. currently, only the static checks).", + "ci:php:fixer": "Checks the code style with PHP CS Fixer.", + "ci:php:sniffer": "Checks the code style with PHP_CodeSniffer.", + "ci:php:stan": "Checks the types with PHPStan.", + "ci:static": "Runs all static code analysis checks for the code.", + "fix:php": "Autofixes all autofixable issues in the PHP code.", + "fix:php:fixer": "Fixes autofixable issues found by PHP CS Fixer.", + "fix:php:sniffer": "Fixes autofixable issues found by PHP_CodeSniffer.", + "phpstand:baseline": "Updates the PHPStan baseline file to match the code." } } diff --git a/composer.lock b/composer.lock deleted file mode 100644 index c40bc3d3..00000000 --- a/composer.lock +++ /dev/null @@ -1,1478 +0,0 @@ -{ - "_readme": [ - "This file locks the dependencies of your project to a known state", - "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", - "This file is @generated automatically" - ], - "content-hash": "a09a0e09f5a1356a2c833ec123e09593", - "packages": [], - "packages-dev": [ - { - "name": "codacy/coverage", - "version": "1.4.2", - "source": { - "type": "git", - "url": "https://github.com/codacy/php-codacy-coverage.git", - "reference": "4988cd098db4d578681bfd3176071931ad475150" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/codacy/php-codacy-coverage/zipball/4988cd098db4d578681bfd3176071931ad475150", - "reference": "4988cd098db4d578681bfd3176071931ad475150", - "shasum": "" - }, - "require": { - "gitonomy/gitlib": ">=1.0", - "php": ">=5.3.3", - "symfony/console": "~2.5|~3.0|~4.0" - }, - "require-dev": { - "phpunit/phpunit": "~6.5" - }, - "bin": [ - "bin/codacycoverage" - ], - "type": "library", - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Jakob Pupke", - "email": "jakob.pupke@gmail.com" - } - ], - "description": "Sends PHP test coverage information to Codacy.", - "homepage": "https://github.com/codacy/php-codacy-coverage", - "time": "2018-03-22T16:43:39+00:00" - }, - { - "name": "doctrine/instantiator", - "version": "1.1.0", - "source": { - "type": "git", - "url": "https://github.com/doctrine/instantiator.git", - "reference": "185b8868aa9bf7159f5f953ed5afb2d7fcdc3bda" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/doctrine/instantiator/zipball/185b8868aa9bf7159f5f953ed5afb2d7fcdc3bda", - "reference": "185b8868aa9bf7159f5f953ed5afb2d7fcdc3bda", - "shasum": "" - }, - "require": { - "php": "^7.1" - }, - "require-dev": { - "athletic/athletic": "~0.1.8", - "ext-pdo": "*", - "ext-phar": "*", - "phpunit/phpunit": "^6.2.3", - "squizlabs/php_codesniffer": "^3.0.2" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.2.x-dev" - } - }, - "autoload": { - "psr-4": { - "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Marco Pivetta", - "email": "ocramius@gmail.com", - "homepage": "http://ocramius.github.com/" - } - ], - "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors", - "homepage": "https://github.com/doctrine/instantiator", - "keywords": [ - "constructor", - "instantiate" - ], - "time": "2017-07-22T11:58:36+00:00" - }, - { - "name": "gitonomy/gitlib", - "version": "v1.0.4", - "source": { - "type": "git", - "url": "https://github.com/gitonomy/gitlib.git", - "reference": "932a960221ae3484a3e82553b3be478e56beb68d" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/gitonomy/gitlib/zipball/932a960221ae3484a3e82553b3be478e56beb68d", - "reference": "932a960221ae3484a3e82553b3be478e56beb68d", - "shasum": "" - }, - "require": { - "php": "^5.3 || ^7.0", - "symfony/process": "^2.3|^3.0|^4.0" - }, - "require-dev": { - "phpunit/phpunit": "^4.8.35|^5.7", - "psr/log": "^1.0" - }, - "suggest": { - "psr/log": "Add some log" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0-dev" - } - }, - "autoload": { - "psr-4": { - "Gitonomy\\Git\\": "src/Gitonomy/Git/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Alexandre Salomé", - "email": "alexandre.salome@gmail.com", - "homepage": "http://alexandre-salome.fr" - }, - { - "name": "Julien DIDIER", - "email": "genzo.wm@gmail.com", - "homepage": "http://www.jdidier.net" - } - ], - "description": "Library for accessing git", - "homepage": "http://gitonomy.com", - "time": "2018-04-22T19:55:36+00:00" - }, - { - "name": "phpdocumentor/reflection-common", - "version": "1.0.1", - "source": { - "type": "git", - "url": "https://github.com/phpDocumentor/ReflectionCommon.git", - "reference": "21bdeb5f65d7ebf9f43b1b25d404f87deab5bfb6" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/21bdeb5f65d7ebf9f43b1b25d404f87deab5bfb6", - "reference": "21bdeb5f65d7ebf9f43b1b25d404f87deab5bfb6", - "shasum": "" - }, - "require": { - "php": ">=5.5" - }, - "require-dev": { - "phpunit/phpunit": "^4.6" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0.x-dev" - } - }, - "autoload": { - "psr-4": { - "phpDocumentor\\Reflection\\": [ - "src" - ] - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Jaap van Otterdijk", - "email": "opensource@ijaap.nl" - } - ], - "description": "Common reflection classes used by phpdocumentor to reflect the code structure", - "homepage": "http://www.phpdoc.org", - "keywords": [ - "FQSEN", - "phpDocumentor", - "phpdoc", - "reflection", - "static analysis" - ], - "time": "2017-09-11T18:02:19+00:00" - }, - { - "name": "phpdocumentor/reflection-docblock", - "version": "4.3.0", - "source": { - "type": "git", - "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", - "reference": "94fd0001232e47129dd3504189fa1c7225010d08" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/94fd0001232e47129dd3504189fa1c7225010d08", - "reference": "94fd0001232e47129dd3504189fa1c7225010d08", - "shasum": "" - }, - "require": { - "php": "^7.0", - "phpdocumentor/reflection-common": "^1.0.0", - "phpdocumentor/type-resolver": "^0.4.0", - "webmozart/assert": "^1.0" - }, - "require-dev": { - "doctrine/instantiator": "~1.0.5", - "mockery/mockery": "^1.0", - "phpunit/phpunit": "^6.4" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "4.x-dev" - } - }, - "autoload": { - "psr-4": { - "phpDocumentor\\Reflection\\": [ - "src/" - ] - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Mike van Riel", - "email": "me@mikevanriel.com" - } - ], - "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", - "time": "2017-11-30T07:14:17+00:00" - }, - { - "name": "phpdocumentor/type-resolver", - "version": "0.4.0", - "source": { - "type": "git", - "url": "https://github.com/phpDocumentor/TypeResolver.git", - "reference": "9c977708995954784726e25d0cd1dddf4e65b0f7" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/9c977708995954784726e25d0cd1dddf4e65b0f7", - "reference": "9c977708995954784726e25d0cd1dddf4e65b0f7", - "shasum": "" - }, - "require": { - "php": "^5.5 || ^7.0", - "phpdocumentor/reflection-common": "^1.0" - }, - "require-dev": { - "mockery/mockery": "^0.9.4", - "phpunit/phpunit": "^5.2||^4.8.24" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0.x-dev" - } - }, - "autoload": { - "psr-4": { - "phpDocumentor\\Reflection\\": [ - "src/" - ] - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Mike van Riel", - "email": "me@mikevanriel.com" - } - ], - "time": "2017-07-14T14:27:02+00:00" - }, - { - "name": "phpspec/prophecy", - "version": "1.7.6", - "source": { - "type": "git", - "url": "https://github.com/phpspec/prophecy.git", - "reference": "33a7e3c4fda54e912ff6338c48823bd5c0f0b712" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phpspec/prophecy/zipball/33a7e3c4fda54e912ff6338c48823bd5c0f0b712", - "reference": "33a7e3c4fda54e912ff6338c48823bd5c0f0b712", - "shasum": "" - }, - "require": { - "doctrine/instantiator": "^1.0.2", - "php": "^5.3|^7.0", - "phpdocumentor/reflection-docblock": "^2.0|^3.0.2|^4.0", - "sebastian/comparator": "^1.1|^2.0|^3.0", - "sebastian/recursion-context": "^1.0|^2.0|^3.0" - }, - "require-dev": { - "phpspec/phpspec": "^2.5|^3.2", - "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.5" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.7.x-dev" - } - }, - "autoload": { - "psr-0": { - "Prophecy\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Konstantin Kudryashov", - "email": "ever.zet@gmail.com", - "homepage": "http://everzet.com" - }, - { - "name": "Marcello Duarte", - "email": "marcello.duarte@gmail.com" - } - ], - "description": "Highly opinionated mocking framework for PHP 5.3+", - "homepage": "https://github.com/phpspec/prophecy", - "keywords": [ - "Double", - "Dummy", - "fake", - "mock", - "spy", - "stub" - ], - "time": "2018-04-18T13:57:24+00:00" - }, - { - "name": "phpunit/php-code-coverage", - "version": "2.2.4", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "eabf68b476ac7d0f73793aada060f1c1a9bf8979" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/eabf68b476ac7d0f73793aada060f1c1a9bf8979", - "reference": "eabf68b476ac7d0f73793aada060f1c1a9bf8979", - "shasum": "" - }, - "require": { - "php": ">=5.3.3", - "phpunit/php-file-iterator": "~1.3", - "phpunit/php-text-template": "~1.2", - "phpunit/php-token-stream": "~1.3", - "sebastian/environment": "^1.3.2", - "sebastian/version": "~1.0" - }, - "require-dev": { - "ext-xdebug": ">=2.1.4", - "phpunit/phpunit": "~4" - }, - "suggest": { - "ext-dom": "*", - "ext-xdebug": ">=2.2.1", - "ext-xmlwriter": "*" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.2.x-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sb@sebastian-bergmann.de", - "role": "lead" - } - ], - "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", - "homepage": "https://github.com/sebastianbergmann/php-code-coverage", - "keywords": [ - "coverage", - "testing", - "xunit" - ], - "time": "2015-10-06T15:47:00+00:00" - }, - { - "name": "phpunit/php-file-iterator", - "version": "1.4.5", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/php-file-iterator.git", - "reference": "730b01bc3e867237eaac355e06a36b85dd93a8b4" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/730b01bc3e867237eaac355e06a36b85dd93a8b4", - "reference": "730b01bc3e867237eaac355e06a36b85dd93a8b4", - "shasum": "" - }, - "require": { - "php": ">=5.3.3" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.4.x-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sb@sebastian-bergmann.de", - "role": "lead" - } - ], - "description": "FilterIterator implementation that filters files based on a list of suffixes.", - "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", - "keywords": [ - "filesystem", - "iterator" - ], - "time": "2017-11-27T13:52:08+00:00" - }, - { - "name": "phpunit/php-text-template", - "version": "1.2.1", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/php-text-template.git", - "reference": "31f8b717e51d9a2afca6c9f046f5d69fc27c8686" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/31f8b717e51d9a2afca6c9f046f5d69fc27c8686", - "reference": "31f8b717e51d9a2afca6c9f046f5d69fc27c8686", - "shasum": "" - }, - "require": { - "php": ">=5.3.3" - }, - "type": "library", - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" - } - ], - "description": "Simple template engine.", - "homepage": "https://github.com/sebastianbergmann/php-text-template/", - "keywords": [ - "template" - ], - "time": "2015-06-21T13:50:34+00:00" - }, - { - "name": "phpunit/php-timer", - "version": "1.0.9", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/php-timer.git", - "reference": "3dcf38ca72b158baf0bc245e9184d3fdffa9c46f" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/3dcf38ca72b158baf0bc245e9184d3fdffa9c46f", - "reference": "3dcf38ca72b158baf0bc245e9184d3fdffa9c46f", - "shasum": "" - }, - "require": { - "php": "^5.3.3 || ^7.0" - }, - "require-dev": { - "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sb@sebastian-bergmann.de", - "role": "lead" - } - ], - "description": "Utility class for timing", - "homepage": "https://github.com/sebastianbergmann/php-timer/", - "keywords": [ - "timer" - ], - "time": "2017-02-26T11:10:40+00:00" - }, - { - "name": "phpunit/php-token-stream", - "version": "1.4.12", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/php-token-stream.git", - "reference": "1ce90ba27c42e4e44e6d8458241466380b51fa16" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/1ce90ba27c42e4e44e6d8458241466380b51fa16", - "reference": "1ce90ba27c42e4e44e6d8458241466380b51fa16", - "shasum": "" - }, - "require": { - "ext-tokenizer": "*", - "php": ">=5.3.3" - }, - "require-dev": { - "phpunit/phpunit": "~4.2" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.4-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - } - ], - "description": "Wrapper around PHP's tokenizer extension.", - "homepage": "https://github.com/sebastianbergmann/php-token-stream/", - "keywords": [ - "tokenizer" - ], - "time": "2017-12-04T08:55:13+00:00" - }, - { - "name": "phpunit/phpunit", - "version": "4.8.36", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "46023de9a91eec7dfb06cc56cb4e260017298517" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/46023de9a91eec7dfb06cc56cb4e260017298517", - "reference": "46023de9a91eec7dfb06cc56cb4e260017298517", - "shasum": "" - }, - "require": { - "ext-dom": "*", - "ext-json": "*", - "ext-pcre": "*", - "ext-reflection": "*", - "ext-spl": "*", - "php": ">=5.3.3", - "phpspec/prophecy": "^1.3.1", - "phpunit/php-code-coverage": "~2.1", - "phpunit/php-file-iterator": "~1.4", - "phpunit/php-text-template": "~1.2", - "phpunit/php-timer": "^1.0.6", - "phpunit/phpunit-mock-objects": "~2.3", - "sebastian/comparator": "~1.2.2", - "sebastian/diff": "~1.2", - "sebastian/environment": "~1.3", - "sebastian/exporter": "~1.2", - "sebastian/global-state": "~1.0", - "sebastian/version": "~1.0", - "symfony/yaml": "~2.1|~3.0" - }, - "suggest": { - "phpunit/php-invoker": "~1.1" - }, - "bin": [ - "phpunit" - ], - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "4.8.x-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" - } - ], - "description": "The PHP Unit Testing framework.", - "homepage": "https://phpunit.de/", - "keywords": [ - "phpunit", - "testing", - "xunit" - ], - "time": "2017-06-21T08:07:12+00:00" - }, - { - "name": "phpunit/phpunit-mock-objects", - "version": "2.3.8", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/phpunit-mock-objects.git", - "reference": "ac8e7a3db35738d56ee9a76e78a4e03d97628983" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit-mock-objects/zipball/ac8e7a3db35738d56ee9a76e78a4e03d97628983", - "reference": "ac8e7a3db35738d56ee9a76e78a4e03d97628983", - "shasum": "" - }, - "require": { - "doctrine/instantiator": "^1.0.2", - "php": ">=5.3.3", - "phpunit/php-text-template": "~1.2", - "sebastian/exporter": "~1.2" - }, - "require-dev": { - "phpunit/phpunit": "~4.4" - }, - "suggest": { - "ext-soap": "*" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.3.x-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sb@sebastian-bergmann.de", - "role": "lead" - } - ], - "description": "Mock Object library for PHPUnit", - "homepage": "https://github.com/sebastianbergmann/phpunit-mock-objects/", - "keywords": [ - "mock", - "xunit" - ], - "time": "2015-10-02T06:51:40+00:00" - }, - { - "name": "sebastian/comparator", - "version": "1.2.4", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/comparator.git", - "reference": "2b7424b55f5047b47ac6e5ccb20b2aea4011d9be" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/2b7424b55f5047b47ac6e5ccb20b2aea4011d9be", - "reference": "2b7424b55f5047b47ac6e5ccb20b2aea4011d9be", - "shasum": "" - }, - "require": { - "php": ">=5.3.3", - "sebastian/diff": "~1.2", - "sebastian/exporter": "~1.2 || ~2.0" - }, - "require-dev": { - "phpunit/phpunit": "~4.4" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.2.x-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Jeff Welch", - "email": "whatthejeff@gmail.com" - }, - { - "name": "Volker Dusch", - "email": "github@wallbash.com" - }, - { - "name": "Bernhard Schussek", - "email": "bschussek@2bepublished.at" - }, - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - } - ], - "description": "Provides the functionality to compare PHP values for equality", - "homepage": "http://www.github.com/sebastianbergmann/comparator", - "keywords": [ - "comparator", - "compare", - "equality" - ], - "time": "2017-01-29T09:50:25+00:00" - }, - { - "name": "sebastian/diff", - "version": "1.4.3", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/diff.git", - "reference": "7f066a26a962dbe58ddea9f72a4e82874a3975a4" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/7f066a26a962dbe58ddea9f72a4e82874a3975a4", - "reference": "7f066a26a962dbe58ddea9f72a4e82874a3975a4", - "shasum": "" - }, - "require": { - "php": "^5.3.3 || ^7.0" - }, - "require-dev": { - "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.4-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Kore Nordmann", - "email": "mail@kore-nordmann.de" - }, - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - } - ], - "description": "Diff implementation", - "homepage": "https://github.com/sebastianbergmann/diff", - "keywords": [ - "diff" - ], - "time": "2017-05-22T07:24:03+00:00" - }, - { - "name": "sebastian/environment", - "version": "1.3.8", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/environment.git", - "reference": "be2c607e43ce4c89ecd60e75c6a85c126e754aea" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/be2c607e43ce4c89ecd60e75c6a85c126e754aea", - "reference": "be2c607e43ce4c89ecd60e75c6a85c126e754aea", - "shasum": "" - }, - "require": { - "php": "^5.3.3 || ^7.0" - }, - "require-dev": { - "phpunit/phpunit": "^4.8 || ^5.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.3.x-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - } - ], - "description": "Provides functionality to handle HHVM/PHP environments", - "homepage": "http://www.github.com/sebastianbergmann/environment", - "keywords": [ - "Xdebug", - "environment", - "hhvm" - ], - "time": "2016-08-18T05:49:44+00:00" - }, - { - "name": "sebastian/exporter", - "version": "1.2.2", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/exporter.git", - "reference": "42c4c2eec485ee3e159ec9884f95b431287edde4" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/42c4c2eec485ee3e159ec9884f95b431287edde4", - "reference": "42c4c2eec485ee3e159ec9884f95b431287edde4", - "shasum": "" - }, - "require": { - "php": ">=5.3.3", - "sebastian/recursion-context": "~1.0" - }, - "require-dev": { - "ext-mbstring": "*", - "phpunit/phpunit": "~4.4" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.3.x-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Jeff Welch", - "email": "whatthejeff@gmail.com" - }, - { - "name": "Volker Dusch", - "email": "github@wallbash.com" - }, - { - "name": "Bernhard Schussek", - "email": "bschussek@2bepublished.at" - }, - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - }, - { - "name": "Adam Harvey", - "email": "aharvey@php.net" - } - ], - "description": "Provides the functionality to export PHP variables for visualization", - "homepage": "http://www.github.com/sebastianbergmann/exporter", - "keywords": [ - "export", - "exporter" - ], - "time": "2016-06-17T09:04:28+00:00" - }, - { - "name": "sebastian/global-state", - "version": "1.1.1", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/global-state.git", - "reference": "bc37d50fea7d017d3d340f230811c9f1d7280af4" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/bc37d50fea7d017d3d340f230811c9f1d7280af4", - "reference": "bc37d50fea7d017d3d340f230811c9f1d7280af4", - "shasum": "" - }, - "require": { - "php": ">=5.3.3" - }, - "require-dev": { - "phpunit/phpunit": "~4.2" - }, - "suggest": { - "ext-uopz": "*" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - } - ], - "description": "Snapshotting of global state", - "homepage": "http://www.github.com/sebastianbergmann/global-state", - "keywords": [ - "global state" - ], - "time": "2015-10-12T03:26:01+00:00" - }, - { - "name": "sebastian/recursion-context", - "version": "1.0.5", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/recursion-context.git", - "reference": "b19cc3298482a335a95f3016d2f8a6950f0fbcd7" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/b19cc3298482a335a95f3016d2f8a6950f0fbcd7", - "reference": "b19cc3298482a335a95f3016d2f8a6950f0fbcd7", - "shasum": "" - }, - "require": { - "php": ">=5.3.3" - }, - "require-dev": { - "phpunit/phpunit": "~4.4" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0.x-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Jeff Welch", - "email": "whatthejeff@gmail.com" - }, - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - }, - { - "name": "Adam Harvey", - "email": "aharvey@php.net" - } - ], - "description": "Provides functionality to recursively process PHP variables", - "homepage": "http://www.github.com/sebastianbergmann/recursion-context", - "time": "2016-10-03T07:41:43+00:00" - }, - { - "name": "sebastian/version", - "version": "1.0.6", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/version.git", - "reference": "58b3a85e7999757d6ad81c787a1fbf5ff6c628c6" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/58b3a85e7999757d6ad81c787a1fbf5ff6c628c6", - "reference": "58b3a85e7999757d6ad81c787a1fbf5ff6c628c6", - "shasum": "" - }, - "type": "library", - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" - } - ], - "description": "Library that helps with managing the version number of Git-hosted PHP projects", - "homepage": "https://github.com/sebastianbergmann/version", - "time": "2015-06-21T13:59:46+00:00" - }, - { - "name": "symfony/console", - "version": "v4.1.6", - "source": { - "type": "git", - "url": "https://github.com/symfony/console.git", - "reference": "dc7122fe5f6113cfaba3b3de575d31112c9aa60b" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/dc7122fe5f6113cfaba3b3de575d31112c9aa60b", - "reference": "dc7122fe5f6113cfaba3b3de575d31112c9aa60b", - "shasum": "" - }, - "require": { - "php": "^7.1.3", - "symfony/polyfill-mbstring": "~1.0" - }, - "conflict": { - "symfony/dependency-injection": "<3.4", - "symfony/process": "<3.3" - }, - "require-dev": { - "psr/log": "~1.0", - "symfony/config": "~3.4|~4.0", - "symfony/dependency-injection": "~3.4|~4.0", - "symfony/event-dispatcher": "~3.4|~4.0", - "symfony/lock": "~3.4|~4.0", - "symfony/process": "~3.4|~4.0" - }, - "suggest": { - "psr/log-implementation": "For using the console logger", - "symfony/event-dispatcher": "", - "symfony/lock": "", - "symfony/process": "" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "4.1-dev" - } - }, - "autoload": { - "psr-4": { - "Symfony\\Component\\Console\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony Console Component", - "homepage": "https://symfony.com", - "time": "2018-10-03T08:15:46+00:00" - }, - { - "name": "symfony/polyfill-ctype", - "version": "v1.8.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/polyfill-ctype.git", - "reference": "7cc359f1b7b80fc25ed7796be7d96adc9b354bae" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/7cc359f1b7b80fc25ed7796be7d96adc9b354bae", - "reference": "7cc359f1b7b80fc25ed7796be7d96adc9b354bae", - "shasum": "" - }, - "require": { - "php": ">=5.3.3" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.8-dev" - } - }, - "autoload": { - "psr-4": { - "Symfony\\Polyfill\\Ctype\\": "" - }, - "files": [ - "bootstrap.php" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - }, - { - "name": "Gert de Pagter", - "email": "BackEndTea@gmail.com" - } - ], - "description": "Symfony polyfill for ctype functions", - "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "ctype", - "polyfill", - "portable" - ], - "time": "2018-04-30T19:57:29+00:00" - }, - { - "name": "symfony/polyfill-mbstring", - "version": "v1.9.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "d0cd638f4634c16d8df4508e847f14e9e43168b8" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/d0cd638f4634c16d8df4508e847f14e9e43168b8", - "reference": "d0cd638f4634c16d8df4508e847f14e9e43168b8", - "shasum": "" - }, - "require": { - "php": ">=5.3.3" - }, - "suggest": { - "ext-mbstring": "For best performance" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.9-dev" - } - }, - "autoload": { - "psr-4": { - "Symfony\\Polyfill\\Mbstring\\": "" - }, - "files": [ - "bootstrap.php" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony polyfill for the Mbstring extension", - "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "mbstring", - "polyfill", - "portable", - "shim" - ], - "time": "2018-08-06T14:22:27+00:00" - }, - { - "name": "symfony/process", - "version": "v4.1.6", - "source": { - "type": "git", - "url": "https://github.com/symfony/process.git", - "reference": "ee33c0322a8fee0855afcc11fff81e6b1011b529" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/ee33c0322a8fee0855afcc11fff81e6b1011b529", - "reference": "ee33c0322a8fee0855afcc11fff81e6b1011b529", - "shasum": "" - }, - "require": { - "php": "^7.1.3" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "4.1-dev" - } - }, - "autoload": { - "psr-4": { - "Symfony\\Component\\Process\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony Process Component", - "homepage": "https://symfony.com", - "time": "2018-10-02T12:40:59+00:00" - }, - { - "name": "symfony/yaml", - "version": "v3.4.12", - "source": { - "type": "git", - "url": "https://github.com/symfony/yaml.git", - "reference": "c5010cc1692ce1fa328b1fb666961eb3d4a85bb0" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/c5010cc1692ce1fa328b1fb666961eb3d4a85bb0", - "reference": "c5010cc1692ce1fa328b1fb666961eb3d4a85bb0", - "shasum": "" - }, - "require": { - "php": "^5.5.9|>=7.0.8", - "symfony/polyfill-ctype": "~1.8" - }, - "conflict": { - "symfony/console": "<3.4" - }, - "require-dev": { - "symfony/console": "~3.4|~4.0" - }, - "suggest": { - "symfony/console": "For validating YAML files using the lint command" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.4-dev" - } - }, - "autoload": { - "psr-4": { - "Symfony\\Component\\Yaml\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony Yaml Component", - "homepage": "https://symfony.com", - "time": "2018-05-03T23:18:14+00:00" - }, - { - "name": "webmozart/assert", - "version": "1.3.0", - "source": { - "type": "git", - "url": "https://github.com/webmozart/assert.git", - "reference": "0df1908962e7a3071564e857d86874dad1ef204a" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/webmozart/assert/zipball/0df1908962e7a3071564e857d86874dad1ef204a", - "reference": "0df1908962e7a3071564e857d86874dad1ef204a", - "shasum": "" - }, - "require": { - "php": "^5.3.3 || ^7.0" - }, - "require-dev": { - "phpunit/phpunit": "^4.6", - "sebastian/version": "^1.0.1" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.3-dev" - } - }, - "autoload": { - "psr-4": { - "Webmozart\\Assert\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Bernhard Schussek", - "email": "bschussek@gmail.com" - } - ], - "description": "Assertions to validate method input/output with nice error messages.", - "keywords": [ - "assert", - "check", - "validate" - ], - "time": "2018-01-29T19:49:41+00:00" - } - ], - "aliases": [], - "minimum-stability": "stable", - "stability-flags": [], - "prefer-stable": false, - "prefer-lowest": false, - "platform": { - "php": ">=5.3.2" - }, - "platform-dev": [] -} diff --git a/Doxyfile b/config/Doxyfile similarity index 100% rename from Doxyfile rename to config/Doxyfile diff --git a/config/php-cs-fixer.php b/config/php-cs-fixer.php new file mode 100644 index 00000000..12f8081a --- /dev/null +++ b/config/php-cs-fixer.php @@ -0,0 +1,27 @@ +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']], + + 'php_unit_construct' => true, + 'php_unit_dedicate_assert' => ['target' => '5.0'], + 'php_unit_expectation' => ['target' => '5.6'], + 'php_unit_fqcn_annotation' => true, + 'php_unit_method_casing' => true, + 'php_unit_mock' => ['target' => '5.5'], + 'php_unit_mock_short_will_return' => true, + 'php_unit_namespaced' => ['target' => '5.7'], + 'php_unit_set_up_tear_down_visibility' => true, + 'php_unit_test_annotation' => ['style' => 'annotation'], + 'php_unit_test_case_static_method_calls' => ['call_type' => 'self'], + ] + ); diff --git a/config/phpcs.xml b/config/phpcs.xml new file mode 100644 index 00000000..14473bb2 --- /dev/null +++ b/config/phpcs.xml @@ -0,0 +1,17 @@ + + + + This standard requires PHP_CodeSniffer >= 3.6.0. + + + + + + + + + + + + + diff --git a/config/phpstan-baseline.neon b/config/phpstan-baseline.neon new file mode 100644 index 00000000..b730548c --- /dev/null +++ b/config/phpstan-baseline.neon @@ -0,0 +1,22 @@ +parameters: + ignoreErrors: + - + message: "#^Call to an undefined method Sabberworm\\\\CSS\\\\OutputFormat\\:\\:setIndentation\\(\\)\\.$#" + count: 2 + path: ../src/OutputFormat.php + + - + message: "#^Class Sabberworm\\\\CSS\\\\Value\\\\Size constructor invoked with 5 parameters, 1\\-4 required\\.$#" + count: 2 + path: ../src/RuleSet/DeclarationBlock.php + + - + message: "#^Variable \\$oRule might not be defined\\.$#" + count: 2 + path: ../src/RuleSet/DeclarationBlock.php + + - + message: "#^Variable \\$oVal might not be defined\\.$#" + count: 1 + path: ../src/Value/CalcFunction.php + diff --git a/config/phpstan.neon b/config/phpstan.neon new file mode 100644 index 00000000..3d7611a6 --- /dev/null +++ b/config/phpstan.neon @@ -0,0 +1,18 @@ +includes: + - phpstan-baseline.neon + +parameters: + parallel: + # Don't be overly greedy on machines with more CPU's to be a good neighbor especially on CI + maximumNumberOfProcesses: 5 + + level: 1 + + scanDirectories: + - %currentWorkingDirectory%/bin/ + - %currentWorkingDirectory%/src/ + - %currentWorkingDirectory%/tests/ + paths: + - %currentWorkingDirectory%/bin/ + - %currentWorkingDirectory%/src/ + - %currentWorkingDirectory%/tests/ diff --git a/lib/Sabberworm/CSS/CSSList/AtRuleBlockList.php b/lib/Sabberworm/CSS/CSSList/AtRuleBlockList.php deleted file mode 100644 index 24e79f02..00000000 --- a/lib/Sabberworm/CSS/CSSList/AtRuleBlockList.php +++ /dev/null @@ -1,50 +0,0 @@ -sType = $sType; - $this->sArgs = $sArgs; - } - - public function atRuleName() { - return $this->sType; - } - - public function atRuleArgs() { - return $this->sArgs; - } - - public function __toString() { - return $this->render(new \Sabberworm\CSS\OutputFormat()); - } - - public function render(\Sabberworm\CSS\OutputFormat $oOutputFormat) { - $sArgs = $this->sArgs; - if($sArgs) { - $sArgs = ' ' . $sArgs; - } - $sResult = $oOutputFormat->sBeforeAtRuleBlock; - $sResult .= "@{$this->sType}$sArgs{$oOutputFormat->spaceBeforeOpeningBrace()}{"; - $sResult .= parent::render($oOutputFormat); - $sResult .= '}'; - $sResult .= $oOutputFormat->sAfterAtRuleBlock; - return $sResult; - } - - public function isRootList() { - return false; - } - -} \ No newline at end of file diff --git a/lib/Sabberworm/CSS/CSSList/CSSBlockList.php b/lib/Sabberworm/CSS/CSSList/CSSBlockList.php deleted file mode 100644 index 17c68142..00000000 --- a/lib/Sabberworm/CSS/CSSList/CSSBlockList.php +++ /dev/null @@ -1,82 +0,0 @@ -aContents as $mContent) { - if ($mContent instanceof DeclarationBlock) { - $aResult[] = $mContent; - } else if ($mContent instanceof CSSBlockList) { - $mContent->allDeclarationBlocks($aResult); - } - } - } - - protected function allRuleSets(&$aResult) { - foreach ($this->aContents as $mContent) { - if ($mContent instanceof RuleSet) { - $aResult[] = $mContent; - } else if ($mContent instanceof CSSBlockList) { - $mContent->allRuleSets($aResult); - } - } - } - - protected function allValues($oElement, &$aResult, $sSearchString = null, $bSearchInFunctionArguments = false) { - if ($oElement instanceof CSSBlockList) { - foreach ($oElement->getContents() as $oContent) { - $this->allValues($oContent, $aResult, $sSearchString, $bSearchInFunctionArguments); - } - } else if ($oElement instanceof RuleSet) { - foreach ($oElement->getRules($sSearchString) as $oRule) { - $this->allValues($oRule, $aResult, $sSearchString, $bSearchInFunctionArguments); - } - } else if ($oElement instanceof Rule) { - $this->allValues($oElement->getValue(), $aResult, $sSearchString, $bSearchInFunctionArguments); - } else if ($oElement instanceof ValueList) { - if ($bSearchInFunctionArguments || !($oElement instanceof CSSFunction)) { - foreach ($oElement->getListComponents() as $mComponent) { - $this->allValues($mComponent, $aResult, $sSearchString, $bSearchInFunctionArguments); - } - } - } else { - //Non-List Value or CSSString (CSS identifier) - $aResult[] = $oElement; - } - } - - protected function allSelectors(&$aResult, $sSpecificitySearch = null) { - $aDeclarationBlocks = array(); - $this->allDeclarationBlocks($aDeclarationBlocks); - foreach ($aDeclarationBlocks as $oBlock) { - foreach ($oBlock->getSelectors() as $oSelector) { - if ($sSpecificitySearch === null) { - $aResult[] = $oSelector; - } else { - $sComparison = "\$bRes = {$oSelector->getSpecificity()} $sSpecificitySearch;"; - eval($sComparison); - if ($bRes) { - $aResult[] = $oSelector; - } - } - } - } - } - -} diff --git a/lib/Sabberworm/CSS/CSSList/CSSList.php b/lib/Sabberworm/CSS/CSSList/CSSList.php deleted file mode 100644 index d883df82..00000000 --- a/lib/Sabberworm/CSS/CSSList/CSSList.php +++ /dev/null @@ -1,348 +0,0 @@ -aComments = array(); - $this->aContents = array(); - $this->iLineNo = $iLineNo; - } - - public static function parseList(ParserState $oParserState, CSSList $oList) { - $bIsRoot = $oList instanceof Document; - if(is_string($oParserState)) { - $oParserState = new ParserState($oParserState); - } - $bLenientParsing = $oParserState->getSettings()->bLenientParsing; - while(!$oParserState->isEnd()) { - $comments = $oParserState->consumeWhiteSpace(); - $oListItem = null; - if($bLenientParsing) { - try { - $oListItem = self::parseListItem($oParserState, $oList); - } catch (UnexpectedTokenException $e) { - $oListItem = false; - } - } else { - $oListItem = self::parseListItem($oParserState, $oList); - } - if($oListItem === null) { - // List parsing finished - return; - } - if($oListItem) { - $oListItem->setComments($comments); - $oList->append($oListItem); - } - $oParserState->consumeWhiteSpace(); - } - if(!$bIsRoot && !$bLenientParsing) { - throw new SourceException("Unexpected end of document", $oParserState->currentLine()); - } - } - - private static function parseListItem(ParserState $oParserState, CSSList $oList) { - $bIsRoot = $oList instanceof Document; - if ($oParserState->comes('@')) { - $oAtRule = self::parseAtRule($oParserState); - if($oAtRule instanceof Charset) { - if(!$bIsRoot) { - throw new UnexpectedTokenException('@charset may only occur in root document', '', 'custom', $oParserState->currentLine()); - } - if(count($oList->getContents()) > 0) { - throw new UnexpectedTokenException('@charset must be the first parseable token in a document', '', 'custom', $oParserState->currentLine()); - } - $oParserState->setCharset($oAtRule->getCharset()->getString()); - } - return $oAtRule; - } else if ($oParserState->comes('}')) { - $oParserState->consume('}'); - if ($bIsRoot) { - if ($oParserState->getSettings()->bLenientParsing) { - while ($oParserState->comes('}')) $oParserState->consume('}'); - return DeclarationBlock::parse($oParserState); - } else { - throw new SourceException("Unopened {", $oParserState->currentLine()); - } - } else { - return null; - } - } else { - return DeclarationBlock::parse($oParserState); - } - } - - private static function parseAtRule(ParserState $oParserState) { - $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 = $oParserState->consumeUntil(';'); - } - $oParserState->consume(';'); - return new Import($oLocation, $sMediaQuery, $iIdentifierLineNum); - } else if ($sIdentifier === 'charset') { - $sCharset = CSSString::parse($oParserState); - $oParserState->consumeWhiteSpace(); - $oParserState->consume(';'); - return new Charset($sCharset, $iIdentifierLineNum); - } else if (self::identifierIs($sIdentifier, 'keyframes')) { - $oResult = new KeyFrame($iIdentifierLineNum); - $oResult->setVendorKeyFrame($sIdentifier); - $oResult->setAnimationName(trim($oParserState->consumeUntil('{', false, true))); - CSSList::parseList($oParserState, $oResult); - return $oResult; - } else if ($sIdentifier === 'namespace') { - $sPrefix = null; - $mUrl = Value::parsePrimitiveValue($oParserState); - if (!$oParserState->comes(';')) { - $sPrefix = $mUrl; - $mUrl = Value::parsePrimitiveValue($oParserState); - } - $oParserState->consume(';'); - if ($sPrefix !== null && !is_string($sPrefix)) { - throw new UnexpectedTokenException('Wrong namespace prefix', $sPrefix, 'custom', $iIdentifierLineNum); - } - if (!($mUrl instanceof CSSString || $mUrl instanceof URL)) { - throw new UnexpectedTokenException('Wrong namespace url of invalid type', $mUrl, 'custom', $iIdentifierLineNum); - } - return new CSSNamespace($mUrl, $sPrefix, $iIdentifierLineNum); - } 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) { - return NULL; - } else { - throw new SourceException("Unmatched brace count in media query", $oParserState->currentLine()); - } - } - $bUseRuleSet = true; - foreach(explode('/', AtRule::BLOCK_RULES) as $sBlockRuleName) { - if(self::identifierIs($sIdentifier, $sBlockRuleName)) { - $bUseRuleSet = false; - break; - } - } - if($bUseRuleSet) { - $oAtRule = new AtRuleSet($sIdentifier, $sArgs, $iIdentifierLineNum); - RuleSet::parseRuleSet($oParserState, $oAtRule); - } else { - $oAtRule = new AtRuleBlockList($sIdentifier, $sArgs, $iIdentifierLineNum); - CSSList::parseList($oParserState, $oAtRule); - } - return $oAtRule; - } - } - - /** - * 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. - */ - private static function identifierIs($sIdentifier, $sMatch) { - return (strcasecmp($sIdentifier, $sMatch) === 0) - ?: preg_match("/^(-\\w+-)?$sMatch$/i", $sIdentifier) === 1; - } - - - /** - * @return int - */ - public function getLineNo() { - return $this->iLineNo; - } - - /** - * Prepend item to list of contents. - * - * @param object $oItem Item. - */ - public function prepend($oItem) { - array_unshift($this->aContents, $oItem); - } - - /** - * Append item to list of contents. - * - * @param object $oItem Item. - */ - public function append($oItem) { - $this->aContents[] = $oItem; - } - - /** - * Splice the list of contents. - * - * @param int $iOffset Offset. - * @param int $iLength Length. Optional. - * @param RuleSet[] $mReplacement Replacement. Optional. - */ - public function splice($iOffset, $iLength = null, $mReplacement = null) { - array_splice($this->aContents, $iOffset, $iLength, $mReplacement); - } - - /** - * 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) - * @return bool Whether the item was removed. - */ - public function remove($oItemToRemove) { - $iKey = array_search($oItemToRemove, $this->aContents, true); - if ($iKey !== false) { - unset($this->aContents[$iKey]); - return true; - } - return false; - } - - /** - * Replaces 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) - */ - public function replace($oOldItem, $oNewItem) { - $iKey = array_search($oOldItem, $this->aContents, true); - if ($iKey !== false) { - array_splice($this->aContents, $iKey, 1, $oNewItem); - return true; - } - return false; - } - - /** - * Set the contents. - * @param array $aContents Objects to set as content. - */ - public function setContents(array $aContents) { - $this->aContents = array(); - foreach ($aContents as $content) { - $this->append($content); - } - } - - /** - * Removes a declaration block from the CSS list if it matches all given selectors. - * @param array|string $mSelector The selectors to match. - * @param boolean $bRemoveAll Whether to stop at the first declaration block found or remove all blocks - */ - public function removeDeclarationBlockBySelector($mSelector, $bRemoveAll = false) { - if ($mSelector instanceof DeclarationBlock) { - $mSelector = $mSelector->getSelectors(); - } - if (!is_array($mSelector)) { - $mSelector = explode(',', $mSelector); - } - foreach ($mSelector as $iKey => &$mSel) { - if (!($mSel instanceof Selector)) { - $mSel = new Selector($mSel); - } - } - foreach ($this->aContents as $iKey => $mItem) { - if (!($mItem instanceof DeclarationBlock)) { - continue; - } - if ($mItem->getSelectors() == $mSelector) { - unset($this->aContents[$iKey]); - if (!$bRemoveAll) { - return; - } - } - } - } - - public function __toString() { - return $this->render(new \Sabberworm\CSS\OutputFormat()); - } - - public function render(\Sabberworm\CSS\OutputFormat $oOutputFormat) { - $sResult = ''; - $bIsFirst = true; - $oNextLevel = $oOutputFormat; - if(!$this->isRootList()) { - $oNextLevel = $oOutputFormat->nextLevel(); - } - foreach ($this->aContents as $oContent) { - $sRendered = $oOutputFormat->safely(function() use ($oNextLevel, $oContent) { - return $oContent->render($oNextLevel); - }); - if($sRendered === null) { - continue; - } - if($bIsFirst) { - $bIsFirst = false; - $sResult .= $oNextLevel->spaceBeforeBlocks(); - } else { - $sResult .= $oNextLevel->spaceBetweenBlocks(); - } - $sResult .= $sRendered; - } - - if(!$bIsFirst) { - // Had some output - $sResult .= $oOutputFormat->spaceAfterBlocks(); - } - - return $sResult; - } - - /** - * Return true if the list can not be further outdented. Only important when rendering. - */ - public abstract function isRootList(); - - public function getContents() { - return $this->aContents; - } - - /** - * @param array $aComments Array of comments. - */ - public function addComments(array $aComments) { - $this->aComments = array_merge($this->aComments, $aComments); - } - - /** - * @return array - */ - public function getComments() { - return $this->aComments; - } - - /** - * @param array $aComments Array containing Comment objects. - */ - public function setComments(array $aComments) { - $this->aComments = $aComments; - } - -} diff --git a/lib/Sabberworm/CSS/CSSList/Document.php b/lib/Sabberworm/CSS/CSSList/Document.php deleted file mode 100644 index 873df755..00000000 --- a/lib/Sabberworm/CSS/CSSList/Document.php +++ /dev/null @@ -1,113 +0,0 @@ -currentLine()); - CSSList::parseList($oParserState, $oDocument); - return $oDocument; - } - - /** - * Gets all DeclarationBlock objects recursively. - */ - public function getAllDeclarationBlocks() { - $aResult = array(); - $this->allDeclarationBlocks($aResult); - return $aResult; - } - - /** - * @deprecated use getAllDeclarationBlocks() - */ - public function getAllSelectors() { - return $this->getAllDeclarationBlocks(); - } - - /** - * Returns all RuleSet objects found recursively in the tree. - */ - public function getAllRuleSets() { - $aResult = array(); - $this->allRuleSets($aResult); - return $aResult; - } - - /** - * Returns all Value objects found recursively in the tree. - * @param (object|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 (@see{RuleSet->getRules()}). - * @param (bool) $bSearchInFunctionArguments whether to also return Value objects used as Function arguments. - */ - public function getAllValues($mElement = null, $bSearchInFunctionArguments = false) { - $sSearchString = null; - if ($mElement === null) { - $mElement = $this; - } else if (is_string($mElement)) { - $sSearchString = $mElement; - $mElement = $this; - } - $aResult = array(); - $this->allValues($mElement, $aResult, $sSearchString, $bSearchInFunctionArguments); - return $aResult; - } - - /** - * Returns all Selector objects found recursively in the tree. - * 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 $sSpecificitySearch An optional filter by specificity. May contain a comparison operator and a number or just a number (defaults to "=="). - * @example getSelectorsBySpecificity('>= 100') - */ - public function getSelectorsBySpecificity($sSpecificitySearch = null) { - if (is_numeric($sSpecificitySearch) || is_numeric($sSpecificitySearch[0])) { - $sSpecificitySearch = "== $sSpecificitySearch"; - } - $aResult = array(); - $this->allSelectors($aResult, $sSpecificitySearch); - return $aResult; - } - - /** - * Expands all shorthand properties to their long value - */ - public function expandShorthands() { - foreach ($this->getAllDeclarationBlocks() as $oDeclaration) { - $oDeclaration->expandShorthands(); - } - } - - /** - * Create shorthands properties whenever possible - */ - public function createShorthands() { - foreach ($this->getAllDeclarationBlocks() as $oDeclaration) { - $oDeclaration->createShorthands(); - } - } - - // Override render() to make format argument optional - public function render(\Sabberworm\CSS\OutputFormat $oOutputFormat = null) { - if($oOutputFormat === null) { - $oOutputFormat = new \Sabberworm\CSS\OutputFormat(); - } - return parent::render($oOutputFormat); - } - - public function isRootList() { - return true; - } - -} \ No newline at end of file diff --git a/lib/Sabberworm/CSS/CSSList/KeyFrame.php b/lib/Sabberworm/CSS/CSSList/KeyFrame.php deleted file mode 100644 index 0334b1b3..00000000 --- a/lib/Sabberworm/CSS/CSSList/KeyFrame.php +++ /dev/null @@ -1,56 +0,0 @@ -vendorKeyFrame = null; - $this->animationName = null; - } - - public function setVendorKeyFrame($vendorKeyFrame) { - $this->vendorKeyFrame = $vendorKeyFrame; - } - - public function getVendorKeyFrame() { - return $this->vendorKeyFrame; - } - - public function setAnimationName($animationName) { - $this->animationName = $animationName; - } - - public function getAnimationName() { - return $this->animationName; - } - - public function __toString() { - return $this->render(new \Sabberworm\CSS\OutputFormat()); - } - - public function render(\Sabberworm\CSS\OutputFormat $oOutputFormat) { - $sResult = "@{$this->vendorKeyFrame} {$this->animationName}{$oOutputFormat->spaceBeforeOpeningBrace()}{"; - $sResult .= parent::render($oOutputFormat); - $sResult .= '}'; - return $sResult; - } - - public function isRootList() { - return false; - } - - public function atRuleName() { - return $this->vendorKeyFrame; - } - - public function atRuleArgs() { - return $this->animationName; - } -} diff --git a/lib/Sabberworm/CSS/Comment/Comment.php b/lib/Sabberworm/CSS/Comment/Comment.php deleted file mode 100644 index 70521b16..00000000 --- a/lib/Sabberworm/CSS/Comment/Comment.php +++ /dev/null @@ -1,51 +0,0 @@ -sComment = $sComment; - $this->iLineNo = $iLineNo; - } - - /** - * @return string - */ - public function getComment() { - return $this->sComment; - } - - /** - * @return int - */ - public function getLineNo() { - return $this->iLineNo; - } - - /** - * @return string - */ - public function setComment($sComment) { - $this->sComment = $sComment; - } - - /** - * @return string - */ - public function __toString() { - return $this->render(new \Sabberworm\CSS\OutputFormat()); - } - - /** - * @return string - */ - public function render(\Sabberworm\CSS\OutputFormat $oOutputFormat) { - return '/*' . $this->sComment . '*/'; - } - -} diff --git a/lib/Sabberworm/CSS/Comment/Commentable.php b/lib/Sabberworm/CSS/Comment/Commentable.php deleted file mode 100644 index 3100f17a..00000000 --- a/lib/Sabberworm/CSS/Comment/Commentable.php +++ /dev/null @@ -1,23 +0,0 @@ -set('Space*Rules', "\n");`) - */ - public $sSpaceAfterRuleName = ' '; - - public $sSpaceBeforeRules = ''; - public $sSpaceAfterRules = ''; - public $sSpaceBetweenRules = ''; - - public $sSpaceBeforeBlocks = ''; - public $sSpaceAfterBlocks = ''; - public $sSpaceBetweenBlocks = "\n"; - - // Content injected in and around @-rule blocks. - public $sBeforeAtRuleBlock = ''; - public $sAfterAtRuleBlock = ''; - - // This is what’s printed before and after the comma if a declaration block contains multiple selectors. - public $sSpaceBeforeSelectorSeparator = ''; - public $sSpaceAfterSelectorSeparator = ' '; - // This is what’s printed after the comma of value lists - public $sSpaceBeforeListArgumentSeparator = ''; - public $sSpaceAfterListArgumentSeparator = ''; - - public $sSpaceBeforeOpeningBrace = ' '; - - // Content injected in and around declaration blocks. - public $sBeforeDeclarationBlock = ''; - public $sAfterDeclarationBlockSelectors = ''; - public $sAfterDeclarationBlock = ''; - - /** - * Indentation - */ - // Indentation character(s) per level. Only applicable if newlines are used in any of the spacing settings. - public $sIndentation = "\t"; - - /** - * Output exceptions. - */ - public $bIgnoreExceptions = false; - - - private $oFormatter = null; - private $oNextLevelFormat = null; - private $iIndentationLevel = 0; - - public function __construct() { - } - - public function get($sName) { - $aVarPrefixes = array('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; - } - - public function set($aNames, $mValue) { - $aVarPrefixes = array('a', 's', 'm', 'b', 'f', 'o', 'c', 'i'); - if(is_string($aNames) && strpos($aNames, '*') !== false) { - $aNames = array(str_replace('*', 'Before', $aNames), str_replace('*', 'Between', $aNames), str_replace('*', 'After', $aNames)); - } else if(!is_array($aNames)) { - $aNames = array($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; - } - - public function __call($sMethodName, $aArguments) { - if(strpos($sMethodName, 'set') === 0) { - return $this->set(substr($sMethodName, 3), $aArguments[0]); - } else if(strpos($sMethodName, 'get') === 0) { - return $this->get(substr($sMethodName, 3)); - } else if(method_exists('\\Sabberworm\\CSS\\OutputFormatter', $sMethodName)) { - return call_user_func_array(array($this->getFormatter(), $sMethodName), $aArguments); - } else { - throw new \Exception('Unknown OutputFormat method called: '.$sMethodName); - } - } - - public function indentWithTabs($iNumber = 1) { - return $this->setIndentation(str_repeat("\t", $iNumber)); - } - - public function indentWithSpaces($iNumber = 2) { - return $this->setIndentation(str_repeat(" ", $iNumber)); - } - - public function nextLevel() { - if($this->oNextLevelFormat === null) { - $this->oNextLevelFormat = clone $this; - $this->oNextLevelFormat->iIndentationLevel++; - $this->oNextLevelFormat->oFormatter = null; - } - return $this->oNextLevelFormat; - } - - public function beLenient() { - $this->bIgnoreExceptions = true; - } - - public function getFormatter() { - if($this->oFormatter === null) { - $this->oFormatter = new OutputFormatter($this); - } - return $this->oFormatter; - } - - public function level() { - return $this->iIndentationLevel; - } - - /** - * Create format. - * - * @return OutputFormat Format. - */ - public static function create() { - return new OutputFormat(); - } - - /** - * Create compact format. - * - * @return OutputFormat Format. - */ - public static function createCompact() { - $format = self::create(); - $format->set('Space*Rules', "")->set('Space*Blocks', "")->setSpaceAfterRuleName('')->setSpaceBeforeOpeningBrace('')->setSpaceAfterSelectorSeparator(''); - return $format; - } - - /** - * Create pretty format. - * - * @return OutputFormat Format. - */ - public static function createPretty() { - $format = self::create(); - $format->set('Space*Rules', "\n")->set('Space*Blocks', "\n")->setSpaceBetweenBlocks("\n\n")->set('SpaceAfterListArgumentSeparator', array('default' => '', ',' => ' ')); - return $format; - } -} - -class OutputFormatter { - private $oFormat; - - public function __construct(OutputFormat $oFormat) { - $this->oFormat = $oFormat; - } - - public function space($sName, $sType = null) { - $sSpaceString = $this->oFormat->get("Space$sName"); - // If $sSpaceString is an array, we have multple 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); - } - } - return $this->prepareSpace($sSpaceString); - } - - public function spaceAfterRuleName() { - return $this->space('AfterRuleName'); - } - - public function spaceBeforeRules() { - return $this->space('BeforeRules'); - } - - public function spaceAfterRules() { - return $this->space('AfterRules'); - } - - public function spaceBetweenRules() { - return $this->space('BetweenRules'); - } - - public function spaceBeforeBlocks() { - return $this->space('BeforeBlocks'); - } - - public function spaceAfterBlocks() { - return $this->space('AfterBlocks'); - } - - public function spaceBetweenBlocks() { - return $this->space('BetweenBlocks'); - } - - public function spaceBeforeSelectorSeparator() { - return $this->space('BeforeSelectorSeparator'); - } - - public function spaceAfterSelectorSeparator() { - return $this->space('AfterSelectorSeparator'); - } - - public function spaceBeforeListArgumentSeparator($sSeparator) { - return $this->space('BeforeListArgumentSeparator', $sSeparator); - } - - public function spaceAfterListArgumentSeparator($sSeparator) { - return $this->space('AfterListArgumentSeparator', $sSeparator); - } - - public function spaceBeforeOpeningBrace() { - return $this->space('BeforeOpeningBrace'); - } - - /** - * Runs the given code, either swallowing or passing exceptions, depending on the bIgnoreExceptions setting. - */ - public function safely($cCode) { - if($this->oFormat->get('IgnoreExceptions')) { - // If output exceptions are ignored, run the code with exception guards - try { - return $cCode(); - } catch (OutputException $e) { - return null; - } //Do nothing - } else { - // Run the code as-is - return $cCode(); - } - } - - /** - * Clone of the implode function but calls ->render with the current output format instead of __toString() - */ - public function implode($sSeparator, $aValues, $bIncreaseLevel = false) { - $sResult = ''; - $oFormat = $this->oFormat; - if($bIncreaseLevel) { - $oFormat = $oFormat->nextLevel(); - } - $bIsFirst = true; - foreach($aValues as $mValue) { - if($bIsFirst) { - $bIsFirst = false; - } else { - $sResult .= $sSeparator; - } - if($mValue instanceof \Sabberworm\CSS\Renderable) { - $sResult .= $mValue->render($oFormat); - } else { - $sResult .= $mValue; - } - } - return $sResult; - } - - public function removeLastSemicolon($sString) { - if($this->oFormat->get('SemicolonAfterLastRule')) { - return $sString; - } - $sString = explode(';', $sString); - if(count($sString) < 2) { - return $sString[0]; - } - $sLast = array_pop($sString); - $sNextToLast = array_pop($sString); - array_push($sString, $sNextToLast.$sLast); - return implode(';', $sString); - } - - private function prepareSpace($sSpaceString) { - return str_replace("\n", "\n".$this->indent(), $sSpaceString); - } - - private function indent() { - return str_repeat($this->oFormat->sIndentation, $this->oFormat->level()); - } -} diff --git a/lib/Sabberworm/CSS/Parser.php b/lib/Sabberworm/CSS/Parser.php deleted file mode 100644 index 2520cb34..00000000 --- a/lib/Sabberworm/CSS/Parser.php +++ /dev/null @@ -1,41 +0,0 @@ -oParserState = new ParserState($sText, $oParserSettings, $iLineNo); - } - - public function setCharset($sCharset) { - $this->oParserState->setCharset($sCharset); - } - - public function getCharset() { - $this->oParserState->getCharset(); - } - - public function parse() { - return Document::parse($this->oParserState); - } - -} diff --git a/lib/Sabberworm/CSS/Parsing/OutputException.php b/lib/Sabberworm/CSS/Parsing/OutputException.php deleted file mode 100644 index 1c811770..00000000 --- a/lib/Sabberworm/CSS/Parsing/OutputException.php +++ /dev/null @@ -1,12 +0,0 @@ -oParserSettings = $oParserSettings; - $this->sText = $sText; - $this->iCurrentPosition = 0; - $this->iLineNo = $iLineNo; - $this->setCharset($this->oParserSettings->sDefaultCharset); - } - - public function setCharset($sCharset) { - $this->sCharset = $sCharset; - $this->aText = $this->strsplit($this->sText); - $this->iLength = count($this->aText); - } - - public function getCharset() { - $this->oParserHelper->getCharset(); - return $this->sCharset; - } - - public function currentLine() { - return $this->iLineNo; - } - - public function getSettings() { - return $this->oParserSettings; - } - - public function parseIdentifier($bIgnoreCase = true) { - $sResult = $this->parseCharacter(true); - if ($sResult === null) { - throw new UnexpectedTokenException($sResult, $this->peek(5), 'identifier', $this->iLineNo); - } - $sCharacter = null; - while (($sCharacter = $this->parseCharacter(true)) !== null) { - $sResult .= $sCharacter; - } - if ($bIgnoreCase) { - $sResult = $this->strtolower($sResult); - } - return $sResult; - } - - public function parseCharacter($bIsForIdentifier) { - if ($this->peek() === '\\') { - if ($bIsForIdentifier && $this->oParserSettings->bLenientParsing && ($this->comes('\0') || $this->comes('\9'))) { - // Non-strings can contain \0 or \9 which is an IE hack supported in lenient parsing. - return null; - } - $this->consume('\\'); - if ($this->comes('\n') || $this->comes('\r')) { - return ''; - } - if (preg_match('/[0-9a-fA-F]/Su', $this->peek()) === 0) { - return $this->consume(1); - } - $sUnicode = $this->consumeExpression('/^[0-9a-fA-F]{1,6}/u', 6); - if ($this->strlen($sUnicode) < 6) { - //Consume whitespace after incomplete unicode escape - if (preg_match('/\\s/isSu', $this->peek())) { - if ($this->comes('\r\n')) { - $this->consume(2); - } else { - $this->consume(1); - } - } - } - $iUnicode = intval($sUnicode, 16); - $sUtf32 = ""; - for ($i = 0; $i < 4; ++$i) { - $sUtf32 .= chr($iUnicode & 0xff); - $iUnicode = $iUnicode >> 8; - } - return iconv('utf-32le', $this->sCharset, $sUtf32); - } - if ($bIsForIdentifier) { - $peek = ord($this->peek()); - // Ranges: a-z A-Z 0-9 - _ - if (($peek >= 97 && $peek <= 122) || - ($peek >= 65 && $peek <= 90) || - ($peek >= 48 && $peek <= 57) || - ($peek === 45) || - ($peek === 95) || - ($peek > 0xa1)) { - return $this->consume(1); - } - } else { - return $this->consume(1); - } - return null; - } - - public function consumeWhiteSpace() { - $comments = array(); - do { - while (preg_match('/\\s/isSu', $this->peek()) === 1) { - $this->consume(1); - } - if($this->oParserSettings->bLenientParsing) { - try { - $oComment = $this->consumeComment(); - } catch(UnexpectedTokenException $e) { - // When we can’t find the end of a comment, we assume the document is finished. - $this->iCurrentPosition = $this->iLength; - return; - } - } else { - $oComment = $this->consumeComment(); - } - if ($oComment !== false) { - $comments[] = $oComment; - } - } while($oComment !== false); - return $comments; - } - - public function comes($sString, $bCaseInsensitive = false) { - $sPeek = $this->peek(strlen($sString)); - return ($sPeek == '') - ? false - : $this->streql($sPeek, $sString, $bCaseInsensitive); - } - - public function peek($iLength = 1, $iOffset = 0) { - $iOffset += $this->iCurrentPosition; - if ($iOffset >= $this->iLength) { - return ''; - } - return $this->substr($iOffset, $iLength); - } - - public function consume($mValue = 1) { - 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); - } - $this->iLineNo += $iLineCount; - $this->iCurrentPosition += $this->strlen($mValue); - return $mValue; - } else { - if ($this->iCurrentPosition + $mValue > $this->iLength) { - throw new UnexpectedTokenException($mValue, $this->peek(5), 'count', $this->iLineNo); - } - $sResult = $this->substr($this->iCurrentPosition, $mValue); - $iLineCount = substr_count($sResult, "\n"); - $this->iLineNo += $iLineCount; - $this->iCurrentPosition += $mValue; - return $sResult; - } - } - - public function consumeExpression($mExpression, $iMaxLength = null) { - $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]); - } - throw new UnexpectedTokenException($mExpression, $this->peek(5), 'expression', $this->iLineNo); - } - - /** - * @return false|Comment - */ - public function consumeComment() { - $mComment = false; - if ($this->comes('/*')) { - $iLineNo = $this->iLineNo; - $this->consume(1); - $mComment = ''; - while (($char = $this->consume(1)) !== '') { - $mComment .= $char; - if ($this->comes('*/')) { - $this->consume(2); - break; - } - } - } - - if ($mComment !== false) { - // We skip the * which was included in the comment. - return new Comment(substr($mComment, 1), $iLineNo); - } - - return $mComment; - } - - public function isEnd() { - return $this->iCurrentPosition >= $this->iLength; - } - - public function consumeUntil($aEnd, $bIncludeEnd = false, $consumeEnd = false, array &$comments = array()) { - $aEnd = is_array($aEnd) ? $aEnd : array($aEnd); - $out = ''; - $start = $this->iCurrentPosition; - - while (($char = $this->consume(1)) !== '') { - if (in_array($char, $aEnd)) { - if ($bIncludeEnd) { - $out .= $char; - } elseif (!$consumeEnd) { - $this->iCurrentPosition -= $this->strlen($char); - } - return $out; - } - $out .= $char; - if ($comment = $this->consumeComment()) { - $comments[] = $comment; - } - } - - $this->iCurrentPosition = $start; - throw new UnexpectedTokenException('One of ("'.implode('","', $aEnd).'")', $this->peek(5), 'search', $this->iLineNo); - } - - private function inputLeft() { - return $this->substr($this->iCurrentPosition, -1); - } - - public function streql($sString1, $sString2, $bCaseInsensitive = true) { - if($bCaseInsensitive) { - return $this->strtolower($sString1) === $this->strtolower($sString2); - } else { - return $sString1 === $sString2; - } - } - - public function backtrack($iAmount) { - $this->iCurrentPosition -= $iAmount; - } - - public function strlen($sString) { - if ($this->oParserSettings->bMultibyteSupport) { - return mb_strlen($sString, $this->sCharset); - } else { - return strlen($sString); - } - } - - private function substr($iStart, $iLength) { - if ($iLength < 0) { - $iLength = $this->iLength - $iStart + $iLength; - } - if ($iStart + $iLength > $this->iLength) { - $iLength = $this->iLength - $iStart; - } - $sResult = ''; - while ($iLength > 0) { - $sResult .= $this->aText[$iStart]; - $iStart++; - $iLength--; - } - return $sResult; - } - - private function strtolower($sString) { - if ($this->oParserSettings->bMultibyteSupport) { - return mb_strtolower($sString, $this->sCharset); - } else { - return strtolower($sString); - } - } - - private function strsplit($sString) { - if ($this->oParserSettings->bMultibyteSupport) { - if ($this->streql($this->sCharset, 'utf-8')) { - return preg_split('//u', $sString, null, PREG_SPLIT_NO_EMPTY); - } else { - $iLength = mb_strlen($sString, $this->sCharset); - $aResult = array(); - for ($i = 0; $i < $iLength; ++$i) { - $aResult[] = mb_substr($sString, $i, 1, $this->sCharset); - } - return $aResult; - } - } else { - if($sString === '') { - return array(); - } else { - return str_split($sString); - } - } - } - - private function strpos($sString, $sNeedle, $iOffset) { - if ($this->oParserSettings->bMultibyteSupport) { - return mb_strpos($sString, $sNeedle, $iOffset, $this->sCharset); - } else { - return strpos($sString, $sNeedle, $iOffset); - } - } -} \ No newline at end of file diff --git a/lib/Sabberworm/CSS/Parsing/SourceException.php b/lib/Sabberworm/CSS/Parsing/SourceException.php deleted file mode 100644 index 9bb99138..00000000 --- a/lib/Sabberworm/CSS/Parsing/SourceException.php +++ /dev/null @@ -1,18 +0,0 @@ -iLineNo = $iLineNo; - if (!empty($iLineNo)) { - $sMessage .= " [line no: $iLineNo]"; - } - parent::__construct($sMessage); - } - - public function getLineNo() { - return $this->iLineNo; - } -} \ No newline at end of file diff --git a/lib/Sabberworm/CSS/Parsing/UnexpectedTokenException.php b/lib/Sabberworm/CSS/Parsing/UnexpectedTokenException.php deleted file mode 100644 index 0ef88184..00000000 --- a/lib/Sabberworm/CSS/Parsing/UnexpectedTokenException.php +++ /dev/null @@ -1,31 +0,0 @@ -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}”."; - } else if($this->sMatchType === 'count') { - $sMessage = "Next token was expected to have {$sExpected} chars. Context: “{$sFound}”."; - } else if($this->sMatchType === 'identifier') { - $sMessage = "Identifier expected. Got “{$sFound}”"; - } else if($this->sMatchType === 'custom') { - $sMessage = trim("$sExpected $sFound"); - } - - parent::__construct($sMessage, $iLineNo); - } -} \ No newline at end of file diff --git a/lib/Sabberworm/CSS/Property/AtRule.php b/lib/Sabberworm/CSS/Property/AtRule.php deleted file mode 100644 index b20c8c6e..00000000 --- a/lib/Sabberworm/CSS/Property/AtRule.php +++ /dev/null @@ -1,16 +0,0 @@ -mUrl = $mUrl; - $this->sPrefix = $sPrefix; - $this->iLineNo = $iLineNo; - $this->aComments = array(); - } - - /** - * @return int - */ - public function getLineNo() { - return $this->iLineNo; - } - - public function __toString() { - return $this->render(new \Sabberworm\CSS\OutputFormat()); - } - - public function render(\Sabberworm\CSS\OutputFormat $oOutputFormat) { - return '@namespace '.($this->sPrefix === null ? '' : $this->sPrefix.' ').$this->mUrl->render($oOutputFormat).';'; - } - - public function getUrl() { - return $this->mUrl; - } - - public function getPrefix() { - return $this->sPrefix; - } - - public function setUrl($mUrl) { - $this->mUrl = $mUrl; - } - - public function setPrefix($sPrefix) { - $this->sPrefix = $sPrefix; - } - - public function atRuleName() { - return 'namespace'; - } - - public function atRuleArgs() { - $aResult = array($this->mUrl); - if($this->sPrefix) { - array_unshift($aResult, $this->sPrefix); - } - return $aResult; - } - - public function addComments(array $aComments) { - $this->aComments = array_merge($this->aComments, $aComments); - } - - public function getComments() { - return $this->aComments; - } - - public function setComments(array $aComments) { - $this->aComments = $aComments; - } -} \ No newline at end of file diff --git a/lib/Sabberworm/CSS/Property/Charset.php b/lib/Sabberworm/CSS/Property/Charset.php deleted file mode 100644 index 61c6ebc5..00000000 --- a/lib/Sabberworm/CSS/Property/Charset.php +++ /dev/null @@ -1,66 +0,0 @@ -sCharset = $sCharset; - $this->iLineNo = $iLineNo; - $this->aComments = array(); - } - - /** - * @return int - */ - public function getLineNo() { - return $this->iLineNo; - } - - public function setCharset($sCharset) { - $this->sCharset = $sCharset; - } - - public function getCharset() { - return $this->sCharset; - } - - public function __toString() { - return $this->render(new \Sabberworm\CSS\OutputFormat()); - } - - public function render(\Sabberworm\CSS\OutputFormat $oOutputFormat) { - return "@charset {$this->sCharset->render($oOutputFormat)};"; - } - - public function atRuleName() { - return 'charset'; - } - - public function atRuleArgs() { - return $this->sCharset; - } - - public function addComments(array $aComments) { - $this->aComments = array_merge($this->aComments, $aComments); - } - - public function getComments() { - return $this->aComments; - } - - public function setComments(array $aComments) { - $this->aComments = $aComments; - } -} \ No newline at end of file diff --git a/lib/Sabberworm/CSS/Property/Import.php b/lib/Sabberworm/CSS/Property/Import.php deleted file mode 100644 index 4d40f5a0..00000000 --- a/lib/Sabberworm/CSS/Property/Import.php +++ /dev/null @@ -1,69 +0,0 @@ -oLocation = $oLocation; - $this->sMediaQuery = $sMediaQuery; - $this->iLineNo = $iLineNo; - $this->aComments = array(); - } - - /** - * @return int - */ - public function getLineNo() { - return $this->iLineNo; - } - - public function setLocation($oLocation) { - $this->oLocation = $oLocation; - } - - public function getLocation() { - return $this->oLocation; - } - - public function __toString() { - return $this->render(new \Sabberworm\CSS\OutputFormat()); - } - - public function render(\Sabberworm\CSS\OutputFormat $oOutputFormat) { - return "@import ".$this->oLocation->render($oOutputFormat).($this->sMediaQuery === null ? '' : ' '.$this->sMediaQuery).';'; - } - - public function atRuleName() { - return 'import'; - } - - public function atRuleArgs() { - $aResult = array($this->oLocation); - if($this->sMediaQuery) { - array_push($aResult, $this->sMediaQuery); - } - return $aResult; - } - - public function addComments(array $aComments) { - $this->aComments = array_merge($this->aComments, $aComments); - } - - public function getComments() { - return $this->aComments; - } - - public function setComments(array $aComments) { - $this->aComments = $aComments; - } -} \ No newline at end of file diff --git a/lib/Sabberworm/CSS/Property/Selector.php b/lib/Sabberworm/CSS/Property/Selector.php deleted file mode 100644 index d84171f5..00000000 --- a/lib/Sabberworm/CSS/Property/Selector.php +++ /dev/null @@ -1,74 +0,0 @@ -\~]+)[\w]+ # elements - | - \:{1,2}( # pseudo-elements - after|before|first-letter|first-line|selection - )) - /ix'; - - private $sSelector; - private $iSpecificity; - - public function __construct($sSelector, $bCalculateSpecificity = false) { - $this->setSelector($sSelector); - if ($bCalculateSpecificity) { - $this->getSpecificity(); - } - } - - public function getSelector() { - return $this->sSelector; - } - - public function setSelector($sSelector) { - $this->sSelector = trim($sSelector); - $this->iSpecificity = null; - } - - public function __toString() { - return $this->getSelector(); - } - - public function getSpecificity() { - if ($this->iSpecificity === null) { - $a = 0; - /// @todo should exclude \# as well as "#" - $aMatches = null; - $b = substr_count($this->sSelector, '#'); - $c = preg_match_all(self::NON_ID_ATTRIBUTES_AND_PSEUDO_CLASSES_RX, $this->sSelector, $aMatches); - $d = preg_match_all(self::ELEMENTS_AND_PSEUDO_ELEMENTS_RX, $this->sSelector, $aMatches); - $this->iSpecificity = ($a * 1000) + ($b * 100) + ($c * 10) + $d; - } - return $this->iSpecificity; - } - -} diff --git a/lib/Sabberworm/CSS/Renderable.php b/lib/Sabberworm/CSS/Renderable.php deleted file mode 100644 index 3ac06652..00000000 --- a/lib/Sabberworm/CSS/Renderable.php +++ /dev/null @@ -1,9 +0,0 @@ -sRule = $sRule; - $this->mValue = null; - $this->bIsImportant = false; - $this->aIeHack = array(); - $this->iLineNo = $iLineNo; - $this->aComments = array(); - } - - public static function parse(ParserState $oParserState) { - $aComments = $oParserState->consumeWhiteSpace(); - $oRule = new Rule($oParserState->parseIdentifier(), $oParserState->currentLine()); - $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(); - } - } - $oParserState->consumeWhiteSpace(); - if ($oParserState->comes('!')) { - $oParserState->consume('!'); - $oParserState->consumeWhiteSpace(); - $oParserState->consume('important'); - $oRule->setIsImportant(true); - } - $oParserState->consumeWhiteSpace(); - while ($oParserState->comes(';')) { - $oParserState->consume(';'); - } - $oParserState->consumeWhiteSpace(); - - return $oRule; - } - - private static function listDelimiterForRule($sRule) { - if (preg_match('/^font($|-)/', $sRule)) { - return array(',', '/', ' '); - } - return array(',', ' ', '/'); - } - - /** - * @return int - */ - public function getLineNo() { - return $this->iLineNo; - } - - public function setRule($sRule) { - $this->sRule = $sRule; - } - - public function getRule() { - return $this->sRule; - } - - public function getValue() { - return $this->mValue; - } - - public function setValue($mValue) { - $this->mValue = $mValue; - } - - /** - * @deprecated Old-Style 2-dimensional array given. Retained for (some) backwards-compatibility. Use setValue() instead and wrapp the value inside a RuleValueList if necessary. - */ - public function setValues($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; - } - - /** - * @deprecated Old-Style 2-dimensional array returned. Retained for (some) backwards-compatibility. Use getValue() instead and check for the existance of a (nested set of) ValueList object(s). - */ - public function getValues() { - if (!$this->mValue instanceof RuleValueList) { - return array(array($this->mValue)); - } - if ($this->mValue->getListSeparator() === ',') { - return array($this->mValue->getListComponents()); - } - $aResult = array(); - foreach ($this->mValue->getListComponents() as $mValue) { - if (!$mValue instanceof RuleValueList || $mValue->getListSeparator() !== ',') { - $aResult[] = array($mValue); - continue; - } - if ($this->mValue->getListSeparator() === ' ' || count($aResult) === 0) { - $aResult[] = array(); - } - foreach ($mValue->getListComponents() as $mValue) { - $aResult[count($aResult) - 1][] = $mValue; - } - } - return $aResult; - } - - /** - * 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. - */ - public function addValue($mValue, $sType = ' ') { - if (!is_array($mValue)) { - $mValue = array($mValue); - } - 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); - } - } - foreach ($mValue as $mValueItem) { - $this->mValue->addListComponent($mValueItem); - } - } - - public function addIeHack($iModifier) { - $this->aIeHack[] = $iModifier; - } - - public function setIeHack(array $aModifiers) { - $this->aIeHack = $aModifiers; - } - - public function getIeHack() { - return $this->aIeHack; - } - - public function setIsImportant($bIsImportant) { - $this->bIsImportant = $bIsImportant; - } - - public function getIsImportant() { - return $this->bIsImportant; - } - - public function __toString() { - return $this->render(new \Sabberworm\CSS\OutputFormat()); - } - - public function render(\Sabberworm\CSS\OutputFormat $oOutputFormat) { - $sResult = "{$this->sRule}:{$oOutputFormat->spaceAfterRuleName()}"; - if ($this->mValue instanceof Value) { //Can also be a ValueList - $sResult .= $this->mValue->render($oOutputFormat); - } else { - $sResult .= $this->mValue; - } - if (!empty($this->aIeHack)) { - $sResult .= ' \\' . implode('\\', $this->aIeHack); - } - if ($this->bIsImportant) { - $sResult .= ' !important'; - } - $sResult .= ';'; - return $sResult; - } - - /** - * @param array $aComments Array of comments. - */ - public function addComments(array $aComments) { - $this->aComments = array_merge($this->aComments, $aComments); - } - - /** - * @return array - */ - public function getComments() { - return $this->aComments; - } - - /** - * @param array $aComments Array containing Comment objects. - */ - public function setComments(array $aComments) { - $this->aComments = $aComments; - } - -} diff --git a/lib/Sabberworm/CSS/RuleSet/AtRuleSet.php b/lib/Sabberworm/CSS/RuleSet/AtRuleSet.php deleted file mode 100644 index a1042a95..00000000 --- a/lib/Sabberworm/CSS/RuleSet/AtRuleSet.php +++ /dev/null @@ -1,44 +0,0 @@ -sType = $sType; - $this->sArgs = $sArgs; - } - - public function atRuleName() { - return $this->sType; - } - - public function atRuleArgs() { - return $this->sArgs; - } - - public function __toString() { - return $this->render(new \Sabberworm\CSS\OutputFormat()); - } - - public function render(\Sabberworm\CSS\OutputFormat $oOutputFormat) { - $sArgs = $this->sArgs; - if($sArgs) { - $sArgs = ' ' . $sArgs; - } - $sResult = "@{$this->sType}$sArgs{$oOutputFormat->spaceBeforeOpeningBrace()}{"; - $sResult .= parent::render($oOutputFormat); - $sResult .= '}'; - return $sResult; - } - -} \ No newline at end of file diff --git a/lib/Sabberworm/CSS/RuleSet/DeclarationBlock.php b/lib/Sabberworm/CSS/RuleSet/DeclarationBlock.php deleted file mode 100644 index 6614b1d1..00000000 --- a/lib/Sabberworm/CSS/RuleSet/DeclarationBlock.php +++ /dev/null @@ -1,628 +0,0 @@ -aSelectors = array(); - } - - public static function parse(ParserState $oParserState) { - $aComments = array(); - $oResult = new DeclarationBlock($oParserState->currentLine()); - $oResult->setSelector($oParserState->consumeUntil('{', false, true, $aComments)); - $oResult->setComments($aComments); - RuleSet::parseRuleSet($oParserState, $oResult); - return $oResult; - } - - - public function setSelectors($mSelector) { - if (is_array($mSelector)) { - $this->aSelectors = $mSelector; - } else { - $this->aSelectors = explode(',', $mSelector); - } - foreach ($this->aSelectors as $iKey => $mSelector) { - if (!($mSelector instanceof Selector)) { - $this->aSelectors[$iKey] = new Selector($mSelector); - } - } - } - - // remove one of the selector of the block - public function removeSelector($mSelector) { - if($mSelector instanceof Selector) { - $mSelector = $mSelector->getSelector(); - } - foreach($this->aSelectors as $iKey => $oSelector) { - if($oSelector->getSelector() === $mSelector) { - unset($this->aSelectors[$iKey]); - return true; - } - } - return false; - } - - /** - * @deprecated use getSelectors() - */ - public function getSelector() { - return $this->getSelectors(); - } - - /** - * @deprecated use setSelectors() - */ - public function setSelector($mSelector) { - $this->setSelectors($mSelector); - } - - /** - * Get selectors. - * - * @return Selector[] Selectors. - */ - public function getSelectors() { - return $this->aSelectors; - } - - /** - * Split shorthand declarations (e.g. +margin+ or +font+) into their constituent parts. - * */ - public function expandShorthands() { - // border must be expanded before dimensions - $this->expandBorderShorthand(); - $this->expandDimensionsShorthand(); - $this->expandFontShorthand(); - $this->expandBackgroundShorthand(); - $this->expandListStyleShorthand(); - } - - /** - * Create shorthand declarations (e.g. +margin+ or +font+) whenever possible. - * */ - public function createShorthands() { - $this->createBackgroundShorthand(); - $this->createDimensionsShorthand(); - // border must be shortened after dimensions - $this->createBorderShorthand(); - $this->createFontShorthand(); - $this->createListStyleShorthand(); - } - - /** - * Split shorthand border declarations (e.g. border: 1px red;) - * Additional splitting happens in expandDimensionsShorthand - * Multiple borders are not yet supported as of 3 - * */ - public function expandBorderShorthand() { - $aBorderRules = array( - 'border', 'border-left', 'border-right', 'border-top', 'border-bottom' - ); - $aBorderSizes = array( - 'thin', 'medium', 'thick' - ); - $aRules = $this->getRulesAssoc(); - foreach ($aBorderRules as $sBorderRule) { - if (!isset($aRules[$sBorderRule])) - continue; - $oRule = $aRules[$sBorderRule]; - $mRuleValue = $oRule->getValue(); - $aValues = array(); - 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"; - } else if ($mValue instanceof Color) { - $sNewRuleName = $sBorderRule . "-color"; - } else { - if (in_array($mValue, $aBorderSizes)) { - $sNewRuleName = $sBorderRule . "-width"; - } else/* if(in_array($mValue, $aBorderStyles)) */ { - $sNewRuleName = $sBorderRule . "-style"; - } - } - $oNewRule = new Rule($sNewRuleName, $this->iLineNo); - $oNewRule->setIsImportant($oRule->getIsImportant()); - $oNewRule->addValue(array($mNewValue)); - $this->addRule($oNewRule); - } - $this->removeRule($sBorderRule); - } - } - - /** - * Split shorthand dimensional declarations (e.g. margin: 0px auto;) - * into their constituent parts. - * Handles margin, padding, border-color, border-style and border-width. - * */ - public function expandDimensionsShorthand() { - $aExpansions = array( - '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 = array(); - 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 (array('top', 'right', 'bottom', 'left') as $sPosition) { - $oNewRule = new Rule(sprintf($sExpanded, $sPosition), $this->iLineNo); - $oNewRule->setIsImportant($oRule->getIsImportant()); - $oNewRule->addValue(${$sPosition}); - $this->addRule($oNewRule); - } - $this->removeRule($sProperty); - } - } - - /** - * Convert shorthand font declarations - * (e.g. font: 300 italic 11px/14px verdana, helvetica, sans-serif;) - * into their constituent parts. - * */ - 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 = array( - 'font-style' => 'normal', - 'font-variant' => 'normal', - 'font-weight' => 'normal', - 'font-size' => 'normal', - 'line-height' => 'normal' - ); - $mRuleValue = $oRule->getValue(); - $aValues = array(); - 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, array('normal', 'inherit'))) { - foreach (array('font-style', 'font-weight', 'font-variant') as $sProperty) { - if (!isset($aFontProperties[$sProperty])) { - $aFontProperties[$sProperty] = $mValue; - } - } - } else if (in_array($mValue, array('italic', 'oblique'))) { - $aFontProperties['font-style'] = $mValue; - } else if ($mValue == 'small-caps') { - $aFontProperties['font-variant'] = $mValue; - } else if ( - in_array($mValue, array('bold', 'bolder', 'lighter')) - || ($mValue instanceof Size - && in_array($mValue->getSize(), range(100, 900, 100))) - ) { - $aFontProperties['font-weight'] = $mValue; - } else if ($mValue instanceof RuleValueList && $mValue->getListSeparator() == '/') { - list($oSize, $oHeight) = $mValue->getListComponents(); - $aFontProperties['font-size'] = $oSize; - $aFontProperties['line-height'] = $oHeight; - } else if ($mValue instanceof Size && $mValue->getUnit() !== null) { - $aFontProperties['font-size'] = $mValue; - } else { - $aFontProperties['font-family'] = $mValue; - } - } - foreach ($aFontProperties as $sProperty => $mValue) { - $oNewRule = new Rule($sProperty, $this->iLineNo); - $oNewRule->addValue($mValue); - $oNewRule->setIsImportant($oRule->getIsImportant()); - $this->addRule($oNewRule); - } - $this->removeRule('font'); - } - - /* - * Convert 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 - * */ - - public function expandBackgroundShorthand() { - $aRules = $this->getRulesAssoc(); - if (!isset($aRules['background'])) - return; - $oRule = $aRules['background']; - $aBgProperties = array( - 'background-color' => array('transparent'), 'background-image' => array('none'), - 'background-repeat' => array('repeat'), 'background-attachment' => array('scroll'), - 'background-position' => array(new Size(0, '%', null, false, $this->iLineNo), new Size(0, '%', null, false, $this->iLineNo)) - ); - $mRuleValue = $oRule->getValue(); - $aValues = array(); - 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, $this->iLineNo); - $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; - } else if ($mValue instanceof Color) { - $aBgProperties['background-color'] = $mValue; - } else if (in_array($mValue, array('scroll', 'fixed'))) { - $aBgProperties['background-attachment'] = $mValue; - } else if (in_array($mValue, array('repeat', 'no-repeat', 'repeat-x', 'repeat-y'))) { - $aBgProperties['background-repeat'] = $mValue; - } else if (in_array($mValue, array('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, $this->iLineNo); - $oNewRule->setIsImportant($oRule->getIsImportant()); - $oNewRule->addValue($mValue); - $this->addRule($oNewRule); - } - $this->removeRule('background'); - } - - public function expandListStyleShorthand() { - $aListProperties = array( - 'list-style-type' => 'disc', - 'list-style-position' => 'outside', - 'list-style-image' => 'none' - ); - $aListStyleTypes = array( - '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 = array( - 'inside', 'outside' - ); - $aRules = $this->getRulesAssoc(); - if (!isset($aRules['list-style'])) - return; - $oRule = $aRules['list-style']; - $mRuleValue = $oRule->getValue(); - $aValues = array(); - 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, $this->iLineNo); - $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; - } else if (in_array($mValue, $aListStyleTypes)) { - $aListProperties['list-style-types'] = $mValue; - } else if (in_array($mValue, $aListStylePositions)) { - $aListProperties['list-style-position'] = $mValue; - } - } - foreach ($aListProperties as $sProperty => $mValue) { - $oNewRule = new Rule($sProperty, $this->iLineNo); - $oNewRule->setIsImportant($oRule->getIsImportant()); - $oNewRule->addValue($mValue); - $this->addRule($oNewRule); - } - $this->removeRule('list-style'); - } - - public function createShorthandProperties(array $aProperties, $sShorthand) { - $aRules = $this->getRulesAssoc(); - $aNewValues = array(); - foreach ($aProperties as $sProperty) { - if (!isset($aRules[$sProperty])) - continue; - $oRule = $aRules[$sProperty]; - if (!$oRule->getIsImportant()) { - $mRuleValue = $oRule->getValue(); - $aValues = array(); - 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, $this->iLineNo); - foreach ($aNewValues as $mValue) { - $oNewRule->addValue($mValue); - } - $this->addRule($oNewRule); - } - } - - public function createBackgroundShorthand() { - $aProperties = array( - 'background-color', 'background-image', 'background-repeat', - 'background-position', 'background-attachment' - ); - $this->createShorthandProperties($aProperties, 'background'); - } - - public function createListStyleShorthand() { - $aProperties = array( - 'list-style-type', 'list-style-position', 'list-style-image' - ); - $this->createShorthandProperties($aProperties, 'list-style'); - } - - /** - * Combine border-color, border-style and border-width into border - * Should be run after create_dimensions_shorthand! - * */ - public function createBorderShorthand() { - $aProperties = array( - '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. - * */ - - public function createDimensionsShorthand() { - $aPositions = array('top', 'right', 'bottom', 'left'); - $aExpansions = array( - '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 = array(); - 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 = array(); - foreach ($aPositions as $sPosition) { - $oRule = $aRules[sprintf($sExpanded, $sPosition)]; - $mRuleValue = $oRule->getValue(); - $aRuleValues = array(); - if (!$mRuleValue instanceof RuleValueList) { - $aRuleValues[] = $mRuleValue; - } else { - $aRuleValues = $mRuleValue->getListComponents(); - } - $aValues[$sPosition] = $aRuleValues; - } - $oNewRule = new Rule($sProperty, $this->iLineNo); - 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. - * */ - public function createFontShorthand() { - $aFontProperties = array( - '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; - } - $oNewRule = new Rule('font', $this->iLineNo); - foreach (array('font-style', 'font-variant', 'font-weight') as $sProperty) { - if (isset($aRules[$sProperty])) { - $oRule = $aRules[$sProperty]; - $mRuleValue = $oRule->getValue(); - $aValues = array(); - 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 = array(); - 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 = array(); - 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 = array(); - 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); - } - } - - public function __toString() { - return $this->render(new \Sabberworm\CSS\OutputFormat()); - } - - public function render(\Sabberworm\CSS\OutputFormat $oOutputFormat) { - if(count($this->aSelectors) === 0) { - // If all the selectors have been removed, this declaration block becomes invalid - throw new OutputException("Attempt to print declaration block with missing selector", $this->iLineNo); - } - $sResult = $oOutputFormat->sBeforeDeclarationBlock; - $sResult .= $oOutputFormat->implode($oOutputFormat->spaceBeforeSelectorSeparator() . ',' . $oOutputFormat->spaceAfterSelectorSeparator(), $this->aSelectors); - $sResult .= $oOutputFormat->sAfterDeclarationBlockSelectors; - $sResult .= $oOutputFormat->spaceBeforeOpeningBrace() . '{'; - $sResult .= parent::render($oOutputFormat); - $sResult .= '}'; - $sResult .= $oOutputFormat->sAfterDeclarationBlock; - return $sResult; - } - -} diff --git a/lib/Sabberworm/CSS/RuleSet/RuleSet.php b/lib/Sabberworm/CSS/RuleSet/RuleSet.php deleted file mode 100644 index e5d5e415..00000000 --- a/lib/Sabberworm/CSS/RuleSet/RuleSet.php +++ /dev/null @@ -1,212 +0,0 @@ -aRules = array(); - $this->iLineNo = $iLineNo; - $this->aComments = array(); - } - - public static function parseRuleSet(ParserState $oParserState, RuleSet $oRuleSet) { - while ($oParserState->comes(';')) { - $oParserState->consume(';'); - } - while (!$oParserState->comes('}')) { - $oRule = null; - if($oParserState->getSettings()->bLenientParsing) { - try { - $oRule = Rule::parse($oParserState); - } catch (UnexpectedTokenException $e) { - try { - $sConsume = $oParserState->consumeUntil(array("\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); - } else { - while ($oParserState->comes(';')) { - $oParserState->consume(';'); - } - } - } catch (UnexpectedTokenException $e) { - // We’ve reached the end of the document. Just close the RuleSet. - return; - } - } - } else { - $oRule = Rule::parse($oParserState); - } - if($oRule) { - $oRuleSet->addRule($oRule); - } - } - $oParserState->consume('}'); - } - - /** - * @return int - */ - public function getLineNo() { - return $this->iLineNo; - } - - public function addRule(Rule $oRule, Rule $oSibling = null) { - $sRule = $oRule->getRule(); - if(!isset($this->aRules[$sRule])) { - $this->aRules[$sRule] = array(); - } - - $iPosition = count($this->aRules[$sRule]); - - if ($oSibling !== null) { - $iSiblingPos = array_search($oSibling, $this->aRules[$sRule], true); - if ($iSiblingPos !== false) { - $iPosition = $iSiblingPos; - } - } - - array_splice($this->aRules[$sRule], $iPosition, 0, array($oRule)); - } - - /** - * Returns all rules matching the given rule name - * @param (null|string|Rule) $mRule pattern to search for. If null, returns all rules. if the pattern ends with a dash, all rules starting with the pattern are returned as well as one matching the pattern with the dash excluded. passing a Rule behaves like calling getRules($mRule->getRule()). - * @example $oRuleSet->getRules('font-') //returns an array of all rules either beginning with font- or matching font. - * @example $oRuleSet->getRules('font') //returns array(0 => $oRule, …) or array(). - * @return Rule[] Rules. - */ - public function getRules($mRule = null) { - if ($mRule instanceof Rule) { - $mRule = $mRule->getRule(); - } - $aResult = array(); - 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. - if(!$mRule || $sName === $mRule || (strrpos($mRule, '-') === strlen($mRule) - strlen('-') && (strpos($sName, $mRule) === 0 || $sName === substr($mRule, 0, -1)))) { - $aResult = array_merge($aResult, $aRules); - } - } - return $aResult; - } - - /** - * Override all the rules of this set. - * @param Rule[] $aRules The rules to override with. - */ - public function setRules(array $aRules) { - $this->aRules = array(); - foreach ($aRules as $rule) { - $this->addRule($rule); - } - } - - /** - * Returns all rules matching the given pattern and returns them in an associative array with the rule’s name as keys. This method exists mainly for backwards-compatibility and is really only partially useful. - * @param (string) $mRule pattern to search for. If null, returns all rules. if the pattern ends with a dash, all rules starting with the pattern are returned as well as one matching the pattern with the dash excluded. passing a Rule behaves like calling getRules($mRule->getRule()). - * Note: This method loses some information: Calling this (with an argument of 'background-') on a declaration block like { background-color: green; background-color; rgba(0, 127, 0, 0.7); } will only yield an associative array containing the rgba-valued rule while @link{getRules()} would yield an indexed array containing both. - * @return Rule[] Rules. - */ - public function getRulesAssoc($mRule = null) { - $aResult = array(); - foreach($this->getRules($mRule) as $oRule) { - $aResult[$oRule->getRule()] = $oRule; - } - return $aResult; - } - - /** - * Remove a rule from this RuleSet. This accepts all the possible values that @link{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 behvaiour, use removeRule($oRule->getRule()). - * @param (null|string|Rule) $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. - */ - public function removeRule($mRule) { - 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]); - } - } - } - } - - public function __toString() { - return $this->render(new \Sabberworm\CSS\OutputFormat()); - } - - public function render(\Sabberworm\CSS\OutputFormat $oOutputFormat) { - $sResult = ''; - $bIsFirst = true; - foreach ($this->aRules as $aRules) { - foreach($aRules as $oRule) { - $sRendered = $oOutputFormat->safely(function() use ($oRule, $oOutputFormat) { - return $oRule->render($oOutputFormat->nextLevel()); - }); - if($sRendered === null) { - continue; - } - if($bIsFirst) { - $bIsFirst = false; - $sResult .= $oOutputFormat->nextLevel()->spaceBeforeRules(); - } else { - $sResult .= $oOutputFormat->nextLevel()->spaceBetweenRules(); - } - $sResult .= $sRendered; - } - } - - if(!$bIsFirst) { - // Had some output - $sResult .= $oOutputFormat->spaceAfterRules(); - } - - return $oOutputFormat->removeLastSemicolon($sResult); - } - - /** - * @param array $aComments Array of comments. - */ - public function addComments(array $aComments) { - $this->aComments = array_merge($this->aComments, $aComments); - } - - /** - * @return array - */ - public function getComments() { - return $this->aComments; - } - - /** - * @param array $aComments Array containing Comment objects. - */ - public function setComments(array $aComments) { - $this->aComments = $aComments; - } - -} diff --git a/lib/Sabberworm/CSS/Settings.php b/lib/Sabberworm/CSS/Settings.php deleted file mode 100644 index cb89a863..00000000 --- a/lib/Sabberworm/CSS/Settings.php +++ /dev/null @@ -1,54 +0,0 @@ -bMultibyteSupport = extension_loaded('mbstring'); - } - - public static function create() { - return new Settings(); - } - - public function withMultibyteSupport($bMultibyteSupport = true) { - $this->bMultibyteSupport = $bMultibyteSupport; - return $this; - } - - public function withDefaultCharset($sDefaultCharset) { - $this->sDefaultCharset = $sDefaultCharset; - return $this; - } - - public function withLenientParsing($bLenientParsing = true) { - $this->bLenientParsing = $bLenientParsing; - return $this; - } - - public function beStrict() { - return $this->withLenientParsing(false); - } -} \ No newline at end of file diff --git a/lib/Sabberworm/CSS/Value/CSSFunction.php b/lib/Sabberworm/CSS/Value/CSSFunction.php deleted file mode 100644 index 941df236..00000000 --- a/lib/Sabberworm/CSS/Value/CSSFunction.php +++ /dev/null @@ -1,40 +0,0 @@ -getListSeparator(); - $aArguments = $aArguments->getListComponents(); - } - $this->sName = $sName; - $this->iLineNo = $iLineNo; - parent::__construct($aArguments, $sSeparator, $iLineNo); - } - - public function getName() { - return $this->sName; - } - - public function setName($sName) { - $this->sName = $sName; - } - - public function getArguments() { - return $this->aComponents; - } - - public function __toString() { - return $this->render(new \Sabberworm\CSS\OutputFormat()); - } - - public function render(\Sabberworm\CSS\OutputFormat $oOutputFormat) { - $aArguments = parent::render($oOutputFormat); - return "{$this->sName}({$aArguments})"; - } - -} diff --git a/lib/Sabberworm/CSS/Value/CSSString.php b/lib/Sabberworm/CSS/Value/CSSString.php deleted file mode 100644 index 9f9c050e..00000000 --- a/lib/Sabberworm/CSS/Value/CSSString.php +++ /dev/null @@ -1,66 +0,0 @@ -sString = $sString; - parent::__construct($iLineNo); - } - - public static function parse(ParserState $oParserState) { - $sBegin = $oParserState->peek(); - $sQuote = null; - if ($sBegin === "'") { - $sQuote = "'"; - } else if ($sBegin === '"') { - $sQuote = '"'; - } - if ($sQuote !== null) { - $oParserState->consume($sQuote); - } - $sResult = ""; - $sContent = null; - if ($sQuote === null) { - // Unquoted strings end in whitespace or with braces, brackets, parentheses - while (!preg_match('/[\\s{}()<>\\[\\]]/isu', $oParserState->peek())) { - $sResult .= $oParserState->parseCharacter(false); - } - } else { - while (!$oParserState->comes($sQuote)) { - $sContent = $oParserState->parseCharacter(false); - if ($sContent === null) { - throw new SourceException("Non-well-formed quoted string {$oParserState->peek(3)}", $oParserState->currentLine()); - } - $sResult .= $sContent; - } - $oParserState->consume($sQuote); - } - return new CSSString($sResult, $oParserState->currentLine()); - } - - public function setString($sString) { - $this->sString = $sString; - } - - public function getString() { - return $this->sString; - } - - public function __toString() { - return $this->render(new \Sabberworm\CSS\OutputFormat()); - } - - public function render(\Sabberworm\CSS\OutputFormat $oOutputFormat) { - $sString = addslashes($this->sString); - $sString = str_replace("\n", '\A', $sString); - return $oOutputFormat->getStringQuotingType() . $sString . $oOutputFormat->getStringQuotingType(); - } - -} \ No newline at end of file diff --git a/lib/Sabberworm/CSS/Value/CalcFunction.php b/lib/Sabberworm/CSS/Value/CalcFunction.php deleted file mode 100644 index 92475209..00000000 --- a/lib/Sabberworm/CSS/Value/CalcFunction.php +++ /dev/null @@ -1,62 +0,0 @@ -consumeUntil('(', false, true)); - $oCalcList = new CalcRuleValueList($oParserState->currentLine()); - $oList = new RuleValueList(',', $oParserState->currentLine()); - $iNestingLevel = 0; - $iLastComponentType = NULL; - while(!$oParserState->comes(')') || $iNestingLevel > 0) { - $oParserState->consumeWhiteSpace(); - if ($oParserState->comes('(')) { - $iNestingLevel++; - $oCalcList->addListComponent($oParserState->consume(1)); - continue; - } else if ($oParserState->comes(')')) { - $iNestingLevel--; - $oCalcList->addListComponent($oParserState->consume(1)); - continue; - } - if ($iLastComponentType != CalcFunction::T_OPERAND) { - $oVal = Value::parsePrimitiveValue($oParserState); - $oCalcList->addListComponent($oVal); - $iLastComponentType = CalcFunction::T_OPERAND; - } else { - if (in_array($oParserState->peek(), $aOperators)) { - if (($oParserState->comes('-') || $oParserState->comes('+'))) { - if ($oParserState->peek(1, -1) != ' ' || !($oParserState->comes('- ') || $oParserState->comes('+ '))) { - throw new UnexpectedTokenException(" {$oParserState->peek()} ", $oParserState->peek(1, -1) . $oParserState->peek(2), 'literal', $oParserState->currentLine()); - } - } - $oCalcList->addListComponent($oParserState->consume(1)); - $iLastComponentType = CalcFunction::T_OPERATOR; - } else { - throw new UnexpectedTokenException( - sprintf( - 'Next token was expected to be an operand of type %s. Instead "%s" was found.', - implode(', ', $aOperators), - $oVal - ), - '', - 'custom', - $oParserState->currentLine() - ); - } - } - } - $oList->addListComponent($oCalcList); - $oParserState->consume(')'); - return new CalcFunction($sFunction, $oList, ',', $oParserState->currentLine()); - } - -} diff --git a/lib/Sabberworm/CSS/Value/CalcRuleValueList.php b/lib/Sabberworm/CSS/Value/CalcRuleValueList.php deleted file mode 100644 index bde8a9d3..00000000 --- a/lib/Sabberworm/CSS/Value/CalcRuleValueList.php +++ /dev/null @@ -1,14 +0,0 @@ -implode(' ', $this->aComponents); - } - -} diff --git a/lib/Sabberworm/CSS/Value/Color.php b/lib/Sabberworm/CSS/Value/Color.php deleted file mode 100644 index c6ed9b18..00000000 --- a/lib/Sabberworm/CSS/Value/Color.php +++ /dev/null @@ -1,95 +0,0 @@ -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]; - } else if ($oParserState->strlen($sValue) === 4) { - $sValue = $sValue[0] . $sValue[0] . $sValue[1] . $sValue[1] . $sValue[2] . $sValue[2] . $sValue[3] . $sValue[3]; - } - - if ($oParserState->strlen($sValue) === 8) { - $aColor = array( - '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()) - ); - } else { - $aColor = array( - 'r' => new Size(intval($sValue[0] . $sValue[1], 16), null, true, $oParserState->currentLine()), - 'g' => new Size(intval($sValue[2] . $sValue[3], 16), null, true, $oParserState->currentLine()), - 'b' => new Size(intval($sValue[4] . $sValue[5], 16), null, true, $oParserState->currentLine()) - ); - } - } else { - $sColorMode = $oParserState->parseIdentifier(true); - $oParserState->consumeWhiteSpace(); - $oParserState->consume('('); - $iLength = $oParserState->strlen($sColorMode); - for ($i = 0; $i < $iLength; ++$i) { - $oParserState->consumeWhiteSpace(); - $aColor[$sColorMode[$i]] = Size::parse($oParserState, true); - $oParserState->consumeWhiteSpace(); - if ($i < ($iLength - 1)) { - $oParserState->consume(','); - } - } - $oParserState->consume(')'); - } - return new Color($aColor, $oParserState->currentLine()); - } - - private static function mapRange($fVal, $fFromMin, $fFromMax, $fToMin, $fToMax) { - $fFromRange = $fFromMax - $fFromMin; - $fToRange = $fToMax - $fToMin; - $fMultiplier = $fToRange / $fFromRange; - $fNewVal = $fVal - $fFromMin; - $fNewVal *= $fMultiplier; - return $fNewVal + $fToMin; - } - - public function getColor() { - return $this->aComponents; - } - - public function setColor($aColor) { - $this->setName(implode('', array_keys($aColor))); - $this->aComponents = $aColor; - } - - public function getColorDescription() { - return $this->getName(); - } - - public function __toString() { - return $this->render(new \Sabberworm\CSS\OutputFormat()); - } - - public function render(\Sabberworm\CSS\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); - } - return parent::render($oOutputFormat); - } -} diff --git a/lib/Sabberworm/CSS/Value/LineName.php b/lib/Sabberworm/CSS/Value/LineName.php deleted file mode 100644 index eb7392d7..00000000 --- a/lib/Sabberworm/CSS/Value/LineName.php +++ /dev/null @@ -1,41 +0,0 @@ -consume('['); - $oParserState->consumeWhiteSpace(); - $aNames = array(); - do { - if($oParserState->getSettings()->bLenientParsing) { - try { - $aNames[] = $oParserState->parseIdentifier(); - } catch(UnexpectedTokenException $e) {} - } else { - $aNames[] = $oParserState->parseIdentifier(); - } - $oParserState->consumeWhiteSpace(); - } while (!$oParserState->comes(']')); - $oParserState->consume(']'); - return new LineName($aNames, $oParserState->currentLine()); - } - - - - public function __toString() { - return $this->render(new \Sabberworm\CSS\OutputFormat()); - } - - public function render(\Sabberworm\CSS\OutputFormat $oOutputFormat) { - return '[' . parent::render(\Sabberworm\CSS\OutputFormat::createCompact()) . ']'; - } - -} diff --git a/lib/Sabberworm/CSS/Value/PrimitiveValue.php b/lib/Sabberworm/CSS/Value/PrimitiveValue.php deleted file mode 100644 index 187ce7e6..00000000 --- a/lib/Sabberworm/CSS/Value/PrimitiveValue.php +++ /dev/null @@ -1,10 +0,0 @@ -fSize = floatval($fSize); - $this->sUnit = $sUnit; - $this->bIsColorComponent = $bIsColorComponent; - } - - public static function parse(ParserState $oParserState, $bIsColorComponent = false) { - $sSize = ''; - if ($oParserState->comes('-')) { - $sSize .= $oParserState->consume('-'); - } - while (is_numeric($oParserState->peek()) || $oParserState->comes('.')) { - if ($oParserState->comes('.')) { - $sSize .= $oParserState->consume('.'); - } else { - $sSize .= $oParserState->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); - break; - } - } - } - return new Size(floatval($sSize), $sUnit, $bIsColorComponent, $oParserState->currentLine()); - } - - private static function getSizeUnits() { - if(self::$SIZE_UNITS === null) { - self::$SIZE_UNITS = array(); - foreach (explode('/', Size::ABSOLUTE_SIZE_UNITS.'/'.Size::RELATIVE_SIZE_UNITS.'/'.Size::NON_SIZE_UNITS) as $val) { - $iSize = strlen($val); - if(!isset(self::$SIZE_UNITS[$iSize])) { - self::$SIZE_UNITS[$iSize] = array(); - } - self::$SIZE_UNITS[$iSize][strtolower($val)] = $val; - } - - // FIXME: Should we not order the longest units first? - ksort(self::$SIZE_UNITS, SORT_NUMERIC); - } - - return self::$SIZE_UNITS; - } - - public function setUnit($sUnit) { - $this->sUnit = $sUnit; - } - - public function getUnit() { - return $this->sUnit; - } - - public function setSize($fSize) { - $this->fSize = floatval($fSize); - } - - public function getSize() { - return $this->fSize; - } - - public function isColorComponent() { - return $this->bIsColorComponent; - } - - /** - * 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. - */ - public function isSize() { - if (in_array($this->sUnit, explode('/', self::NON_SIZE_UNITS))) { - return false; - } - return !$this->isColorComponent(); - } - - public function isRelative() { - if (in_array($this->sUnit, explode('/', self::RELATIVE_SIZE_UNITS))) { - return true; - } - if ($this->sUnit === null && $this->fSize != 0) { - return true; - } - return false; - } - - public function __toString() { - return $this->render(new \Sabberworm\CSS\OutputFormat()); - } - - public function render(\Sabberworm\CSS\OutputFormat $oOutputFormat) { - $l = localeconv(); - $sPoint = preg_quote($l['decimal_point'], '/'); - return preg_replace(array("/$sPoint/", "/^(-?)0\./"), array('.', '$1.'), $this->fSize) . ($this->sUnit === null ? '' : $this->sUnit); - } - -} diff --git a/lib/Sabberworm/CSS/Value/URL.php b/lib/Sabberworm/CSS/Value/URL.php deleted file mode 100644 index b4f37e16..00000000 --- a/lib/Sabberworm/CSS/Value/URL.php +++ /dev/null @@ -1,49 +0,0 @@ -oURL = $oURL; - } - - public static function parse(ParserState $oParserState) { - $bUseUrl = $oParserState->comes('url', true); - if ($bUseUrl) { - $oParserState->consume('url'); - $oParserState->consumeWhiteSpace(); - $oParserState->consume('('); - } - $oParserState->consumeWhiteSpace(); - $oResult = new URL(CSSString::parse($oParserState), $oParserState->currentLine()); - if ($bUseUrl) { - $oParserState->consumeWhiteSpace(); - $oParserState->consume(')'); - } - return $oResult; - } - - - public function setURL(CSSString $oURL) { - $this->oURL = $oURL; - } - - public function getURL() { - return $this->oURL; - } - - public function __toString() { - return $this->render(new \Sabberworm\CSS\OutputFormat()); - } - - public function render(\Sabberworm\CSS\OutputFormat $oOutputFormat) { - return "url({$this->oURL->render($oOutputFormat)})"; - } - -} \ No newline at end of file diff --git a/lib/Sabberworm/CSS/Value/Value.php b/lib/Sabberworm/CSS/Value/Value.php deleted file mode 100644 index fccc26bb..00000000 --- a/lib/Sabberworm/CSS/Value/Value.php +++ /dev/null @@ -1,131 +0,0 @@ -iLineNo = $iLineNo; - } - - public static function parseValue(ParserState $oParserState, $aListDelimiters = array()) { - $aStack = array(); - $oParserState->consumeWhiteSpace(); - //Build a list of delimiters and parsed values - while (!($oParserState->comes('}') || $oParserState->comes(';') || $oParserState->comes('!') || $oParserState->comes(')') || $oParserState->comes('\\'))) { - if (count($aStack) > 0) { - $bFoundDelimiter = false; - foreach ($aListDelimiters as $sDelimiter) { - if ($oParserState->comes($sDelimiter)) { - array_push($aStack, $oParserState->consume($sDelimiter)); - $oParserState->consumeWhiteSpace(); - $bFoundDelimiter = true; - break; - } - } - if (!$bFoundDelimiter) { - //Whitespace was the list delimiter - array_push($aStack, ' '); - } - } - array_push($aStack, self::parsePrimitiveValue($oParserState)); - $oParserState->consumeWhiteSpace(); - } - //Convert the list to list objects - foreach ($aListDelimiters as $sDelimiter) { - if (count($aStack) === 1) { - return $aStack[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]) { - break; - } - } - $oList = new RuleValueList($sDelimiter, $oParserState->currentLine()); - for ($i = $iStartPosition - 1; $i - $iStartPosition + 1 < $iLength * 2; $i+=2) { - $oList->addListComponent($aStack[$i]); - } - array_splice($aStack, $iStartPosition - 1, $iLength * 2 - 1, array($oList)); - } - } - if (!isset($aStack[0])) { - throw new UnexpectedTokenException(" {$oParserState->peek()} ", $oParserState->peek(1, -1) . $oParserState->peek(2), 'literal', $oParserState->currentLine()); - } - return $aStack[0]; - } - - public static function parseIdentifierOrFunction(ParserState $oParserState, $bIgnoreCase = false) { - $sResult = $oParserState->parseIdentifier($bIgnoreCase); - - if ($oParserState->comes('(')) { - $oParserState->consume('('); - $aArguments = Value::parseValue($oParserState, array('=', ' ', ',')); - $sResult = new CSSFunction($sResult, $aArguments, ',', $oParserState->currentLine()); - $oParserState->consume(')'); - } - - return $sResult; - } - - public static function parsePrimitiveValue(ParserState $oParserState) { - $oValue = null; - $oParserState->consumeWhiteSpace(); - if (is_numeric($oParserState->peek()) || ($oParserState->comes('-.') && is_numeric($oParserState->peek(1, 2))) || (($oParserState->comes('-') || $oParserState->comes('.')) && is_numeric($oParserState->peek(1, 1)))) { - $oValue = Size::parse($oParserState); - } else if ($oParserState->comes('#') || $oParserState->comes('rgb', true) || $oParserState->comes('hsl', true)) { - $oValue = Color::parse($oParserState); - } else if ($oParserState->comes('url', true)) { - $oValue = URL::parse($oParserState); - } else if ($oParserState->comes('calc', true) || $oParserState->comes('-webkit-calc', true) || $oParserState->comes('-moz-calc', true)) { - $oValue = CalcFunction::parse($oParserState); - } else if ($oParserState->comes("'") || $oParserState->comes('"')) { - $oValue = CSSString::parse($oParserState); - } else if ($oParserState->comes("progid:") && $oParserState->getSettings()->bLenientParsing) { - $oValue = self::parseMicrosoftFilter($oParserState); - } else if ($oParserState->comes("[")) { - $oValue = LineName::parse($oParserState); - } else if ($oParserState->comes("U+")) { - $oValue = self::parseUnicodeRangeValue($oParserState); - } else { - $oValue = self::parseIdentifierOrFunction($oParserState); - } - $oParserState->consumeWhiteSpace(); - return $oValue; - } - - private static function parseMicrosoftFilter(ParserState $oParserState) { - $sFunction = $oParserState->consumeUntil('(', false, true); - $aArguments = Value::parseValue($oParserState, array(',', '=')); - return new CSSFunction($sFunction, $aArguments, ',', $oParserState->currentLine()); - } - - private static function parseUnicodeRangeValue(ParserState $oParserState) { - $iCodepointMaxLenth = 6; // Code points outside BMP can use up to six digits - $sRange = ""; - $oParserState->consume("U+"); - do { - if ($oParserState->comes('-')) $iCodepointMaxLenth = 13; // Max length is 2 six digit code points + the dash(-) between them - $sRange .= $oParserState->consume(1); - } while (strlen($sRange) < $iCodepointMaxLenth && preg_match("/[A-Fa-f0-9\?-]/", $oParserState->peek())); - return "U+{$sRange}"; - } - - /** - * @return int - */ - public function getLineNo() { - return $this->iLineNo; - } - - //Methods are commented out because re-declaring them here is a fatal error in PHP < 5.3.9 - //public abstract function __toString(); - //public abstract function render(\Sabberworm\CSS\OutputFormat $oOutputFormat); -} diff --git a/lib/Sabberworm/CSS/Value/ValueList.php b/lib/Sabberworm/CSS/Value/ValueList.php deleted file mode 100644 index 5c3d0e4f..00000000 --- a/lib/Sabberworm/CSS/Value/ValueList.php +++ /dev/null @@ -1,47 +0,0 @@ -aComponents = $aComponents; - $this->sSeparator = $sSeparator; - } - - public function addListComponent($mComponent) { - $this->aComponents[] = $mComponent; - } - - public function getListComponents() { - return $this->aComponents; - } - - public function setListComponents($aComponents) { - $this->aComponents = $aComponents; - } - - public function getListSeparator() { - return $this->sSeparator; - } - - public function setListSeparator($sSeparator) { - $this->sSeparator = $sSeparator; - } - - public function __toString() { - return $this->render(new \Sabberworm\CSS\OutputFormat()); - } - - public function render(\Sabberworm\CSS\OutputFormat $oOutputFormat) { - return $oOutputFormat->implode($oOutputFormat->spaceBeforeListArgumentSeparator($this->sSeparator) . $this->sSeparator . $oOutputFormat->spaceAfterListArgumentSeparator($this->sSeparator), $this->aComponents); - } - -} diff --git a/phpunit.xml b/phpunit.xml index 33cbef56..5f3dd458 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -1,10 +1,15 @@ + xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/4.8/phpunit.xsd"> tests + + + + src + + diff --git a/src/CSSList/AtRuleBlockList.php b/src/CSSList/AtRuleBlockList.php new file mode 100644 index 00000000..218adb9a --- /dev/null +++ b/src/CSSList/AtRuleBlockList.php @@ -0,0 +1,83 @@ +sType = $sType; + $this->sArgs = $sArgs; + } + + /** + * @return string + */ + public function atRuleName() + { + return $this->sType; + } + + /** + * @return string + */ + public function atRuleArgs() + { + return $this->sArgs; + } + + /** + * @return string + */ + public function __toString() + { + return $this->render(new OutputFormat()); + } + + /** + * @return string + */ + public function render(OutputFormat $oOutputFormat) + { + $sArgs = $this->sArgs; + if ($sArgs) { + $sArgs = ' ' . $sArgs; + } + $sResult = $oOutputFormat->sBeforeAtRuleBlock; + $sResult .= "@{$this->sType}$sArgs{$oOutputFormat->spaceBeforeOpeningBrace()}{"; + $sResult .= parent::render($oOutputFormat); + $sResult .= '}'; + $sResult .= $oOutputFormat->sAfterAtRuleBlock; + return $sResult; + } + + /** + * @return bool + */ + public function isRootList() + { + return false; + } +} diff --git a/src/CSSList/CSSBlockList.php b/src/CSSList/CSSBlockList.php new file mode 100644 index 00000000..fce7913e --- /dev/null +++ b/src/CSSList/CSSBlockList.php @@ -0,0 +1,143 @@ + $aResult + * + * @return void + */ + protected function allDeclarationBlocks(array &$aResult) + { + foreach ($this->aContents as $mContent) { + if ($mContent instanceof DeclarationBlock) { + $aResult[] = $mContent; + } elseif ($mContent instanceof CSSBlockList) { + $mContent->allDeclarationBlocks($aResult); + } + } + } + + /** + * @param array $aResult + * + * @return void + */ + protected function allRuleSets(array &$aResult) + { + foreach ($this->aContents as $mContent) { + if ($mContent instanceof RuleSet) { + $aResult[] = $mContent; + } elseif ($mContent instanceof CSSBlockList) { + $mContent->allRuleSets($aResult); + } + } + } + + /** + * @param CSSList|Rule|RuleSet|Value $oElement + * @param array $aResult + * @param string|null $sSearchString + * @param bool $bSearchInFunctionArguments + * + * @return void + */ + 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); + } + } elseif ($oElement instanceof RuleSet) { + foreach ($oElement->getRules($sSearchString) as $oRule) { + $this->allValues($oRule, $aResult, $sSearchString, $bSearchInFunctionArguments); + } + } 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); + } + } + } else { + // Non-List `Value` or `CSSString` (CSS identifier) + $aResult[] = $oElement; + } + } + + /** + * @param array $aResult + * @param string|null $sSpecificitySearch + * + * @return void + */ + protected function allSelectors(array &$aResult, $sSpecificitySearch = null) + { + /** @var array $aDeclarationBlocks */ + $aDeclarationBlocks = []; + $this->allDeclarationBlocks($aDeclarationBlocks); + foreach ($aDeclarationBlocks as $oBlock) { + foreach ($oBlock->getSelectors() as $oSelector) { + if ($sSpecificitySearch === null) { + $aResult[] = $oSelector; + } else { + $sComparator = '==='; + $aSpecificitySearch = explode(' ', $sSpecificitySearch); + $iTargetSpecificity = $aSpecificitySearch[0]; + if (count($aSpecificitySearch) > 1) { + $sComparator = $aSpecificitySearch[0]; + $iTargetSpecificity = $aSpecificitySearch[1]; + } + $iTargetSpecificity = (int)$iTargetSpecificity; + $iSelectorSpecificity = $oSelector->getSpecificity(); + $bMatches = false; + switch ($sComparator) { + case '<=': + $bMatches = $iSelectorSpecificity <= $iTargetSpecificity; + break; + case '<': + $bMatches = $iSelectorSpecificity < $iTargetSpecificity; + break; + case '>=': + $bMatches = $iSelectorSpecificity >= $iTargetSpecificity; + break; + case '>': + $bMatches = $iSelectorSpecificity > $iTargetSpecificity; + break; + default: + $bMatches = $iSelectorSpecificity === $iTargetSpecificity; + break; + } + if ($bMatches) { + $aResult[] = $oSelector; + } + } + } + } + } +} diff --git a/src/CSSList/CSSList.php b/src/CSSList/CSSList.php new file mode 100644 index 00000000..946740a4 --- /dev/null +++ b/src/CSSList/CSSList.php @@ -0,0 +1,479 @@ + + */ + protected $aComments; + + /** + * @var array + */ + protected $aContents; + + /** + * @var int + */ + protected $iLineNo; + + /** + * @param int $iLineNo + */ + public function __construct($iLineNo = 0) + { + $this->aComments = []; + $this->aContents = []; + $this->iLineNo = $iLineNo; + } + + /** + * @return void + * + * @throws UnexpectedTokenException + * @throws SourceException + */ + public static function parseList(ParserState $oParserState, CSSList $oList) + { + $bIsRoot = $oList instanceof Document; + if (is_string($oParserState)) { + $oParserState = new ParserState($oParserState, Settings::create()); + } + $bLenientParsing = $oParserState->getSettings()->bLenientParsing; + while (!$oParserState->isEnd()) { + $comments = $oParserState->consumeWhiteSpace(); + $oListItem = null; + if ($bLenientParsing) { + try { + $oListItem = self::parseListItem($oParserState, $oList); + } catch (UnexpectedTokenException $e) { + $oListItem = false; + } + } else { + $oListItem = self::parseListItem($oParserState, $oList); + } + if ($oListItem === null) { + // List parsing finished + return; + } + if ($oListItem) { + $oListItem->setComments($comments); + $oList->append($oListItem); + } + $oParserState->consumeWhiteSpace(); + } + if (!$bIsRoot && !$bLenientParsing) { + throw new SourceException("Unexpected end of document", $oParserState->currentLine()); + } + } + + /** + * @return AtRuleBlockList|KeyFrame|Charset|CSSNamespace|Import|AtRuleSet|DeclarationBlock|null|false + * + * @throws SourceException + * @throws UnexpectedEOFException + * @throws UnexpectedTokenException + */ + private static function parseListItem(ParserState $oParserState, CSSList $oList) + { + $bIsRoot = $oList instanceof Document; + if ($oParserState->comes('@')) { + $oAtRule = self::parseAtRule($oParserState); + if ($oAtRule instanceof Charset) { + if (!$bIsRoot) { + throw new UnexpectedTokenException( + '@charset may only occur in root document', + '', + 'custom', + $oParserState->currentLine() + ); + } + if (count($oList->getContents()) > 0) { + throw new UnexpectedTokenException( + '@charset must be the first parseable token in a document', + '', + 'custom', + $oParserState->currentLine() + ); + } + $oParserState->setCharset($oAtRule->getCharset()->getString()); + } + 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()); + } + } else { + return null; + } + } + } else { + return DeclarationBlock::parse($oParserState, $oList); + } + } + + /** + * @param ParserState $oParserState + * + * @return AtRuleBlockList|KeyFrame|Charset|CSSNamespace|Import|AtRuleSet|null + * + * @throws SourceException + * @throws UnexpectedTokenException + * @throws UnexpectedEOFException + */ + private static function parseAtRule(ParserState $oParserState) + { + $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])); + } + $oParserState->consumeUntil([';', ParserState::EOF], true, true); + return new Import($oLocation, $sMediaQuery ?: null, $iIdentifierLineNum); + } elseif ($sIdentifier === 'charset') { + $sCharset = CSSString::parse($oParserState); + $oParserState->consumeWhiteSpace(); + $oParserState->consumeUntil([';', ParserState::EOF], true, true); + return new Charset($sCharset, $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('}'); + } + return $oResult; + } elseif ($sIdentifier === 'namespace') { + $sPrefix = null; + $mUrl = Value::parsePrimitiveValue($oParserState); + if (!$oParserState->comes(';')) { + $sPrefix = $mUrl; + $mUrl = Value::parsePrimitiveValue($oParserState); + } + $oParserState->consumeUntil([';', ParserState::EOF], true, true); + if ($sPrefix !== null && !is_string($sPrefix)) { + throw new UnexpectedTokenException('Wrong namespace prefix', $sPrefix, 'custom', $iIdentifierLineNum); + } + if (!($mUrl instanceof CSSString || $mUrl instanceof URL)) { + throw new UnexpectedTokenException( + 'Wrong namespace url of invalid type', + $mUrl, + 'custom', + $iIdentifierLineNum + ); + } + return new CSSNamespace($mUrl, $sPrefix, $iIdentifierLineNum); + } 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) { + return null; + } else { + throw new SourceException("Unmatched brace count in media query", $oParserState->currentLine()); + } + } + $bUseRuleSet = true; + foreach (explode('/', AtRule::BLOCK_RULES) as $sBlockRuleName) { + if (self::identifierIs($sIdentifier, $sBlockRuleName)) { + $bUseRuleSet = false; + break; + } + } + if ($bUseRuleSet) { + $oAtRule = new AtRuleSet($sIdentifier, $sArgs, $iIdentifierLineNum); + RuleSet::parseRuleSet($oParserState, $oAtRule); + } else { + $oAtRule = new AtRuleBlockList($sIdentifier, $sArgs, $iIdentifierLineNum); + CSSList::parseList($oParserState, $oAtRule); + if ($oParserState->comes('}')) { + $oParserState->consume('}'); + } + } + return $oAtRule; + } + } + + /** + * 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) + { + return (strcasecmp($sIdentifier, $sMatch) === 0) + ?: preg_match("/^(-\\w+-)?$sMatch$/i", $sIdentifier) === 1; + } + + /** + * @return int + */ + public function getLineNo() + { + return $this->iLineNo; + } + + /** + * Prepends an item to the list of contents. + * + * @param RuleSet|CSSList|Import|Charset $oItem + * + * @return void + */ + public function prepend($oItem) + { + array_unshift($this->aContents, $oItem); + } + + /** + * Appends an item to tje list of contents. + * + * @param RuleSet|CSSList|Import|Charset $oItem + * + * @return void + */ + public function append($oItem) + { + $this->aContents[] = $oItem; + } + + /** + * Splices the list of contents. + * + * @param int $iOffset + * @param int $iLength + * @param array $mReplacement + * + * @return void + */ + public function splice($iOffset, $iLength = null, $mReplacement = null) + { + array_splice($this->aContents, $iOffset, $iLength, $mReplacement); + } + + /** + * 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) + * + * @return bool whether the item was removed + */ + public function remove($oItemToRemove) + { + $iKey = array_search($oItemToRemove, $this->aContents, true); + if ($iKey !== false) { + unset($this->aContents[$iKey]); + return true; + } + return false; + } + + /** + * Replaces an item from the CSS list. + * + * @param RuleSet|Import|Charset|CSSList $oOldItem + * May be a `RuleSet` (most likely a `DeclarationBlock`), an `Import`, a `Charset` + * or another `CSSList` (most likely a `MediaQuery`) + * + * @return bool + */ + public function replace($oOldItem, $mNewItem) + { + $iKey = array_search($oOldItem, $this->aContents, true); + if ($iKey !== false) { + if (is_array($mNewItem)) { + array_splice($this->aContents, $iKey, 1, $mNewItem); + } else { + array_splice($this->aContents, $iKey, 1, [$mNewItem]); + } + return true; + } + return false; + } + + /** + * @param array $aContents + */ + public function setContents(array $aContents) + { + $this->aContents = []; + foreach ($aContents as $content) { + $this->append($content); + } + } + + /** + * 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 + */ + public function removeDeclarationBlockBySelector($mSelector, $bRemoveAll = false) + { + if ($mSelector instanceof DeclarationBlock) { + $mSelector = $mSelector->getSelectors(); + } + if (!is_array($mSelector)) { + $mSelector = explode(',', $mSelector); + } + foreach ($mSelector as $iKey => &$mSel) { + if (!($mSel instanceof Selector)) { + if (!Selector::isValid($mSel)) { + throw new UnexpectedTokenException( + "Selector did not match '" . Selector::SELECTOR_VALIDATION_RX . "'.", + $mSel, + "custom" + ); + } + $mSel = new Selector($mSel); + } + } + foreach ($this->aContents as $iKey => $mItem) { + if (!($mItem instanceof DeclarationBlock)) { + continue; + } + if ($mItem->getSelectors() == $mSelector) { + unset($this->aContents[$iKey]); + if (!$bRemoveAll) { + return; + } + } + } + } + + /** + * @return string + */ + public function __toString() + { + return $this->render(new OutputFormat()); + } + + /** + * @return string + */ + public function render(OutputFormat $oOutputFormat) + { + $sResult = ''; + $bIsFirst = true; + $oNextLevel = $oOutputFormat; + if (!$this->isRootList()) { + $oNextLevel = $oOutputFormat->nextLevel(); + } + foreach ($this->aContents as $oContent) { + $sRendered = $oOutputFormat->safely(function () use ($oNextLevel, $oContent) { + return $oContent->render($oNextLevel); + }); + if ($sRendered === null) { + continue; + } + if ($bIsFirst) { + $bIsFirst = false; + $sResult .= $oNextLevel->spaceBeforeBlocks(); + } else { + $sResult .= $oNextLevel->spaceBetweenBlocks(); + } + $sResult .= $sRendered; + } + + if (!$bIsFirst) { + // Had some output + $sResult .= $oOutputFormat->spaceAfterBlocks(); + } + + return $sResult; + } + + /** + * Return true if the list can not be further outdented. Only important when rendering. + * + * @return bool + */ + abstract public function isRootList(); + + /** + * @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 + */ + public function setComments(array $aComments) + { + $this->aComments = $aComments; + } +} diff --git a/src/CSSList/Document.php b/src/CSSList/Document.php new file mode 100644 index 00000000..91ab2c6b --- /dev/null +++ b/src/CSSList/Document.php @@ -0,0 +1,172 @@ +currentLine()); + CSSList::parseList($oParserState, $oDocument); + return $oDocument; + } + + /** + * Gets all `DeclarationBlock` objects recursively. + * + * @return array + */ + public function getAllDeclarationBlocks() + { + /** @var array $aResult */ + $aResult = []; + $this->allDeclarationBlocks($aResult); + return $aResult; + } + + /** + * 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 found recursively in the tree. + * + * @return array + */ + public function getAllRuleSets() + { + /** @var array $aResult */ + $aResult = []; + $this->allRuleSets($aResult); + return $aResult; + } + + /** + * Returns all `Value` objects found recursively in the tree. + * + * @param CSSList|RuleSet|string $mElement + * the `CSSList` or `RuleSet` to start the search from (defaults to the whole document). + * If a string is given, it is used as rule name filter. + * @param bool $bSearchInFunctionArguments whether to also return Value objects used as Function arguments. + * + * @return array + * + * @see RuleSet->getRules() + */ + public function getAllValues($mElement = null, $bSearchInFunctionArguments = false) + { + $sSearchString = null; + if ($mElement === null) { + $mElement = $this; + } elseif (is_string($mElement)) { + $sSearchString = $mElement; + $mElement = $this; + } + /** @var array $aResult */ + $aResult = []; + $this->allValues($mElement, $aResult, $sSearchString, $bSearchInFunctionArguments); + return $aResult; + } + + /** + * Returns all `Selector` objects found recursively in the tree. + * + * 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 + * 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 void + */ + public function createShorthands() + { + foreach ($this->getAllDeclarationBlocks() as $oDeclaration) { + $oDeclaration->createShorthands(); + } + } + + /** + * Overrides `render()` to make format argument optional. + * + * @param OutputFormat|null $oOutputFormat + * + * @return string + */ + public function render(OutputFormat $oOutputFormat = null) + { + if ($oOutputFormat === null) { + $oOutputFormat = new OutputFormat(); + } + return parent::render($oOutputFormat); + } + + /** + * @return bool + */ + public function isRootList() + { + return true; + } +} diff --git a/src/CSSList/KeyFrame.php b/src/CSSList/KeyFrame.php new file mode 100644 index 00000000..d9420e9c --- /dev/null +++ b/src/CSSList/KeyFrame.php @@ -0,0 +1,104 @@ +vendorKeyFrame = null; + $this->animationName = null; + } + + /** + * @param string $vendorKeyFrame + */ + public function setVendorKeyFrame($vendorKeyFrame) + { + $this->vendorKeyFrame = $vendorKeyFrame; + } + + /** + * @return string|null + */ + public function getVendorKeyFrame() + { + return $this->vendorKeyFrame; + } + + /** + * @param string $animationName + */ + public function setAnimationName($animationName) + { + $this->animationName = $animationName; + } + + /** + * @return string|null + */ + public function getAnimationName() + { + return $this->animationName; + } + + /** + * @return string + */ + public function __toString() + { + return $this->render(new OutputFormat()); + } + + /** + * @return string + */ + public function render(OutputFormat $oOutputFormat) + { + $sResult = "@{$this->vendorKeyFrame} {$this->animationName}{$oOutputFormat->spaceBeforeOpeningBrace()}{"; + $sResult .= parent::render($oOutputFormat); + $sResult .= '}'; + return $sResult; + } + + /** + * @return bool + */ + public function isRootList() + { + return false; + } + + /** + * @return string|null + */ + public function atRuleName() + { + return $this->vendorKeyFrame; + } + + /** + * @return string|null + */ + public function atRuleArgs() + { + return $this->animationName; + } +} diff --git a/src/Comment/Comment.php b/src/Comment/Comment.php new file mode 100644 index 00000000..6128d749 --- /dev/null +++ b/src/Comment/Comment.php @@ -0,0 +1,71 @@ +sComment = $sComment; + $this->iLineNo = $iLineNo; + } + + /** + * @return string + */ + public function getComment() + { + return $this->sComment; + } + + /** + * @return int + */ + public function getLineNo() + { + return $this->iLineNo; + } + + /** + * @param string $sComment + * + * @return void + */ + public function setComment($sComment) + { + $this->sComment = $sComment; + } + + /** + * @return string + */ + public function __toString() + { + return $this->render(new OutputFormat()); + } + + /** + * @return string + */ + public function render(OutputFormat $oOutputFormat) + { + return '/*' . $this->sComment . '*/'; + } +} diff --git a/src/Comment/Commentable.php b/src/Comment/Commentable.php new file mode 100644 index 00000000..5e450bfb --- /dev/null +++ b/src/Comment/Commentable.php @@ -0,0 +1,25 @@ + $aComments + * + * @return void + */ + public function addComments(array $aComments); + + /** + * @return array + */ + public function getComments(); + + /** + * @param array $aComments + * + * @return void + */ + public function setComments(array $aComments); +} diff --git a/src/OutputFormat.php b/src/OutputFormat.php new file mode 100644 index 00000000..595d3064 --- /dev/null +++ b/src/OutputFormat.php @@ -0,0 +1,334 @@ +set('Space*Rules', "\n");`) + */ + public $sSpaceAfterRuleName = ' '; + + /** + * @var string + */ + public $sSpaceBeforeRules = ''; + + /** + * @var string + */ + public $sSpaceAfterRules = ''; + + /** + * @var string + */ + public $sSpaceBetweenRules = ''; + + /** + * @var string + */ + public $sSpaceBeforeBlocks = ''; + + /** + * @var string + */ + public $sSpaceAfterBlocks = ''; + + /** + * @var string + */ + public $sSpaceBetweenBlocks = "\n"; + + /** + * Content injected in and around at-rule blocks. + * + * @var string + */ + public $sBeforeAtRuleBlock = ''; + + /** + * @var string + */ + public $sAfterAtRuleBlock = ''; + + /** + * This is what’s printed before and after the comma if a declaration block contains multiple selectors. + * + * @var string + */ + public $sSpaceBeforeSelectorSeparator = ''; + + /** + * @var string + */ + public $sSpaceAfterSelectorSeparator = ' '; + + /** + * This is what’s printed after the comma of value lists + * + * @var string + */ + public $sSpaceBeforeListArgumentSeparator = ''; + + /** + * @var string + */ + public $sSpaceAfterListArgumentSeparator = ''; + + /** + * @var string + */ + public $sSpaceBeforeOpeningBrace = ' '; + + /** + * Content injected in and around declaration blocks. + * + * @var string + */ + public $sBeforeDeclarationBlock = ''; + + /** + * @var string + */ + public $sAfterDeclarationBlockSelectors = ''; + + /** + * @var string + */ + public $sAfterDeclarationBlock = ''; + + /** + * Indentation character(s) per level. Only applicable if newlines are used in any of the spacing settings. + * + * @var string + */ + public $sIndentation = "\t"; + + /** + * Output exceptions. + * + * @var bool + */ + public $bIgnoreExceptions = false; + + /** + * @var OutputFormatter|null + */ + private $oFormatter = null; + + /** + * @var OutputFormat|null + */ + private $oNextLevelFormat = null; + + /** + * @var int + */ + private $iIndentationLevel = 0; + + public function __construct() + { + } + + /** + * @param string $sName + * + * @return string|null + */ + public function get($sName) + { + $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; + } + + /** + * @param array|string $aNames + * @param mixed $mValue + * + * @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; + } + + /** + * @param string $sMethodName + * @param array $aArguments + * + * @return mixed + * + * @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); + } + } + + /** + * @param int $iNumber + * + * @return self + */ + public function indentWithTabs($iNumber = 1) + { + return $this->setIndentation(str_repeat("\t", $iNumber)); + } + + /** + * @param int $iNumber + * + * @return self + */ + public function indentWithSpaces($iNumber = 2) + { + return $this->setIndentation(str_repeat(" ", $iNumber)); + } + + /** + * @return OutputFormat + */ + public function nextLevel() + { + if ($this->oNextLevelFormat === null) { + $this->oNextLevelFormat = clone $this; + $this->oNextLevelFormat->iIndentationLevel++; + $this->oNextLevelFormat->oFormatter = null; + } + return $this->oNextLevelFormat; + } + + /** + * @return void + */ + public function beLenient() + { + $this->bIgnoreExceptions = true; + } + + /** + * @return OutputFormatter + */ + public function getFormatter() + { + if ($this->oFormatter === null) { + $this->oFormatter = new OutputFormatter($this); + } + return $this->oFormatter; + } + + /** + * @return int + */ + public function level() + { + return $this->iIndentationLevel; + } + + /** + * Creates an instance of this class without any particular formatting settings. + * + * @return self + */ + public static function create() + { + return new OutputFormat(); + } + + /** + * Creates an instance of this class with a preset for compact formatting. + * + * @return self + */ + public static function createCompact() + { + $format = self::create(); + $format->set('Space*Rules', "")->set('Space*Blocks', "")->setSpaceAfterRuleName('') + ->setSpaceBeforeOpeningBrace('')->setSpaceAfterSelectorSeparator(''); + return $format; + } + + /** + * Creates an instance of this class with a preset for pretty formatting. + * + * @return self + */ + public static function createPretty() + { + $format = self::create(); + $format->set('Space*Rules', "\n")->set('Space*Blocks', "\n") + ->setSpaceBetweenBlocks("\n\n")->set('SpaceAfterListArgumentSeparator', ['default' => '', ',' => ' ']); + return $format; + } +} diff --git a/src/OutputFormatter.php b/src/OutputFormatter.php new file mode 100644 index 00000000..535feca7 --- /dev/null +++ b/src/OutputFormatter.php @@ -0,0 +1,231 @@ +oFormat = $oFormat; + } + + /** + * @param string $sName + * @param string|null $sType + * + * @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); + } + } + return $this->prepareSpace($sSpaceString); + } + + /** + * @return string + */ + public function spaceAfterRuleName() + { + return $this->space('AfterRuleName'); + } + + /** + * @return string + */ + public function spaceBeforeRules() + { + return $this->space('BeforeRules'); + } + + /** + * @return string + */ + public function spaceAfterRules() + { + return $this->space('AfterRules'); + } + + /** + * @return string + */ + public function spaceBetweenRules() + { + return $this->space('BetweenRules'); + } + + /** + * @return string + */ + public function spaceBeforeBlocks() + { + return $this->space('BeforeBlocks'); + } + + /** + * @return string + */ + public function spaceAfterBlocks() + { + return $this->space('AfterBlocks'); + } + + /** + * @return string + */ + public function spaceBetweenBlocks() + { + return $this->space('BetweenBlocks'); + } + + /** + * @return string + */ + public function spaceBeforeSelectorSeparator() + { + return $this->space('BeforeSelectorSeparator'); + } + + /** + * @return string + */ + public function spaceAfterSelectorSeparator() + { + return $this->space('AfterSelectorSeparator'); + } + + /** + * @param string $sSeparator + * + * @return string + */ + public function spaceBeforeListArgumentSeparator($sSeparator) + { + return $this->space('BeforeListArgumentSeparator', $sSeparator); + } + + /** + * @param string $sSeparator + * + * @return string + */ + public function spaceAfterListArgumentSeparator($sSeparator) + { + return $this->space('AfterListArgumentSeparator', $sSeparator); + } + + /** + * @return string + */ + public function spaceBeforeOpeningBrace() + { + 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 + */ + public function safely($cCode) + { + if ($this->oFormat->get('IgnoreExceptions')) { + // If output exceptions are ignored, run the code with exception guards + try { + return $cCode(); + } catch (OutputException $e) { + return null; + } // Do nothing + } else { + // Run the code as-is + return $cCode(); + } + } + + /** + * 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 + * + * @return string + */ + public function implode($sSeparator, array $aValues, $bIncreaseLevel = false) + { + $sResult = ''; + $oFormat = $this->oFormat; + if ($bIncreaseLevel) { + $oFormat = $oFormat->nextLevel(); + } + $bIsFirst = true; + foreach ($aValues as $mValue) { + if ($bIsFirst) { + $bIsFirst = false; + } else { + $sResult .= $sSeparator; + } + if ($mValue instanceof Renderable) { + $sResult .= $mValue->render($oFormat); + } else { + $sResult .= $mValue; + } + } + return $sResult; + } + + /** + * @param string $sString + * + * @return string + */ + public function removeLastSemicolon($sString) + { + if ($this->oFormat->get('SemicolonAfterLastRule')) { + return $sString; + } + $sString = explode(';', $sString); + if (count($sString) < 2) { + return $sString[0]; + } + $sLast = array_pop($sString); + $sNextToLast = array_pop($sString); + array_push($sString, $sNextToLast . $sLast); + return implode(';', $sString); + } + + /** + * @param string $sSpaceString + * + * @return string + */ + private function prepareSpace($sSpaceString) + { + return str_replace("\n", "\n" . $this->indent(), $sSpaceString); + } + + /** + * @return string + */ + private function indent() + { + return str_repeat($this->oFormat->sIndentation, $this->oFormat->level()); + } +} diff --git a/src/Parser.php b/src/Parser.php new file mode 100644 index 00000000..f3b0493a --- /dev/null +++ b/src/Parser.php @@ -0,0 +1,60 @@ +oParserState = new ParserState($sText, $oParserSettings, $iLineNo); + } + + /** + * @param string $sCharset + * + * @return void + */ + public function setCharset($sCharset) + { + $this->oParserState->setCharset($sCharset); + } + + /** + * @return void + */ + public function getCharset() + { + // Note: The `return` statement is missing here. This is a bug that needs to be fixed. + $this->oParserState->getCharset(); + } + + /** + * @return Document + * + * @throws SourceException + */ + public function parse() + { + return Document::parse($this->oParserState); + } +} diff --git a/src/Parsing/OutputException.php b/src/Parsing/OutputException.php new file mode 100644 index 00000000..9bfbc75f --- /dev/null +++ b/src/Parsing/OutputException.php @@ -0,0 +1,18 @@ + + */ + private $aText; + + /** + * @var int + */ + private $iCurrentPosition; + + /** + * @var string + */ + private $sCharset; + + /** + * @var int + */ + private $iLength; + + /** + * @var int + */ + private $iLineNo; + + /** + * @param string $sText + * @param int $iLineNo + */ + public function __construct($sText, Settings $oParserSettings, $iLineNo = 1) + { + $this->oParserSettings = $oParserSettings; + $this->sText = $sText; + $this->iCurrentPosition = 0; + $this->iLineNo = $iLineNo; + $this->setCharset($this->oParserSettings->sDefaultCharset); + } + + /** + * @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); + } + } + + /** + * @return string + */ + public function getCharset() + { + return $this->sCharset; + } + + /** + * @return int + */ + public function currentLine() + { + return $this->iLineNo; + } + + /** + * @return int + */ + public function currentColumn() + { + return $this->iCurrentPosition; + } + + /** + * @return Settings + */ + public function getSettings() + { + return $this->oParserSettings; + } + + /** + * @param bool $bIgnoreCase + * + * @return string + * + * @throws UnexpectedTokenException + */ + public function parseIdentifier($bIgnoreCase = true) + { + $sResult = $this->parseCharacter(true); + if ($sResult === null) { + throw new UnexpectedTokenException($sResult, $this->peek(5), 'identifier', $this->iLineNo); + } + $sCharacter = null; + while (($sCharacter = $this->parseCharacter(true)) !== null) { + if (preg_match('/[a-zA-Z0-9\x{00A0}-\x{FFFF}_-]/Sux', $sCharacter)) { + $sResult .= $sCharacter; + } else { + $sResult .= '\\' . $sCharacter; + } + } + if ($bIgnoreCase) { + $sResult = $this->strtolower($sResult); + } + return $sResult; + } + + /** + * @param bool $bIsForIdentifier + * + * @return string|null + * + * @throws UnexpectedEOFException + * @throws UnexpectedTokenException + */ + public function parseCharacter($bIsForIdentifier) + { + if ($this->peek() === '\\') { + if ( + $bIsForIdentifier && $this->oParserSettings->bLenientParsing + && ($this->comes('\0') || $this->comes('\9')) + ) { + // Non-strings can contain \0 or \9 which is an IE hack supported in lenient parsing. + return null; + } + $this->consume('\\'); + if ($this->comes('\n') || $this->comes('\r')) { + return ''; + } + if (preg_match('/[0-9a-fA-F]/Su', $this->peek()) === 0) { + return $this->consume(1); + } + $sUnicode = $this->consumeExpression('/^[0-9a-fA-F]{1,6}/u', 6); + if ($this->strlen($sUnicode) < 6) { + // Consume whitespace after incomplete unicode escape + if (preg_match('/\\s/isSu', $this->peek())) { + if ($this->comes('\r\n')) { + $this->consume(2); + } else { + $this->consume(1); + } + } + } + $iUnicode = intval($sUnicode, 16); + $sUtf32 = ""; + for ($i = 0; $i < 4; ++$i) { + $sUtf32 .= chr($iUnicode & 0xff); + $iUnicode = $iUnicode >> 8; + } + return iconv('utf-32le', $this->sCharset, $sUtf32); + } + if ($bIsForIdentifier) { + $peek = ord($this->peek()); + // Ranges: a-z A-Z 0-9 - _ + if ( + ($peek >= 97 && $peek <= 122) + || ($peek >= 65 && $peek <= 90) + || ($peek >= 48 && $peek <= 57) + || ($peek === 45) + || ($peek === 95) + || ($peek > 0xa1) + ) { + return $this->consume(1); + } + } else { + return $this->consume(1); + } + return null; + } + + /** + * @return array|void + * + * @throws UnexpectedEOFException + * @throws UnexpectedTokenException + */ + public function consumeWhiteSpace() + { + $comments = []; + do { + while (preg_match('/\\s/isSu', $this->peek()) === 1) { + $this->consume(1); + } + if ($this->oParserSettings->bLenientParsing) { + try { + $oComment = $this->consumeComment(); + } catch (UnexpectedEOFException $e) { + $this->iCurrentPosition = $this->iLength; + return; + } + } else { + $oComment = $this->consumeComment(); + } + if ($oComment !== false) { + $comments[] = $oComment; + } + } while ($oComment !== false); + return $comments; + } + + /** + * @param string $sString + * @param bool $bCaseInsensitive + * + * @return bool + */ + public function comes($sString, $bCaseInsensitive = false) + { + $sPeek = $this->peek(strlen($sString)); + return ($sPeek == '') + ? false + : $this->streql($sPeek, $sString, $bCaseInsensitive); + } + + /** + * @param int $iLength + * @param int $iOffset + * + * @return string + */ + public function peek($iLength = 1, $iOffset = 0) + { + $iOffset += $this->iCurrentPosition; + if ($iOffset >= $this->iLength) { + return ''; + } + return $this->substr($iOffset, $iLength); + } + + /** + * @param int $mValue + * + * @return string + * + * @throws UnexpectedEOFException + * @throws UnexpectedTokenException + */ + public function consume($mValue = 1) + { + 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); + } + $this->iLineNo += $iLineCount; + $this->iCurrentPosition += $this->strlen($mValue); + return $mValue; + } else { + if ($this->iCurrentPosition + $mValue > $this->iLength) { + throw new UnexpectedEOFException($mValue, $this->peek(5), 'count', $this->iLineNo); + } + $sResult = $this->substr($this->iCurrentPosition, $mValue); + $iLineCount = substr_count($sResult, "\n"); + $this->iLineNo += $iLineCount; + $this->iCurrentPosition += $mValue; + return $sResult; + } + } + + /** + * @param string $mExpression + * @param int|null $iMaxLength + * + * @return string + * + * @throws UnexpectedEOFException + * @throws UnexpectedTokenException + */ + public function consumeExpression($mExpression, $iMaxLength = null) + { + $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]); + } + throw new UnexpectedTokenException($mExpression, $this->peek(5), 'expression', $this->iLineNo); + } + + /** + * @return Comment|false + */ + public function consumeComment() + { + $mComment = false; + if ($this->comes('/*')) { + $iLineNo = $this->iLineNo; + $this->consume(1); + $mComment = ''; + while (($char = $this->consume(1)) !== '') { + $mComment .= $char; + if ($this->comes('*/')) { + $this->consume(2); + break; + } + } + } + + if ($mComment !== false) { + // We skip the * which was included in the comment. + return new Comment(substr($mComment, 1), $iLineNo); + } + + return $mComment; + } + + /** + * @return bool + */ + public function isEnd() + { + return $this->iCurrentPosition >= $this->iLength; + } + + /** + * @param array|string $aEnd + * @param string $bIncludeEnd + * @param string $consumeEnd + * @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; + + while (!$this->isEnd()) { + $char = $this->consume(1); + if (in_array($char, $aEnd)) { + if ($bIncludeEnd) { + $out .= $char; + } elseif (!$consumeEnd) { + $this->iCurrentPosition -= $this->strlen($char); + } + return $out; + } + $out .= $char; + if ($comment = $this->consumeComment()) { + $comments[] = $comment; + } + } + + if (in_array(self::EOF, $aEnd)) { + return $out; + } + + $this->iCurrentPosition = $start; + throw new UnexpectedEOFException( + 'One of ("' . implode('","', $aEnd) . '")', + $this->peek(5), + 'search', + $this->iLineNo + ); + } + + /** + * @return string + */ + private function inputLeft() + { + return $this->substr($this->iCurrentPosition, -1); + } + + /** + * @param string $sString1 + * @param string $sString2 + * @param bool $bCaseInsensitive + * + * @return bool + */ + public function streql($sString1, $sString2, $bCaseInsensitive = true) + { + if ($bCaseInsensitive) { + return $this->strtolower($sString1) === $this->strtolower($sString2); + } else { + return $sString1 === $sString2; + } + } + + /** + * @param int $iAmount + * + * @return void + */ + public function backtrack($iAmount) + { + $this->iCurrentPosition -= $iAmount; + } + + /** + * @param string $sString + * + * @return int + */ + public function strlen($sString) + { + if ($this->oParserSettings->bMultibyteSupport) { + return mb_strlen($sString, $this->sCharset); + } else { + return strlen($sString); + } + } + + /** + * @param int $iStart + * @param int $iLength + * + * @return string + */ + private function substr($iStart, $iLength) + { + if ($iLength < 0) { + $iLength = $this->iLength - $iStart + $iLength; + } + if ($iStart + $iLength > $this->iLength) { + $iLength = $this->iLength - $iStart; + } + $sResult = ''; + while ($iLength > 0) { + $sResult .= $this->aText[$iStart]; + $iStart++; + $iLength--; + } + return $sResult; + } + + /** + * @param string $sString + * + * @return string + */ + private function strtolower($sString) + { + if ($this->oParserSettings->bMultibyteSupport) { + return mb_strtolower($sString, $this->sCharset); + } else { + return strtolower($sString); + } + } + + /** + * @param string $sString + * + * @return array + */ + private function strsplit($sString) + { + if ($this->oParserSettings->bMultibyteSupport) { + if ($this->streql($this->sCharset, 'utf-8')) { + return preg_split('//u', $sString, -1, PREG_SPLIT_NO_EMPTY); + } else { + $iLength = mb_strlen($sString, $this->sCharset); + $aResult = []; + for ($i = 0; $i < $iLength; ++$i) { + $aResult[] = mb_substr($sString, $i, 1, $this->sCharset); + } + return $aResult; + } + } else { + if ($sString === '') { + return []; + } else { + return str_split($sString); + } + } + } + + /** + * @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); + } + } +} diff --git a/src/Parsing/SourceException.php b/src/Parsing/SourceException.php new file mode 100644 index 00000000..1ca668a9 --- /dev/null +++ b/src/Parsing/SourceException.php @@ -0,0 +1,32 @@ +iLineNo = $iLineNo; + if (!empty($iLineNo)) { + $sMessage .= " [line no: $iLineNo]"; + } + parent::__construct($sMessage); + } + + /** + * @return int + */ + public function getLineNo() + { + return $this->iLineNo; + } +} diff --git a/src/Parsing/UnexpectedEOFException.php b/src/Parsing/UnexpectedEOFException.php new file mode 100644 index 00000000..368ec70c --- /dev/null +++ b/src/Parsing/UnexpectedEOFException.php @@ -0,0 +1,12 @@ +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"); + } + + parent::__construct($sMessage, $iLineNo); + } +} diff --git a/src/Property/AtRule.php b/src/Property/AtRule.php new file mode 100644 index 00000000..9536ff5e --- /dev/null +++ b/src/Property/AtRule.php @@ -0,0 +1,34 @@ + + */ + protected $aComments; + + /** + * @param string $mUrl + * @param string|null $sPrefix + * @param int $iLineNo + */ + public function __construct($mUrl, $sPrefix = null, $iLineNo = 0) + { + $this->mUrl = $mUrl; + $this->sPrefix = $sPrefix; + $this->iLineNo = $iLineNo; + $this->aComments = []; + } + + /** + * @return int + */ + public function getLineNo() + { + return $this->iLineNo; + } + + /** + * @return string + */ + public function __toString() + { + return $this->render(new OutputFormat()); + } + + /** + * @return string + */ + public function render(OutputFormat $oOutputFormat) + { + return '@namespace ' . ($this->sPrefix === null ? '' : $this->sPrefix . ' ') + . $this->mUrl->render($oOutputFormat) . ';'; + } + + /** + * @return string + */ + public function getUrl() + { + return $this->mUrl; + } + + /** + * @return string|null + */ + public function getPrefix() + { + return $this->sPrefix; + } + + /** + * @param string $mUrl + * + * @return void + */ + public function setUrl($mUrl) + { + $this->mUrl = $mUrl; + } + + /** + * @param string $sPrefix + * + * @return void + */ + public function setPrefix($sPrefix) + { + $this->sPrefix = $sPrefix; + } + + /** + * @return string + */ + public function atRuleName() + { + return 'namespace'; + } + + /** + * @return array + */ + public function atRuleArgs() + { + $aResult = [$this->mUrl]; + if ($this->sPrefix) { + array_unshift($aResult, $this->sPrefix); + } + 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; + } +} diff --git a/src/Property/Charset.php b/src/Property/Charset.php new file mode 100644 index 00000000..3ee0c3d0 --- /dev/null +++ b/src/Property/Charset.php @@ -0,0 +1,129 @@ + + */ + protected $aComments; + + /** + * @param string $sCharset + * @param int $iLineNo + */ + public function __construct($sCharset, $iLineNo = 0) + { + $this->sCharset = $sCharset; + $this->iLineNo = $iLineNo; + $this->aComments = []; + } + + /** + * @return int + */ + public function getLineNo() + { + return $this->iLineNo; + } + + /** + * @param string $sCharset + * + * @return void + */ + public function setCharset($sCharset) + { + $this->sCharset = $sCharset; + } + + /** + * @return string + */ + public function getCharset() + { + return $this->sCharset; + } + + /** + * @return string + */ + public function __toString() + { + return $this->render(new OutputFormat()); + } + + /** + * @return string + */ + public function render(OutputFormat $oOutputFormat) + { + return "@charset {$this->sCharset->render($oOutputFormat)};"; + } + + /** + * @return string + */ + public function atRuleName() + { + return 'charset'; + } + + /** + * @return string + */ + public function atRuleArgs() + { + return $this->sCharset; + } + + /** + * @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; + } +} diff --git a/src/Property/Import.php b/src/Property/Import.php new file mode 100644 index 00000000..a2253016 --- /dev/null +++ b/src/Property/Import.php @@ -0,0 +1,137 @@ + + */ + protected $aComments; + + /** + * @param URL $oLocation + * @param string $sMediaQuery + * @param int $iLineNo + */ + public function __construct(URL $oLocation, $sMediaQuery, $iLineNo = 0) + { + $this->oLocation = $oLocation; + $this->sMediaQuery = $sMediaQuery; + $this->iLineNo = $iLineNo; + $this->aComments = []; + } + + /** + * @return int + */ + public function getLineNo() + { + return $this->iLineNo; + } + + /** + * @param URL $oLocation + * + * @return void + */ + public function setLocation($oLocation) + { + $this->oLocation = $oLocation; + } + + /** + * @return URL + */ + public function getLocation() + { + return $this->oLocation; + } + + /** + * @return string + */ + public function __toString() + { + return $this->render(new OutputFormat()); + } + + /** + * @return string + */ + public function render(OutputFormat $oOutputFormat) + { + return "@import " . $this->oLocation->render($oOutputFormat) + . ($this->sMediaQuery === null ? '' : ' ' . $this->sMediaQuery) . ';'; + } + + /** + * @return string + */ + public function atRuleName() + { + return 'import'; + } + + /** + * @return array + */ + public function atRuleArgs() + { + $aResult = [$this->oLocation]; + if ($this->sMediaQuery) { + array_push($aResult, $this->sMediaQuery); + } + 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; + } +} diff --git a/src/Property/KeyframeSelector.php b/src/Property/KeyframeSelector.php new file mode 100644 index 00000000..14ea5ebb --- /dev/null +++ b/src/Property/KeyframeSelector.php @@ -0,0 +1,23 @@ +]* # 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 = '/ + ^( + (?: + [a-zA-Z0-9\x{00A0}-\x{FFFF}_^$|*="\'~\[\]()\-\s\.:#+>]* # any sequence of valid unescaped characters + (?:\\\\.)? # a single escaped character + (?:([\'"]).*?(?setSelector($sSelector); + if ($bCalculateSpecificity) { + $this->getSpecificity(); + } + } + + /** + * @return string + */ + public function getSelector() + { + return $this->sSelector; + } + + /** + * @param string $sSelector + * + * @return void + */ + public function setSelector($sSelector) + { + $this->sSelector = trim($sSelector); + $this->iSpecificity = null; + } + + /** + * @return string + */ + public function __toString() + { + return $this->getSelector(); + } + + /** + * @return int + */ + public function getSpecificity() + { + if ($this->iSpecificity === null) { + $a = 0; + /// @todo should exclude \# as well as "#" + $aMatches = null; + $b = substr_count($this->sSelector, '#'); + $c = preg_match_all(self::NON_ID_ATTRIBUTES_AND_PSEUDO_CLASSES_RX, $this->sSelector, $aMatches); + $d = preg_match_all(self::ELEMENTS_AND_PSEUDO_ELEMENTS_RX, $this->sSelector, $aMatches); + $this->iSpecificity = ($a * 1000) + ($b * 100) + ($c * 10) + $d; + } + return $this->iSpecificity; + } +} diff --git a/src/Renderable.php b/src/Renderable.php new file mode 100644 index 00000000..dc1bff3c --- /dev/null +++ b/src/Renderable.php @@ -0,0 +1,21 @@ + + */ + private $aIeHack; + + /** + * @var int + */ + protected $iLineNo; + + /** + * @var int + */ + protected $iColNo; + + /** + * @var array + */ + protected $aComments; + + /** + * @param string $sRule + * @param int $iLineNo + * @param int $iColNo + */ + public function __construct($sRule, $iLineNo = 0, $iColNo = 0) + { + $this->sRule = $sRule; + $this->mValue = null; + $this->bIsImportant = false; + $this->aIeHack = []; + $this->iLineNo = $iLineNo; + $this->iColNo = $iColNo; + $this->aComments = []; + } + + /** + * @return Rule + * + * @throws UnexpectedEOFException + * @throws UnexpectedTokenException + */ + public static function parse(ParserState $oParserState) + { + $aComments = $oParserState->consumeWhiteSpace(); + $oRule = new Rule( + $oParserState->parseIdentifier(!$oParserState->comes("--")), + $oParserState->currentLine(), + $oParserState->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(); + } + } + $oParserState->consumeWhiteSpace(); + if ($oParserState->comes('!')) { + $oParserState->consume('!'); + $oParserState->consumeWhiteSpace(); + $oParserState->consume('important'); + $oRule->setIsImportant(true); + } + $oParserState->consumeWhiteSpace(); + while ($oParserState->comes(';')) { + $oParserState->consume(';'); + } + $oParserState->consumeWhiteSpace(); + + return $oRule; + } + + /** + * @param string $sRule + * + * @return array + */ + private static function listDelimiterForRule($sRule) + { + if (preg_match('/^font($|-)/', $sRule)) { + return [',', '/', ' ']; + } + return [',', ' ', '/']; + } + + /** + * @return int + */ + public function getLineNo() + { + return $this->iLineNo; + } + + /** + * @return int + */ + public function getColNo() + { + return $this->iColNo; + } + + /** + * @param int $iLine + * @param int $iColumn + * + * @return void + */ + public function setPosition($iLine, $iColumn) + { + $this->iColNo = $iColumn; + $this->iLineNo = $iLine; + } + + /** + * @param string $sRule + * + * @return void + */ + public function setRule($sRule) + { + $this->sRule = $sRule; + } + + /** + * @return string + */ + public function getRule() + { + return $this->sRule; + } + + /** + * @return RuleValueList|null + */ + public function getValue() + { + return $this->mValue; + } + + /** + * @param RuleValueList|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 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). + */ + public function getValues() + { + 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; + } + + /** + * 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 + */ + public function addValue($mValue, $sType = ' ') + { + if (!is_array($mValue)) { + $mValue = [$mValue]; + } + 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); + } + } + foreach ($mValue as $mValueItem) { + $this->mValue->addListComponent($mValueItem); + } + } + + /** + * @param int $iModifier + * + * @return void + */ + public function addIeHack($iModifier) + { + $this->aIeHack[] = $iModifier; + } + + /** + * @param array $aModifiers + * + * @return void + */ + public function setIeHack(array $aModifiers) + { + $this->aIeHack = $aModifiers; + } + + /** + * @return array + */ + public function getIeHack() + { + 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 = "{$this->sRule}:{$oOutputFormat->spaceAfterRuleName()}"; + if ($this->mValue instanceof Value) { //Can also be a ValueList + $sResult .= $this->mValue->render($oOutputFormat); + } else { + $sResult .= $this->mValue; + } + if (!empty($this->aIeHack)) { + $sResult .= ' \\' . implode('\\', $this->aIeHack); + } + if ($this->bIsImportant) { + $sResult .= ' !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; + } +} diff --git a/src/RuleSet/AtRuleSet.php b/src/RuleSet/AtRuleSet.php new file mode 100644 index 00000000..88bc5bd3 --- /dev/null +++ b/src/RuleSet/AtRuleSet.php @@ -0,0 +1,73 @@ +sType = $sType; + $this->sArgs = $sArgs; + } + + /** + * @return string + */ + public function atRuleName() + { + return $this->sType; + } + + /** + * @return string + */ + public function atRuleArgs() + { + return $this->sArgs; + } + + /** + * @return string + */ + public function __toString() + { + return $this->render(new OutputFormat()); + } + + /** + * @return string + */ + public function render(OutputFormat $oOutputFormat) + { + $sArgs = $this->sArgs; + if ($sArgs) { + $sArgs = ' ' . $sArgs; + } + $sResult = "@{$this->sType}$sArgs{$oOutputFormat->spaceBeforeOpeningBrace()}{"; + $sResult .= parent::render($oOutputFormat); + $sResult .= '}'; + return $sResult; + } +} diff --git a/src/RuleSet/DeclarationBlock.php b/src/RuleSet/DeclarationBlock.php new file mode 100644 index 00000000..c27cdd4c --- /dev/null +++ b/src/RuleSet/DeclarationBlock.php @@ -0,0 +1,831 @@ + + */ + private $aSelectors; + + /** + * @param int $iLineNo + */ + public function __construct($iLineNo = 0) + { + parent::__construct($iLineNo); + $this->aSelectors = []; + } + + /** + * @param CSSList|null $oList + * + * @return DeclarationBlock|false + * + * @throws UnexpectedTokenException + * @throws UnexpectedEOFException + */ + public static function parse(ParserState $oParserState, $oList = null) + { + $aComments = []; + $oResult = new DeclarationBlock($oParserState->currentLine()); + try { + $aSelectorParts = []; + $sStringWrapperChar = false; + 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; + } + } + } while (!in_array($oParserState->peek(), ['{', '}']) || $sStringWrapperChar !== false); + $oResult->setSelectors(implode('', $aSelectorParts), $oList); + if ($oParserState->comes('{')) { + $oParserState->consume(1); + } + } catch (UnexpectedTokenException $e) { + if ($oParserState->getSettings()->bLenientParsing) { + if (!$oParserState->comes('}')) { + $oParserState->consumeUntil('}', false, true); + } + return false; + } else { + throw $e; + } + } + $oResult->setComments($aComments); + RuleSet::parseRuleSet($oParserState, $oResult); + return $oResult; + } + + /** + * @param array|string $mSelector + * @param CSSList|null $oList + * + * @throws UnexpectedTokenException + */ + public function setSelectors($mSelector, $oList = null) + { + if (is_array($mSelector)) { + $this->aSelectors = $mSelector; + } else { + $this->aSelectors = explode(',', $mSelector); + } + foreach ($this->aSelectors as $iKey => $mSelector) { + if (!($mSelector instanceof Selector)) { + if ($oList === null || !($oList instanceof KeyFrame)) { + if (!Selector::isValid($mSelector)) { + throw new UnexpectedTokenException( + "Selector did not match '" . Selector::SELECTOR_VALIDATION_RX . "'.", + $mSelector, + "custom" + ); + } + $this->aSelectors[$iKey] = new Selector($mSelector); + } else { + if (!KeyframeSelector::isValid($mSelector)) { + throw new UnexpectedTokenException( + "Selector did not match '" . KeyframeSelector::SELECTOR_VALIDATION_RX . "'.", + $mSelector, + "custom" + ); + } + $this->aSelectors[$iKey] = new KeyframeSelector($mSelector); + } + } + } + } + + /** + * Remove one of the selectors of the block. + * + * @param Selector|string $mSelector + * + * @return bool + */ + public function removeSelector($mSelector) + { + if ($mSelector instanceof Selector) { + $mSelector = $mSelector->getSelector(); + } + foreach ($this->aSelectors as $iKey => $oSelector) { + if ($oSelector->getSelector() === $mSelector) { + unset($this->aSelectors[$iKey]); + return true; + } + } + return false; + } + + /** + * @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 + */ + public function expandDimensionsShorthand() + { + $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); + } + } + + /** + * 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 + * + * @throws OutputException + */ + public function render(OutputFormat $oOutputFormat) + { + if (count($this->aSelectors) === 0) { + // If all the selectors have been removed, this declaration block becomes invalid + throw new OutputException("Attempt to print declaration block with missing selector", $this->iLineNo); + } + $sResult = $oOutputFormat->sBeforeDeclarationBlock; + $sResult .= $oOutputFormat->implode( + $oOutputFormat->spaceBeforeSelectorSeparator() . ',' . $oOutputFormat->spaceAfterSelectorSeparator(), + $this->aSelectors + ); + $sResult .= $oOutputFormat->sAfterDeclarationBlockSelectors; + $sResult .= $oOutputFormat->spaceBeforeOpeningBrace() . '{'; + $sResult .= parent::render($oOutputFormat); + $sResult .= '}'; + $sResult .= $oOutputFormat->sAfterDeclarationBlock; + return $sResult; + } +} diff --git a/src/RuleSet/RuleSet.php b/src/RuleSet/RuleSet.php new file mode 100644 index 00000000..9404bb0b --- /dev/null +++ b/src/RuleSet/RuleSet.php @@ -0,0 +1,326 @@ + + */ + private $aRules; + + /** + * @var int + */ + protected $iLineNo; + + /** + * @var array + */ + protected $aComments; + + /** + * @param int $iLineNo + */ + public function __construct($iLineNo = 0) + { + $this->aRules = []; + $this->iLineNo = $iLineNo; + $this->aComments = []; + } + + /** + * @return void + * + * @throws UnexpectedTokenException + * @throws UnexpectedEOFException + */ + public static function parseRuleSet(ParserState $oParserState, RuleSet $oRuleSet) + { + while ($oParserState->comes(';')) { + $oParserState->consume(';'); + } + while (!$oParserState->comes('}')) { + $oRule = null; + if ($oParserState->getSettings()->bLenientParsing) { + try { + $oRule = Rule::parse($oParserState); + } catch (UnexpectedTokenException $e) { + try { + $sConsume = $oParserState->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); + } else { + while ($oParserState->comes(';')) { + $oParserState->consume(';'); + } + } + } catch (UnexpectedTokenException $e) { + // We’ve reached the end of the document. Just close the RuleSet. + return; + } + } + } else { + $oRule = Rule::parse($oParserState); + } + if ($oRule) { + $oRuleSet->addRule($oRule); + } + } + $oParserState->consume('}'); + } + + /** + * @return int + */ + public function getLineNo() + { + 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] = []; + } + + $iPosition = count($this->aRules[$sRule]); + + if ($oSibling !== null) { + $iSiblingPos = array_search($oSibling, $this->aRules[$sRule], true); + if ($iSiblingPos !== false) { + $iPosition = $iSiblingPos; + $oRule->setPosition($oSibling->getLineNo(), $oSibling->getColNo() - 1); + } + } + if ($oRule->getLineNo() === 0 && $oRule->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); + } + } + + array_splice($this->aRules[$sRule], $iPosition, 0, [$oRule]); + } + + /** + * Returns all rules matching the given rule name + * + * @example $oRuleSet->getRules('font') // returns array(0 => $oRule, …) or array(). + * + * @example $oRuleSet->getRules('font-') + * //returns an array of all rules either beginning with font- or matching font. + * + * @param Rule|string|null $mRule + * Pattern to search for. If null, returns all rules. + * If the pattern ends with a dash, all rules starting with the pattern are returned + * as well as one matching the pattern with the dash excluded. + * Passing a Rule behaves like calling `getRules($mRule->getRule())`. + * + * @return array + */ + public function getRules($mRule = null) + { + if ($mRule instanceof Rule) { + $mRule = $mRule->getRule(); + } + /** @var array $aResult */ + $aResult = []; + 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. + if ( + !$mRule || $sName === $mRule + || ( + strrpos($mRule, '-') === strlen($mRule) - strlen('-') + && (strpos($sName, $mRule) === 0 || $sName === substr($mRule, 0, -1)) + ) + ) { + $aResult = array_merge($aResult, $aRules); + } + } + usort($aResult, function (Rule $first, Rule $second) { + if ($first->getLineNo() === $second->getLineNo()) { + return $first->getColNo() - $second->getColNo(); + } + return $first->getLineNo() - $second->getLineNo(); + }); + return $aResult; + } + + /** + * Overrides all the rules of this set. + * + * @param array $aRules The rules to override with. + * + * @return void + */ + public function setRules(array $aRules) + { + $this->aRules = []; + foreach ($aRules as $rule) { + $this->addRule($rule); + } + } + + /** + * Returns all rules matching the given pattern and returns them in an associative array with the rule’s name + * as keys. This method exists mainly for backwards-compatibility and is really only partially useful. + * + * Note: This method loses some information: Calling this (with an argument of `background-`) on a declaration block + * 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 + * 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 + */ + public function getRulesAssoc($mRule = null) + { + /** @var array $aResult */ + $aResult = []; + foreach ($this->getRules($mRule) as $oRule) { + $aResult[$oRule->getRule()] = $oRule; + } + return $aResult; + } + + /** + * 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 + */ + public function removeRule($mRule) + { + 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]); + } + } + } + } + + /** + * @return string + */ + public function __toString() + { + return $this->render(new OutputFormat()); + } + + /** + * @return string + */ + public function render(OutputFormat $oOutputFormat) + { + $sResult = ''; + $bIsFirst = true; + foreach ($this->aRules as $aRules) { + foreach ($aRules as $oRule) { + $sRendered = $oOutputFormat->safely(function () use ($oRule, $oOutputFormat) { + return $oRule->render($oOutputFormat->nextLevel()); + }); + if ($sRendered === null) { + continue; + } + if ($bIsFirst) { + $bIsFirst = false; + $sResult .= $oOutputFormat->nextLevel()->spaceBeforeRules(); + } else { + $sResult .= $oOutputFormat->nextLevel()->spaceBetweenRules(); + } + $sResult .= $sRendered; + } + } + + if (!$bIsFirst) { + // Had some output + $sResult .= $oOutputFormat->spaceAfterRules(); + } + + return $oOutputFormat->removeLastSemicolon($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; + } +} diff --git a/src/Settings.php b/src/Settings.php new file mode 100644 index 00000000..7b858096 --- /dev/null +++ b/src/Settings.php @@ -0,0 +1,89 @@ +bMultibyteSupport = extension_loaded('mbstring'); + } + + /** + * @return self new instance + */ + public static function create() + { + return new Settings(); + } + + /** + * @param bool $bMultibyteSupport + * + * @return self fluent interface + */ + public function withMultibyteSupport($bMultibyteSupport = true) + { + $this->bMultibyteSupport = $bMultibyteSupport; + return $this; + } + + /** + * @param string $sDefaultCharset + * + * @return self fluent interface + */ + public function withDefaultCharset($sDefaultCharset) + { + $this->sDefaultCharset = $sDefaultCharset; + return $this; + } + + /** + * @param bool $bLenientParsing + * + * @return self fluent interface + */ + public function withLenientParsing($bLenientParsing = true) + { + $this->bLenientParsing = $bLenientParsing; + return $this; + } + + /** + * @return self fluent interface + */ + public function beStrict() + { + return $this->withLenientParsing(false); + } +} diff --git a/src/Value/CSSFunction.php b/src/Value/CSSFunction.php new file mode 100644 index 00000000..e6b8c118 --- /dev/null +++ b/src/Value/CSSFunction.php @@ -0,0 +1,73 @@ + $aArguments + * @param string $sSeparator + * @param int $iLineNo + */ + public function __construct($sName, $aArguments, $sSeparator = ',', $iLineNo = 0) + { + if ($aArguments instanceof RuleValueList) { + $sSeparator = $aArguments->getListSeparator(); + $aArguments = $aArguments->getListComponents(); + } + $this->sName = $sName; + $this->iLineNo = $iLineNo; + parent::__construct($aArguments, $sSeparator, $iLineNo); + } + + /** + * @return string + */ + public function getName() + { + return $this->sName; + } + + /** + * @param string $sName + * + * @return void + */ + public function setName($sName) + { + $this->sName = $sName; + } + + /** + * @return array + */ + public function getArguments() + { + return $this->aComponents; + } + + /** + * @return string + */ + public function __toString() + { + return $this->render(new OutputFormat()); + } + + /** + * @return string + */ + public function render(OutputFormat $oOutputFormat) + { + $aArguments = parent::render($oOutputFormat); + return "{$this->sName}({$aArguments})"; + } +} diff --git a/src/Value/CSSString.php b/src/Value/CSSString.php new file mode 100644 index 00000000..9fafedd7 --- /dev/null +++ b/src/Value/CSSString.php @@ -0,0 +1,105 @@ +sString = $sString; + parent::__construct($iLineNo); + } + + /** + * @return CSSString + * + * @throws SourceException + * @throws UnexpectedEOFException + * @throws UnexpectedTokenException + */ + public static function parse(ParserState $oParserState) + { + $sBegin = $oParserState->peek(); + $sQuote = null; + if ($sBegin === "'") { + $sQuote = "'"; + } elseif ($sBegin === '"') { + $sQuote = '"'; + } + if ($sQuote !== null) { + $oParserState->consume($sQuote); + } + $sResult = ""; + $sContent = null; + if ($sQuote === null) { + // Unquoted strings end in whitespace or with braces, brackets, parentheses + while (!preg_match('/[\\s{}()<>\\[\\]]/isu', $oParserState->peek())) { + $sResult .= $oParserState->parseCharacter(false); + } + } else { + while (!$oParserState->comes($sQuote)) { + $sContent = $oParserState->parseCharacter(false); + if ($sContent === null) { + throw new SourceException( + "Non-well-formed quoted string {$oParserState->peek(3)}", + $oParserState->currentLine() + ); + } + $sResult .= $sContent; + } + $oParserState->consume($sQuote); + } + return new CSSString($sResult, $oParserState->currentLine()); + } + + /** + * @param string $sString + * + * @return void + */ + public function setString($sString) + { + $this->sString = $sString; + } + + /** + * @return string + */ + public function getString() + { + return $this->sString; + } + + /** + * @return string + */ + public function __toString() + { + return $this->render(new OutputFormat()); + } + + /** + * @return string + */ + public function render(OutputFormat $oOutputFormat) + { + $sString = addslashes($this->sString); + $sString = str_replace("\n", '\A', $sString); + return $oOutputFormat->getStringQuotingType() . $sString . $oOutputFormat->getStringQuotingType(); + } +} diff --git a/src/Value/CalcFunction.php b/src/Value/CalcFunction.php new file mode 100644 index 00000000..5c92e0c0 --- /dev/null +++ b/src/Value/CalcFunction.php @@ -0,0 +1,89 @@ +consumeUntil('(', false, true)); + $oCalcList = new CalcRuleValueList($oParserState->currentLine()); + $oList = new RuleValueList(',', $oParserState->currentLine()); + $iNestingLevel = 0; + $iLastComponentType = null; + while (!$oParserState->comes(')') || $iNestingLevel > 0) { + $oParserState->consumeWhiteSpace(); + if ($oParserState->comes('(')) { + $iNestingLevel++; + $oCalcList->addListComponent($oParserState->consume(1)); + $oParserState->consumeWhiteSpace(); + continue; + } elseif ($oParserState->comes(')')) { + $iNestingLevel--; + $oCalcList->addListComponent($oParserState->consume(1)); + $oParserState->consumeWhiteSpace(); + continue; + } + if ($iLastComponentType != CalcFunction::T_OPERAND) { + $oVal = Value::parsePrimitiveValue($oParserState); + $oCalcList->addListComponent($oVal); + $iLastComponentType = CalcFunction::T_OPERAND; + } else { + if (in_array($oParserState->peek(), $aOperators)) { + if (($oParserState->comes('-') || $oParserState->comes('+'))) { + if ( + $oParserState->peek(1, -1) != ' ' + || !($oParserState->comes('- ') + || $oParserState->comes('+ ')) + ) { + throw new UnexpectedTokenException( + " {$oParserState->peek()} ", + $oParserState->peek(1, -1) . $oParserState->peek(2), + 'literal', + $oParserState->currentLine() + ); + } + } + $oCalcList->addListComponent($oParserState->consume(1)); + $iLastComponentType = CalcFunction::T_OPERATOR; + } else { + throw new UnexpectedTokenException( + sprintf( + 'Next token was expected to be an operand of type %s. Instead "%s" was found.', + implode(', ', $aOperators), + $oVal + ), + '', + 'custom', + $oParserState->currentLine() + ); + } + } + $oParserState->consumeWhiteSpace(); + } + $oList->addListComponent($oCalcList); + $oParserState->consume(')'); + return new CalcFunction($sFunction, $oList, ',', $oParserState->currentLine()); + } +} diff --git a/src/Value/CalcRuleValueList.php b/src/Value/CalcRuleValueList.php new file mode 100644 index 00000000..7dbd26a1 --- /dev/null +++ b/src/Value/CalcRuleValueList.php @@ -0,0 +1,24 @@ +implode(' ', $this->aComponents); + } +} diff --git a/src/Value/Color.php b/src/Value/Color.php new file mode 100644 index 00000000..8dc52960 --- /dev/null +++ b/src/Value/Color.php @@ -0,0 +1,166 @@ + $aColor + * @param int $iLineNo + */ + public function __construct(array $aColor, $iLineNo = 0) + { + parent::__construct(implode('', array_keys($aColor)), $aColor, ',', $iLineNo); + } + + /** + * @return Color|CSSFunction + * + * @throws UnexpectedEOFException + * @throws UnexpectedTokenException + */ + public static function parse(ParserState $oParserState) + { + $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]; + } + + 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() + ), + ]; + } 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()), + ]; + } + } 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; + } + + $oParserState->consumeWhiteSpace(); + if ($i < ($iLength - 1)) { + $oParserState->consume(','); + } + } + $oParserState->consume(')'); + + if ($bContainsVar) { + return new CSSFunction($sColorMode, array_values($aColor), ',', $oParserState->currentLine()); + } + } + return new Color($aColor, $oParserState->currentLine()); + } + + /** + * @param float $fVal + * @param float $fFromMin + * @param float $fFromMax + * @param float $fToMin + * @param float $fToMax + * + * @return float + */ + private static function mapRange($fVal, $fFromMin, $fFromMax, $fToMin, $fToMax) + { + $fFromRange = $fFromMax - $fFromMin; + $fToRange = $fToMax - $fToMin; + $fMultiplier = $fToRange / $fFromRange; + $fNewVal = $fVal - $fFromMin; + $fNewVal *= $fMultiplier; + return $fNewVal + $fToMin; + } + + /** + * @return array + */ + public function getColor() + { + return $this->aComponents; + } + + /** + * @param array $aColor + * + * @return void + */ + public function setColor(array $aColor) + { + $this->setName(implode('', array_keys($aColor))); + $this->aComponents = $aColor; + } + + /** + * @return string + */ + public function getColorDescription() + { + return $this->getName(); + } + + /** + * @return string + */ + public function __toString() + { + return $this->render(new OutputFormat()); + } + + /** + * @return 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); + } + return parent::render($oOutputFormat); + } +} diff --git a/src/Value/LineName.php b/src/Value/LineName.php new file mode 100644 index 00000000..e231ce38 --- /dev/null +++ b/src/Value/LineName.php @@ -0,0 +1,65 @@ + $aComponents + * @param int $iLineNo + */ + public function __construct(array $aComponents = [], $iLineNo = 0) + { + parent::__construct($aComponents, ' ', $iLineNo); + } + + /** + * @return LineName + * + * @throws UnexpectedTokenException + * @throws UnexpectedEOFException + */ + public static function parse(ParserState $oParserState) + { + $oParserState->consume('['); + $oParserState->consumeWhiteSpace(); + $aNames = []; + do { + if ($oParserState->getSettings()->bLenientParsing) { + try { + $aNames[] = $oParserState->parseIdentifier(); + } catch (UnexpectedTokenException $e) { + if (!$oParserState->comes(']')) { + throw $e; + } + } + } else { + $aNames[] = $oParserState->parseIdentifier(); + } + $oParserState->consumeWhiteSpace(); + } while (!$oParserState->comes(']')); + $oParserState->consume(']'); + return new LineName($aNames, $oParserState->currentLine()); + } + + /** + * @return string + */ + public function __toString() + { + return $this->render(new OutputFormat()); + } + + /** + * @return string + */ + public function render(OutputFormat $oOutputFormat) + { + return '[' . parent::render(OutputFormat::createCompact()) . ']'; + } +} diff --git a/src/Value/PrimitiveValue.php b/src/Value/PrimitiveValue.php new file mode 100644 index 00000000..055a4397 --- /dev/null +++ b/src/Value/PrimitiveValue.php @@ -0,0 +1,14 @@ + + */ + const ABSOLUTE_SIZE_UNITS = ['px', 'cm', 'mm', 'mozmm', 'in', 'pt', 'pc', 'vh', 'vw', 'vmin', 'vmax', 'rem']; + + /** + * @var array + */ + const RELATIVE_SIZE_UNITS = ['%', 'em', 'ex', 'ch', 'fr']; + + /** + * @var array + */ + const NON_SIZE_UNITS = ['deg', 'grad', 'rad', 's', 'ms', 'turns', 'Hz', 'kHz']; + + /** + * @var array>|null + */ + private static $SIZE_UNITS = null; + + /** + * @var float + */ + private $fSize; + + /** + * @var string|null + */ + private $sUnit; + + /** + * @var bool + */ + private $bIsColorComponent; + + /** + * @param float|int|string $fSize + * @param string|null $sUnit + * @param bool $bIsColorComponent + * @param int $iLineNo + */ + public function __construct($fSize, $sUnit = null, $bIsColorComponent = false, $iLineNo = 0) + { + parent::__construct($iLineNo); + $this->fSize = (float)$fSize; + $this->sUnit = $sUnit; + $this->bIsColorComponent = $bIsColorComponent; + } + + /** + * @param bool $bIsColorComponent + * + * @return Size + * + * @throws UnexpectedEOFException + * @throws UnexpectedTokenException + */ + public static function parse(ParserState $oParserState, $bIsColorComponent = false) + { + $sSize = ''; + if ($oParserState->comes('-')) { + $sSize .= $oParserState->consume('-'); + } + while (is_numeric($oParserState->peek()) || $oParserState->comes('.')) { + if ($oParserState->comes('.')) { + $sSize .= $oParserState->consume('.'); + } else { + $sSize .= $oParserState->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); + break; + } + } + } + return new Size((float)$sSize, $sUnit, $bIsColorComponent, $oParserState->currentLine()); + } + + /** + * @return array> + */ + private static function getSizeUnits() + { + 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] = []; + } + self::$SIZE_UNITS[$iSize][strtolower($val)] = $val; + } + + krsort(self::$SIZE_UNITS, SORT_NUMERIC); + } + + return self::$SIZE_UNITS; + } + + /** + * @param string $sUnit + * + * @return void + */ + public function setUnit($sUnit) + { + $this->sUnit = $sUnit; + } + + /** + * @return string|null + */ + public function getUnit() + { + return $this->sUnit; + } + + /** + * @param float|int|string $fSize + */ + public function setSize($fSize) + { + $this->fSize = (float)$fSize; + } + + /** + * @return float + */ + public function getSize() + { + return $this->fSize; + } + + /** + * @return bool + */ + public function isColorComponent() + { + return $this->bIsColorComponent; + } + + /** + * 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. + */ + public function isSize() + { + if (in_array($this->sUnit, self::NON_SIZE_UNITS, true)) { + return false; + } + return !$this->isColorComponent(); + } + + /** + * @return bool + */ + public function isRelative() + { + if (in_array($this->sUnit, self::RELATIVE_SIZE_UNITS, true)) { + return true; + } + if ($this->sUnit === null && $this->fSize != 0) { + return true; + } + return false; + } + + /** + * @return string + */ + public function __toString() + { + return $this->render(new OutputFormat()); + } + + /** + * @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); + } +} diff --git a/src/Value/URL.php b/src/Value/URL.php new file mode 100644 index 00000000..1467d505 --- /dev/null +++ b/src/Value/URL.php @@ -0,0 +1,82 @@ +oURL = $oURL; + } + + /** + * @return URL + * + * @throws SourceException + * @throws UnexpectedEOFException + * @throws UnexpectedTokenException + */ + public static function parse(ParserState $oParserState) + { + $bUseUrl = $oParserState->comes('url', true); + if ($bUseUrl) { + $oParserState->consume('url'); + $oParserState->consumeWhiteSpace(); + $oParserState->consume('('); + } + $oParserState->consumeWhiteSpace(); + $oResult = new URL(CSSString::parse($oParserState), $oParserState->currentLine()); + if ($bUseUrl) { + $oParserState->consumeWhiteSpace(); + $oParserState->consume(')'); + } + return $oResult; + } + + /** + * @return void + */ + public function setURL(CSSString $oURL) + { + $this->oURL = $oURL; + } + + /** + * @return CSSString + */ + public function getURL() + { + return $this->oURL; + } + + /** + * @return string + */ + public function __toString() + { + return $this->render(new OutputFormat()); + } + + /** + * @return string + */ + public function render(OutputFormat $oOutputFormat) + { + return "url({$this->oURL->render($oOutputFormat)})"; + } +} diff --git a/src/Value/Value.php b/src/Value/Value.php new file mode 100644 index 00000000..66cb9fd4 --- /dev/null +++ b/src/Value/Value.php @@ -0,0 +1,198 @@ +iLineNo = $iLineNo; + } + + /** + * @param array $aListDelimiters + * + * @return RuleValueList|CSSFunction|CSSString|LineName|Size|URL|string + * + * @throws UnexpectedTokenException + * @throws UnexpectedEOFException + */ + public static function parseValue(ParserState $oParserState, array $aListDelimiters = []) + { + /** @var array $aStack */ + $aStack = []; + $oParserState->consumeWhiteSpace(); + //Build a list of delimiters and parsed values + while ( + !($oParserState->comes('}') || $oParserState->comes(';') || $oParserState->comes('!') + || $oParserState->comes(')') + || $oParserState->comes('\\')) + ) { + if (count($aStack) > 0) { + $bFoundDelimiter = false; + foreach ($aListDelimiters as $sDelimiter) { + if ($oParserState->comes($sDelimiter)) { + array_push($aStack, $oParserState->consume($sDelimiter)); + $oParserState->consumeWhiteSpace(); + $bFoundDelimiter = true; + break; + } + } + if (!$bFoundDelimiter) { + //Whitespace was the list delimiter + array_push($aStack, ' '); + } + } + array_push($aStack, self::parsePrimitiveValue($oParserState)); + $oParserState->consumeWhiteSpace(); + } + // Convert the list to list objects + foreach ($aListDelimiters as $sDelimiter) { + if (count($aStack) === 1) { + return $aStack[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]) { + break; + } + } + $oList = new RuleValueList($sDelimiter, $oParserState->currentLine()); + for ($i = $iStartPosition - 1; $i - $iStartPosition + 1 < $iLength * 2; $i += 2) { + $oList->addListComponent($aStack[$i]); + } + array_splice($aStack, $iStartPosition - 1, $iLength * 2 - 1, [$oList]); + } + } + if (!isset($aStack[0])) { + throw new UnexpectedTokenException( + " {$oParserState->peek()} ", + $oParserState->peek(1, -1) . $oParserState->peek(2), + 'literal', + $oParserState->currentLine() + ); + } + return $aStack[0]; + } + + /** + * @param bool $bIgnoreCase + * + * @return CSSFunction|string + * + * @throws UnexpectedEOFException + * @throws UnexpectedTokenException + */ + public static function parseIdentifierOrFunction(ParserState $oParserState, $bIgnoreCase = false) + { + $sResult = $oParserState->parseIdentifier($bIgnoreCase); + + if ($oParserState->comes('(')) { + $oParserState->consume('('); + $aArguments = Value::parseValue($oParserState, ['=', ' ', ',']); + $sResult = new CSSFunction($sResult, $aArguments, ',', $oParserState->currentLine()); + $oParserState->consume(')'); + } + + return $sResult; + } + + /** + * @return CSSFunction|CSSString|LineName|Size|URL|string + * + * @throws UnexpectedEOFException + * @throws UnexpectedTokenException + * @throws SourceException + */ + public static function parsePrimitiveValue(ParserState $oParserState) + { + $oValue = null; + $oParserState->consumeWhiteSpace(); + if ( + is_numeric($oParserState->peek()) + || ($oParserState->comes('-.') + && is_numeric($oParserState->peek(1, 2))) + || (($oParserState->comes('-') || $oParserState->comes('.')) && is_numeric($oParserState->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('url', true)) { + $oValue = URL::parse($oParserState); + } elseif ( + $oParserState->comes('calc', true) || $oParserState->comes('-webkit-calc', true) + || $oParserState->comes('-moz-calc', true) + ) { + $oValue = CalcFunction::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); + } else { + $oValue = self::parseIdentifierOrFunction($oParserState); + } + $oParserState->consumeWhiteSpace(); + return $oValue; + } + + /** + * @return CSSFunction + * + * @throws UnexpectedEOFException + * @throws UnexpectedTokenException + */ + private static function parseMicrosoftFilter(ParserState $oParserState) + { + $sFunction = $oParserState->consumeUntil('(', false, true); + $aArguments = Value::parseValue($oParserState, [',', '=']); + return new CSSFunction($sFunction, $aArguments, ',', $oParserState->currentLine()); + } + + /** + * @return string + * + * @throws UnexpectedEOFException + * @throws UnexpectedTokenException + */ + private static function parseUnicodeRangeValue(ParserState $oParserState) + { + $iCodepointMaxLength = 6; // Code points outside BMP can use up to six digits + $sRange = ""; + $oParserState->consume("U+"); + do { + if ($oParserState->comes('-')) { + $iCodepointMaxLength = 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}"; + } + + /** + * @return int + */ + public function getLineNo() + { + return $this->iLineNo; + } +} diff --git a/src/Value/ValueList.php b/src/Value/ValueList.php new file mode 100644 index 00000000..af5348b9 --- /dev/null +++ b/src/Value/ValueList.php @@ -0,0 +1,100 @@ + + */ + protected $aComponents; + + /** + * @var string + */ + protected $sSeparator; + + /** + * phpcs:ignore Generic.Files.LineLength + * @param array|RuleValueList|CSSFunction|CSSString|LineName|Size|URL|string $aComponents + * @param string $sSeparator + * @param int $iLineNo + */ + public function __construct($aComponents = [], $sSeparator = ',', $iLineNo = 0) + { + parent::__construct($iLineNo); + if (!is_array($aComponents)) { + $aComponents = [$aComponents]; + } + $this->aComponents = $aComponents; + $this->sSeparator = $sSeparator; + } + + /** + * @param RuleValueList|CSSFunction|CSSString|LineName|Size|URL|string $mComponent + * + * @return void + */ + public function addListComponent($mComponent) + { + $this->aComponents[] = $mComponent; + } + + /** + * @return array + */ + public function getListComponents() + { + return $this->aComponents; + } + + /** + * @param array $aComponents + * + * @return void + */ + public function setListComponents(array $aComponents) + { + $this->aComponents = $aComponents; + } + + /** + * @return string + */ + public function getListSeparator() + { + return $this->sSeparator; + } + + /** + * @param string $sSeparator + * + * @return void + */ + public function setListSeparator($sSeparator) + { + $this->sSeparator = $sSeparator; + } + + /** + * @return string + */ + public function __toString() + { + return $this->render(new OutputFormat()); + } + + /** + * @return string + */ + public function render(OutputFormat $oOutputFormat) + { + return $oOutputFormat->implode( + $oOutputFormat->spaceBeforeListArgumentSeparator($this->sSeparator) . $this->sSeparator + . $oOutputFormat->spaceAfterListArgumentSeparator($this->sSeparator), + $this->aComponents + ); + } +} diff --git a/tests/CSSList/AtRuleBlockListTest.php b/tests/CSSList/AtRuleBlockListTest.php new file mode 100644 index 00000000..b7f1610d --- /dev/null +++ b/tests/CSSList/AtRuleBlockListTest.php @@ -0,0 +1,53 @@ +> + */ + public function mediaRuleDataProvider() + { + return [ + 'without spaces around arguments' => ['@media(min-width: 768px){.class{color:red}}'], + 'with spaces around arguments' => ['@media (min-width: 768px) {.class{color:red}}'], + ]; + } + + /** + * @test + * + * @param string $css + * + * @dataProvider mediaRuleDataProvider + */ + public function parsesRuleNameOfMediaQueries($css) + { + $contents = (new Parser($css))->parse()->getContents(); + $atRuleBlockList = $contents[0]; + + self::assertSame('media', $atRuleBlockList->atRuleName()); + } + + /** + * @test + * + * @param string $css + * + * @dataProvider mediaRuleDataProvider + */ + public function parsesArgumentsOfMediaQueries($css) + { + $contents = (new Parser($css))->parse()->getContents(); + $atRuleBlockList = $contents[0]; + + self::assertSame('(min-width: 768px)', $atRuleBlockList->atRuleArgs()); + } +} diff --git a/tests/CSSList/DocumentTest.php b/tests/CSSList/DocumentTest.php new file mode 100644 index 00000000..c4071a25 --- /dev/null +++ b/tests/CSSList/DocumentTest.php @@ -0,0 +1,70 @@ +subject = new Document(); + } + + /** + * @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/OutputFormatTest.php b/tests/OutputFormatTest.php new file mode 100644 index 00000000..9e0d4c4e --- /dev/null +++ b/tests/OutputFormatTest.php @@ -0,0 +1,303 @@ +oParser = new Parser(self::TEST_CSS); + $this->oDocument = $this->oParser->parse(); + } + + /** + * @test + */ + public function plain() + { + 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() + ); + } + + /** + * @test + */ + public function compact() + { + 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()) + ); + } + + /** + * @test + */ + public function pretty() + { + self::assertSame(self::TEST_CSS, $this->oDocument->render(OutputFormat::createPretty())); + } + + /** + * @test + */ + public function spaceAfterListArgumentSeparator() + { + 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(" ")) + ); + } + + /** + * @test + */ + public function spaceAfterListArgumentSeparatorComplex() + { + 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", + '/' => '', + ' ' => '', + ])) + ); + } + + /** + * @test + */ + public function spaceAfterSelectorSeparator() + { + 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")) + ); + } + + /** + * @test + */ + public function stringQuotingType() + { + 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("'")) + ); + } + + /** + * @test + */ + public function rGBHashNotation() + { + 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)) + ); + } + + /** + * @test + */ + public function semicolonAfterLastRule() + { + 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)) + ); + } + + /** + * @test + */ + public function spaceAfterRuleName() + { + 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")) + ); + } + + /** + * @test + */ + public function spaceRules() + { + 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*Rules', "\n"))); + } + + /** + * @test + */ + public function spaceBlocks() + { + 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"))); + } + + /** + * @test + */ + public function spaceBoth() + { + 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*Rules', "\n")->set('Space*Blocks', "\n"))); + } + + /** + * @test + */ + public function spaceBetweenBlocks() + { + 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('')) + ); + } + + /** + * @test + */ + public function indentation() + { + 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*Rules', "\n") + ->set('Space*Blocks', "\n") + ->setIndentation(''))); + } + + /** + * @test + */ + public function spaceBeforeBraces() + { + 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('')) + ); + } + + /** + * @expectedException \Sabberworm\CSS\Parsing\OutputException + * + * @test + */ + public function ignoreExceptionsOff() + { + $aBlocks = $this->oDocument->getAllDeclarationBlocks(); + $oFirstBlock = $aBlocks[0]; + $oFirstBlock->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)) + ); + $oFirstBlock->removeSelector('.test'); + $this->oDocument->render(OutputFormat::create()->setIgnoreExceptions(false)); + } + + /** + * @test + */ + public function ignoreExceptionsOn() + { + $aBlocks = $this->oDocument->getAllDeclarationBlocks(); + $oFirstBlock = $aBlocks[0]; + $oFirstBlock->removeSelector('.main'); + $oFirstBlock->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)) + ); + } +} diff --git a/tests/ParserTest.php b/tests/ParserTest.php new file mode 100644 index 00000000..ab247c3d --- /dev/null +++ b/tests/ParserTest.php @@ -0,0 +1,1176 @@ +parse(); + + self::assertInstanceOf(Document::class, $document); + + $cssList = $document->getContents(); + self::assertCount(1, $cssList); + self::assertInstanceOf(RuleSet::class, $cssList[0]); + } + + /** + * @test + */ + public function files() + { + $sDirectory = __DIR__ . '/fixtures'; + if ($rHandle = opendir($sDirectory)) { + /* This is the correct way to loop over the directory. */ + while (false !== ($sFileName = readdir($rHandle))) { + if (strpos($sFileName, '.') === 0) { + continue; + } + if (strrpos($sFileName, '.css') !== strlen($sFileName) - strlen('.css')) { + continue; + } + if (strpos($sFileName, '-') === 0) { + // Either a file which SHOULD fail (at least in strict mode) + // or a future test of a as-of-now missing feature + continue; + } + $oParser = new Parser(file_get_contents($sDirectory . '/' . $sFileName)); + try { + self::assertNotEquals('', $oParser->parse()->render()); + } catch (\Exception $e) { + self::fail($e); + } + } + closedir($rHandle); + } + } + + /** + * @depends files + * + * @test + */ + public function colorParsing() + { + $oDoc = $this->parsedStructureForFile('colortest'); + foreach ($oDoc->getAllRuleSets() as $oRuleSet) { + if (!$oRuleSet instanceof DeclarationBlock) { + continue; + } + $sSelector = $oRuleSet->getSelectors(); + $sSelector = $sSelector[0]->getSelector(); + if ($sSelector === '#mine') { + $aColorRule = $oRuleSet->getRules('color'); + $oColor = $aColorRule[0]->getValue(); + self::assertSame('red', $oColor); + $aColorRule = $oRuleSet->getRules('background-'); + $oColor = $aColorRule[0]->getValue(); + 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(); + 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(); + 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(); + 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(); + 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(); + 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()); + } + } + foreach ($oDoc->getAllValues('color') as $sColor) { + self::assertSame('red', $sColor); + } + self::assertSame( + '#mine {color: red;border-color: #0a64e6;border-color: rgba(10,100,231,.3);outline-color: #222;' + . 'background-color: #232323;}' + . "\n" + . '#yours {background-color: hsl(220,10%,220%);background-color: hsla(220,10%,220%,.3);}' + . "\n" + . '#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: hsl(var(--some-hsl));}' + . "\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() + ); + } + + /** + * @test + */ + public function unicodeParsing() + { + $oDoc = $this->parsedStructureForFile('unicode'); + foreach ($oDoc->getAllDeclarationBlocks() as $oRuleSet) { + $sSelector = $oRuleSet->getSelectors(); + $sSelector = $sSelector[0]->getSelector(); + if (substr($sSelector, 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); + } + if ($sSelector == '.test-2') { + self::assertSame('"é"', $sString); + } + if ($sSelector == '.test-3') { + self::assertSame('" "', $sString); + } + if ($sSelector == '.test-4') { + self::assertSame('"𝄞"', $sString); + } + if ($sSelector == '.test-5') { + self::assertSame('"水"', $sString); + } + if ($sSelector == '.test-6') { + self::assertSame('"¥"', $sString); + } + if ($sSelector == '.test-7') { + self::assertSame('"\A"', $sString); + } + if ($sSelector == '.test-8') { + self::assertSame('"\"\""', $sString); + } + if ($sSelector == '.test-9') { + self::assertSame('"\"\\\'"', $sString); + } + if ($sSelector == '.test-10') { + self::assertSame('"\\\'\\\\"', $sString); + } + if ($sSelector == '.test-11') { + self::assertSame('"test"', $sString); + } + } + } + + /** + * @test + */ + public function unicodeRangeParsing() + { + $oDoc = $this->parsedStructureForFile('unicode-range'); + $sExpected = "@font-face {unicode-range: U+0100-024F,U+0259,U+1E??-2EFF,U+202F;}"; + self::assertSame($sExpected, $oDoc->render()); + } + + /** + * @test + */ + public function specificity() + { + $oDoc = $this->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')); + self::assertEquals( + [new Selector('#test .help', true), new Selector('#file', true)], + $oDoc->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', true), + new Selector('.help:hover', true), + new Selector('li.green', true), + new Selector('ol li::before', true), + ], $oDoc->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)); + } + + /** + * @test + */ + public function manipulation() + { + $oDoc = $this->parsedStructureForFile('atrules'); + self::assertSame( + '@charset "utf-8";' + . "\n" + . '@font-face {font-family: "CrassRoots";src: url("../media/cr.ttf");}' + . "\n" + . 'html, body {font-size: -.6em;}' + . "\n" + . '@keyframes mymove {from {top: 0px;}' + . "\n\t" + . 'to {top: 200px;}}' + . "\n" + . '@-moz-keyframes some-move {from {top: 0px;}' + . "\n\t" + . 'to {top: 200px;}}' + . "\n" + . '@supports ( (perspective: 10px) or (-moz-perspective: 10px) or (-webkit-perspective: 10px) or ' + . '(-ms-perspective: 10px) or (-o-perspective: 10px) ) {body {font-family: "Helvetica";}}' + . "\n" + . '@page :pseudo-class {margin: 2in;}' + . "\n" + . '@-moz-document url(https://www.w3.org/),' + . "\n" + . ' url-prefix(https://www.w3.org/Style/),' + . "\n" + . ' domain(mozilla.org),' + . "\n" + . ' regexp("https:.*") {body {color: purple;background: yellow;}}' + . "\n" + . '@media screen and (orientation: landscape) {@-ms-viewport {width: 1024px;height: 768px;}}' + . "\n" + . '@region-style #intro {p {color: blue;}}', + $oDoc->render() + ); + foreach ($oDoc->getAllDeclarationBlocks() as $oBlock) { + foreach ($oBlock->getSelectors() as $oSelector) { + //Loop over all selector parts (the comma-separated strings in a selector) and prepend the id + $oSelector->setSelector('#my_id ' . $oSelector->getSelector()); + } + } + self::assertSame( + '@charset "utf-8";' + . "\n" + . '@font-face {font-family: "CrassRoots";src: url("../media/cr.ttf");}' + . "\n" + . '#my_id html, #my_id body {font-size: -.6em;}' + . "\n" + . '@keyframes mymove {from {top: 0px;}' + . "\n\t" + . 'to {top: 200px;}}' + . "\n" + . '@-moz-keyframes some-move {from {top: 0px;}' + . "\n\t" + . 'to {top: 200px;}}' + . "\n" + . '@supports ( (perspective: 10px) or (-moz-perspective: 10px) or (-webkit-perspective: 10px) ' + . 'or (-ms-perspective: 10px) or (-o-perspective: 10px) ) {#my_id body {font-family: "Helvetica";}}' + . "\n" + . '@page :pseudo-class {margin: 2in;}' + . "\n" + . '@-moz-document url(https://www.w3.org/),' + . "\n" + . ' url-prefix(https://www.w3.org/Style/),' + . "\n" + . ' domain(mozilla.org),' + . "\n" + . ' regexp("https:.*") {#my_id body {color: purple;background: yellow;}}' + . "\n" + . '@media screen and (orientation: landscape) {@-ms-viewport {width: 1024px;height: 768px;}}' + . "\n" + . '@region-style #intro {#my_id p {color: blue;}}', + $oDoc->render() + ); + + $oDoc = $this->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;} +body {color: green;font: 75% "Lucida Grande","Trebuchet MS",Verdana,sans-serif;}', + $oDoc->render() + ); + foreach ($oDoc->getAllRuleSets() as $oRuleSet) { + $oRuleSet->removeRule('font-'); + } + self::assertSame( + '#header {margin: 10px 2em 1cm 2%;color: red !important;background-color: green;' + . 'background-color: rgba(0,128,0,.7);frequency: 30Hz;} +body {color: green;}', + $oDoc->render() + ); + foreach ($oDoc->getAllRuleSets() as $oRuleSet) { + $oRuleSet->removeRule('background-'); + } + self::assertSame( + '#header {margin: 10px 2em 1cm 2%;color: red !important;frequency: 30Hz;} +body {color: green;}', + $oDoc->render() + ); + } + + /** + * @test + */ + public function ruleGetters() + { + $oDoc = $this->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()); + } + + /** + * @test + */ + public function slashedValues() + { + $oDoc = $this->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() + ); + foreach ($oDoc->getAllValues(null) as $mValue) { + if ($mValue instanceof Size && $mValue->isSize() && !$mValue->isRelative()) { + $mValue->setSize($mValue->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()); + } + self::assertSame( + '.test {font: 36px/1.5 Verdana,Arial,sans-serif;border-radius: 15px 30px 15px 30px/30px 15px 30px 15px;}', + $oDoc->render() + ); + } + + /** + * @test + */ + public function functionSyntax() + { + $oDoc = $this->parsedStructureForFile('functions'); + $sExpected = '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;' + . '-moz-transform-origin: center 60%;}' + . "\n" + . '.collapser.expanded::before, .collapser.expanded::-moz-before,' + . ' .collapser.expanded::-webkit-before {-moz-transform: rotate(90deg);}' + . "\n" + . '.collapser + * {height: 0;overflow: hidden;-moz-transition-property: height;' + . '-moz-transition-duration: .3s;}' + . "\n" + . '.collapser.expanded + * {height: auto;}'; + self::assertSame($sExpected, $oDoc->render()); + + foreach ($oDoc->getAllValues(null, true) as $mValue) { + if ($mValue instanceof Size && $mValue->isSize()) { + $mValue->setSize($mValue->getSize() * 3); + } + } + $sExpected = str_replace(['1.2em', '.2em', '60%'], ['3.6em', '.6em', '180%'], $sExpected); + self::assertSame($sExpected, $oDoc->render()); + + foreach ($oDoc->getAllValues(null, true) as $mValue) { + if ($mValue instanceof Size && !$mValue->isRelative() && !$mValue->isColorComponent()) { + $mValue->setSize($mValue->getSize() * 2); + } + } + $sExpected = str_replace(['.2s', '.3s', '90deg'], ['.4s', '.6s', '180deg'], $sExpected); + self::assertSame($sExpected, $oDoc->render()); + } + + /** + * @test + */ + public function expandShorthands() + { + $oDoc = $this->parsedStructureForFile('expand-shorthands'); + $sExpected = 'body {font: italic 500 14px/1.618 "Trebuchet MS",Georgia,serif;border: 2px solid #f0f;' + . 'background: #ccc url("/images/foo.png") no-repeat left top;margin: 1em !important;' + . 'padding: 2px 6px 3px;}'; + self::assertSame($sExpected, $oDoc->render()); + $oDoc->expandShorthands(); + $sExpected = 'body {margin-top: 1em !important;margin-right: 1em !important;margin-bottom: 1em !important;' + . 'margin-left: 1em !important;padding-top: 2px;padding-right: 6px;padding-bottom: 3px;' + . 'padding-left: 6px;border-top-color: #f0f;border-right-color: #f0f;border-bottom-color: #f0f;' + . 'border-left-color: #f0f;border-top-style: solid;border-right-style: solid;' + . 'border-bottom-style: solid;border-left-style: solid;border-top-width: 2px;' + . 'border-right-width: 2px;border-bottom-width: 2px;border-left-width: 2px;font-style: italic;' + . 'font-variant: normal;font-weight: 500;font-size: 14px;line-height: 1.618;' + . 'font-family: "Trebuchet MS",Georgia,serif;background-color: #ccc;' + . 'background-image: url("/images/foo.png");background-repeat: no-repeat;background-attachment: scroll;' + . 'background-position: left top;}'; + self::assertSame($sExpected, $oDoc->render()); + } + + /** + * @test + */ + public function createShorthands() + { + $oDoc = $this->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 = $this->parsedStructureForFile('namespaces'); + $sExpected = '@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()); + } + + /** + * @test + */ + public function innerColors() + { + $oDoc = $this->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()); + } + + /** + * @test + */ + public function prefixedGradient() + { + $oDoc = $this->parsedStructureForFile('webkit'); + $sExpected = '.test {background: -webkit-linear-gradient(top right,white,black);}'; + self::assertSame($sExpected, $oDoc->render()); + } + + /** + * @test + */ + public function listValueRemoval() + { + $oDoc = $this->parsedStructureForFile('atrules'); + foreach ($oDoc->getContents() as $oItem) { + if ($oItem instanceof AtRule) { + $oDoc->remove($oItem); + continue; + } + } + self::assertSame('html, body {font-size: -.6em;}', $oDoc->render()); + + $oDoc = $this->parsedStructureForFile('nested'); + foreach ($oDoc->getAllDeclarationBlocks() as $oBlock) { + $oDoc->removeDeclarationBlockBySelector($oBlock, false); + break; + } + self::assertSame( + 'html {some-other: -test(val1);} +@media screen {html {some: -test(val2);}} +#unrelated {other: yes;}', + $oDoc->render() + ); + + $oDoc = $this->parsedStructureForFile('nested'); + foreach ($oDoc->getAllDeclarationBlocks() as $oBlock) { + $oDoc->removeDeclarationBlockBySelector($oBlock, true); + break; + } + self::assertSame( + '@media screen {html {some: -test(val2);}} +#unrelated {other: yes;}', + $oDoc->render() + ); + } + + /** + * @expectedException \Sabberworm\CSS\Parsing\OutputException + * + * @test + */ + public function selectorRemoval() + { + $oDoc = $this->parsedStructureForFile('1readme'); + $aBlocks = $oDoc->getAllDeclarationBlocks(); + $oBlock1 = $aBlocks[0]; + self::assertTrue($oBlock1->removeSelector('html')); + $sExpected = '@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')); + // This tries to output a declaration block without a selector and throws. + $oDoc->render(); + } + + /** + * @test + */ + public function comments() + { + $oDoc = $this->parsedStructureForFile('comments'); + $sExpected = '@import url("some/url.css") screen; +.foo, #bar {background-color: #000;} +@media screen {#foo.bar {position: absolute;}}'; + self::assertSame($sExpected, $oDoc->render()); + } + + /** + * @test + */ + public function urlInFile() + { + $oDoc = $this->parsedStructureForFile('url', Settings::create()->withMultibyteSupport(true)); + $sExpected = '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()); + } + + /** + * @test + */ + public function hexAlphaInFile() + { + $oDoc = $this->parsedStructureForFile('hex-alpha', Settings::create()->withMultibyteSupport(true)); + $sExpected = 'div {background: rgba(17,34,51,.27);} +div {background: rgba(17,34,51,.27);}'; + self::assertSame($sExpected, $oDoc->render()); + } + + /** + * @test + */ + public function calcInFile() + { + $oDoc = $this->parsedStructureForFile('calc', Settings::create()->withMultibyteSupport(true)); + $sExpected = '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 {width: calc(50% - ( ( 4% ) * .5 ));}'; + self::assertSame($sExpected, $oDoc->render()); + } + + /** + * @test + */ + public function calcNestedInFile() + { + $oDoc = $this->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()); + } + + /** + * @test + */ + public function gridLineNameInFile() + { + $oDoc = $this->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()); + } + + /** + * @test + */ + public function emptyGridLineNameLenientInFile() + { + $oDoc = $this->parsedStructureForFile('empty-grid-linename'); + $sExpected = '.test {grid-template-columns: [] 100px;}'; + self::assertSame($sExpected, $oDoc->render()); + } + + /** + * @test + */ + public function invalidGridLineNameInFile() + { + $oDoc = $this->parsedStructureForFile('invalid-grid-linename', Settings::create()->withMultibyteSupport(true)); + $sExpected = "div {}"; + self::assertSame($sExpected, $oDoc->render()); + } + + /** + * @test + */ + public function unmatchedBracesInFile() + { + $oDoc = $this->parsedStructureForFile('unmatched_braces', Settings::create()->withMultibyteSupport(true)); + $sExpected = 'button, input, checkbox, textarea {outline: 0;margin: 0;}'; + self::assertSame($sExpected, $oDoc->render()); + } + + /** + * @test + */ + public function invalidSelectorsInFile() + { + $oDoc = $this->parsedStructureForFile('invalid-selectors', Settings::create()->withMultibyteSupport(true)); + $sExpected = '@keyframes mymove {from {top: 0px;}} +#test {color: white;background: green;} +#test {display: block;background: white;color: black;}'; + self::assertSame($sExpected, $oDoc->render()); + + $oDoc = $this->parsedStructureForFile('invalid-selectors-2', Settings::create()->withMultibyteSupport(true)); + $sExpected = '@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()); + } + + /** + * @test + */ + public function selectorEscapesInFile() + { + $oDoc = $this->parsedStructureForFile('selector-escapes', Settings::create()->withMultibyteSupport(true)); + $sExpected = '#\# {color: red;} +.col-sm-1\/5 {width: 20%;}'; + self::assertSame($sExpected, $oDoc->render()); + + $oDoc = $this->parsedStructureForFile('invalid-selectors-2', Settings::create()->withMultibyteSupport(true)); + $sExpected = '@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()); + } + + /** + * @test + */ + public function identifierEscapesInFile() + { + $oDoc = $this->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()); + } + + /** + * @test + */ + public function selectorIgnoresInFile() + { + $oDoc = $this->parsedStructureForFile('selector-ignores', Settings::create()->withMultibyteSupport(true)); + $sExpected = '.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()); + } + + /** + * @test + */ + public function keyframeSelectors() + { + $oDoc = $this->parsedStructureForFile( + 'keyframe-selector-validation', + Settings::create()->withMultibyteSupport(true) + ); + $sExpected = '@-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()); + } + + /** + * @expectedException \Sabberworm\CSS\Parsing\UnexpectedTokenException + * + * @test + */ + public function lineNameFailure() + { + $this->parsedStructureForFile('-empty-grid-linename', Settings::create()->withLenientParsing(false)); + } + + /** + * @expectedException \Sabberworm\CSS\Parsing\UnexpectedTokenException + * + * @test + */ + public function calcFailure() + { + $this->parsedStructureForFile('-calc-no-space-around-minus', Settings::create()->withLenientParsing(false)); + } + + /** + * @test + */ + public function urlInFileMbOff() + { + $oDoc = $this->parsedStructureForFile('url', Settings::create()->withMultibyteSupport(false)); + $sExpected = '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()); + } + + /** + * @test + */ + public function emptyFile() + { + $oDoc = $this->parsedStructureForFile('-empty', Settings::create()->withMultibyteSupport(true)); + $sExpected = ''; + self::assertSame($sExpected, $oDoc->render()); + } + + /** + * @test + */ + public function emptyFileMbOff() + { + $oDoc = $this->parsedStructureForFile('-empty', Settings::create()->withMultibyteSupport(false)); + $sExpected = ''; + self::assertSame($sExpected, $oDoc->render()); + } + + /** + * @test + */ + public function charsetLenient1() + { + $oDoc = $this->parsedStructureForFile('-charset-after-rule', Settings::create()->withLenientParsing(true)); + $sExpected = '#id {prop: var(--val);}'; + self::assertSame($sExpected, $oDoc->render()); + } + + /** + * @test + */ + public function charsetLenient2() + { + $oDoc = $this->parsedStructureForFile('-charset-in-block', Settings::create()->withLenientParsing(true)); + $sExpected = '@media print {}'; + self::assertSame($sExpected, $oDoc->render()); + } + + /** + * @test + */ + public function trailingWhitespace() + { + $oDoc = $this->parsedStructureForFile('trailing-whitespace', Settings::create()->withLenientParsing(false)); + $sExpected = 'div {width: 200px;}'; + self::assertSame($sExpected, $oDoc->render()); + } + + /** + * @expectedException \Sabberworm\CSS\Parsing\UnexpectedTokenException + * + * @test + */ + public function charsetFailure1() + { + $this->parsedStructureForFile('-charset-after-rule', Settings::create()->withLenientParsing(false)); + } + + /** + * @expectedException \Sabberworm\CSS\Parsing\UnexpectedTokenException + * + * @test + */ + public function charsetFailure2() + { + $this->parsedStructureForFile('-charset-in-block', Settings::create()->withLenientParsing(false)); + } + + /** + * @expectedException \Sabberworm\CSS\Parsing\SourceException + * + * @test + */ + public function unopenedClosingBracketFailure() + { + $this->parsedStructureForFile('-unopened-close-brackets', Settings::create()->withLenientParsing(false)); + } + + /** + * Ensure that a missing property value raises an exception. + * + * @expectedException \Sabberworm\CSS\Parsing\UnexpectedTokenException + * @covers \Sabberworm\CSS\Value\Value::parseValue() + * + * @test + */ + public function missingPropertyValueStrict() + { + $this->parsedStructureForFile('missing-property-value', Settings::create()->withLenientParsing(false)); + } + + /** + * Ensure that a missing property value is ignored when in lenient parsing mode. + * + * @covers \Sabberworm\CSS\Value\Value::parseValue() + * + * @test + */ + public function missingPropertyValueLenient() + { + $parsed = $this->parsedStructureForFile('missing-property-value', Settings::create()->withLenientParsing(true)); + $rulesets = $parsed->getAllRuleSets(); + self::assertCount(1, $rulesets); + $block = $rulesets[0]; + self::assertTrue($block instanceof DeclarationBlock); + self::assertEquals([new Selector('div')], $block->getSelectors()); + $rules = $block->getRules(); + self::assertCount(1, $rules); + $rule = $rules[0]; + self::assertSame('display', $rule->getRule()); + self::assertSame('inline-block', $rule->getValue()); + } + + /** + * Parses structure for file. + * + * @param string $sFileName + * @param Settings|null $oSettings + * + * @return Document parsed document + */ + private function parsedStructureForFile($sFileName, $oSettings = null) + { + $sFile = __DIR__ . "/fixtures/$sFileName.css"; + $oParser = new Parser(file_get_contents($sFile), $oSettings); + return $oParser->parse(); + } + + /** + * @depends files + * + * @test + */ + public function lineNumbersParsing() + { + $oDoc = $this->parsedStructureForFile('line-numbers'); + // array key is the expected line number + $aExpected = [ + 1 => [Charset::class], + 3 => [CSSNamespace::class], + 5 => [AtRuleSet::class], + 11 => [DeclarationBlock::class], + // Line Numbers of the inner declaration blocks + 17 => [KeyFrame::class, 18, 20], + 23 => [Import::class], + 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(); + } + } + } + + $aUrlExpected = [7, 26]; // expected line numbers + $aUrlActual = []; + foreach ($oDoc->getAllValues() as $oValue) { + if ($oValue instanceof URL) { + $aUrlActual[] = $oValue->getLineNo(); + } + } + + // Checking for the multiline color rule lines 27-31 + $aExpectedColorLines = [28, 29, 30]; + $aDeclBlocks = $oDoc->getAllDeclarationBlocks(); + // Choose the 2nd one + $oDeclBlock = $aDeclBlocks[1]; + $aRules = $oDeclBlock->getRules(); + // Choose the 2nd one + $oColor = $aRules[1]->getValue(); + self::assertSame(27, $aRules[1]->getLineNo()); + + $aActualColorLines = []; + foreach ($oColor->getColor() as $oSize) { + $aActualColorLines[] = $oSize->getLineNo(); + } + + self::assertSame($aExpectedColorLines, $aActualColorLines); + self::assertSame($aUrlExpected, $aUrlActual); + self::assertSame($aExpected, $aActual); + } + + /** + * @expectedException \Sabberworm\CSS\Parsing\UnexpectedTokenException + * + * @test + */ + public function unexpectedTokenExceptionLineNo() + { + $oParser = new Parser("\ntest: 1;", Settings::create()->beStrict()); + try { + $oParser->parse(); + } catch (UnexpectedTokenException $e) { + self::assertSame(2, $e->getLineNo()); + throw $e; + } + } + + /** + * @expectedException \Sabberworm\CSS\Parsing\UnexpectedTokenException + * + * @test + */ + public function ieHacksStrictParsing() + { + // We can't strictly parse IE hacks. + $this->parsedStructureForFile('ie-hacks', Settings::create()->beStrict()); + } + + /** + * @test + */ + public function ieHacksParsing() + { + $oDoc = $this->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() + { + $oDoc = $this->parsedStructureForFile('comments'); + $aNodes = $oDoc->getContents(); + + // Import property. + $importComments = $aNodes[0]->getComments(); + self::assertCount(1, $importComments); + self::assertSame("*\n * Comments Hell.\n ", $importComments[0]->getComment()); + + // Declaration block. + $fooBarBlock = $aNodes[1]; + $fooBarBlockComments = $fooBarBlock->getComments(); + // TODO Support comments in selectors. + // $this->assertCount(2, $fooBarBlockComments); + // $this->assertSame("* Number 4 *", $fooBarBlockComments[0]->getComment()); + // $this->assertSame("* Number 5 *", $fooBarBlockComments[1]->getComment()); + + // Declaration rules. + $fooBarRules = $fooBarBlock->getRules(); + $fooBarRule = $fooBarRules[0]; + $fooBarRuleComments = $fooBarRule->getComments(); + self::assertCount(1, $fooBarRuleComments); + self::assertSame(" Number 6 ", $fooBarRuleComments[0]->getComment()); + + // Media property. + $mediaComments = $aNodes[2]->getComments(); + self::assertCount(0, $mediaComments); + + // Media children. + $mediaRules = $aNodes[2]->getContents(); + $fooBarComments = $mediaRules[0]->getComments(); + self::assertCount(1, $fooBarComments); + self::assertSame("* Number 10 *", $fooBarComments[0]->getComment()); + + // Media -> declaration -> rule. + $fooBarRules = $mediaRules[0]->getRules(); + $fooBarChildComments = $fooBarRules[0]->getComments(); + self::assertCount(1, $fooBarChildComments); + self::assertSame("* Number 10b *", $fooBarChildComments[0]->getComment()); + } + + /** + * @test + */ + public function flatCommentExtracting() + { + $parser = new Parser('div {/*Find Me!*/left:10px; text-align:left;}'); + $doc = $parser->parse(); + $contents = $doc->getContents(); + $divRules = $contents[0]->getRules(); + $comments = $divRules[0]->getComments(); + self::assertCount(1, $comments); + self::assertSame("Find Me!", $comments[0]->getComment()); + } + + /** + * @test + */ + public function topLevelCommentExtracting() + { + $parser = new Parser('/*Find Me!*/div {left:10px; text-align:left;}'); + $doc = $parser->parse(); + $contents = $doc->getContents(); + $comments = $contents[0]->getComments(); + self::assertCount(1, $comments); + self::assertSame("Find Me!", $comments[0]->getComment()); + } + + /** + * @expectedException \Sabberworm\CSS\Parsing\UnexpectedTokenException + * + * @test + */ + public function microsoftFilterStrictParsing() + { + $oDoc = $this->parsedStructureForFile('ms-filter', Settings::create()->beStrict()); + } + + /** + * @test + */ + public function microsoftFilterParsing() + { + $oDoc = $this->parsedStructureForFile('ms-filter'); + $sExpected = '.test {filter: progid:DXImageTransform.Microsoft.gradient(startColorstr="#80000000",' + . 'endColorstr="#00000000",GradientType=1);}'; + self::assertSame($sExpected, $oDoc->render()); + } + + /** + * @test + */ + public function largeSizeValuesInFile() + { + $oDoc = $this->parsedStructureForFile('large-z-index', Settings::create()->withMultibyteSupport(false)); + $sExpected = '.overlay {z-index: 10000000000000000000000;}'; + self::assertSame($sExpected, $oDoc->render()); + } + + /** + * @test + */ + public function lonelyImport() + { + $oDoc = $this->parsedStructureForFile('lonely-import'); + $sExpected = "@import url(\"example.css\") only screen and (max-width: 600px);"; + self::assertSame($sExpected, $oDoc->render()); + } +} diff --git a/tests/RuleSet/DeclarationBlockTest.php b/tests/RuleSet/DeclarationBlockTest.php new file mode 100644 index 00000000..49526952 --- /dev/null +++ b/tests/RuleSet/DeclarationBlockTest.php @@ -0,0 +1,453 @@ +parse(); + foreach ($oDoc->getAllDeclarationBlocks() as $oDeclaration) { + $oDeclaration->expandBorderShorthand(); + } + self::assertSame(trim((string)$oDoc), $sExpected); + } + + /** + * @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;}'], + ]; + } + + /** + * @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); + } + + /** + * @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) + { + $oParser = new Parser($sCss); + $oDoc = $oParser->parse(); + foreach ($oDoc->getAllDeclarationBlocks() as $oDeclaration) { + $oDeclaration->expandBackgroundShorthand(); + } + self::assertSame(trim((string)$oDoc), $sExpected); + } + + /** + * @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;}', + ], + ]; + } + + /** + * @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); + } + + /** + * @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;}', + ], + ]; + } + + /** + * @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); + } + + /** + * @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;}'], + ]; + } + + /** + * @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); + } + + /** + * @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;}'], + ]; + } + + /** + * @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); + } + + /** + * @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;}', + ], + ]; + } + + /** + * @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); + } + + /** + * @return array> + */ + public function createBackgroundShorthandProvider() + { + 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;}', + ], + ]; + } + + /** + * @test + */ + 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]; + + $oSecond = $oWrapper->getRules('text-'); + self::assertCount(1, $oSecond); + $oSecond = $oSecond[0]; + + $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() + ); + } + + /** + * @test + * + * TODO: The order is different on PHP 5.6 than on PHP >= 7.0. + */ + public function orderOfElementsMatchingOriginalOrderAfterExpandingShorthands() + { + $sCss = '.rule{padding:5px;padding-top: 20px}'; + $oParser = new Parser($sCss); + $oDoc = $oParser->parse(); + $aDocs = $oDoc->getAllDeclarationBlocks(); + + self::assertCount(1, $aDocs); + + $oDeclaration = array_pop($aDocs); + $oDeclaration->expandShorthands(); + + self::assertEquals( + [ + 'padding-top' => 'padding-top: 20px;', + 'padding-right' => 'padding-right: 5px;', + 'padding-bottom' => 'padding-bottom: 5px;', + 'padding-left' => 'padding-left: 5px;', + ], + array_map('strval', $oDeclaration->getRulesAssoc()) + ); + } +} diff --git a/tests/RuleSet/LenientParsingTest.php b/tests/RuleSet/LenientParsingTest.php new file mode 100644 index 00000000..65218db4 --- /dev/null +++ b/tests/RuleSet/LenientParsingTest.php @@ -0,0 +1,133 @@ +beStrict()); + $oParser->parse(); + } + + /** + * @test + */ + public function faultToleranceOn() + { + $sFile = __DIR__ . '/../fixtures/-fault-tolerance.css'; + $oParser = new Parser(file_get_contents($sFile), Settings::create()->withLenientParsing(true)); + $oResult = $oParser->parse(); + self::assertSame( + '.test1 {}' . "\n" . '.test2 {hello: 2.2;hello: 2000000000000.2;}' . "\n" . '#test {}' . "\n" + . '#test2 {help: none;}', + $oResult->render() + ); + } + + /** + * @expectedException \Sabberworm\CSS\Parsing\UnexpectedTokenException + * + * @test + */ + public function endToken() + { + $sFile = __DIR__ . '/../fixtures/-end-token.css'; + $oParser = new Parser(file_get_contents($sFile), Settings::create()->beStrict()); + $oParser->parse(); + } + + /** + * @expectedException \Sabberworm\CSS\Parsing\UnexpectedTokenException + * + * @test + */ + public function endToken2() + { + $sFile = __DIR__ . '/../fixtures/-end-token-2.css'; + $oParser = new Parser(file_get_contents($sFile), Settings::create()->beStrict()); + $oParser->parse(); + } + + /** + * @test + */ + public function endTokenPositive() + { + $sFile = __DIR__ . '/../fixtures/-end-token.css'; + $oParser = new Parser(file_get_contents($sFile), Settings::create()->withLenientParsing(true)); + $oResult = $oParser->parse(); + self::assertSame("", $oResult->render()); + } + + /** + * @test + */ + public function endToken2Positive() + { + $sFile = __DIR__ . '/../fixtures/-end-token-2.css'; + $oParser = new Parser(file_get_contents($sFile), Settings::create()->withLenientParsing(true)); + $oResult = $oParser->parse(); + self::assertSame( + '#home .bg-layout {background-image: url("/bundles/main/img/bg1.png?5");}', + $oResult->render() + ); + } + + /** + * @test + */ + public function localeTrap() + { + 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(); + self::assertSame( + '.test1 {}' . "\n" . '.test2 {hello: 2.2;hello: 2000000000000.2;}' . "\n" . '#test {}' . "\n" + . '#test2 {help: none;}', + $oResult->render() + ); + } + + /** + * @test + */ + public function caseInsensitivity() + { + $sFile = __DIR__ . '/../fixtures/case-insensitivity.css'; + $oParser = new Parser(file_get_contents($sFile)); + $oResult = $oParser->parse(); + + self::assertSame( + '@charset "utf-8";' . "\n" + . '@import url("test.css");' + . "\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() + ); + } +} diff --git a/tests/Sabberworm/CSS/CSSList/AtRuleBlockListTest.php b/tests/Sabberworm/CSS/CSSList/AtRuleBlockListTest.php deleted file mode 100644 index 551d0ea5..00000000 --- a/tests/Sabberworm/CSS/CSSList/AtRuleBlockListTest.php +++ /dev/null @@ -1,27 +0,0 @@ -parse(); - $aContents = $oDoc->getContents(); - $oMediaQuery = $aContents[0]; - $this->assertSame('media', $oMediaQuery->atRuleName(), 'Does not interpret the type as a function'); - $this->assertSame('(min-width: 768px)', $oMediaQuery->atRuleArgs(), 'The media query is the value'); - - $sCss = '@media (min-width: 768px) {.class{color:red}}'; - $oParser = new Parser($sCss); - $oDoc = $oParser->parse(); - $aContents = $oDoc->getContents(); - $oMediaQuery = $aContents[0]; - $this->assertSame('media', $oMediaQuery->atRuleName(), 'Does not interpret the type as a function'); - $this->assertSame('(min-width: 768px)', $oMediaQuery->atRuleArgs(), 'The media query is the value'); - } - -} diff --git a/tests/Sabberworm/CSS/CSSList/DocumentTest.php b/tests/Sabberworm/CSS/CSSList/DocumentTest.php deleted file mode 100644 index 00395809..00000000 --- a/tests/Sabberworm/CSS/CSSList/DocumentTest.php +++ /dev/null @@ -1,26 +0,0 @@ -parse(); - $aContents = $oDoc->getContents(); - $this->assertCount(1, $aContents); - - $sCss2 = '.otherthing { right: 10px; }'; - $oParser2 = new Parser($sCss); - $oDoc2 = $oParser2->parse(); - $aContents2 = $oDoc2->getContents(); - - $oDoc->setContents(array($aContents[0], $aContents2[0])); - $aFinalContents = $oDoc->getContents(); - $this->assertCount(2, $aFinalContents); - } - -} diff --git a/tests/Sabberworm/CSS/OutputFormatTest.php b/tests/Sabberworm/CSS/OutputFormatTest.php deleted file mode 100644 index 238b5ba5..00000000 --- a/tests/Sabberworm/CSS/OutputFormatTest.php +++ /dev/null @@ -1,170 +0,0 @@ -oParser = new Parser($TEST_CSS); - $this->oDocument = $this->oParser->parse(); - } - - public function testPlain() { - $this->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()); - } - - public function testCompact() { - $this->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())); - } - - public function testPretty() { - global $TEST_CSS; - $this->assertSame($TEST_CSS, $this->oDocument->render(OutputFormat::createPretty())); - } - - public function testSpaceAfterListArgumentSeparator() { - $this->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()->setSpaceAfterListArgumentSeparator(" "))); - } - - public function testSpaceAfterListArgumentSeparatorComplex() { - $this->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()->setSpaceAfterListArgumentSeparator(array('default' => ' ', ',' => "\t", '/' => '', ' ' => '')))); - } - - public function testSpaceAfterSelectorSeparator() { - $this->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"))); - } - - public function testStringQuotingType() { - $this->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("'"))); - } - - public function testRGBHashNotation() { - $this->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))); - } - - public function testSemicolonAfterLastRule() { - $this->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))); - } - - public function testSpaceAfterRuleName() { - $this->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"))); - } - - public function testSpaceRules() { - $this->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*Rules', "\n"))); - } - - public function testSpaceBlocks() { - $this->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"))); - } - - public function testSpaceBoth() { - $this->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*Rules', "\n")->set('Space*Blocks', "\n"))); - } - - public function testSpaceBetweenBlocks() { - $this->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(''))); - } - - public function testIndentation() { - $this->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*Rules', "\n")->set('Space*Blocks', "\n")->setIndentation(''))); - } - - public function testSpaceBeforeBraces() { - $this->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(''))); - } - - /** - * @expectedException Sabberworm\CSS\Parsing\OutputException - */ - public function testIgnoreExceptionsOff() { - $aBlocks = $this->oDocument->getAllDeclarationBlocks(); - $oFirstBlock = $aBlocks[0]; - $oFirstBlock->removeSelector('.main'); - $this->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))); - $oFirstBlock->removeSelector('.test'); - $this->oDocument->render(OutputFormat::create()->setIgnoreExceptions(false)); - } - - public function testIgnoreExceptionsOn() { - $aBlocks = $this->oDocument->getAllDeclarationBlocks(); - $oFirstBlock = $aBlocks[0]; - $oFirstBlock->removeSelector('.main'); - $oFirstBlock->removeSelector('.test'); - $this->assertSame('@media screen {.main {background-size: 100% 100%;font-size: 1.3em;background-color: #fff;}}', $this->oDocument->render(OutputFormat::create()->setIgnoreExceptions(true))); - } - -} \ No newline at end of file diff --git a/tests/Sabberworm/CSS/ParserTest.php b/tests/Sabberworm/CSS/ParserTest.php deleted file mode 100644 index d9f3ec64..00000000 --- a/tests/Sabberworm/CSS/ParserTest.php +++ /dev/null @@ -1,707 +0,0 @@ -assertNotEquals('', $oParser->parse()->render()); - } catch (\Exception $e) { - $this->fail($e); - } - } - closedir($rHandle); - } - } - - /** - * @depends testFiles - */ - function testColorParsing() { - $oDoc = $this->parsedStructureForFile('colortest'); - foreach ($oDoc->getAllRuleSets() as $oRuleSet) { - if (!$oRuleSet instanceof DeclarationBlock) { - continue; - } - $sSelector = $oRuleSet->getSelectors(); - $sSelector = $sSelector[0]->getSelector(); - if ($sSelector === '#mine') { - $aColorRule = $oRuleSet->getRules('color'); - $oColor = $aColorRule[0]->getValue(); - $this->assertSame('red', $oColor); - $aColorRule = $oRuleSet->getRules('background-'); - $oColor = $aColorRule[0]->getValue(); - $this->assertEquals(array('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(); - $this->assertEquals(array('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(); - $this->assertEquals(array('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(); - $this->assertEquals(array('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()); - } else if($sSelector === '#yours') { - $aColorRule = $oRuleSet->getRules('background-color'); - $oColor = $aColorRule[0]->getValue(); - $this->assertEquals(array('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(); - $this->assertEquals(array('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()); - } - } - foreach ($oDoc->getAllValues('color') as $sColor) { - $this->assertSame('red', $sColor); - } - $this->assertSame('#mine {color: red;border-color: #0a64e6;border-color: rgba(10,100,231,.3);outline-color: #222;background-color: #232323;} -#yours {background-color: hsl(220,10%,220%);background-color: hsla(220,10%,220%,.3);}', $oDoc->render()); - } - - function testUnicodeParsing() { - $oDoc = $this->parsedStructureForFile('unicode'); - foreach ($oDoc->getAllDeclarationBlocks() as $oRuleSet) { - $sSelector = $oRuleSet->getSelectors(); - $sSelector = $sSelector[0]->getSelector(); - if (substr($sSelector, 0, strlen('.test-')) !== '.test-') { - continue; - } - $aContentRules = $oRuleSet->getRules('content'); - $aContents = $aContentRules[0]->getValues(); - $sString = $aContents[0][0]->__toString(); - if ($sSelector == '.test-1') { - $this->assertSame('" "', $sString); - } - if ($sSelector == '.test-2') { - $this->assertSame('"é"', $sString); - } - if ($sSelector == '.test-3') { - $this->assertSame('" "', $sString); - } - if ($sSelector == '.test-4') { - $this->assertSame('"𝄞"', $sString); - } - if ($sSelector == '.test-5') { - $this->assertSame('"水"', $sString); - } - if ($sSelector == '.test-6') { - $this->assertSame('"¥"', $sString); - } - if ($sSelector == '.test-7') { - $this->assertSame('"\A"', $sString); - } - if ($sSelector == '.test-8') { - $this->assertSame('"\"\""', $sString); - } - if ($sSelector == '.test-9') { - $this->assertSame('"\"\\\'"', $sString); - } - if ($sSelector == '.test-10') { - $this->assertSame('"\\\'\\\\"', $sString); - } - if ($sSelector == '.test-11') { - $this->assertSame('"test"', $sString); - } - } - } - - function testUnicodeRangeParsing() { - $oDoc = $this->parsedStructureForFile('unicode-range'); - $sExpected = "@font-face {unicode-range: U+0100-024F,U+0259,U+1E??-2EFF,U+202F;}"; - $this->assertSame($sExpected, $oDoc->render()); - } - - function testSpecificity() { - $oDoc = $this->parsedStructureForFile('specificity'); - $oDeclarationBlock = $oDoc->getAllDeclarationBlocks(); - $oDeclarationBlock = $oDeclarationBlock[0]; - $aSelectors = $oDeclarationBlock->getSelectors(); - foreach ($aSelectors as $oSelector) { - switch ($oSelector->getSelector()) { - case "#test .help": - $this->assertSame(110, $oSelector->getSpecificity()); - break; - case "#file": - $this->assertSame(100, $oSelector->getSpecificity()); - break; - case ".help:hover": - $this->assertSame(20, $oSelector->getSpecificity()); - break; - case "ol li::before": - $this->assertSame(3, $oSelector->getSpecificity()); - break; - case "li.green": - $this->assertSame(11, $oSelector->getSpecificity()); - break; - default: - $this->fail("specificity: untested selector " . $oSelector->getSelector()); - } - } - $this->assertEquals(array(new Selector('#test .help', true)), $oDoc->getSelectorsBySpecificity('> 100')); - } - - function testManipulation() { - $oDoc = $this->parsedStructureForFile('atrules'); - $this->assertSame('@charset "utf-8"; -@font-face {font-family: "CrassRoots";src: url("../media/cr.ttf");} -html, body {font-size: -.6em;} -@keyframes mymove {from {top: 0px;} - to {top: 200px;}} -@-moz-keyframes some-move {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";}} -@page :pseudo-class {margin: 2in;} -@-moz-document url(http://www.w3.org/), - url-prefix(http://www.w3.org/Style/), - domain(mozilla.org), - regexp("https:.*") {body {color: purple;background: yellow;}} -@media screen and (orientation: landscape) {@-ms-viewport {width: 1024px;height: 768px;}} -@region-style #intro {p {color: blue;}}', $oDoc->render()); - foreach ($oDoc->getAllDeclarationBlocks() as $oBlock) { - foreach ($oBlock->getSelectors() as $oSelector) { - //Loop over all selector parts (the comma-separated strings in a selector) and prepend the id - $oSelector->setSelector('#my_id ' . $oSelector->getSelector()); - } - } - $this->assertSame('@charset "utf-8"; -@font-face {font-family: "CrassRoots";src: url("../media/cr.ttf");} -#my_id html, #my_id body {font-size: -.6em;} -@keyframes mymove {from {top: 0px;} - to {top: 200px;}} -@-moz-keyframes some-move {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) ) {#my_id body {font-family: "Helvetica";}} -@page :pseudo-class {margin: 2in;} -@-moz-document url(http://www.w3.org/), - url-prefix(http://www.w3.org/Style/), - domain(mozilla.org), - regexp("https:.*") {#my_id body {color: purple;background: yellow;}} -@media screen and (orientation: landscape) {@-ms-viewport {width: 1024px;height: 768px;}} -@region-style #intro {#my_id p {color: blue;}}', $oDoc->render()); - - $oDoc = $this->parsedStructureForFile('values'); - $this->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;} -body {color: green;font: 75% "Lucida Grande","Trebuchet MS",Verdana,sans-serif;}', $oDoc->render()); - foreach ($oDoc->getAllRuleSets() as $oRuleSet) { - $oRuleSet->removeRule('font-'); - } - $this->assertSame('#header {margin: 10px 2em 1cm 2%;color: red !important;background-color: green;background-color: rgba(0,128,0,.7);frequency: 30Hz;} -body {color: green;}', $oDoc->render()); - foreach ($oDoc->getAllRuleSets() as $oRuleSet) { - $oRuleSet->removeRule('background-'); - } - $this->assertSame('#header {margin: 10px 2em 1cm 2%;color: red !important;frequency: 30Hz;} -body {color: green;}', $oDoc->render()); - } - - function testRuleGetters() { - $oDoc = $this->parsedStructureForFile('values'); - $aBlocks = $oDoc->getAllDeclarationBlocks(); - $oHeaderBlock = $aBlocks[0]; - $oBodyBlock = $aBlocks[1]; - $aHeaderRules = $oHeaderBlock->getRules('background-'); - $this->assertSame(2, count($aHeaderRules)); - $this->assertSame('background-color', $aHeaderRules[0]->getRule()); - $this->assertSame('background-color', $aHeaderRules[1]->getRule()); - $aHeaderRules = $oHeaderBlock->getRulesAssoc('background-'); - $this->assertSame(1, count($aHeaderRules)); - $this->assertSame(true, $aHeaderRules['background-color']->getValue() instanceof \Sabberworm\CSS\Value\Color); - $this->assertSame('rgba', $aHeaderRules['background-color']->getValue()->getColorDescription()); - $oHeaderBlock->removeRule($aHeaderRules['background-color']); - $aHeaderRules = $oHeaderBlock->getRules('background-'); - $this->assertSame(1, count($aHeaderRules)); - $this->assertSame('green', $aHeaderRules[0]->getValue()); - } - - function testSlashedValues() { - $oDoc = $this->parsedStructureForFile('slashed'); - $this->assertSame('.test {font: 12px/1.5 Verdana,Arial,sans-serif;border-radius: 5px 10px 5px 10px/10px 5px 10px 5px;}', $oDoc->render()); - foreach ($oDoc->getAllValues(null) as $mValue) { - if ($mValue instanceof Size && $mValue->isSize() && !$mValue->isRelative()) { - $mValue->setSize($mValue->getSize() * 3); - } - } - foreach ($oDoc->getAllDeclarationBlocks() as $oBlock) { - $oRule = $oBlock->getRules('font'); - $oRule = $oRule[0]; - $oSpaceList = $oRule->getValue(); - $this->assertEquals(' ', $oSpaceList->getListSeparator()); - $oSlashList = $oSpaceList->getListComponents(); - $oCommaList = $oSlashList[1]; - $oSlashList = $oSlashList[0]; - $this->assertEquals(',', $oCommaList->getListSeparator()); - $this->assertEquals('/', $oSlashList->getListSeparator()); - $oRule = $oBlock->getRules('border-radius'); - $oRule = $oRule[0]; - $oSlashList = $oRule->getValue(); - $this->assertEquals('/', $oSlashList->getListSeparator()); - $oSpaceList1 = $oSlashList->getListComponents(); - $oSpaceList2 = $oSpaceList1[1]; - $oSpaceList1 = $oSpaceList1[0]; - $this->assertEquals(' ', $oSpaceList1->getListSeparator()); - $this->assertEquals(' ', $oSpaceList2->getListSeparator()); - } - $this->assertSame('.test {font: 36px/1.5 Verdana,Arial,sans-serif;border-radius: 15px 30px 15px 30px/30px 15px 30px 15px;}', $oDoc->render()); - } - - function testFunctionSyntax() { - $oDoc = $this->parsedStructureForFile('functions'); - $sExpected = 'div.main {background-image: linear-gradient(#000,#fff);} -.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;-moz-transform-origin: center 60%;} -.collapser.expanded::before, .collapser.expanded::-moz-before, .collapser.expanded::-webkit-before {-moz-transform: rotate(90deg);} -.collapser + * {height: 0;overflow: hidden;-moz-transition-property: height;-moz-transition-duration: .3s;} -.collapser.expanded + * {height: auto;}'; - $this->assertSame($sExpected, $oDoc->render()); - - foreach ($oDoc->getAllValues(null, true) as $mValue) { - if ($mValue instanceof Size && $mValue->isSize()) { - $mValue->setSize($mValue->getSize() * 3); - } - } - $sExpected = str_replace(array('1.2em', '.2em', '60%'), array('3.6em', '.6em', '180%'), $sExpected); - $this->assertSame($sExpected, $oDoc->render()); - - foreach ($oDoc->getAllValues(null, true) as $mValue) { - if ($mValue instanceof Size && !$mValue->isRelative() && !$mValue->isColorComponent()) { - $mValue->setSize($mValue->getSize() * 2); - } - } - $sExpected = str_replace(array('.2s', '.3s', '90deg'), array('.4s', '.6s', '180deg'), $sExpected); - $this->assertSame($sExpected, $oDoc->render()); - } - - function testExpandShorthands() { - $oDoc = $this->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;}'; - $this->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;}'; - $this->assertSame($sExpected, $oDoc->render()); - } - - function testCreateShorthands() { - $oDoc = $this->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;}'; - $this->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;}'; - $this->assertSame($sExpected, $oDoc->render()); - } - - function testNamespaces() { - $oDoc = $this->parsedStructureForFile('namespaces'); - $sExpected = '@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;}'; - $this->assertSame($sExpected, $oDoc->render()); - } - - function testInnerColors() { - $oDoc = $this->parsedStructureForFile('inner-color'); - $sExpected = 'test {background: -webkit-gradient(linear,0 0,0 bottom,from(#006cad),to(hsl(202,100%,49%)));}'; - $this->assertSame($sExpected, $oDoc->render()); - } - - function testPrefixedGradient() { - $oDoc = $this->parsedStructureForFile('webkit'); - $sExpected = '.test {background: -webkit-linear-gradient(top right,white,black);}'; - $this->assertSame($sExpected, $oDoc->render()); - } - - function testListValueRemoval() { - $oDoc = $this->parsedStructureForFile('atrules'); - foreach ($oDoc->getContents() as $oItem) { - if ($oItem instanceof AtRule) { - $oDoc->remove($oItem); - continue; - } - } - $this->assertSame('html, body {font-size: -.6em;}', $oDoc->render()); - - $oDoc = $this->parsedStructureForFile('nested'); - foreach ($oDoc->getAllDeclarationBlocks() as $oBlock) { - $oDoc->removeDeclarationBlockBySelector($oBlock, false); - break; - } - $this->assertSame('html {some-other: -test(val1);} -@media screen {html {some: -test(val2);}} -#unrelated {other: yes;}', $oDoc->render()); - - $oDoc = $this->parsedStructureForFile('nested'); - foreach ($oDoc->getAllDeclarationBlocks() as $oBlock) { - $oDoc->removeDeclarationBlockBySelector($oBlock, true); - break; - } - $this->assertSame('@media screen {html {some: -test(val2);}} -#unrelated {other: yes;}', $oDoc->render()); - } - - /** - * @expectedException Sabberworm\CSS\Parsing\OutputException - */ - function testSelectorRemoval() { - $oDoc = $this->parsedStructureForFile('1readme'); - $aBlocks = $oDoc->getAllDeclarationBlocks(); - $oBlock1 = $aBlocks[0]; - $this->assertSame(true, $oBlock1->removeSelector('html')); - $sExpected = '@charset "utf-8"; -@font-face {font-family: "CrassRoots";src: url("../media/cr.ttf");} -body {font-size: 1.6em;}'; - $this->assertSame($sExpected, $oDoc->render()); - $this->assertSame(false, $oBlock1->removeSelector('html')); - $this->assertSame(true, $oBlock1->removeSelector('body')); - // This tries to output a declaration block without a selector and throws. - $oDoc->render(); - } - - function testComments() { - $oDoc = $this->parsedStructureForFile('comments'); - $sExpected = '@import url("some/url.css") screen; -.foo, #bar {background-color: #000;} -@media screen {#foo.bar {position: absolute;}}'; - $this->assertSame($sExpected, $oDoc->render()); - } - - function testUrlInFile() { - $oDoc = $this->parsedStructureForFile('url', Settings::create()->withMultibyteSupport(true)); - $sExpected = 'body {background: #fff url("http://somesite.com/images/someimage.gif") repeat top center;} -body {background-url: url("http://somesite.com/images/someimage.gif");}'; - $this->assertSame($sExpected, $oDoc->render()); - } - - function testHexAlphaInFile() { - $oDoc = $this->parsedStructureForFile('hex-alpha', Settings::create()->withMultibyteSupport(true)); - $sExpected = 'div {background: rgba(17,34,51,.27);} -div {background: rgba(17,34,51,.27);}'; - $this->assertSame($sExpected, $oDoc->render()); - } - - function testCalcInFile() { - $oDoc = $this->parsedStructureForFile('calc', Settings::create()->withMultibyteSupport(true)); - $sExpected = '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);}'; - $this->assertSame($sExpected, $oDoc->render()); - } - - function testCalcNestedInFile() { - $oDoc = $this->parsedStructureForFile('calc-nested', Settings::create()->withMultibyteSupport(true)); - $sExpected = '.test {font-size: calc(( 3 * 4px ) + -2px);top: calc(200px - calc(20 * 3px));}'; - $this->assertSame($sExpected, $oDoc->render()); - } - - function testGridLineNameInFile() { - $oDoc = $this->parsedStructureForFile('grid-linename', Settings::create()->withMultibyteSupport(true)); - $sExpected = "div {grid-template-columns: [linename] 100px;}\nspan {grid-template-columns: [linename1 linename2] 100px;}"; - $this->assertSame($sExpected, $oDoc->render()); - } - - function testEmptyGridLineNameLenientInFile() { - $oDoc = $this->parsedStructureForFile('empty-grid-linename'); - $sExpected = '.test {grid-template-columns: [] 100px;}'; - $this->assertSame($sExpected, $oDoc->render()); - } - - function testUnmatchedBracesInFile() { - $oDoc = $this->parsedStructureForFile('unmatched_braces', Settings::create()->withMultibyteSupport(true)); - $sExpected = 'button, input, checkbox, textarea {outline: 0;margin: 0;}'; - $this->assertSame($sExpected, $oDoc->render()); - } - - /** - * @expectedException Sabberworm\CSS\Parsing\UnexpectedTokenException - */ - function testLineNameFailure() { - $this->parsedStructureForFile('-empty-grid-linename', Settings::create()->withLenientParsing(false)); - } - - /** - * @expectedException Sabberworm\CSS\Parsing\UnexpectedTokenException - */ - function testCalcFailure() { - $this->parsedStructureForFile('-calc-no-space-around-minus', Settings::create()->withLenientParsing(false)); - } - - function testUrlInFileMbOff() { - $oDoc = $this->parsedStructureForFile('url', Settings::create()->withMultibyteSupport(false)); - $sExpected = 'body {background: #fff url("http://somesite.com/images/someimage.gif") repeat top center;} -body {background-url: url("http://somesite.com/images/someimage.gif");}'; - $this->assertSame($sExpected, $oDoc->render()); - } - - function testEmptyFile() { - $oDoc = $this->parsedStructureForFile('-empty', Settings::create()->withMultibyteSupport(true)); - $sExpected = ''; - $this->assertSame($sExpected, $oDoc->render()); - } - - function testEmptyFileMbOff() { - $oDoc = $this->parsedStructureForFile('-empty', Settings::create()->withMultibyteSupport(false)); - $sExpected = ''; - $this->assertSame($sExpected, $oDoc->render()); - } - - function testCharsetLenient1() { - $oDoc = $this->parsedStructureForFile('-charset-after-rule', Settings::create()->withLenientParsing(true)); - $sExpected = '#id {prop: var(--val);}'; - $this->assertSame($sExpected, $oDoc->render()); - } - - function testCharsetLenient2() { - $oDoc = $this->parsedStructureForFile('-charset-in-block', Settings::create()->withLenientParsing(true)); - $sExpected = '@media print {}'; - $this->assertSame($sExpected, $oDoc->render()); - } - - function testTrailingWhitespace() { - $oDoc = $this->parsedStructureForFile('trailing-whitespace', Settings::create()->withLenientParsing(false)); - $sExpected = 'div {width: 200px;}'; - $this->assertSame($sExpected, $oDoc->render()); - } - - /** - * @expectedException \Sabberworm\CSS\Parsing\UnexpectedTokenException - */ - function testCharsetFailure1() { - $this->parsedStructureForFile('-charset-after-rule', Settings::create()->withLenientParsing(false)); - } - - /** - * @expectedException \Sabberworm\CSS\Parsing\UnexpectedTokenException - */ - function testCharsetFailure2() { - $this->parsedStructureForFile('-charset-in-block', Settings::create()->withLenientParsing(false)); - } - - /** - * @expectedException \Sabberworm\CSS\Parsing\SourceException - */ - function testUnopenedClosingBracketFailure() { - $this->parsedStructureForFile('unopened-close-brackets', Settings::create()->withLenientParsing(false)); - } - - /** - * Ensure that a missing property value raises an exception. - * - * @expectedException \Sabberworm\CSS\Parsing\UnexpectedTokenException - * @covers \Sabberworm\CSS\Value\Value::parseValue() - */ - function testMissingPropertyValueStrict() { - $this->parsedStructureForFile('missing-property-value', Settings::create()->withLenientParsing(false)); - } - - /** - * Ensure that a missing property value is ignored when in lenient parsing mode. - * - * @covers \Sabberworm\CSS\Value\Value::parseValue() - */ - function testMissingPropertyValueLenient() { - $parsed = $this->parsedStructureForFile('missing-property-value', Settings::create()->withLenientParsing(true)); - $rulesets = $parsed->getAllRuleSets(); - $this->assertCount( 1, $rulesets ); - $block = $rulesets[0]; - $this->assertTrue( $block instanceof DeclarationBlock ); - $this->assertEquals( array( 'div' ), $block->getSelectors() ); - $rules = $block->getRules(); - $this->assertCount( 1, $rules ); - $rule = $rules[0]; - $this->assertEquals( 'display', $rule->getRule() ); - $this->assertEquals( 'inline-block', $rule->getValue() ); - } - - /** - * Parse structure for file. - * - * @param string $sFileName Filename. - * @param null|obJeCt $oSettings Settings. - * - * @return CSSList\Document Parsed document. - */ - function parsedStructureForFile($sFileName, $oSettings = null) { - $sFile = dirname(__FILE__) . '/../../files' . DIRECTORY_SEPARATOR . "$sFileName.css"; - $oParser = new Parser(file_get_contents($sFile), $oSettings); - return $oParser->parse(); - } - - /** - * @depends testFiles - */ - function testLineNumbersParsing() { - $oDoc = $this->parsedStructureForFile('line-numbers'); - // array key is the expected line number - $aExpected = array( - 1 => array('Sabberworm\CSS\Property\Charset'), - 3 => array('Sabberworm\CSS\Property\CSSNamespace'), - 5 => array('Sabberworm\CSS\RuleSet\AtRuleSet'), - 11 => array('Sabberworm\CSS\RuleSet\DeclarationBlock'), - // Line Numbers of the inner declaration blocks - 17 => array('Sabberworm\CSS\CSSList\KeyFrame', 18, 20), - 23 => array('Sabberworm\CSS\Property\Import'), - 25 => array('Sabberworm\CSS\RuleSet\DeclarationBlock') - ); - - $aActual = array(); - foreach ($oDoc->getContents() as $oContent) { - $aActual[$oContent->getLineNo()] = array(get_class($oContent)); - if ($oContent instanceof KeyFrame) { - foreach ($oContent->getContents() as $block) { - $aActual[$oContent->getLineNo()][] = $block->getLineNo(); - } - } - } - - $aUrlExpected = array(7, 26); // expected line numbers - $aUrlActual = array(); - foreach ($oDoc->getAllValues() as $oValue) { - if ($oValue instanceof URL) { - $aUrlActual[] = $oValue->getLineNo(); - } - } - - // Checking for the multiline color rule lines 27-31 - $aExpectedColorLines = array(28, 29, 30); - $aDeclBlocks = $oDoc->getAllDeclarationBlocks(); - // Choose the 2nd one - $oDeclBlock = $aDeclBlocks[1]; - $aRules = $oDeclBlock->getRules(); - // Choose the 2nd one - $oColor = $aRules[1]->getValue(); - $this->assertEquals(27, $aRules[1]->getLineNo()); - - foreach ($oColor->getColor() as $oSize) { - $aActualColorLines[] = $oSize->getLineNo(); - } - - $this->assertEquals($aExpectedColorLines, $aActualColorLines); - $this->assertEquals($aUrlExpected, $aUrlActual); - $this->assertEquals($aExpected, $aActual); - } - - /** - * @expectedException \Sabberworm\CSS\Parsing\UnexpectedTokenException - * Credit: This test by @sabberworm (from https://github.com/sabberworm/PHP-CSS-Parser/pull/105#issuecomment-229643910 ) - */ - function testUnexpectedTokenExceptionLineNo() { - $oParser = new Parser("\ntest: 1;", Settings::create()->beStrict()); - try { - $oParser->parse(); - } catch (UnexpectedTokenException $e) { - $this->assertSame(2, $e->getLineNo()); - throw $e; - } - } - - /** - * @expectedException Sabberworm\CSS\Parsing\UnexpectedTokenException - */ - function testIeHacksStrictParsing() { - // We can't strictly parse IE hacks. - $this->parsedStructureForFile('ie-hacks', Settings::create()->beStrict()); - } - - function testIeHacksParsing() { - $oDoc = $this->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઼";}'; - $this->assertEquals($sExpected, $oDoc->render()); - } - - /** - * @depends testFiles - */ - function testCommentExtracting() { - $oDoc = $this->parsedStructureForFile('comments'); - $aNodes = $oDoc->getContents(); - - // Import property. - $importComments = $aNodes[0]->getComments(); - $this->assertCount(1, $importComments); - $this->assertEquals("*\n * Comments Hell.\n ", $importComments[0]->getComment()); - - // Declaration block. - $fooBarBlock = $aNodes[1]; - $fooBarBlockComments = $fooBarBlock->getComments(); - // TODO Support comments in selectors. - // $this->assertCount(2, $fooBarBlockComments); - // $this->assertEquals("* Number 4 *", $fooBarBlockComments[0]->getComment()); - // $this->assertEquals("* Number 5 *", $fooBarBlockComments[1]->getComment()); - - // Declaration rules. - $fooBarRules = $fooBarBlock->getRules(); - $fooBarRule = $fooBarRules[0]; - $fooBarRuleComments = $fooBarRule->getComments(); - $this->assertCount(1, $fooBarRuleComments); - $this->assertEquals(" Number 6 ", $fooBarRuleComments[0]->getComment()); - - // Media property. - $mediaComments = $aNodes[2]->getComments(); - $this->assertCount(0, $mediaComments); - - // Media children. - $mediaRules = $aNodes[2]->getContents(); - $fooBarComments = $mediaRules[0]->getComments(); - $this->assertCount(1, $fooBarComments); - $this->assertEquals("* Number 10 *", $fooBarComments[0]->getComment()); - - // Media -> declaration -> rule. - $fooBarRules = $mediaRules[0]->getRules(); - $fooBarChildComments = $fooBarRules[0]->getComments(); - $this->assertCount(1, $fooBarChildComments); - $this->assertEquals("* Number 10b *", $fooBarChildComments[0]->getComment()); - } - - function testFlatCommentExtracting() { - $parser = new Parser('div {/*Find Me!*/left:10px; text-align:left;}'); - $doc = $parser->parse(); - $contents = $doc->getContents(); - $divRules = $contents[0]->getRules(); - $comments = $divRules[0]->getComments(); - $this->assertCount(1, $comments); - $this->assertEquals("Find Me!", $comments[0]->getComment()); - } - - function testTopLevelCommentExtracting() { - $parser = new Parser('/*Find Me!*/div {left:10px; text-align:left;}'); - $doc = $parser->parse(); - $contents = $doc->getContents(); - $comments = $contents[0]->getComments(); - $this->assertCount(1, $comments); - $this->assertEquals("Find Me!", $comments[0]->getComment()); - } - - /** - * @expectedException Sabberworm\CSS\Parsing\UnexpectedTokenException - */ - function testMicrosoftFilterStrictParsing() { - $oDoc = $this->parsedStructureForFile('ms-filter', Settings::create()->beStrict()); - } - - function testMicrosoftFilterParsing() { - $oDoc = $this->parsedStructureForFile('ms-filter'); - $sExpected = ".test {filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=\"#80000000\",endColorstr=\"#00000000\",GradientType=1);}"; - $this->assertSame($sExpected, $oDoc->render()); - } -} diff --git a/tests/Sabberworm/CSS/RuleSet/DeclarationBlockTest.php b/tests/Sabberworm/CSS/RuleSet/DeclarationBlockTest.php deleted file mode 100644 index 16a4a2d1..00000000 --- a/tests/Sabberworm/CSS/RuleSet/DeclarationBlockTest.php +++ /dev/null @@ -1,267 +0,0 @@ -parse(); - foreach ($oDoc->getAllDeclarationBlocks() as $oDeclaration) { - $oDeclaration->expandBorderShorthand(); - } - $this->assertSame(trim((string) $oDoc), $sExpected); - } - - public function expandBorderShorthandProvider() { - return array( - array('body{ border: 2px solid #000 }', 'body {border-width: 2px;border-style: solid;border-color: #000;}'), - array('body{ border: none }', 'body {border-style: none;}'), - array('body{ border: 2px }', 'body {border-width: 2px;}'), - array('body{ border: #f00 }', 'body {border-color: #f00;}'), - array('body{ border: 1em solid }', 'body {border-width: 1em;border-style: solid;}'), - array('body{ margin: 1em; }', 'body {margin: 1em;}') - ); - } - - /** - * @dataProvider expandFontShorthandProvider - * */ - public function testExpandFontShorthand($sCss, $sExpected) { - $oParser = new Parser($sCss); - $oDoc = $oParser->parse(); - foreach ($oDoc->getAllDeclarationBlocks() as $oDeclaration) { - $oDeclaration->expandFontShorthand(); - } - $this->assertSame(trim((string) $oDoc), $sExpected); - } - - public function expandFontShorthandProvider() { - return array( - array( - 'body{ margin: 1em; }', - 'body {margin: 1em;}' - ), - array( - 'body {font: 12px serif;}', - 'body {font-style: normal;font-variant: normal;font-weight: normal;font-size: 12px;line-height: normal;font-family: serif;}' - ), - array( - 'body {font: italic 12px serif;}', - 'body {font-style: italic;font-variant: normal;font-weight: normal;font-size: 12px;line-height: normal;font-family: serif;}' - ), - array( - '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;}' - ), - array( - '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;}' - ), - array( - '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;}' - ), - ); - } - - /** - * @dataProvider expandBackgroundShorthandProvider - * */ - public function testExpandBackgroundShorthand($sCss, $sExpected) { - $oParser = new Parser($sCss); - $oDoc = $oParser->parse(); - foreach ($oDoc->getAllDeclarationBlocks() as $oDeclaration) { - $oDeclaration->expandBackgroundShorthand(); - } - $this->assertSame(trim((string) $oDoc), $sExpected); - } - - public function expandBackgroundShorthandProvider() { - return array( - array('body {border: 1px;}', 'body {border: 1px;}'), - array('body {background: #f00;}', 'body {background-color: #f00;background-image: none;background-repeat: repeat;background-attachment: scroll;background-position: 0% 0%;}'), - array('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%;}'), - array('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%;}'), - array('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;}'), - array('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;}'), - ); - } - - /** - * @dataProvider expandDimensionsShorthandProvider - * */ - public function testExpandDimensionsShorthand($sCss, $sExpected) { - $oParser = new Parser($sCss); - $oDoc = $oParser->parse(); - foreach ($oDoc->getAllDeclarationBlocks() as $oDeclaration) { - $oDeclaration->expandDimensionsShorthand(); - } - $this->assertSame(trim((string) $oDoc), $sExpected); - } - - public function expandDimensionsShorthandProvider() { - return array( - array('body {border: 1px;}', 'body {border: 1px;}'), - array('body {margin-top: 1px;}', 'body {margin-top: 1px;}'), - array('body {margin: 1em;}', 'body {margin-top: 1em;margin-right: 1em;margin-bottom: 1em;margin-left: 1em;}'), - array('body {margin: 1em 2em;}', 'body {margin-top: 1em;margin-right: 2em;margin-bottom: 1em;margin-left: 2em;}'), - array('body {margin: 1em 2em 3em;}', 'body {margin-top: 1em;margin-right: 2em;margin-bottom: 3em;margin-left: 2em;}'), - ); - } - - /** - * @dataProvider createBorderShorthandProvider - * */ - public function testCreateBorderShorthand($sCss, $sExpected) { - $oParser = new Parser($sCss); - $oDoc = $oParser->parse(); - foreach ($oDoc->getAllDeclarationBlocks() as $oDeclaration) { - $oDeclaration->createBorderShorthand(); - } - $this->assertSame(trim((string) $oDoc), $sExpected); - } - - public function createBorderShorthandProvider() { - return array( - array('body {border-width: 2px;border-style: solid;border-color: #000;}', 'body {border: 2px solid #000;}'), - array('body {border-style: none;}', 'body {border: none;}'), - array('body {border-width: 1em;border-style: solid;}', 'body {border: 1em solid;}'), - array('body {margin: 1em;}', 'body {margin: 1em;}') - ); - } - - /** - * @dataProvider createFontShorthandProvider - * */ - public function testCreateFontShorthand($sCss, $sExpected) { - $oParser = new Parser($sCss); - $oDoc = $oParser->parse(); - foreach ($oDoc->getAllDeclarationBlocks() as $oDeclaration) { - $oDeclaration->createFontShorthand(); - } - $this->assertSame(trim((string) $oDoc), $sExpected); - } - - public function createFontShorthandProvider() { - return array( - array('body {font-size: 12px; font-family: serif}', 'body {font: 12px serif;}'), - array('body {font-size: 12px; font-family: serif; font-style: italic;}', 'body {font: italic 12px serif;}'), - array('body {font-size: 12px; font-family: serif; font-style: italic; font-weight: bold;}', 'body {font: italic bold 12px serif;}'), - array('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;}'), - array('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;}'), - array('body {margin: 1em;}', 'body {margin: 1em;}') - ); - } - - /** - * @dataProvider createDimensionsShorthandProvider - * */ - public function testCreateDimensionsShorthand($sCss, $sExpected) { - $oParser = new Parser($sCss); - $oDoc = $oParser->parse(); - foreach ($oDoc->getAllDeclarationBlocks() as $oDeclaration) { - $oDeclaration->createDimensionsShorthand(); - } - $this->assertSame(trim((string) $oDoc), $sExpected); - } - - public function createDimensionsShorthandProvider() { - return array( - array('body {border: 1px;}', 'body {border: 1px;}'), - array('body {margin-top: 1px;}', 'body {margin-top: 1px;}'), - array('body {margin-top: 1em; margin-right: 1em; margin-bottom: 1em; margin-left: 1em;}', 'body {margin: 1em;}'), - array('body {margin-top: 1em; margin-right: 2em; margin-bottom: 1em; margin-left: 2em;}', 'body {margin: 1em 2em;}'), - array('body {margin-top: 1em; margin-right: 2em; margin-bottom: 3em; margin-left: 2em;}', 'body {margin: 1em 2em 3em;}'), - ); - } - - /** - * @dataProvider createBackgroundShorthandProvider - * */ - public function testCreateBackgroundShorthand($sCss, $sExpected) { - $oParser = new Parser($sCss); - $oDoc = $oParser->parse(); - foreach ($oDoc->getAllDeclarationBlocks() as $oDeclaration) { - $oDeclaration->createBackgroundShorthand(); - } - $this->assertSame(trim((string) $oDoc), $sExpected); - } - - public function createBackgroundShorthandProvider() { - return array( - array('body {border: 1px;}', 'body {border: 1px;}'), - array('body {background-color: #f00;}', 'body {background: #f00;}'), - array('body {background-color: #f00;background-image: url(foobar.png);}', 'body {background: #f00 url("foobar.png");}'), - array('body {background-color: #f00;background-image: url(foobar.png);background-repeat: no-repeat;}', 'body {background: #f00 url("foobar.png") no-repeat;}'), - array('body {background-color: #f00;background-image: url(foobar.png);background-repeat: no-repeat;}', 'body {background: #f00 url("foobar.png") no-repeat;}'), - array('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;}'), - array('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;}'), - ); - } - - public function testOverrideRules() { - $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]; - - $this->assertCount(2, $oWrapper->getRules()); - $aContents[0]->setRules(array($oRule)); - - $aRules = $oWrapper->getRules(); - $this->assertCount(1, $aRules); - $this->assertEquals('right', $aRules[0]->getRule()); - $this->assertEquals('-10px', $aRules[0]->getValue()); - } - - public function testRuleInsertion() { - $sCss = '.wrapper { left: 10px; text-align: left; }'; - $oParser = new Parser($sCss); - $oDoc = $oParser->parse(); - $aContents = $oDoc->getContents(); - $oWrapper = $aContents[0]; - - $oFirst = $oWrapper->getRules('left'); - $this->assertCount(1, $oFirst); - $oFirst = $oFirst[0]; - - $oSecond = $oWrapper->getRules('text-'); - $this->assertCount(1, $oSecond); - $oSecond = $oSecond[0]; - - $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(); - - $this->assertSame($oBefore, $aRules[0]); - $this->assertSame($oFirst, $aRules[1]); - $this->assertSame($oMiddle, $aRules[2]); - $this->assertSame($oSecond, $aRules[3]); - $this->assertSame($oAfter, $aRules[4]); - - $this->assertSame('.wrapper {left: 16em;left: 10px;text-align: 1;text-align: left;border-bottom-width: 1px;}', $oDoc->render()); - } - -} diff --git a/tests/Sabberworm/CSS/RuleSet/LenientParsingTest.php b/tests/Sabberworm/CSS/RuleSet/LenientParsingTest.php deleted file mode 100644 index d7005ba2..00000000 --- a/tests/Sabberworm/CSS/RuleSet/LenientParsingTest.php +++ /dev/null @@ -1,76 +0,0 @@ -beStrict()); - $oParser->parse(); - } - - public function testFaultToleranceOn() { - $sFile = dirname(__FILE__) . '/../../../files' . DIRECTORY_SEPARATOR . "-fault-tolerance.css"; - $oParser = new Parser(file_get_contents($sFile), Settings::create()->withLenientParsing(true)); - $oResult = $oParser->parse(); - $this->assertSame('.test1 {}'."\n".'.test2 {hello: 2.2;hello: 2000000000000.2;}'."\n".'#test {}'."\n".'#test2 {help: none;}', $oResult->render()); - } - - /** - * @expectedException Sabberworm\CSS\Parsing\UnexpectedTokenException - */ - public function testEndToken() { - $sFile = dirname(__FILE__) . '/../../../files' . DIRECTORY_SEPARATOR . "-end-token.css"; - $oParser = new Parser(file_get_contents($sFile), Settings::create()->beStrict()); - $oParser->parse(); - } - - /** - * @expectedException Sabberworm\CSS\Parsing\UnexpectedTokenException - */ - public function testEndToken2() { - $sFile = dirname(__FILE__) . '/../../../files' . DIRECTORY_SEPARATOR . "-end-token-2.css"; - $oParser = new Parser(file_get_contents($sFile), Settings::create()->beStrict()); - $oParser->parse(); - } - - public function testEndTokenPositive() { - $sFile = dirname(__FILE__) . '/../../../files' . DIRECTORY_SEPARATOR . "-end-token.css"; - $oParser = new Parser(file_get_contents($sFile), Settings::create()->withLenientParsing(true)); - $oResult = $oParser->parse(); - $this->assertSame("", $oResult->render()); - } - - public function testEndToken2Positive() { - $sFile = dirname(__FILE__) . '/../../../files' . DIRECTORY_SEPARATOR . "-end-token-2.css"; - $oParser = new Parser(file_get_contents($sFile), Settings::create()->withLenientParsing(true)); - $oResult = $oParser->parse(); - $this->assertSame('#home .bg-layout {background-image: url("/bundles/main/img/bg1.png?5");}', $oResult->render()); - } - - public function testLocaleTrap() { - setlocale(LC_ALL, "pt_PT", "no"); - $sFile = dirname(__FILE__) . '/../../../files' . DIRECTORY_SEPARATOR . "-fault-tolerance.css"; - $oParser = new Parser(file_get_contents($sFile), Settings::create()->withLenientParsing(true)); - $oResult = $oParser->parse(); - $this->assertSame('.test1 {}'."\n".'.test2 {hello: 2.2;hello: 2000000000000.2;}'."\n".'#test {}'."\n".'#test2 {help: none;}', $oResult->render()); - } - - public function testCaseInsensitivity() { - $sFile = dirname(__FILE__) . '/../../../files' . DIRECTORY_SEPARATOR . "case-insensitivity.css"; - $oParser = new Parser(file_get_contents($sFile)); - $oResult = $oParser->parse(); - $this->assertSame('@charset "utf-8"; -@import url("test.css"); -@media screen {} -#myid {case: insensitive !important;frequency: 30Hz;font-size: 1em;color: #ff0;color: hsl(40,40%,30%);font-family: Arial;}', $oResult->render()); - } - -} diff --git a/tests/Value/CalcRuleValueListTest.php b/tests/Value/CalcRuleValueListTest.php new file mode 100644 index 00000000..0a2c5304 --- /dev/null +++ b/tests/Value/CalcRuleValueListTest.php @@ -0,0 +1,55 @@ +getLineNo()); + } + + /** + * @test + */ + public function getLineNoReturnsLineNumberProvidedToConstructor() + { + $lineNumber = 42; + + $subject = new CalcRuleValueList($lineNumber); + + self::assertSame($lineNumber, $subject->getLineNo()); + } + + /** + * @test + */ + public function separatorAlwaysIsComma() + { + $subject = new CalcRuleValueList(); + + self::assertSame(',', $subject->getListSeparator()); + } +} diff --git a/tests/bootstrap.php b/tests/bootstrap.php deleted file mode 100644 index 7c4de814..00000000 --- a/tests/bootstrap.php +++ /dev/null @@ -1,10 +0,0 @@ - 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; +} +html[dir="rtl"] .super-menu.menu-floated > li:first-of-type +border-right-width:0; +} +} + + +.super-menu.menu-floated{ +border-right-width:1px; +border-left-width:1px; +border-color:rgb(90, 66, 66); +border-style:dotted; +} + +body { + background-color: red; +} diff --git a/tests/fixtures/invalid-selectors.css b/tests/fixtures/invalid-selectors.css new file mode 100644 index 00000000..1c633c57 --- /dev/null +++ b/tests/fixtures/invalid-selectors.css @@ -0,0 +1,24 @@ +@keyframes mymove { + from { top: 0px; } +} + +#test { + color: white; + background: green; +} + +body + background: black; + } + +#test { + display: block; + background: red; + color: white; +} +#test { + display: block; + background: white; + color: black; +} + diff --git a/tests/fixtures/keyframe-selector-validation.css b/tests/fixtures/keyframe-selector-validation.css new file mode 100644 index 00000000..1a1addd7 --- /dev/null +++ b/tests/fixtures/keyframe-selector-validation.css @@ -0,0 +1,11 @@ +@-webkit-keyframes zoom { + 0% { + -webkit-transform: scale(1,1); + } + 50% { + -webkit-transform: scale(1.2,1.2); + } + 100% { + -webkit-transform: scale(1,1); + } +} diff --git a/tests/fixtures/large-z-index.css b/tests/fixtures/large-z-index.css new file mode 100644 index 00000000..779c8d09 --- /dev/null +++ b/tests/fixtures/large-z-index.css @@ -0,0 +1,3 @@ +.overlay { + z-index: 9999999999999999999999; +} diff --git a/tests/files/line-numbers.css b/tests/fixtures/line-numbers.css similarity index 77% rename from tests/files/line-numbers.css rename to tests/fixtures/line-numbers.css index 73d3189f..6c27f489 100644 --- a/tests/files/line-numbers.css +++ b/tests/fixtures/line-numbers.css @@ -4,7 +4,7 @@ @font-face { /* line 5 */ font-family: "CrassRoots"; - src: url("http://example.com/media/cr.ttf") /* line 7 */ + src: url("https://example.com/media/cr.ttf") /* line 7 */ } @@ -23,7 +23,7 @@ @IMPORT uRL(test.css); /* line 23 */ body { - background: #FFFFFF url("http://somesite.com/images/someimage.gif") repeat top center; /* line 25 */ + background: #FFFFFF url("https://somesite.com/images/someimage.gif") repeat top center; /* line 25 */ color: rgb( /* line 27 */ 233, /* line 28 */ 100, /* line 29 */ diff --git a/tests/fixtures/lonely-import.css b/tests/fixtures/lonely-import.css new file mode 100644 index 00000000..87767bba --- /dev/null +++ b/tests/fixtures/lonely-import.css @@ -0,0 +1 @@ +@import "example.css" only screen and (max-width: 600px) \ No newline at end of file diff --git a/tests/files/missing-property-value.css b/tests/fixtures/missing-property-value.css similarity index 100% rename from tests/files/missing-property-value.css rename to tests/fixtures/missing-property-value.css diff --git a/tests/files/ms-filter.css b/tests/fixtures/ms-filter.css similarity index 100% rename from tests/files/ms-filter.css rename to tests/fixtures/ms-filter.css diff --git a/tests/files/namespaces.css b/tests/fixtures/namespaces.css similarity index 58% rename from tests/files/namespaces.css rename to tests/fixtures/namespaces.css index ffd7a589..c396c974 100644 --- a/tests/files/namespaces.css +++ b/tests/fixtures/namespaces.css @@ -1,10 +1,10 @@ -/* From the spec at http://www.w3.org/TR/css3-namespace/ */ +/* From the spec at https://www.w3.org/TR/css3-namespace/ */ @namespace toto "http://toto.example.org"; @namespace "http://example.com/foo"; -/* From an introduction at http://www.blooberry.com/indexdot/css/syntax/atrules/namespace.htm */ +/* From an introduction at https://www.blooberry.com/indexdot/css/syntax/atrules/namespace.htm */ @namespace foo url("http://www.example.com/"); @namespace foo url('http://www.example.com/'); diff --git a/tests/files/nested.css b/tests/fixtures/nested.css similarity index 100% rename from tests/files/nested.css rename to tests/fixtures/nested.css diff --git a/tests/fixtures/selector-escapes.css b/tests/fixtures/selector-escapes.css new file mode 100644 index 00000000..7797e06f --- /dev/null +++ b/tests/fixtures/selector-escapes.css @@ -0,0 +1,7 @@ +#\# { + color: red; +} + +.col-sm-1\/5 { + width: 20%; +} diff --git a/tests/files/-tobedone.css b/tests/fixtures/selector-ignores.css similarity index 62% rename from tests/files/-tobedone.css rename to tests/fixtures/selector-ignores.css index d9fc1117..5834e009 100644 --- a/tests/files/-tobedone.css +++ b/tests/fixtures/selector-ignores.css @@ -1,9 +1,13 @@ .some[selectors-may='contain-a-{'] { - + +} + +.this-selector /* should remain-} */ .valid { + width:100px; } @media only screen and (min-width: 200px) { .test { prop: val; } -} \ No newline at end of file +} diff --git a/tests/files/slashed.css b/tests/fixtures/slashed.css similarity index 100% rename from tests/files/slashed.css rename to tests/fixtures/slashed.css diff --git a/tests/files/specificity.css b/tests/fixtures/specificity.css similarity index 100% rename from tests/files/specificity.css rename to tests/fixtures/specificity.css diff --git a/tests/files/trailing-whitespace.css b/tests/fixtures/trailing-whitespace.css similarity index 100% rename from tests/files/trailing-whitespace.css rename to tests/fixtures/trailing-whitespace.css diff --git a/tests/files/unicode-range.css b/tests/fixtures/unicode-range.css similarity index 100% rename from tests/files/unicode-range.css rename to tests/fixtures/unicode-range.css diff --git a/tests/files/unicode.css b/tests/fixtures/unicode.css similarity index 100% rename from tests/files/unicode.css rename to tests/fixtures/unicode.css diff --git a/tests/files/unmatched_braces.css b/tests/fixtures/unmatched_braces.css similarity index 100% rename from tests/files/unmatched_braces.css rename to tests/fixtures/unmatched_braces.css diff --git a/tests/fixtures/url.css b/tests/fixtures/url.css new file mode 100644 index 00000000..93aae97f --- /dev/null +++ b/tests/fixtures/url.css @@ -0,0 +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 diff --git a/tests/files/values.css b/tests/fixtures/values.css similarity index 100% rename from tests/files/values.css rename to tests/fixtures/values.css diff --git a/tests/files/webkit.css b/tests/fixtures/webkit.css similarity index 100% rename from tests/files/webkit.css rename to tests/fixtures/webkit.css diff --git a/tests/files/whitespace.css b/tests/fixtures/whitespace.css similarity index 100% rename from tests/files/whitespace.css rename to tests/fixtures/whitespace.css diff --git a/tests/phpunit.xml b/tests/phpunit.xml deleted file mode 100644 index 5dcbab2b..00000000 --- a/tests/phpunit.xml +++ /dev/null @@ -1 +0,0 @@ - diff --git a/tests/quickdump.php b/tests/quickdump.php deleted file mode 100755 index 4b54c71e..00000000 --- a/tests/quickdump.php +++ /dev/null @@ -1,20 +0,0 @@ -#!/usr/bin/env php -parse(); -echo "\n".'#### Input'."\n\n```css\n"; -print $sSource; - -echo "\n```\n\n".'#### Structure (`var_dump()`)'."\n\n```php\n"; -var_dump($oDoc); - -echo "\n```\n\n".'#### Output (`render()`)'."\n\n```css\n"; -print $oDoc->render(); - -echo "\n```\n"; -