diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 951992d2..2bdb67c1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,8 +1,10 @@ # https://help.github.com/en/categories/automating-your-workflow-with-github-actions on: - pull_request: push: + branches: + - main + pull_request: schedule: - cron: '3 3 * * 1' @@ -14,11 +16,11 @@ jobs: runs-on: ubuntu-22.04 strategy: matrix: - php-version: [ '5.6', '7.0', '7.1', '7.2', '7.3', '7.4', '8.0', '8.1', '8.2' ] + php-version: [ '7.2', '7.3', '7.4', '8.0', '8.1', '8.2', '8.3' ] steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Install PHP uses: shivammathur/setup-php@v2 @@ -39,15 +41,11 @@ jobs: strategy: fail-fast: false matrix: - php-version: [ '5.6', '7.0', '7.1', '7.2', '7.3' ] - coverage: [ 'none' ] - include: - - php-version: '7.4' - coverage: xdebug + php-version: [ '7.2', '7.3', '7.4', '8.0', '8.1', '8.2', '8.3' ] steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Install PHP uses: shivammathur/setup-php@v2 @@ -55,13 +53,13 @@ jobs: php-version: ${{ matrix.php-version }} ini-values: error_reporting=E_ALL tools: composer:v2 - coverage: "${{ matrix.coverage }}" + coverage: none - name: Show the Composer configuration run: composer config --global --list - name: Cache dependencies installed with composer - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: ~/.cache/composer key: php${{ matrix.php-version }}-composer-${{ hashFiles('**/composer.json') }} @@ -74,14 +72,7 @@ jobs: 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 + run: ./vendor/bin/phpunit static-analysis: name: Static Analysis @@ -93,17 +84,15 @@ jobs: strategy: fail-fast: false matrix: - include: - - command: sniffer - php-version: '7.4' - - command: fixer - php-version: '7.4' - - command: stan - php-version: '7.4' + command: + - fixer + - stan + php-version: + - '8.3' steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Install PHP uses: shivammathur/setup-php@v2 @@ -116,7 +105,7 @@ jobs: run: composer config --global --list - name: Cache dependencies installed with composer - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: ~/.cache/composer key: php${{ matrix.php-version }}-composer-${{ hashFiles('**/composer.json') }} @@ -130,7 +119,7 @@ jobs: - name: Install development tools run: | - phive --no-progress install --trust-gpg-keys BBAB5DF0A0D6672989CF1869E82B2FB314E9906E,A972B9ABB95D0B760B51442231C7E470E2138192,D32680D5957DC7116BE29C14CF1A108D0E7AE720 + phive --no-progress install --trust-gpg-keys BBAB5DF0A0D6672989CF1869E82B2FB314E9906E - name: Run Command run: composer ci:php:${{ matrix.command }} diff --git a/.github/workflows/codecoverage.yml b/.github/workflows/codecoverage.yml new file mode 100644 index 00000000..adcca2ee --- /dev/null +++ b/.github/workflows/codecoverage.yml @@ -0,0 +1,57 @@ +# https://help.github.com/en/categories/automating-your-workflow-with-github-actions + +on: + push: + branches: + - main + pull_request: + +name: Code coverage + +jobs: + code-coverage: + name: Code coverage + + runs-on: ubuntu-22.04 + + strategy: + matrix: + php-version: [ '7.4' ] + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php-version }} + ini-values: error_reporting=E_ALL + tools: composer:v2 + coverage: xdebug + + - name: Show the Composer configuration + run: composer config --global --list + + - name: Cache dependencies installed with composer + uses: actions/cache@v4 + with: + path: ~/.cache/composer + key: php${{ matrix.php-version }}-composer-${{ hashFiles('**/composer.json') }} + restore-keys: | + php${{ matrix.php-version }}-composer- + + - name: Install Composer dependencies + run: | + composer update --with-dependencies --no-progress; + composer show; + + - name: Run Tests + run: ./vendor/bin/phpunit --coverage-clover build/coverage/xml + + - name: Upload coverage results to Codacy + env: + CODACY_PROJECT_TOKEN: ${{ secrets.CODACY_PROJECT_TOKEN }} + if: "${{ env.CODACY_PROJECT_TOKEN != '' }}" + run: | + ./vendor/bin/codacycoverage clover build/coverage/xml diff --git a/.gitignore b/.gitignore index c1747f26..acf0d9d8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ /.phive/* /.php-cs-fixer.cache /.php_cs.cache +/.phpunit.result.cache /composer.lock /phpstan.neon /vendor/ diff --git a/.phive/phars.xml b/.phive/phars.xml index d353fbf9..8b65eea8 100644 --- a/.phive/phars.xml +++ b/.phive/phars.xml @@ -1,7 +1,4 @@ - - - - + diff --git a/CHANGELOG.md b/CHANGELOG.md index a23fd0e6..f31a4a61 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,47 @@ -# Revision History +# Changelog + +All notable changes to this project will be documented in this file. +This project adheres to [Semantic Versioning](https://semver.org/). + +## x.y.z + +### Added + +- Add a class diagram to the README (#482) +- Add support for the `dvh`, `lvh` and `svh` length units (#415) +- Add more tests (#449) + +### Changed + +- Improve performance of Value::parseValue with many delimiters by refactoring to remove array_search() +- Add visibility to all class/interface constants (#469) + +### Deprecated + +### Removed + +- Drop support for PHP < 7.2 (#420) + +### Fixed + +- Fix PHP notice caused by parsing invalid color values having less than 6 characters (#485) +- Fix (regression) failure to parse at-rules with strict parsing (#456) + +## 8.5.0 + +### Added + +- Add a method to get an import's media queries (#384) +- Add more unit tests (#381, #382) + +### Fixed + +- Retain CSSList and Rule comments when rendering CSS (#351) +- Replace invalid `turns` unit with `turn` (#350) +- Also allow string values for rules (#348) +- Fix invalid calc parsing (#169) +- Handle scientific notation when parsing sizes (#179) +- Fix PHP 8.1 compatibility in `ParserState::strsplit()` (#344) ## 8.4.0 diff --git a/README.md b/README.md index 90428cbf..9dab4549 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # PHP CSS Parser -[![Build Status](https://github.com/sabberworm/PHP-CSS-Parser/workflows/CI/badge.svg?branch=master)](https://github.com/sabberworm/PHP-CSS-Parser/actions/) +[![Build Status](https://github.com/MyIntervals/PHP-CSS-Parser/workflows/CI/badge.svg?branch=main)](https://github.com/MyIntervals/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. @@ -158,7 +158,7 @@ foreach($cssDocument->getAllRuleSets() as $oRuleSet) { // Note that the added dash will make this remove all rules starting with // `font-` (like `font-size`, `font-weight`, etc.) as well as a potential // `font` rule. - $oRuleSet->removeRule('font-'); + $oRuleSet->removeRule('font-'); $oRuleSet->removeRule('cursor'); } ``` @@ -615,6 +615,170 @@ class Sabberworm\CSS\CSSList\Document#4 (2) { #header {margin: 10px 2em 1cm 2%;font-family: Verdana,Helvetica,"Gill Sans",sans-serif;color: red !important;} ``` +## Class diagram + +```mermaid +classDiagram + direction LR + + %% Start of the part generated from the PHP code using tasuku43/mermaid-class-diagram + + class Renderable { + <> + } + class DeclarationBlock { + } + class RuleSet { + <> + } + class AtRuleSet { + } + class KeyframeSelector { + } + class AtRule { + <> + } + class Charset { + } + class Import { + } + class Selector { + } + class CSSNamespace { + } + class Settings { + } + class Rule { + } + class Parser { + } + class OutputFormatter { + } + class OutputFormat { + } + class OutputException { + } + class UnexpectedEOFException { + } + class SourceException { + } + class UnexpectedTokenException { + } + class ParserState { + } + class Anchor { + } + class CSSBlockList { + <> + } + class Document { + } + class CSSList { + <> + } + class KeyFrame { + } + class AtRuleBlockList { + } + class Color { + } + class URL { + } + class CalcRuleValueList { + } + class ValueList { + <> + } + class CalcFunction { + } + class LineName { + } + class Value { + <> + } + class Size { + } + class CSSString { + } + class PrimitiveValue { + <> + } + class CSSFunction { + } + class RuleValueList { + } + class Commentable { + <> + } + class Comment { + } + + RuleSet <|-- DeclarationBlock: inheritance + Renderable <|.. RuleSet: realization + Commentable <|.. RuleSet: realization + RuleSet <|-- AtRuleSet: inheritance + AtRule <|.. AtRuleSet: realization + Selector <|-- KeyframeSelector: inheritance + Renderable <|-- AtRule: inheritance + Commentable <|-- AtRule: inheritance + AtRule <|.. Charset: realization + AtRule <|.. Import: realization + AtRule <|.. CSSNamespace: realization + Renderable <|.. Rule: realization + Commentable <|.. Rule: realization + SourceException <|-- OutputException: inheritance + UnexpectedTokenException <|-- UnexpectedEOFException: inheritance + Exception <|-- SourceException: inheritance + SourceException <|-- UnexpectedTokenException: inheritance + CSSList <|-- CSSBlockList: inheritance + CSSBlockList <|-- Document: inheritance + Renderable <|.. CSSList: realization + Commentable <|.. CSSList: realization + CSSList <|-- KeyFrame: inheritance + AtRule <|.. KeyFrame: realization + CSSBlockList <|-- AtRuleBlockList: inheritance + AtRule <|.. AtRuleBlockList: realization + CSSFunction <|-- Color: inheritance + PrimitiveValue <|-- URL: inheritance + RuleValueList <|-- CalcRuleValueList: inheritance + Value <|-- ValueList: inheritance + CSSFunction <|-- CalcFunction: inheritance + ValueList <|-- LineName: inheritance + Renderable <|.. Value: realization + PrimitiveValue <|-- Size: inheritance + PrimitiveValue <|-- CSSString: inheritance + Value <|-- PrimitiveValue: inheritance + ValueList <|-- CSSFunction: inheritance + ValueList <|-- RuleValueList: inheritance + Renderable <|.. Comment: realization + + %% end of the generated part + + + Anchor --> "1" ParserState : oParserState + CSSList --> "*" CSSList : aContents + CSSList --> "*" Charset : aContents + CSSList --> "*" Comment : aComments + CSSList --> "*" Import : aContents + CSSList --> "*" RuleSet : aContents + CSSNamespace --> "*" Comment : aComments + Charset --> "*" Comment : aComments + Charset --> "1" CSSString : oCharset + DeclarationBlock --> "*" Selector : aSelectors + Import --> "*" Comment : aComments + OutputFormat --> "1" OutputFormat : oNextLevelFormat + OutputFormat --> "1" OutputFormatter : oFormatter + OutputFormatter --> "1" OutputFormat : oFormat + Parser --> "1" ParserState : oParserState + ParserState --> "1" Settings : oParserSettings + Rule --> "*" Comment : aComments + Rule --> "1" RuleValueList : mValue + RuleSet --> "*" Comment : aComments + RuleSet --> "*" Rule : aRules + URL --> "1" CSSString : oURL + ValueList --> "*" Value : aComponents +``` + ## Contributors/Thanks to * [oliverklee](https://github.com/oliverklee) for lots of refactorings, code modernizations and CI integrations @@ -633,5 +797,17 @@ 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/bin/phpunit`. +### 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 continuous integration (CI) checks for this project (including unit tests), +* run `composer install` to install the development dependencies managed with Composer; +* run `phive install` to install the development dependencies managed with PHIVE; + * [Installation of PHIVE](https://github.com/phar-io/phive?tab=readme-ov-file#getting-phive) +* run `composer ci` to run all static and dynamic CI checks. + +Details of other Composer scripts available (e.g. to run one specific CI check) are provided with `composer list`. + diff --git a/composer.json b/composer.json index a1f5677b..e8e42ce9 100644 --- a/composer.json +++ b/composer.json @@ -12,15 +12,26 @@ "authors": [ { "name": "Raphael Schweikert" + }, + { + "name": "Oliver Klee", + "email": "github@oliverklee.de" + }, + { + "name": "Jake Hotson", + "email": "jake.github@qzdesign.co.uk" } ], "require": { - "php": ">=5.6.20", + "php": ">=7.2.0", "ext-iconv": "*" }, "require-dev": { - "phpunit/phpunit": "^5.7.27", - "codacy/coverage": "^1.4.3" + "codacy/coverage": "^1.4.3", + "phpstan/extension-installer": "^1.3.1", + "phpstan/phpstan": "^1.10.65", + "phpstan/phpstan-phpunit": "^1.3.16", + "phpunit/phpunit": "^8.5.37" }, "suggest": { "ext-mbstring": "for parsing UTF-8 CSS" @@ -35,35 +46,56 @@ "Sabberworm\\CSS\\Tests\\": "tests/" } }, + "config": { + "allow-plugins": { + "phpstan/extension-installer": true + }, + "preferred-install": { + "*": "dist" + }, + "sort-packages": true + }, + "extra": { + "branch-alias": { + "dev-main": "9.0.x-dev" + } + }, "scripts": { "ci": [ - "@ci:static" + "@ci:static", + "@ci:dynamic" + ], + "ci:dynamic": [ + "@ci:tests" ], - "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:php:fixer": "\"./.phive/php-cs-fixer\" --config=config/php-cs-fixer.php fix --dry-run -v --show-progress=dots --diff bin src tests", + "ci:php:stan": "phpstan --no-progress --configuration=config/phpstan.neon", "ci:static": [ "@ci:php:fixer", - "@ci:php:sniffer", "@ci:php:stan" ], + "ci:tests": [ + "@ci:tests:unit" + ], + "ci:tests:sof": "\"./vendor/bin/phpunit\" --stop-on-failure --do-not-cache-result", + "ci:tests:unit": "\"./vendor/bin/phpunit\" --do-not-cache-result", "fix:php": [ - "@fix:php:fixer", - "@fix:php:sniffer" + "@fix:php:fixer" ], - "fix:php:fixer": "@php ./.phive/php-cs-fixer.phar --config=config/php-cs-fixer.php fix bin src tests", - "fix:php:sniffer": "@php ./.phive/phpcbf.phar --standard=config/phpcs.xml bin src tests", - "phpstan:baseline": "@php ./.phive/phpstan.phar --configuration=config/phpstan.neon --generate-baseline=config/phpstan-baseline.neon" + "fix:php:fixer": "\"./.phive/php-cs-fixer\" --config=config/php-cs-fixer.php fix bin src tests", + "phpstan:baseline": "phpstan --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": "Runs all dynamic and static code checks.", + "ci:dynamic": "Runs all dynamic code checks (i.e., currently, the unit tests).", "ci:php:fixer": "Checks the code style with PHP CS Fixer.", - "ci:php:sniffer": "Checks the code style with PHP_CodeSniffer.", "ci:php:stan": "Checks the types with PHPStan.", "ci:static": "Runs all static code analysis checks for the code.", + "ci:tests": "Runs all dynamic tests (i.e., currently, the unit tests).", + "ci:tests:sof": "Runs the unit tests and stops at the first failure.", + "ci:tests:unit": "Runs all unit tests.", "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." + "phpstan:baseline": "Updates the PHPStan baseline file to match the code." } } diff --git a/config/php-cs-fixer.php b/config/php-cs-fixer.php index 88a9a692..76f52077 100644 --- a/config/php-cs-fixer.php +++ b/config/php-cs-fixer.php @@ -8,9 +8,8 @@ ->setRiskyAllowed(true) ->setRules( [ - '@PSR12' => true, - // Disable constant visibility from the PSR12 rule set as this would break compatibility with PHP < 7.1. - 'visibility_required' => ['elements' => ['property', 'method']], + '@PER-CS2.0' => true, + '@PER-CS2.0:risky' => true, '@PHPUnit50Migration:risky' => true, '@PHPUnit52Migration:risky' => true, @@ -18,15 +17,18 @@ '@PHPUnit55Migration:risky' => true, '@PHPUnit56Migration:risky' => true, '@PHPUnit57Migration:risky' => true, + '@PHPUnit60Migration:risky' => true, + '@PHPUnit75Migration:risky' => true, + '@PHPUnit84Migration:risky' => true, 'php_unit_construct' => true, - 'php_unit_dedicate_assert' => ['target' => '5.6'], - 'php_unit_expectation' => ['target' => '5.6'], + 'php_unit_dedicate_assert' => ['target' => 'newest'], + 'php_unit_expectation' => ['target' => 'newest'], 'php_unit_fqcn_annotation' => true, 'php_unit_method_casing' => true, - 'php_unit_mock' => ['target' => '5.5'], + 'php_unit_mock' => ['target' => 'newest'], 'php_unit_mock_short_will_return' => true, - 'php_unit_namespaced' => ['target' => '5.7'], + 'php_unit_namespaced' => ['target' => 'newest'], 'php_unit_set_up_tear_down_visibility' => true, 'php_unit_test_annotation' => ['style' => 'annotation'], 'php_unit_test_case_static_method_calls' => ['call_type' => 'self'], diff --git a/config/phpcs.xml b/config/phpcs.xml deleted file mode 100644 index 14473bb2..00000000 --- a/config/phpcs.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - This standard requires PHP_CodeSniffer >= 3.6.0. - - - - - - - - - - - - - diff --git a/config/phpstan-baseline.neon b/config/phpstan-baseline.neon index b730548c..243e36c4 100644 --- a/config/phpstan-baseline.neon +++ b/config/phpstan-baseline.neon @@ -10,11 +10,6 @@ parameters: 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 diff --git a/phpunit.xml b/phpunit.xml index 5f3dd458..1060f329 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -1,6 +1,12 @@ + xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/8.5/phpunit.xsd" + beStrictAboutChangesToGlobalState="true" + beStrictAboutCoversAnnotation="true" + cacheResult="false" + colors="true" + forceCoversAnnotation="true" +> tests diff --git a/src/CSSList/CSSBlockList.php b/src/CSSList/CSSBlockList.php index fce7913e..6b02edfb 100644 --- a/src/CSSList/CSSBlockList.php +++ b/src/CSSList/CSSBlockList.php @@ -113,7 +113,7 @@ protected function allSelectors(array &$aResult, $sSpecificitySearch = null) $sComparator = $aSpecificitySearch[0]; $iTargetSpecificity = $aSpecificitySearch[1]; } - $iTargetSpecificity = (int)$iTargetSpecificity; + $iTargetSpecificity = (int) $iTargetSpecificity; $iSelectorSpecificity = $oSelector->getSpecificity(); $bMatches = false; switch ($sComparator) { diff --git a/src/CSSList/CSSList.php b/src/CSSList/CSSList.php index dcd8c331..603f662b 100644 --- a/src/CSSList/CSSList.php +++ b/src/CSSList/CSSList.php @@ -131,18 +131,15 @@ private static function parseListItem(ParserState $oParserState, CSSList $oList) } return $oAtRule; } elseif ($oParserState->comes('}')) { - if (!$oParserState->getSettings()->bLenientParsing) { - throw new UnexpectedTokenException('CSS selector', '}', 'identifier', $oParserState->currentLine()); - } else { - if ($bIsRoot) { - if ($oParserState->getSettings()->bLenientParsing) { - return DeclarationBlock::parse($oParserState); - } else { - throw new SourceException("Unopened {", $oParserState->currentLine()); - } + if ($bIsRoot) { + if ($oParserState->getSettings()->bLenientParsing) { + return DeclarationBlock::parse($oParserState); } else { - return null; + throw new SourceException("Unopened {", $oParserState->currentLine()); } + } else { + // End of list + return null; } } else { return DeclarationBlock::parse($oParserState, $oList); diff --git a/src/OutputFormat.php b/src/OutputFormat.php index 96f26e14..9cc2ae4d 100644 --- a/src/OutputFormat.php +++ b/src/OutputFormat.php @@ -165,9 +165,7 @@ class OutputFormat */ private $iIndentationLevel = 0; - public function __construct() - { - } + public function __construct() {} /** * @param string $sName diff --git a/src/OutputFormatter.php b/src/OutputFormatter.php index 7418494c..501d15da 100644 --- a/src/OutputFormatter.php +++ b/src/OutputFormatter.php @@ -215,6 +215,7 @@ public function removeLastSemicolon($sString) /** * * @param array $aComments + * * @return string */ public function comments(Commentable $oCommentable) diff --git a/src/Parsing/ParserState.php b/src/Parsing/ParserState.php index 7a99f327..06b083e6 100644 --- a/src/Parsing/ParserState.php +++ b/src/Parsing/ParserState.php @@ -9,8 +9,10 @@ class ParserState { /** * @var null + * + * @internal */ - const EOF = null; + public const EOF = null; /** * @var Settings diff --git a/src/Parsing/UnexpectedEOFException.php b/src/Parsing/UnexpectedEOFException.php index 368ec70c..825cc0c4 100644 --- a/src/Parsing/UnexpectedEOFException.php +++ b/src/Parsing/UnexpectedEOFException.php @@ -7,6 +7,4 @@ * * Extends `UnexpectedTokenException` in order to preserve backwards compatibility. */ -class UnexpectedEOFException extends UnexpectedTokenException -{ -} +class UnexpectedEOFException extends UnexpectedTokenException {} diff --git a/src/Property/AtRule.php b/src/Property/AtRule.php index 9536ff5e..64efd4de 100644 --- a/src/Property/AtRule.php +++ b/src/Property/AtRule.php @@ -12,15 +12,19 @@ interface AtRule extends Renderable, Commentable * we’re whitelisting the block rules and have anything else be treated as a set rule. * * @var string + * + * @internal */ - const BLOCK_RULES = 'media/document/supports/region-style/font-feature-values'; + public const BLOCK_RULES = 'media/document/supports/region-style/font-feature-values'; /** * … and more font-specific ones (to be used inside font-feature-values) * * @var string + * + * @internal */ - const SET_RULES = 'font-face/counter-style/page/swash/styleset/annotation'; + public const SET_RULES = 'font-face/counter-style/page/swash/styleset/annotation'; /** * @return string|null diff --git a/src/Property/KeyframeSelector.php b/src/Property/KeyframeSelector.php index 14ea5ebb..7675252c 100644 --- a/src/Property/KeyframeSelector.php +++ b/src/Property/KeyframeSelector.php @@ -8,8 +8,10 @@ class KeyframeSelector extends Selector * regexp for specificity calculations * * @var string + * + * @internal */ - const SELECTOR_VALIDATION_RX = '/ + public const SELECTOR_VALIDATION_RX = '/ ^( (?: [a-zA-Z0-9\x{00A0}-\x{FFFF}_^$|*="\'~\[\]()\-\s\.:#+>]* # any sequence of valid unescaped characters diff --git a/src/Property/Selector.php b/src/Property/Selector.php index 70c9b2fd..0bf433d1 100644 --- a/src/Property/Selector.php +++ b/src/Property/Selector.php @@ -13,7 +13,7 @@ class Selector * * @var string */ - const NON_ID_ATTRIBUTES_AND_PSEUDO_CLASSES_RX = '/ + private const NON_ID_ATTRIBUTES_AND_PSEUDO_CLASSES_RX = '/ (\.[\w]+) # classes | \[(\w+) # attributes @@ -37,7 +37,7 @@ class Selector * * @var string */ - const ELEMENTS_AND_PSEUDO_ELEMENTS_RX = '/ + private const ELEMENTS_AND_PSEUDO_ELEMENTS_RX = '/ ((^|[\s\+\>\~]+)[\w]+ # elements | \:{1,2}( # pseudo-elements @@ -49,8 +49,10 @@ class Selector * regexp for specificity calculations * * @var string + * + * @internal */ - const SELECTOR_VALIDATION_RX = '/ + public const SELECTOR_VALIDATION_RX = '/ ^( (?: [a-zA-Z0-9\x{00A0}-\x{FFFF}_^$|*="\'~\[\]()\-\s\.:#+>]* # any sequence of valid unescaped characters diff --git a/src/RuleSet/DeclarationBlock.php b/src/RuleSet/DeclarationBlock.php index de487bc1..91f5ecef 100644 --- a/src/RuleSet/DeclarationBlock.php +++ b/src/RuleSet/DeclarationBlock.php @@ -565,6 +565,7 @@ public function expandListStyleShorthand() public function createShorthandProperties(array $aProperties, $sShorthand) { $aRules = $this->getRulesAssoc(); + $oRule = null; $aNewValues = []; foreach ($aProperties as $sProperty) { if (!isset($aRules[$sProperty])) { @@ -585,7 +586,7 @@ public function createShorthandProperties(array $aProperties, $sShorthand) $this->removeRule($sProperty); } } - if (count($aNewValues)) { + if ($aNewValues !== [] && $oRule instanceof Rule) { $oNewRule = new Rule($sShorthand, $oRule->getLineNo(), $oRule->getColNo()); foreach ($aNewValues as $mValue) { $oNewRule->addValue($mValue); @@ -681,9 +682,9 @@ public function createDimensionsShorthand() $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]) { + 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 { diff --git a/src/Value/CSSFunction.php b/src/Value/CSSFunction.php index 300dc3ec..e3235c81 100644 --- a/src/Value/CSSFunction.php +++ b/src/Value/CSSFunction.php @@ -18,7 +18,7 @@ class CSSFunction extends ValueList /** * @param string $sName - * @param RuleValueList|array $aArguments + * @param RuleValueList|array $aArguments * @param string $sSeparator * @param int $iLineNo */ @@ -72,7 +72,7 @@ public function setName($sName) } /** - * @return array + * @return array */ public function getArguments() { diff --git a/src/Value/CalcFunction.php b/src/Value/CalcFunction.php index 5ffd071f..f06d9b7e 100644 --- a/src/Value/CalcFunction.php +++ b/src/Value/CalcFunction.php @@ -11,12 +11,12 @@ class CalcFunction extends CSSFunction /** * @var int */ - const T_OPERAND = 1; + private const T_OPERAND = 1; /** * @var int */ - const T_OPERATOR = 2; + private const T_OPERATOR = 2; /** * @param ParserState $oParserState diff --git a/src/Value/Color.php b/src/Value/Color.php index 1cf00cce..55acdaaa 100644 --- a/src/Value/Color.php +++ b/src/Value/Color.php @@ -14,7 +14,7 @@ class Color extends CSSFunction { /** - * @param array $aColor + * @param array $aColor * @param int $iLineNo */ public function __construct(array $aColor, $iLineNo = 0) @@ -56,12 +56,19 @@ public static function parse(ParserState $oParserState, $bIgnoreCase = false) $oParserState->currentLine() ), ]; - } else { + } elseif ($oParserState->strlen($sValue) === 6) { $aColor = [ 'r' => new Size(intval($sValue[0] . $sValue[1], 16), null, true, $oParserState->currentLine()), 'g' => new Size(intval($sValue[2] . $sValue[3], 16), null, true, $oParserState->currentLine()), 'b' => new Size(intval($sValue[4] . $sValue[5], 16), null, true, $oParserState->currentLine()), ]; + } else { + throw new UnexpectedTokenException( + 'Invalid hex color value', + $sValue, + 'custom', + $oParserState->currentLine() + ); } } else { $sColorMode = $oParserState->parseIdentifier(true); @@ -118,7 +125,7 @@ private static function mapRange($fVal, $fFromMin, $fFromMax, $fToMin, $fToMax) } /** - * @return array + * @return array */ public function getColor() { @@ -126,7 +133,7 @@ public function getColor() } /** - * @param array $aColor + * @param array $aColor * * @return void */ diff --git a/src/Value/LineName.php b/src/Value/LineName.php index e231ce38..6eaf7620 100644 --- a/src/Value/LineName.php +++ b/src/Value/LineName.php @@ -10,7 +10,7 @@ class LineName extends ValueList { /** - * @param array $aComponents + * @param array $aComponents * @param int $iLineNo */ public function __construct(array $aComponents = [], $iLineNo = 0) diff --git a/src/Value/Size.php b/src/Value/Size.php index 36a32381..f8336b1f 100644 --- a/src/Value/Size.php +++ b/src/Value/Size.php @@ -17,17 +17,22 @@ class Size extends PrimitiveValue * * @var array */ - const ABSOLUTE_SIZE_UNITS = ['px', 'cm', 'mm', 'mozmm', 'in', 'pt', 'pc', 'vh', 'vw', 'vmin', 'vmax', 'rem']; + private const ABSOLUTE_SIZE_UNITS = [ + 'px', 'pt', 'pc', + 'cm', 'mm', 'mozmm', 'in', + 'vh', 'dvh', 'svh', 'lvh', + 'vw', 'vmin', 'vmax', 'rem', + ]; /** * @var array */ - const RELATIVE_SIZE_UNITS = ['%', 'em', 'ex', 'ch', 'fr']; + private const RELATIVE_SIZE_UNITS = ['%', 'em', 'ex', 'ch', 'fr']; /** * @var array */ - const NON_SIZE_UNITS = ['deg', 'grad', 'rad', 's', 'ms', 'turn', 'Hz', 'kHz']; + private const NON_SIZE_UNITS = ['deg', 'grad', 'rad', 's', 'ms', 'turn', 'Hz', 'kHz']; /** * @var array>|null @@ -58,7 +63,7 @@ class Size extends PrimitiveValue public function __construct($fSize, $sUnit = null, $bIsColorComponent = false, $iLineNo = 0) { parent::__construct($iLineNo); - $this->fSize = (float)$fSize; + $this->fSize = (float) $fSize; $this->sUnit = $sUnit; $this->bIsColorComponent = $bIsColorComponent; } @@ -103,7 +108,7 @@ public static function parse(ParserState $oParserState, $bIsColorComponent = fal } } } - return new Size((float)$sSize, $sUnit, $bIsColorComponent, $oParserState->currentLine()); + return new Size((float) $sSize, $sUnit, $bIsColorComponent, $oParserState->currentLine()); } /** @@ -150,7 +155,7 @@ public function getUnit() */ public function setSize($fSize) { - $this->fSize = (float)$fSize; + $this->fSize = (float) $fSize; } /** @@ -211,7 +216,7 @@ public function render(OutputFormat $oOutputFormat) { $l = localeconv(); $sPoint = preg_quote($l['decimal_point'], '/'); - $sSize = preg_match("/[\d\.]+e[+-]?\d+/i", (string)$this->fSize) + $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/Value.php b/src/Value/Value.php index a920396b..5b52a22d 100644 --- a/src/Value/Value.php +++ b/src/Value/Value.php @@ -30,22 +30,22 @@ public function __construct($iLineNo = 0) /** * @param array $aListDelimiters * - * @return RuleValueList|CSSFunction|CSSString|LineName|Size|URL|string + * @return Value|string * * @throws UnexpectedTokenException * @throws UnexpectedEOFException */ public static function parseValue(ParserState $oParserState, array $aListDelimiters = []) { - /** @var array $aStack */ + /** @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('\\') - || $oParserState->isEnd()) + || $oParserState->comes(')') + || $oParserState->comes('\\') + || $oParserState->isEnd()) ) { if (count($aStack) > 0) { $bFoundDelimiter = false; @@ -67,23 +67,30 @@ public static function parseValue(ParserState $oParserState, array $aListDelimit } // Convert the list to list objects foreach ($aListDelimiters as $sDelimiter) { - if (count($aStack) === 1) { + $iStackLength = count($aStack); + if ($iStackLength === 1) { return $aStack[0]; } - $iStartPosition = null; - while (($iStartPosition = array_search($sDelimiter, $aStack, true)) !== false) { + $aNewStack = []; + for ($iStartPosition = 0; $iStartPosition < $iStackLength; ++$iStartPosition) { + if ($iStartPosition === ($iStackLength - 1) || $sDelimiter !== $aStack[$iStartPosition + 1]) { + $aNewStack[] = $aStack[$iStartPosition]; + continue; + } $iLength = 2; //Number of elements to be joined - for ($i = $iStartPosition + 2; $i < count($aStack); $i += 2, ++$iLength) { + for ($i = $iStartPosition + 3; $i < $iStackLength; $i += 2, ++$iLength) { if ($sDelimiter !== $aStack[$i]) { break; } } $oList = new RuleValueList($sDelimiter, $oParserState->currentLine()); - for ($i = $iStartPosition - 1; $i - $iStartPosition + 1 < $iLength * 2; $i += 2) { + for ($i = $iStartPosition; $i - $iStartPosition < $iLength * 2; $i += 2) { $oList->addListComponent($aStack[$i]); } - array_splice($aStack, $iStartPosition - 1, $iLength * 2 - 1, [$oList]); + $aNewStack[] = $oList; + $iStartPosition += $iLength * 2 - 2; } + $aStack = $aNewStack; } if (!isset($aStack[0])) { throw new UnexpectedTokenException( diff --git a/src/Value/ValueList.php b/src/Value/ValueList.php index a93acc7b..382c3baa 100644 --- a/src/Value/ValueList.php +++ b/src/Value/ValueList.php @@ -13,7 +13,7 @@ abstract class ValueList extends Value { /** - * @var array + * @var array */ protected $aComponents; @@ -23,8 +23,7 @@ abstract class ValueList extends Value protected $sSeparator; /** - * phpcs:ignore Generic.Files.LineLength - * @param array|RuleValueList|CSSFunction|CSSString|LineName|Size|URL|string $aComponents + * @param array|Value|string $aComponents * @param string $sSeparator * @param int $iLineNo */ @@ -39,7 +38,7 @@ public function __construct($aComponents = [], $sSeparator = ',', $iLineNo = 0) } /** - * @param RuleValueList|CSSFunction|CSSString|LineName|Size|URL|string $mComponent + * @param Value|string $mComponent * * @return void */ @@ -49,7 +48,7 @@ public function addListComponent($mComponent) } /** - * @return array + * @return array */ public function getListComponents() { @@ -57,7 +56,7 @@ public function getListComponents() } /** - * @param array $aComponents + * @param array $aComponents * * @return void */ diff --git a/tests/CSSList/AtRuleBlockListTest.php b/tests/CSSList/AtRuleBlockListTest.php index 48e6e578..dcc8db5c 100644 --- a/tests/CSSList/AtRuleBlockListTest.php +++ b/tests/CSSList/AtRuleBlockListTest.php @@ -7,12 +7,46 @@ use Sabberworm\CSS\CSSList\AtRuleBlockList; use Sabberworm\CSS\Parser; use Sabberworm\CSS\Renderable; +use Sabberworm\CSS\Settings; /** * @covers \Sabberworm\CSS\CSSList\AtRuleBlockList */ -class AtRuleBlockListTest extends TestCase +final class AtRuleBlockListTest extends TestCase { + /** + * @return array + */ + public static function provideMinWidthMediaRule(): array + { + return [ + 'without spaces around arguments' => ['@media(min-width: 768px){.class{color:red}}'], + 'with spaces around arguments' => ['@media (min-width: 768px) {.class{color:red}}'], + ]; + } + + /** + * @return array + */ + public static function provideSyntacticlyCorrectAtRule(): array + { + return [ + 'media print' => ['@media print { html { background: white; color: black; } }'], + 'keyframes' => ['@keyframes mymove { from { top: 0px; } }'], + 'supports' => [' + @supports (display: flex) { + .flex-container > * { + text-shadow: 0 0 2px blue; + float: none; + } + .flex-container { + display: flex; + } + } + '], + ]; + } + /** * @test */ @@ -43,23 +77,12 @@ public function implementsCommentable() self::assertInstanceOf(Commentable::class, $subject); } - /** - * @return array> - */ - public function mediaRuleDataProvider() - { - return [ - 'without spaces around arguments' => ['@media(min-width: 768px){.class{color:red}}'], - 'with spaces around arguments' => ['@media (min-width: 768px) {.class{color:red}}'], - ]; - } - /** * @test * * @param string $css * - * @dataProvider mediaRuleDataProvider + * @dataProvider provideMinWidthMediaRule */ public function parsesRuleNameOfMediaQueries($css) { @@ -74,7 +97,7 @@ public function parsesRuleNameOfMediaQueries($css) * * @param string $css * - * @dataProvider mediaRuleDataProvider + * @dataProvider provideMinWidthMediaRule */ public function parsesArgumentsOfMediaQueries($css) { @@ -83,4 +106,17 @@ public function parsesArgumentsOfMediaQueries($css) self::assertSame('(min-width: 768px)', $atRuleBlockList->atRuleArgs()); } + + /** + * @test + * + * @dataProvider provideMinWidthMediaRule + * @dataProvider provideSyntacticlyCorrectAtRule + */ + public function parsesSyntacticlyCorrectAtRuleInStrictMode(string $css): void + { + $contents = (new Parser($css, Settings::create()->beStrict()))->parse()->getContents(); + + self::assertNotEmpty($contents, 'Failing CSS: `' . $css . '`'); + } } diff --git a/tests/CSSList/DocumentTest.php b/tests/CSSList/DocumentTest.php index a727400b..a052e449 100644 --- a/tests/CSSList/DocumentTest.php +++ b/tests/CSSList/DocumentTest.php @@ -11,14 +11,14 @@ /** * @covers \Sabberworm\CSS\CSSList\Document */ -class DocumentTest extends TestCase +final class DocumentTest extends TestCase { /** * @var Document */ private $subject; - protected function setUp() + protected function setUp(): void { $this->subject = new Document(); } @@ -50,7 +50,7 @@ public function getContentsInitiallyReturnsEmptyArray() /** * @return array>> */ - public function contentsDataProvider() + public static function contentsDataProvider() { return [ 'empty array' => [[]], diff --git a/tests/CSSList/KeyFrameTest.php b/tests/CSSList/KeyFrameTest.php index 080d5f94..293e4f3a 100644 --- a/tests/CSSList/KeyFrameTest.php +++ b/tests/CSSList/KeyFrameTest.php @@ -11,14 +11,14 @@ /** * @covers \Sabberworm\CSS\CSSList\KeyFrame */ -class KeyFrameTest extends TestCase +final class KeyFrameTest extends TestCase { /** * @var KeyFrame */ protected $subject; - protected function setUp() + protected function setUp(): void { $this->subject = new KeyFrame(); } diff --git a/tests/Comment/CommentTest.php b/tests/Comment/CommentTest.php index 29385f01..7261e9ba 100644 --- a/tests/Comment/CommentTest.php +++ b/tests/Comment/CommentTest.php @@ -14,7 +14,7 @@ * @covers \Sabberworm\CSS\OutputFormat * @covers \Sabberworm\CSS\OutputFormatter */ -class CommentTest extends TestCase +final class CommentTest extends TestCase { /** * @test @@ -91,7 +91,7 @@ public function toStringRendersCommentEnclosedInCommentDelimiters() $subject->setComment($comment); - self::assertSame('/*' . $comment . '*/', (string)$subject); + self::assertSame('/*' . $comment . '*/', (string) $subject); } /** @@ -140,11 +140,11 @@ public function keepCommentsInOutput() ', $oCss->render(OutputFormat::createPretty())); self::assertSame( '/** Number 11 **//**' . "\n" - . ' * Comments' . "\n" - . ' *//* Hell */@import url("some/url.css") screen;' - . '/* Number 4 *//* Number 5 */.foo,#bar{' - . '/* Number 6 */background-color:#000;}@media screen{' - . '/** Number 10 **/#foo.bar{/** Number 10b **/position:absolute;}}', + . ' * Comments' . "\n" + . ' *//* Hell */@import url("some/url.css") screen;' + . '/* Number 4 *//* Number 5 */.foo,#bar{' + . '/* Number 6 */background-color:#000;}@media screen{' + . '/** Number 10 **/#foo.bar{/** Number 10b **/position:absolute;}}', $oCss->render(OutputFormat::createCompact()->setRenderComments(true)) ); } @@ -170,8 +170,8 @@ public function stripCommentsFromOutput() ', $oCss->render(OutputFormat::createPretty()->setRenderComments(false))); self::assertSame( '@import url("some/url.css") screen;' - . '.foo,#bar{background-color:#000;}' - . '@media screen{#foo.bar{position:absolute;}}', + . '.foo,#bar{background-color:#000;}' + . '@media screen{#foo.bar{position:absolute;}}', $oCss->render(OutputFormat::createCompact()) ); } diff --git a/tests/OutputFormatTest.php b/tests/OutputFormatTest.php index 0de39123..66843eae 100644 --- a/tests/OutputFormatTest.php +++ b/tests/OutputFormatTest.php @@ -11,12 +11,12 @@ /** * @covers \Sabberworm\CSS\OutputFormat */ -class OutputFormatTest extends TestCase +final class OutputFormatTest extends TestCase { /** * @var string */ - const TEST_CSS = <<oParser = new Parser(self::TEST_CSS); $this->oDocument = $this->oParser->parse(); diff --git a/tests/ParserTest.php b/tests/ParserTest.php index 74449ee2..a48ac0e7 100644 --- a/tests/ParserTest.php +++ b/tests/ParserTest.php @@ -35,7 +35,7 @@ * @covers \Sabberworm\CSS\Value\Size::parse * @covers \Sabberworm\CSS\Value\URL::parse */ -class ParserTest extends TestCase +final class ParserTest extends TestCase { /** * @test @@ -146,6 +146,8 @@ public function colorParsing() 'l' => new Size(220.0, '%', true, $oColor->getLineNo()), 'a' => new Size(0000.3, null, true, $oColor->getLineNo()), ], $oColor->getColor()); + $aColorRule = $oRuleSet->getRules('outline-color'); + self::assertEmpty($aColorRule); } } foreach ($oDoc->getAllValues('color') as $sColor) { diff --git a/tests/RuleSet/DeclarationBlockTest.php b/tests/RuleSet/DeclarationBlockTest.php index 49526952..31537a9b 100644 --- a/tests/RuleSet/DeclarationBlockTest.php +++ b/tests/RuleSet/DeclarationBlockTest.php @@ -10,7 +10,7 @@ /** * @covers \Sabberworm\CSS\RuleSet\DeclarationBlock */ -class DeclarationBlockTest extends TestCase +final class DeclarationBlockTest extends TestCase { /** * @param string $sCss @@ -27,13 +27,13 @@ public function expandBorderShorthand($sCss, $sExpected) foreach ($oDoc->getAllDeclarationBlocks() as $oDeclaration) { $oDeclaration->expandBorderShorthand(); } - self::assertSame(trim((string)$oDoc), $sExpected); + self::assertSame(trim((string) $oDoc), $sExpected); } /** * @return array> */ - public function expandBorderShorthandProvider() + public static function expandBorderShorthandProvider() { return [ ['body{ border: 2px solid #000 }', 'body {border-width: 2px;border-style: solid;border-color: #000;}'], @@ -60,13 +60,13 @@ public function expandFontShorthand($sCss, $sExpected) foreach ($oDoc->getAllDeclarationBlocks() as $oDeclaration) { $oDeclaration->expandFontShorthand(); } - self::assertSame(trim((string)$oDoc), $sExpected); + self::assertSame(trim((string) $oDoc), $sExpected); } /** * @return array> */ - public function expandFontShorthandProvider() + public static function expandFontShorthandProvider() { return [ [ @@ -116,13 +116,13 @@ public function expandBackgroundShorthand($sCss, $sExpected) foreach ($oDoc->getAllDeclarationBlocks() as $oDeclaration) { $oDeclaration->expandBackgroundShorthand(); } - self::assertSame(trim((string)$oDoc), $sExpected); + self::assertSame(trim((string) $oDoc), $sExpected); } /** * @return array> */ - public function expandBackgroundShorthandProvider() + public static function expandBackgroundShorthandProvider() { return [ ['body {border: 1px;}', 'body {border: 1px;}'], @@ -169,13 +169,13 @@ public function expandDimensionsShorthand($sCss, $sExpected) foreach ($oDoc->getAllDeclarationBlocks() as $oDeclaration) { $oDeclaration->expandDimensionsShorthand(); } - self::assertSame(trim((string)$oDoc), $sExpected); + self::assertSame(trim((string) $oDoc), $sExpected); } /** * @return array> */ - public function expandDimensionsShorthandProvider() + public static function expandDimensionsShorthandProvider() { return [ ['body {border: 1px;}', 'body {border: 1px;}'], @@ -207,13 +207,13 @@ public function createBorderShorthand($sCss, $sExpected) foreach ($oDoc->getAllDeclarationBlocks() as $oDeclaration) { $oDeclaration->createBorderShorthand(); } - self::assertSame(trim((string)$oDoc), $sExpected); + self::assertSame(trim((string) $oDoc), $sExpected); } /** * @return array> */ - public function createBorderShorthandProvider() + public static function createBorderShorthandProvider() { return [ ['body {border-width: 2px;border-style: solid;border-color: #000;}', 'body {border: 2px solid #000;}'], @@ -238,13 +238,13 @@ public function createFontShorthand($sCss, $sExpected) foreach ($oDoc->getAllDeclarationBlocks() as $oDeclaration) { $oDeclaration->createFontShorthand(); } - self::assertSame(trim((string)$oDoc), $sExpected); + self::assertSame(trim((string) $oDoc), $sExpected); } /** * @return array> */ - public function createFontShorthandProvider() + public static function createFontShorthandProvider() { return [ ['body {font-size: 12px; font-family: serif}', 'body {font: 12px serif;}'], @@ -281,13 +281,13 @@ public function createDimensionsShorthand($sCss, $sExpected) foreach ($oDoc->getAllDeclarationBlocks() as $oDeclaration) { $oDeclaration->createDimensionsShorthand(); } - self::assertSame(trim((string)$oDoc), $sExpected); + self::assertSame(trim((string) $oDoc), $sExpected); } /** * @return array> */ - public function createDimensionsShorthandProvider() + public static function createDimensionsShorthandProvider() { return [ ['body {border: 1px;}', 'body {border: 1px;}'], @@ -319,13 +319,13 @@ public function createBackgroundShorthand($sCss, $sExpected) foreach ($oDoc->getAllDeclarationBlocks() as $oDeclaration) { $oDeclaration->createBackgroundShorthand(); } - self::assertSame(trim((string)$oDoc), $sExpected); + self::assertSame(trim((string) $oDoc), $sExpected); } /** * @return array> */ - public function createBackgroundShorthandProvider() + public static function createBackgroundShorthandProvider() { return [ ['body {border: 1px;}', 'body {border: 1px;}'], diff --git a/tests/RuleSet/LenientParsingTest.php b/tests/RuleSet/LenientParsingTest.php index 5f5f224a..39e7bf74 100644 --- a/tests/RuleSet/LenientParsingTest.php +++ b/tests/RuleSet/LenientParsingTest.php @@ -19,7 +19,7 @@ * @covers \Sabberworm\CSS\Value\Size::parse * @covers \Sabberworm\CSS\Value\URL::parse */ -class LenientParsingTest extends TestCase +final class LenientParsingTest extends TestCase { /** * @test @@ -131,4 +131,26 @@ public function caseInsensitivity() $oResult->render() ); } + + /** + * @test + */ + public function invalidColor() + { + $sFile = __DIR__ . '/../fixtures/invalid-color.css'; + $oParser = new Parser(file_get_contents($sFile), Settings::create()->withLenientParsing(true)); + $oParser->parse(); + } + + /** + * @test + */ + public function invalidColorStrict() + { + $this->expectException(UnexpectedTokenException::class); + + $sFile = __DIR__ . '/../fixtures/invalid-color.css'; + $oParser = new Parser(file_get_contents($sFile), Settings::create()->beStrict()); + $oParser->parse(); + } } diff --git a/tests/SettingsTest.php b/tests/SettingsTest.php new file mode 100644 index 00000000..859059a3 --- /dev/null +++ b/tests/SettingsTest.php @@ -0,0 +1,155 @@ +subject = Settings::create(); + } + + /** + * @test + */ + public function createReturnsInstance(): void + { + $settings = Settings::create(); + + self::assertInstanceOf(Settings::class, $settings); + } + + /** + * @test + */ + public function createReturnsANewInstanceForEachCall(): void + { + $settings1 = Settings::create(); + $settings2 = Settings::create(); + + self::assertNotSame($settings1, $settings2); + } + + /** + * @test + */ + public function multibyteSupportByDefaultStateOfMbStringExtension(): void + { + self::assertSame(extension_loaded('mbstring'), $this->subject->bMultibyteSupport); + } + + /** + * @test + */ + public function withMultibyteSupportProvidesFluentInterface(): void + { + self::assertSame($this->subject, $this->subject->withMultibyteSupport()); + } + + /** + * @return array + */ + public static function booleanDataProvider(): array + { + return [ + 'true' => [true], + 'false' => [false], + ]; + } + + /** + * @test + * @dataProvider booleanDataProvider + */ + public function withMultibyteSupportSetsMultibyteSupport(bool $value): void + { + $this->subject->withMultibyteSupport($value); + + self::assertSame($value, $this->subject->bMultibyteSupport); + } + + /** + * @test + */ + public function defaultCharsetByDefaultIsUtf8(): void + { + self::assertSame('utf-8', $this->subject->sDefaultCharset); + } + + /** + * @test + */ + public function withDefaultCharsetProvidesFluentInterface(): void + { + self::assertSame($this->subject, $this->subject->withDefaultCharset('UTF-8')); + } + + /** + * @test + */ + public function withDefaultCharsetSetsDefaultCharset(): void + { + $charset = 'ISO-8859-1'; + $this->subject->withDefaultCharset($charset); + + self::assertSame($charset, $this->subject->sDefaultCharset); + } + + /** + * @test + */ + public function lenientParsingByDefaultIsTrue(): void + { + self::assertTrue($this->subject->bLenientParsing); + } + + /** + * @test + */ + public function withLenientParsingProvidesFluentInterface(): void + { + self::assertSame($this->subject, $this->subject->withLenientParsing()); + } + + /** + * @test + * @dataProvider booleanDataProvider + */ + public function withLenientParsingSetsLenientParsing(bool $value): void + { + $this->subject->withLenientParsing($value); + + self::assertSame($value, $this->subject->bLenientParsing); + } + + /** + * @test + */ + public function beStrictProvidesFluentInterface(): void + { + self::assertSame($this->subject, $this->subject->beStrict()); + } + + /** + * @test + */ + public function beStrictSetsLenientParsingToFalse(): void + { + $this->subject->beStrict(); + + self::assertFalse($this->subject->bLenientParsing); + } +} diff --git a/tests/Value/CalcRuleValueListTest.php b/tests/Value/CalcRuleValueListTest.php index 0a2c5304..0ce279fd 100644 --- a/tests/Value/CalcRuleValueListTest.php +++ b/tests/Value/CalcRuleValueListTest.php @@ -9,7 +9,7 @@ /** * @covers \Sabberworm\CSS\Value\CalcRuleValueList */ -class CalcRuleValueListTest extends TestCase +final class CalcRuleValueListTest extends TestCase { /** * @test diff --git a/tests/Value/SizeTest.php b/tests/Value/SizeTest.php new file mode 100644 index 00000000..3aecce05 --- /dev/null +++ b/tests/Value/SizeTest.php @@ -0,0 +1,51 @@ + + */ + public static function provideUnit(): array + { + $units = [ + 'px', 'pt', 'pc', + 'cm', 'mm', 'mozmm', 'in', + 'vh', 'dvh', 'svh', 'lvh', + 'vw', 'vmin', 'vmax', 'rem', + '%', 'em', 'ex', 'ch', 'fr', + 'deg', 'grad', 'rad', 's', 'ms', 'turn', 'Hz', 'kHz', + ]; + + return \array_combine( + $units, + \array_map( + function (string $unit): array { + return [$unit]; + }, + $units + ) + ); + } + + /** + * @test + * + * @dataProvider provideUnit + */ + public function parsesUnit(string $unit): void + { + $subject = Size::parse(new ParserState('1' . $unit, Settings::create())); + + self::assertSame($unit, $subject->getUnit()); + } +} diff --git a/tests/fixtures/colortest.css b/tests/fixtures/colortest.css index 1c89cf41..f834aa77 100644 --- a/tests/fixtures/colortest.css +++ b/tests/fixtures/colortest.css @@ -9,6 +9,7 @@ #yours { background-color: hsl(220, 10%, 220%); background-color: hsla(220, 10%, 220%, 0.3); + outline-color: #22; } #variables { diff --git a/tests/fixtures/invalid-color.css b/tests/fixtures/invalid-color.css new file mode 100644 index 00000000..31602f37 --- /dev/null +++ b/tests/fixtures/invalid-color.css @@ -0,0 +1,11 @@ +#test { + color: #a; + background: #ab; +} + +body + color: #abcd; + background: #abcde; +} + +a { color: #fffff;}